Documentation
Complete technical reference for the on-chain roulette protocol on Avalanche C-Chain.
Overview
Permissionless Roulette Protocol is a fully on-chain, permissionless roulette protocol on Avalanche C-Chain. Anyone can deploy a roulette contract with any ERC-20 token, configure game parameters, and open it to players. Community members can stake into the pool to earn fees from every bet.
- No off-chain oracles — SHA-256 commit-reveal randomness
- Factory pattern — permissionless contract deployment
- Community staking — Synthetix-style fee distribution to liquidity providers
- Multi-bet support — up to 10 positions per spin
- Configurable RTP, fees, pool limits, and branding
- Unlimited claim window — blockhash auto-cached, no 256-block expiry
- All contracts verified on Sourcify
How It Works
The protocol uses a two-phase commit-reveal mechanism. Neither the player nor the validator can predict or manipulate the result at the time of betting.
Choose your bet position(s) on the roulette board — numbers, colors, dozens, columns, or outside bets. Enter your wager amount and click Spin. Your tokens are transferred to the contract and a fee is deducted for stakers. The current block number is recorded as the commit block.
The protocol waits for at least one block on Avalanche C-Chain. The result depends on the blockhash of the commit block, which is unknown at commit time.
The frontend automatically calls the reveal function. The contract computes the result using sha256(blockhash, player, amount, betId) % 37 and determines the outcome.
If you win, the payout is automatically transferred from the pool to your wallet. All outcome data is stored on-chain and can be independently verified.
Commit-Reveal Mechanism
The randomness seed is computed deterministically from on-chain data that cannot be known in advance:
bytes32 seed = sha256(abi.encodePacked(
blockhash(bet.commitBlock),
bet.player,
bet.amount,
betId
));
uint8 result = uint8(uint256(seed) % 37);There is no time limit to claim your bet. The contract uses a blockhash caching mechanism: when reveal is called, the commit block's hash is automatically cached in the commitBlockHashes mapping. Even after the EVM's native 256-block window expires, your bet can still be revealed using the cached hash. You can also manually call snapshotBlockHash(blockNum) within the 256-block window to cache it proactively.
A bet can only be forfeited if more than 256 blocks have passed AND no blockhash snapshot was cached. If anyone has either revealed another bet on the same block or called snapshotBlockHash, the hash is permanently cached and the bet can be claimed forever.
Why is this fair?
- ✓The player commits before the blockhash exists — they can't choose a favorable hash
- ✓The blockhash is determined by the network — no single party controls it
- ✓The betId, player address, and amount are mixed in — making the seed unique per bet
- ✓Anyone can verify the result by replaying the seed computation on-chain
Bet Types & Payouts
Standard European roulette rules with a single zero (0). Payouts are multiplied by the configured RTP (Return To Player) percentage, typically 97%.
| Type | ID | Description | Coverage | Raw Payout | With 97% RTP |
|---|---|---|---|---|---|
| Straight | 0 | Single number (0-36) | 1/37 | 36x | 34.92x |
| Red | 1 | All red numbers | 18/37 | 2x | 1.94x |
| Black | 2 | All black numbers | 18/37 | 2x | 1.94x |
| Even | 3 | Even numbers (2,4,6...) | 18/37 | 2x | 1.94x |
| Odd | 4 | Odd numbers (1,3,5...) | 18/37 | 2x | 1.94x |
| Low | 5 | Numbers 1-18 | 18/37 | 2x | 1.94x |
| High | 6 | Numbers 19-36 | 18/37 | 2x | 1.94x |
| 1st Dozen | 7 | Numbers 1-12 | 12/37 | 3x | 2.91x |
| 2nd Dozen | 8 | Numbers 13-24 | 12/37 | 3x | 2.91x |
| 3rd Dozen | 9 | Numbers 25-36 | 12/37 | 3x | 2.91x |
| Column 1 | 10 | 3,6,9,12,15,18,21,24,27,30,33,36 | 12/37 | 3x | 2.91x |
| Column 2 | 11 | 2,5,8,11,14,17,20,23,26,29,32,35 | 12/37 | 3x | 2.91x |
| Column 3 | 12 | 1,4,7,10,13,16,19,22,25,28,31,34 | 12/37 | 3x | 2.91x |
Multi-Bet
You can select up to 10 positions in a single spin. All bets share the same random result, just like placing multiple chips on a real roulette table.
Click multiple positions on the betting board (e.g. Red + Number 17 + 1st Dozen). Enter your wager per position. The total amount = per-position amount × number of positions. All positions are committed in a single transaction via commitMultiBet and revealed together via revealMultiBet.
Limits
- • Maximum 10 positions per spin
- • Total bet amount across all positions must respect pool limits
- • Worst-case total payout must not exceed the max win threshold
- • Each individual bet must be above the minimum bet
Staking & Earn
Every roulette contract has a community staking mechanism. Anyone can deposit tokens into the pool and earn a proportional share of fees generated by every bet placed on that contract.
How Staking Works
Call stake(amount) to deposit tokens into the pool. You receive shares proportional to your deposit relative to the current pool: shares = amount × totalShares / poolBalance. If you're the first staker, shares = amount (1:1).
Every bet deducts a fee (configurable, max 25%). This fee is distributed to all stakers using a Synthetix-style reward accumulator: rewardPerShare += fee × 1e18 / totalShares. Your pending rewards grow with every bet, no need to restake.
Call claimFees() to withdraw accumulated rewards anytime. Call unstake(shares) to burn shares and receive your proportional pool tokens: amount = shares × poolBalance / totalShares.
The pool balance changes as players win or lose. When players win, pool decreases; when players lose, pool increases. Stakers absorb this variance in exchange for earning bet fees. Your shares represent a percentage of the pool, not a fixed token amount — so the token value of your shares fluctuates.
When you unstake, you receive shares × poolBalance / totalShares tokens. If the pool grew (more player losses), you get more than you staked. If the pool shrank (more player wins), you get less. The Earn page shows your estimated token value in real-time before you unstake.
Staking Functions
// Deposit tokens and receive shares function stake(uint256 amount) // Burn shares and withdraw proportional pool tokens function unstake(uint256 shares) // Claim accumulated fee rewards function claimFees() // View pending unclaimed fees function earned(address account) → uint256 // View staker position details function getStakerInfo(address) → ( uint256 shares, uint256 stakeValue, uint256 pendingFees, uint256 sharePercent // basis points (5000 = 50%) ) // Paginated staker list function getStakers(uint256 offset, uint256 limit) → ( address[] addresses, uint256[] shares, uint256[] values )
Pool & Limits
Every roulette contract has a liquidity pool funded by stakers. The pool balance determines how much players can bet, enforced by two on-chain constraints:
Maximum bet as a percentage of the pool. E.g., if the pool has 100K tokens and the ratio is 10%, the max bet (total, across all positions) is 10K tokens.
Maximum payout as a percentage of the pool. Prevents a single lucky bet from draining the entire pool. E.g., 50% means max payout is half the pool.
Each contract has a configurable minimum bet amount. Bets below this amount will be rejected. The UI shows the allowed range before you commit, so you never waste gas on rejected transactions.
For Creators
Anyone can deploy their own roulette contract via the Factory. Choose the token, configure the game parameters, fund the initial pool, and your contract goes live instantly.
Deployment Flow
Any ERC-20 token on Avalanche C-Chain.
Set RTP, fee %, min bet, max bet ratio, max win %. All in basis points.
Provide initial liquidity. Community members can also stake to grow the pool.
Your contract appears on the homepage. Players can bet and stakers can earn immediately.
Owner Controls
- • Stake/Unstake — Owner is a staker like everyone else via the staking system
- • Emergency Withdraw — Pull all owner shares + pending rewards instantly
- • Pause/Resume — Temporarily halt new bets (existing bets can still be revealed)
- • Update Config — Change RTP, fees, limits (takes effect immediately)
- • Customize Branding — Update contract name and token logo
- • Transfer Ownership — Hand over control to another address
Configuration
| Parameter | Type | Description | Range |
|---|---|---|---|
| rtpBasisPoints | uint256 | Return to player percentage | 1–10000 (0.01%–100%) |
| feeBasisPoints | uint256 | Fee deducted from each bet (distributed to stakers) | 0–2500 (0%–25%) |
| maxWinBasisPoints | uint256 | Max payout as % of pool | 1–10000 |
| minBet | uint256 | Minimum bet in token wei | ≥ 1 wei |
| maxBetPoolRatio | uint256 | Max bet as % of pool | 1–10000 |
All percentage values use basis points (1/100th of a percent). 10000 = 100%, 9700 = 97%, 500 = 5%. This allows precise configuration without floating point math on-chain.
Security
The contract has been tested with a comprehensive security test suite covering 86 test cases across multiple categories including staking, multi-bet, and edge cases.
Protections
Settled flag prevents replay attacks
Bet IDs must belong to same player in multi-bet
Max bet ratio + max win caps protect liquidity
OpenZeppelin ReentrancyGuard on all mutating functions
Owner-only modifier on sensitive functions
Prevents transfer failures from non-standard tokens
Blocks new bets while allowing existing reveals
Fee capped at 25%, RTP validated, all params range-checked
sha256(blockhash, player, amount, betId) — no nonce manipulation
commitBlockHashes mapping removes 256-block expiry risk
Synthetix accumulator prevents rounding exploits
Multi-bet commits all share same block to prevent cherry-picking
Randomness uses sha256(blockhash, player, amount, betId). The seed is fully deterministic — fixed at commit time with no player-controlled nonce. While sufficient for most use cases, a block producer could theoretically influence results by withholding blocks. For high-stakes deployments, consider integrating Chainlink VRF. For small-to-medium stakes on Avalanche (high validator decentralization), this risk is minimal.
Smart Contracts
Deploys and indexes all roulette games (Sourcify verified)
Native platform token used for deployment fees
Avalanche C-Chain
Integration
Use the ABIs below to integrate with the Roulette platform from your own dApp, bot, or backend. All contracts are verified on Sourcify.
RouletteFactory
Address: 0x262Ac490bc04D708333512909A611d803aC1e518
Write Functions
// Deploy a new roulette game function createGame( address _token, string _tokenLogoUrl, string _betName, uint256 _rtpBasisPoints, uint256 _feeBasisPoints, uint256 _maxWinBasisPoints, uint256 _minBet, uint256 _maxBetPoolRatio, uint256 _initialPool ) → address gameAddress // Admin only function setDeployFee(uint256 _newFee) function setTreasury(address _treasury) function transferOwnership(address newOwner) function recoverTokens(address _token, uint256 amount)
Read Functions
function getGames(uint256 offset, uint256 limit) → GameInfo[] function getGamesByOwner(address _owner) → uint256[] function getTotalGames() → uint256 function isGame(address) → bool function deployFee() → uint256 function avloToken() → address function treasury() → address
RouletteGame
Each game is deployed via the factory. Interact with individual game addresses.
Write Functions — Player
// Single bet: commit → wait 1 block → reveal function commitBet(uint8 betType, uint8 number, uint256 amount) → uint256 betId function revealBet(uint256 betId) → (uint8 result, bool won, uint256 payout) function forfeitBet(uint256 betId) // Multi-bet: up to 10 positions in one spin function commitMultiBet(uint8[] betTypes, uint8[] numbers, uint256[] amounts) → uint256[] betIds function revealMultiBet(uint256[] betIds) → uint8 result // Blockhash caching (anyone can call) function snapshotBlockHash(uint256 blockNum) // Chat function sendMessage(string message, string username)
Write Functions — Staking
function stake(uint256 amount) // Deposit tokens, receive shares function unstake(uint256 shares) // Burn shares, withdraw tokens function claimFees() // Claim accumulated fee rewards
Write Functions — Owner
function fundPool(uint256 amount) // Legacy alias for stake() function emergencyWithdraw() // Withdraw all owner shares + rewards function updateConfig( uint256 _rtpBasisPoints, uint256 _feeBasisPoints, uint256 _maxWinBasisPoints, uint256 _minBet, uint256 _maxBetPoolRatio ) function updateBetName(string _betName) function updateTokenLogo(string _tokenLogoUrl) function setPaused(bool _paused) function transferOwnership(address newOwner)
Read Functions
function getGameInfo() → ( address owner, address token, string tokenLogoUrl, string betName, uint256 poolBalance, uint256 totalBets, uint256 totalVolume, bool paused, uint256 totalFeesCollected, uint256 totalPayouts, uint256 totalShares, uint256 stakerCount ) function getStakerInfo(address) → (shares, stakeValue, pendingFees, sharePercent) function getStakers(offset, limit) → (addresses[], shares[], values[]) function earned(address) → uint256 function getRecentBets(uint256 count) → Bet[] function getUserBets(address, uint256 offset, uint256 limit) → Bet[] function getUserBetCount(address) → uint256 function getRecentMessages(uint256 count) → ChatMessage[] function config() → (rtpBP, feeBP, maxWinBP, minBet, maxBetRatio) function poolBalance() → uint256 function totalShares() → uint256 function totalBets() → uint256 function totalVolume() → uint256 function paused() → bool
Events
event BetCommitted(uint256 indexed betId, address indexed player, uint8 betType, uint8 number, uint256 amount) event BetSettled(uint256 indexed betId, address indexed player, uint8 result, bool won, uint256 payout) event BetForfeited(uint256 indexed betId, address indexed player) event MultiBetCommitted(address indexed player, uint256[] betIds, uint256 totalAmount) event MultiBetSettled(address indexed player, uint256[] betIds, uint8 result, uint256 totalPayout) event PoolFunded(address indexed funder, uint256 amount) event EmergencyWithdraw(address indexed owner, uint256 amount) event Staked(address indexed user, uint256 amount, uint256 shares) event Unstaked(address indexed user, uint256 shares, uint256 amount) event RewardsClaimed(address indexed user, uint256 amount) event ConfigUpdated() event OwnershipTransferred(address indexed previousOwner, address indexed newOwner)
Bet Type IDs
0 = Straight (single number 0-36) 1 = Red 2 = Black 3 = Even 4 = Odd 5 = Low (1-18) 6 = High (19-36) 7 = 1st Dozen (1-12) 8 = 2nd Dozen (13-24) 9 = 3rd Dozen (25-36) 10 = Column 1 (3,6,9,...,36) 11 = Column 2 (2,5,8,...,35) 12 = Column 3 (1,4,7,...,34)
Quick Start (viem)
import { createPublicClient, http, parseAbi } from 'viem';
import { avalanche } from 'viem/chains';
const client = createPublicClient({
chain: avalanche,
transport: http(),
});
// Read all games from factory
const games = await client.readContract({
address: '0x262Ac490bc04D708333512909A611d803aC1e518',
abi: parseAbi([
'function getGames(uint256, uint256) view returns ((address,address,address,string,string,uint256)[])'
]),
functionName: 'getGames',
args: [0n, 100n],
});
// Read game info (includes staking data)
const info = await client.readContract({
address: gameAddress,
abi: parseAbi([
'function getGameInfo() view returns (address,address,string,string,uint256,uint256,uint256,bool,uint256,uint256,uint256,uint256)'
]),
functionName: 'getGameInfo',
});
// info[10] = totalShares, info[11] = stakerCountComplete ABI JSON files are available on Sourcify. Import them directly or use the function signatures above with parseAbi.
FAQ
Yes. The result is determined by sha256(blockhash, player, amount, betId). The seed is fully deterministic — fixed at commit time with no player-controlled inputs. The blockhash is set by the network after you commit, so neither you nor the house can predict it. Anyone can verify any past result on-chain.
There is no time limit. The contract caches the blockhash automatically on first reveal. Even if the 256-block window passes, your bet is still claimable as long as the blockhash was cached (by any player or keeper). A bet can only be forfeited if >256 blocks passed AND no snapshot exists.
Deposit tokens into the pool via the Earn tab. You receive shares proportional to your deposit. Every bet on that contract generates a fee (e.g. 5%) which is distributed to all stakers in real-time using a Synthetix-style accumulator. Claim fees anytime. Unstake anytime — you receive your proportional share of the pool.
Shares represent a percentage of the pool, not a fixed token amount. If the pool shrank (players won more than they lost), your shares are worth fewer tokens. The Earn page shows the estimated token value before you unstake. Conversely, if players lose more, your shares are worth more.
The owner can call emergencyWithdraw to pull their own shares and pending rewards. This only withdraws what belongs to the owner — other stakers’ funds remain. Check the pool balance and staker distribution before playing.
Yes, anyone can call revealBet on any bet. The payout always goes to the original player, so there’s no theft risk. This allows relayers and keepers to reveal on behalf of players.
Your bet amount exceeds the pool’s max bet ratio or the potential win exceeds the max win cap. Reduce your bet amount or wait for the pool to grow. The UI shows the allowed range.
The factory charges a deployment fee in AVLO tokens (set by the platform admin). You also need to fund the initial pool with your chosen token. Community stakers can grow the pool after launch.