House-pool casino protocol on Robinhood Chain (Arbitrum Orbit L2). LPs deposit WETH into an ERC-4626 bankroll vault and collectively act as the house; game contracts draw on the pool through a risk-capped controller with provably fair randomness.
LP yield is share appreciation from realized house edge. There is no token in this repo — no minting, no emissions, no exceptions.
This is a ground-up redesign informed by the failure modes of Party Dice / RISKY.LOL (Arbitrum, 2022–23). Each is designed against explicitly:
| Their flaw | DICE's counter-design |
|---|---|
| Token WAS the bankroll (mint-on-win / burn-on-loss) | Bankroll is an ERC-4626 vault of plain WETH; nothing is ever minted or burned but vault shares |
| Games held unlimited mint authority | Games can't touch the vault; the controller enforces per-bet (0.5%) and aggregate (5%) exposure caps against available liquidity |
| Trusted, unverifiable RNG | Two verifiable sources: commit-reveal (outcomes locked at placement, anyone-can-refund on timeout) and drand League-of-Entropy beacons verified on-chain via BLS pairing — the relayer is untrusted |
| Transfer taxes / max-wallet / freeze switches | Vault shares are a plain ERC20; no hooks, no taxes, no pause on transfers, withdrawals can never be frozen |
| Permissionless per-token pools, toxic-flow drain | One governance-approved WETH pool; per-game registry with a hard house-edge floor |
| Token before product | Product only. Referral/points bookkeeping exists, token handled elsewhere, later |
flowchart TB
subgraph Players
P[Player]
end
subgraph LPs
LP[LP depositors]
end
P -- "placeBet{ETH}(target, clientSeed)" --> DG[DiceGame]
DG -- "wrap ETH → WETH" --> W[(WETH)]
DG -- "placeBet / settle / refund" --> HC[HouseController<br/>risk engine]
DG -- "requestRandomness(betId, clientSeed)" --> RNG[CommitRevealRandom]
RNG -- "fulfillRandomness / fulfillTimeout" --> DG
OP[Operator hot key] -- "commitEpoch / reveal" --> RNG
HC -- "reserve / release / transferOut" --> V[BankrollVault<br/>ERC-4626 WETH]
LP -- "deposit (instant)" --> V
V -- "requestRedeem → 24h → claimRedeem" --> LP
HC -- "protocol fee (15% of realized edge)" --> T[Treasury]
HC -- "recordWager (fail-open)" --> PL[PointsLedger]
PL --- RR[ReferralRegistry]
Trust boundaries. The vault trusts only the controller. The controller trusts only registered games, within caps. Games trust their pinned random source per bet. The RNG operator is a hot key with zero admin power: it can delay (→ refunds), never decide outcomes.
Bet lifecycle. DiceGame.placeBet (ETH in, wrapped) →
HouseController.placeBet validates the edge floor and both caps against
availableLiquidity(), pulls the wager into the vault, reserves the max
payout → requestRandomness → operator reveals the epoch seed → anyone
fulfills → game computes the roll → settle pays ≤ the reservation, skims
the fee from realized edge only. If the operator never reveals: after
revealTimeout blocks anyone can timeout(requestId) → full wager
refund. Pauses (guardian or the 5%-daily-drawdown auto-breaker) block only
new bets — never settlements, refunds, or withdrawals.
Roles. DEFAULT_ADMIN (multisig + timelock on mainnet) registers games
and wires contracts; RISK_MANAGER changes parameters inside hard-coded
code bounds and clears the breaker; GUARDIAN can only pause new bets; the
RNG OPERATOR can only commit/reveal seeds.
Every input to your bet's outcome is public on Blockscout; the operator committed to its seed before your bet existed and cannot change anything afterwards.
- Before betting (optional): note the current epoch's
commitmentfrom theEpochCommitted(epochId, commitment)event onCommitRevealRandom. - Place your bet with your own
clientSeed(any 32 bytes — your entropy). From your transaction, record from theBetPlacedevent:betId,targetBps,wager,maxPayout,clientSeed,requestId, and from the source'sRandomnessRequestedevent: yourepochId. - After the reveal, read
serverSeedfromEpochRevealed(epochId, serverSeed). - Check the commitment (the seed existed before your bet):
cast keccak $SERVER_SEED # must equal the pre-bet commitment
- Recompute your roll:
cast keccak $(cast abi-encode "f(bytes32,address,uint256,bytes32)" \ $SERVER_SEED $DICE_GAME_ADDRESS $BET_ID $CLIENT_SEED) # roll = uint256(that hash) % 10000
- Check the outcome: you win iff
roll < targetBps, and theRandomFulfilled(betId, randomness, roll)/BetSettledevents must show exactly this roll andpayout = wager × (10000 − houseEdgeBps) / targetBps(edge at placement, from yourBetPlacedevent).
If the operator stalls instead of revealing: after revealTimeout blocks,
call CommitRevealRandom.timeout(requestId) from any address — your full
wager is refunded. No permission needed.
Encoded in the fuzz/invariant suite (test/invariant/Invariants.t.sol and
per-contract tests):
totalAssets() ≥ reservedPayouts, always.- No settlement can pay more than its reservation at placement.
- Single bet ≤ per-bet cap; open exposure ≤ aggregate cap (vs available, not raw, liquidity).
- Full conservation: no code path moves vault assets except controller settle / refund / fee, deposits, and matured queue claims (ghost-accounted under hostile donations and operator-goes-dark sequences).
- The withdrawal delay cannot be bypassed; pauses never block exits or settlements.
- CEI + ReentrancyGuard on external entry points; malicious ERC20s (fee-on-transfer, reentrant) are rejected/blocked by balance-delta checks and guards.
Coverage on vault / controller / game: 97–100% lines.
src/
BankrollVault.sol ERC-4626 bankroll, reservations, withdraw queue
HouseController.sol game registry, caps, fees, circuit breakers
interfaces/ IBankrollVault, IHouseController
rng/ IRandomSource, CommitRevealRandom, VRF stub notes
rng/drand/ DrandRandomSource + on-chain BLS12-381 verification
games/DiceGame.sol roll-under dice, first game module
referrals/ ReferralRegistry, PointsLedger (no token)
script/ Deploy.s.sol (testnet/mainnet), DeployLocal.s.sol
test/ unit + fuzz suites, invariant harness, mocks
docs/DEPLOYMENT.md deployment + operator runbook
Games are source-agnostic behind IRandomSource; both adapters ship:
- CommitRevealRandom — operator pre-commits
keccak256(serverSeed)per epoch; outcome locked at placement; reveal closes the epoch; timeout → anyone triggers a full refund. Works on any chain; requires an operator server. - DrandRandomSource — drand quicknet beacons (League of Entropy, 3s cadence) verified on-chain with the EIP-2537 BLS12-381 precompiles (live on Robinhood Chain, ArbOS ≥ 51; ~160k gas per settlement). Each bet pins a strictly-future round at placement, so nobody — including the relayer — can shop or bias the beacon; fulfillment is permissionless and the timeout refund guarantee is identical. No operator key exists at all. Tests verify real League-of-Entropy signatures end-to-end.
Verification for drand bets: fetch round R from api.drand.sh, check
randomness = sha256(signature), then
roll = uint256(keccak256(abi.encode(randomness, game, betId, clientSeed))) % 10000.
cp .env.example .env # fill in — never committed
forge build
forge test # 102 tests
forge coverage --report summary
forge snapshot --check # gas regressionFoundry profiles: local, robinhood-testnet, robinhood (select with
FOUNDRY_PROFILE). Deployment: see docs/DEPLOYMENT.md.
Any token or tokenomics; multi-asset or permissionless pools; frontend (separate repo); cross-chain; stock-token-denominated pools.