# RAIN Channels — Audit Scope (v4.1.0, 2026-07-28)

Prepared pre-audit. Companion docs: `rain-channels-spec-v1.md` (design spec), `DEPLOYMENT.md` (live deployment log + incidents).

## 1. Contracts in scope (protocol/src/channels/)

| Contract | SLoC (raw lines) | Deployed (Arbitrum One, 42161) | Sourcify |
|---|---|---|---|
| `ChannelManager.sol` (**V3**, simulator-parity limits, includes `reclaimStale`) | 635 | `0xe13DC056111906aA4d3659880EBDC67138c76DA7` | exact_match |
| `ChannelManager.sol` (V1, commit `e7da239`, retired/drained) | 601 | `0x82698e4C14DC5D144bdA2a028aDe60d886681AFb` | exact_match |
| `HouseBond.sol` | 116 | `0x37E41E1d1EE2F96cE0203af06d639448547ade84` | exact_match |
| `BlackjackRulesVerifier.sol` | 330 | `0xFdcCE2DE2F8A28cBBdBaB01131C3bab138Eb9293` | exact_match |
| `IRulesVerifier.sol` (interface) | 51 | — | — |
| `MockEntropyLocal.sol` (test-only, OUT of scope) | 27 | — | — |

Total in-scope: ~1,130 raw lines (~850 SLoC excluding comments/blanks). Toolchain: Solidity 0.8.24, foundry, `via_ir = true`.

External deps: OpenZeppelin (SafeERC20, ReentrancyGuard, Ownable, ECDSA/EIP-712 style hashing done manually), Pyth Entropy (`0x7698E925FfC29655576D0b361D75Af579e20AdAc`, provider callback), RUSD ERC-20 (`0x260c7019E760763988843Bc5b873d749b5937469`, 6 decimals, standard).

Off-chain components (context, NOT in on-chain scope but relevant to threat model): `services/house-node/` (TS house node: WAL, checkpointing, grace timers, reconciliation, recover_close), `app/channels-sdk.js` (browser SDK: session keys, hash chains, EIP-712 signing, snapshot/restore persistence).

## 1a. v5.0.0 addition — Roulette channel module (2026-07-29)

| Contract | SLoC (raw lines) | Deployed (Arbitrum One, 42161) | Sourcify |
|---|---|---|---|
| `RouletteRulesVerifier.sol` — European single-zero roulette rules (BET/SPIN state machine, 13 bet types incl. splits/streets/corners/six-lines, exact casino payouts 35/17/11/8/5/2/1:1, per-bet solvency check, 1.25% fee on lost gross) | 296 | `0x5239969ff32e5733F319d3e80eD64D19adF4962f` | exact_match |
| `GameMuxVerifier.sol` — action-range dispatcher: actions 0–31 → BlackjackRulesVerifier, 32–63 → RouletteRulesVerifier (local = action − 32). Now the manager's active `verifier`. | 45 | `0x14155583112F87777B95AB0EA76786bf079c6A83` | exact_match |

New invariants to attack: (a) roulette spin outcome `keccak(pRev,hRev,sessionSeed,channelId,k)%37` must be unbiased given one honest party; (b) `_bet` solvency check must guarantee `_spin` can never underflow `houseBal` (worst-case payout + max fee ≤ gross + houseBal); (c) mux dispatch must never let a payload of one game validate under the other game's rules; (d) exposure mapping: house node rejects bet batches whose worst-case payout exceeds 8×maxUnitStake (same bound the ChannelManager enforces for one blackjack hand, `8×maxUnitStake ≤ allocation`).

## 2. System summary

