The settlement layer of the risk-trading protocol, prepared for external audit: contracts in scope with verified source, system architecture, the invariants to try to break, the threat model, test & coverage summary, known incidents, and accepted risks. Prepared honestly — including the gaps.
| CONTRACT | SLOC | STATUS | SOURCIFY |
|---|---|---|---|
| ChannelManager.sol (V3) — simulator-parity limits, incl. reclaimStale | 635 | ACTIVE | exact_match ✅ |
| ChannelManager.sol (V2) | 635 | RETIRED | exact_match ✅ |
| ChannelManager.sol (V1, commit e7da239) | 601 | RETIRED / DRAINED | exact_match ✅ |
| HouseBond.sol | 116 | ACTIVE | exact_match ✅ |
| BlackjackRulesVerifier.sol | 330 | ACTIVE | exact_match ✅ |
| RouletteRulesVerifier.sol — European roulette rules (13 bet types, exact payouts, solvency check) | 296 | ACTIVE | exact_match ✅ |
| GameMuxVerifier.sol — action-range dispatcher (0–31 blackjack, 32–63 roulette) | 45 | ACTIVE | exact_match ✅ |
| IRulesVerifier.sol (interface) | 51 | IN SCOPE | — |
| MockEntropyLocal.sol (test-only) | 27 | OUT OF SCOPE | — |
External dependencies: OpenZeppelin (SafeERC20, ReentrancyGuard, Ownable), Pyth Entropy (provider callback), RUSD ERC-20 (6 decimals, standard).
Off-chain components (threat-model context, not in on-chain scope): services/house-node/ (TS house node: WAL, checkpointing, grace timers, reconciliation, recover_close) and app/channels-sdk.js (browser SDK: session keys, hash chains, EIP-712 signing, snapshot/restore).
Player deposits RUSD; the operator allocates house float. Hands are played off-chain via dual-signed EIP-712 ChannelStates (domain "RAIN ChannelManager" v1). Randomness = mutual commit-reveal hash chains (N=4096) + a Pyth Entropy session seed mixed per card: card_k = keccak(pRev_k, hRev_k, sessionSeed, channelId, k) % 52 (% 37 for roulette), house-reveals-first ordering.
On-chain surface: open (entropy seed request) · checkpoint (nonce floor) · cooperative close · forced close + challenge window + finalize · reveal verification (submitReveals/demandReveal) · fraud proof (proveBadTransition) · stale-channel recovery (reclaimStale, 30-day) · fee waterfall at close (1.25% of lost gross → burn 4000 / rebate 1700 / standby 500 / residual 6300 ppm).
Key parameters: 180 s player grace → forfeit of open hand's gross · house fault → hand void + comp max(2× gross, 5 RUSD) from HouseBond + equal penalty burned · tiered challenge windows (≤100,000 RUSD → 10 min; above → 24 h) · exposure caps (Σ allocations ≤ 10% vault float, single ≤ 1.5%, 8×maxUnitStake ≤ allocation) · HouseBond 7-day withdrawal delay, pending withdrawals remain slashable · funds always pay to playerOwner, never session keys.
| ChannelManager V3 (active) | 0xe13DC056111906aA4d3659880EBDC67138c76DA7 |
| ChannelManager V2 (retired) | 0xacbfCf3523DDA59419a181Afb2e17A80BE453fEb |
| ChannelManager V1 (retired) | 0x82698e4C14DC5D144bdA2a028aDe60d886681AFb |
| GameMuxVerifier (manager.verifier) | 0x14155583112F87777B95AB0EA76786bf079c6A83 |
| BlackjackRulesVerifier | 0xFdcCE2DE2F8A28cBBdBaB01131C3bab138Eb9293 |
| RouletteRulesVerifier | 0x5239969ff32e5733F319d3e80eD64D19adF4962f |
| HouseBond (2×100k RUSD bonds) | 0x37E41E1d1EE2F96cE0203af06d639448547ade84 |
| RUSD token (demo, 6 dec) | 0x260c7019E760763988843Bc5b873d749b5937469 |
| Pyth Entropy | 0x7698E925FfC29655576D0b361D75Af579e20AdAc |
House node: wss://api.rainsandbox.xyz · health: api.rainsandbox.xyz/health · V3 constants: EXPOSURE_SINGLE_PPM 15,000 · EXPOSURE_TOTAL_PPM 100,000 · TIER_THRESHOLD 100k RUSD.
playerBal + houseBal + feeAccrued + grossAtRisk == deposit + allocation. No path may mint or destroy escrowed RUSD.playerOwner (never session keys); house payouts only to operator; the fee waterfall split is exact and applied exactly once._entropyCallback; voidOpen full-refund only in SeedPending after 1 h; exposure caps enforced at open.nonReentrant; RUSD is standard but assume-hostile analysis welcome.proveBadTransition accepts only a dual-signed prev and an accused-signed illegal next; a legal transition must never be punishable; card/number derivation must be unbiased given one honest party.keccak(...)%37 unbiased given one honest party; (b) _bet solvency guarantees _spin can never underflow houseBal; (c) mux dispatch never lets one game's payload validate under the other's rules; (d) node-enforced worst-case payout per spin ≤ 8×maxUnitStake.finalizeClose, reclaimStale, submitReveals) → must not worsen an honest party's outcomeforge coverage is unobtainable — via_ir + coverage instrumentation hits "stack too deep" even with --ir-minimum. Line-coverage numbers unavailable.v4.0.0 kept session material in memory only — page refresh abandoned open channels. 4 channels (2,400 RUSD) recovered via operator cooperativeClose from WAL states. Fix: localStorage snapshot/restore, auto-resume, recover_close, 30-min reconciliation sweep.
Opened mid-maintenance; WAL wiped by redeploys, no dual-signed state on either side. V1 has no recovery path by design (re-audited: all settle paths need dual signatures). Player's 500 RUSD deposit refunded from treasury; 2,000 RUSD house allocation written off. This failure mode is exactly why reclaimStale exists from V2 on.
Bar: a redeploy mid-session must never destroy the only copy of a signed state. Three layers now: per-hand on-chain checkpoints (~130k gas, default ON), checkpoint-on-disconnect, SIGTERM flush — plus client snapshots and reclaimStale as the durable floor.
Idle-tab channels closed by reconciliation left clients in an endless "Reconnecting…" loop. Fix: typed unknown_channel errors, on-chain state check, self-healing reopen. Funds were never at risk — UX bug, disclosed anyway.
/tmp — mitigated in v4.1.0 by per-hand checkpoints, disconnect checkpoints, SIGTERM flush, client snapshots + recover_close. On-chain checkpoint + reclaimStale is the durable floor.proveBadReveal intentionally subsumed by submitReveals chain verification.Contact for audit engagement: via the dApp at rainsandbox.xyz or the operator address in the deployment log. Suggested firms & budget rationale in spec §7.3.