- Foundry ≥ 1.7 (
foundryup) - A funded deployer key on the target chain
- Testnet funds: https://faucet.testnet.chain.robinhood.com
.envcreated from.env.example(never committed):
| Variable | Meaning |
|---|---|
DEPLOYER_PRIVATE_KEY |
Throwaway key on testnet; hardware-backed on mainnet |
WETH_ADDRESS |
Canonical WETH on the target chain (the vault asset) |
ADMIN_MULTISIG |
Receives DEFAULT_ADMIN on every contract |
TREASURY_ADDRESS |
Protocol fee receiver |
RNG_OPERATOR_ADDRESS |
Hot server key that commits/reveals seed epochs — must be distinct from every admin/ops key |
RISK_MANAGER_ADDRESS |
Bounded-parameter changes + breaker clear |
GUARDIAN_ADDRESS |
Pause-only key |
ROBINHOOD_TESTNET_RPC_URL |
From docs.robinhood.com/chain |
BLOCKSCOUT_API_KEY |
Any non-empty string (Blockscout doesn't require a real key) |
anvil &
FOUNDRY_PROFILE=local forge script script/DeployLocal.s.sol \
--rpc-url local --broadcastDeploys MockWETH, all contracts, grants every role to anvil account #0,
commits epoch 0 (seed preimage keccak256("local dev server seed 0")), and
seeds the vault with 100 WETH.
forge build && forge test # must be green
FOUNDRY_PROFILE=robinhood-testnet forge script script/Deploy.s.sol \
--rpc-url robinhood-testnet --broadcast \
--verify --verifier blockscout \
--verifier-url https://testnet.robinhoodchain.blockscout.com/api/The script, in order:
- Deploys
BankrollVault(ERC-4626 over WETH). - Deploys
HouseController, points the vault at it. - Deploys
DrandRandomSource(primary — drand quicknet verified on-chain,roundDelay = 2, no operator key) andCommitRevealRandom(standby — operator key from env; rotate to it with one multisigdice.setRandomSourcecall if ever needed). - Deploys
DiceGamewired to drand, registers it (per-bet 50 bps / exposure 500 bps / edge floor 100 bps), approves it on both sources. - Deploys
ReferralRegistry+PointsLedger, grants the controller the recorder role. - Grants RISK_MANAGER / GUARDIAN, hands DEFAULT_ADMIN everywhere to
ADMIN_MULTISIG, and the deployer renounces all admin.
Record the printed addresses in your ops vault.
-
vault.controller() == HouseController -
controller.games(dice).enabled == true -
dice.randomSource() == DrandRandomSource -
drand.approvedConsumers(dice) == trueandfallbackRng.approvedConsumers(dice) == true - Deployer holds no role on any contract
(
hasRole(0x00, deployer) == falseon all six) - drand relay keeper is running (see below); place a 1-wei test bet end-to-end: place → wait ~2 rounds (6s) → keeper relays pulse → settlement pays/loses correctly
- Timeout drill: place a bet, stop the keeper, wait
fulfillTimeout(default 1h) past the pinned round, calltimeout(requestId)from an unrelated EOA → full wager refunded - Standby drill (testnet only): rotate
dice.setRandomSource(fallbackRng), commit an epoch, run one bet through commit-reveal, rotate back - Seed an initial LP deposit; verify
availableLiquidity()and that a bet breaching the 0.5% per-bet cap reverts - Withdrawal drill:
requestRedeem→ wait delay →claimRedeem
forge verify-contract <ADDRESS> src/BankrollVault.sol:BankrollVault \
--verifier blockscout \
--verifier-url https://testnet.robinhoodchain.blockscout.com/api/ \
--constructor-args $(cast abi-encode "constructor(address,string,string,address)" \
$WETH_ADDRESS "DICE WETH Bankroll" "dWETH" <DEPLOYER>)Repeat per contract (HouseController, CommitRevealRandom, DiceGame,
ReferralRegistry, PointsLedger) with their constructor args. Mainnet
verifier URL: https://robinhoodchain.blockscout.com/api/.
Identical command with FOUNDRY_PROFILE=robinhood and
--rpc-url robinhood, plus:
ADMIN_MULTISIGmust be a real multisig fronted by a timelock (deploy the timelock first; the multisig is the timelock's proposer).DEPLOYER_PRIVATE_KEYfrom a hardware signer.- Run the full post-deploy checklist before announcing; keep the guardian pause key warm during the first week.
The keeper is untrusted — it cannot influence outcomes, only relay them. It must, continuously:
- Watch
RandomnessRequested(requestId, consumer, betId, targetRound, ...). - When drand publishes
targetRound(~3s cadence,GENESIS + (round-1)*3), fetch the pulse:GET https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/public/<round>(mirrors:drand.cloudflare.com,api.drand.secureweb3.com:6875). - Decompress the 48-byte signature to the 128-byte uncompressed EIP-2537
G1 form (x, y over Fp with 16-byte zero padding; y chosen by the 0x20
sign flag) and call
fulfill(requestId, sig128). - Alert if any request is still Pending 10+ minutes past its round — players can self-settle, but the house keeper should always be first.
Failure economics: if the keeper (and everyone else) fails to relay for
fulfillTimeout (default 1h), anyone refunds the bet — LPs lose nothing but
edge. drand itself is a threshold network run by the League of Entropy;
multi-mirror fetching makes beacon unavailability a non-event.
The operator server must, continuously:
- Keep ≥ 1 unrevealed committed epoch at all times (
commitEpochahead of demand) — betting fails closed withNoActiveEpochotherwise. - Reveal the active epoch within
revealTimeoutblocks of its first request (default 1800), thenfulfilleach pending request (both are permissionless — keepers can crank them too). - Rotate: reveal closes an epoch; new bets automatically join the next committed epoch.
Seed hygiene: generate each server seed with a CSPRNG; store until reveal; never reuse; the key that sends commit/reveal txs holds no other privileges. If the key leaks, the attacker can only censor reveals (bets refund via timeout) — outcomes cannot be altered. Rotate by granting OPERATOR_ROLE to the new key and revoking the old (admin multisig).