FunFair-style state channels for casino games. Player deposits RUSD; operator allocates house float. Hands are played off-chain via dual-signed EIP-712 `ChannelState`s (domain "RAIN ChannelManager" v1); randomness = mutual commit-reveal hash chains (N=4096) + Pyth Entropy session seed mixed per card. On-chain: 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`, V2 only), fee waterfall at close.

## 3. Key invariants (what the auditor should try to break)

1. **Conservation:** for any settling state, `playerBal + houseBal + feeAccrued + grossAtRisk == deposit + allocation` (`_requireConservation`). No path may mint or destroy escrowed RUSD.
2. **Nonce monotonicity:** a checkpointed nonce is a hard floor — no close (cooperative/forced/final) may settle on a lower-nonce state; challenge must strictly increase the closing nonce.
3. **Dual signature integrity:** every settlement-bearing state requires valid player-session AND house-session signatures bound to the channel's registered keys, chainId, and verifying contract (no cross-channel/cross-chain replay — fuzz-tested).
4. **Funds destinations:** player payouts only to `playerOwner` (never session keys); house payouts only to `operator`; fee waterfall split (burn 4000 / rebate 1700 / standby 500 / residual 6300 ppm of 1.25% lost gross) is exact and only applied once.
5. **`reclaimStale` conservatism (V2):** only callable after ≥30 days of zero on-chain activity (`lastOnchainActivity`), settles at the last on-chain checkpoint (or opening balances if none), never better for the caller than the last anchored state, permissionless, no way to use it to grief an active channel (any on-chain touch resets the clock).
6. **Forfeit/fault paths:** player-forfeit only after grace, forfeits only the open hand's gross; house-fault pays hand void + comp `max(2× gross, 5 RUSD)` from HouseBond + equal penalty burned — never from other channels' escrow.
7. **HouseBond:** 7-day withdrawal delay; pending withdrawals remain slashable; `slash` pays compensation before burn when underfunded; only manager may slash; `setManager` only owner.
8. **Entropy:** only the Pyth Entropy contract may invoke `_entropyCallback`; `voidOpen` full-refund path only in SeedPending after 1 h; exposure caps (Σ allocations ≤ 2% vault float, single ≤ 0.25%, `8×maxUnitStake ≤ allocation`).
9. **Reentrancy:** all fund-moving functions `nonReentrant`; RUSD is a standard token but assume-hostile analysis welcome.
10. **Rules verifier:** `proveBadTransition` accepts only a dual-signed `prev` and an accused-signed illegal `next`; a legal transition must never be punishable; card derivation `keccak(pRev,hRev,seed,channelId,k)%52` must be unbiased given one honest party.

## 4. Threat model

- **Malicious player:** stale-state forceClose (mitigated: challenge + checkpoint floor), mid-hand stall (grace→forfeit), fake reveals (on-chain chain verification), session-key theft irrelevant to fund destination.
- **Malicious operator/house:** stale-state close (player challenge), withholding reveals (demandReveal escalation), refusing to countersign (player forceClose from last dual-signed state), node death (V2 `reclaimStale` after 30 d guarantees eventual escrow release).
- **Third parties:** griefing via permissionless functions (`finalizeClose`, `reclaimStale`, `submitReveals`) must not be able to worsen an honest party's outcome.
- **Infrastructure:** Arbitrum sequencer outage vs challenge windows (tiered 10 min / 24 h — accepted risk at demo stakes); Pyth Entropy liveness (voidOpen refund).

## 5. Accepted risks / known limitations (do not re-report)

- Demo/play-money deployment (RUSD faucet token). 24 h tier window < Arbitrum force-inclusion delay is an accepted risk at these stakes (spec §4.4).
- Single foundation-run house node; HA/watchtower/HSM out of scope for v1.
- House node WAL on ephemeral `/tmp` — mitigated in v4.1.0 by per-hand on-chain checkpoints (default ON, ~130k gas each on Arbitrum ≈ $0.01–0.02), checkpoint-on-disconnect, and SIGTERM flush; plus client-side snapshot/restore and `recover_close`. On-chain checkpoint + `reclaimStale` is the durable floor.
- Statistical fairness is transparency-only in v1 (no on-chain statistical slashing).
- `proveBadReveal` intentionally subsumed by `submitReveals` chain verification.
- V1 ChannelManager is retired; one stranded V1 channel documented in DEPLOYMENT.md (no V1 recovery path existed; player compensated from treasury).

## 6. Test & coverage summary

- **Forge:** 140/140 green (97 legacy V3 + 43 in `test/Channels.t.sol`: bond mechanics, open/seed/void, exposure caps, cooperative close + waterfall exactness, conservation, stale-state challenge, checkpoint floor, forfeit, comp+burn, tiered windows, reveal chains, demandReveal, fraud proof both directions, `reclaimStale` suite, fuzz conservation + cross-channel replay).
- **Integration (Anvil, house node E2E):** 24/24 — happy path, waterfall, stale-close attack, mid-hand stall forfeit, checkpoint floor, refresh/restore, abandoned-channel reconciliation, WAL-loss recover_close.
- **Mainnet E2E:** open/close + dispute drill executed on Arbitrum One (see DEPLOYMENT.md).
- **`forge coverage`:** not obtainable — the suite requires `via_ir` and coverage instrumentation hits "stack too deep" even with `--ir-minimum`. Honest gap; line-coverage numbers unavailable. Test-to-code ratio ≈ 1.9:1 by lines.

## 7. Deployed addresses (Arbitrum One)

- ChannelManager **V3 (active)**: `0xe13DC056111906aA4d3659880EBDC67138c76DA7` (EXPOSURE_SINGLE_PPM 15000, EXPOSURE_TOTAL_PPM 100000, TIER_THRESHOLD 100k)
- ChannelManager V2 (retired): `0xacbfCf3523DDA59419a181Afb2e17A80BE453fEb`
- ChannelManager V1 (retired): `0x82698e4C14DC5D144bdA2a028aDe60d886681AFb`
- HouseBond: `0x37E41E1d1EE2F96cE0203af06d639448547ade84` (manager → V3; operator bond 100k RUSD each: `0xbc3A…3F29`, deployer)
- BlackjackRulesVerifier: `0xFdcCE2DE2F8A28cBBdBaB01131C3bab138Eb9293`
- RUSD: `0x260c7019E760763988843Bc5b873d749b5937469` · Pyth Entropy: `0x7698E925FfC29655576D0b361D75Af579e20AdAc`
