Permissionless Roulette Protocol

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.

Key Highlights
  • 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.

1
Select & Commit

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.

2
Wait 1 Block (~2s)

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.

3
Reveal

The frontend automatically calls the reveal function. The contract computes the result using sha256(blockhash, player, amount, betId) % 37 and determines the outcome.

4
Instant Payout

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:

Solidity
bytes32 seed = sha256(abi.encodePacked(
    blockhash(bet.commitBlock),
    bet.player,
    bet.amount,
    betId
));
uint8 result = uint8(uint256(seed) % 37);
Unlimited Claim Window

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.

Forfeiting

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%.

TypeIDDescriptionCoverageRaw PayoutWith 97% RTP
Straight0Single number (0-36)1/3736x34.92x
Red1All red numbers18/372x1.94x
Black2All black numbers18/372x1.94x
Even3Even numbers (2,4,6...)18/372x1.94x
Odd4Odd numbers (1,3,5...)18/372x1.94x
Low5Numbers 1-1818/372x1.94x
High6Numbers 19-3618/372x1.94x
1st Dozen7Numbers 1-1212/373x2.91x
2nd Dozen8Numbers 13-2412/373x2.91x
3rd Dozen9Numbers 25-3612/373x2.91x
Column 1103,6,9,12,15,18,21,24,27,30,33,3612/373x2.91x
Column 2112,5,8,11,14,17,20,23,26,29,32,3512/373x2.91x
Column 3121,4,7,10,13,16,19,22,25,28,31,3412/373x2.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.

How it works

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

1
Stake Tokens

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).

2
Earn Fees

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.

3
Claim & Unstake

Call claimFees() to withdraw accumulated rewards anytime. Call unstake(shares) to burn shares and receive your proportional pool tokens: amount = shares × poolBalance / totalShares.

Pool Value Fluctuation

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.

Shares ≠ Tokens

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:

Max Bet Ratio

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.

maxBet = pool × maxBetPoolRatio / 10000
Max Win

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.

maxWin = pool × maxWinBasisPoints / 10000
Minimum Bet

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

1
Choose Token

Any ERC-20 token on Avalanche C-Chain.

2
Configure Parameters

Set RTP, fee %, min bet, max bet ratio, max win %. All in basis points.

3
Fund Initial Pool

Provide initial liquidity. Community members can also stake to grow the pool.

4
Go Live

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

ParameterTypeDescriptionRange
rtpBasisPointsuint256Return to player percentage1–10000 (0.01%–100%)
feeBasisPointsuint256Fee deducted from each bet (distributed to stakers)0–2500 (0%–25%)
maxWinBasisPointsuint256Max payout as % of pool1–10000
minBetuint256Minimum bet in token wei≥ 1 wei
maxBetPoolRatiouint256Max bet as % of pool1–10000
Basis Points

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

Double Reveal

Settled flag prevents replay attacks

Cross-Player Mixing

Bet IDs must belong to same player in multi-bet

Pool Drain

Max bet ratio + max win caps protect liquidity

Reentrancy

OpenZeppelin ReentrancyGuard on all mutating functions

Access Control

Owner-only modifier on sensitive functions

SafeERC20

Prevents transfer failures from non-standard tokens

Pause Mechanism

Blocks new bets while allowing existing reveals

Config Validation

Fee capped at 25%, RTP validated, all params range-checked

Deterministic Seed

sha256(blockhash, player, amount, betId) — no nonce manipulation

Blockhash Caching

commitBlockHashes mapping removes 256-block expiry risk

Staking Math

Synthetix accumulator prevents rounding exploits

Cross-Block Protection

Multi-bet commits all share same block to prevent cherry-picking

Note on Randomness

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

RouletteFactory

Deploys and indexes all roulette games (Sourcify verified)

0x262A...e518
AVLO Token

Native platform token used for deployment fees

0x54eE...b3Bb
Network

Avalanche C-Chain

Chain ID: 43114

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] = stakerCount
Full ABIs

Complete ABI JSON files are available on Sourcify. Import them directly or use the function signatures above with parseAbi.

FAQ

Is the game provably fair?

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.

What happens if I don’t reveal in time?

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.

How does staking work?

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.

Why did I receive fewer tokens than my share count when unstaking?

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.

Can the house owner rug the pool?

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.

Can someone else reveal my bet?

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.

Why do I see a ‘Bet exceeds max’ error?

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.

How much does it cost to deploy a contract?

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.