# RAIN Risk Markets — Arbitrum One Mainnet Deployment

**Date:** 2026-07-27 · **Chain:** Arbitrum One (42161) · **Deployer/Governance:** `0x74a435596b7C76d99320eF960FbF6e4750257aC2`

## Contract Addresses

| Contract | Address | Deploy Tx |
|---|---|---|
| MarketRegistry | `0x076149f4293549b78C909315B3465805922fDf70` | `0x230e35364f16b6c36a4f3fc20d20eeb6eb1bdfb175422610e6e2d701662118b9` |
| RiskVault | `0x9eDCB60620Ad062C4EbA71d5B457eA08fc1EadE8` | `0x2b7c852008cce967db1cf19b5bc730ba30ec180b4ba8bb212681959448eb4945` |
| FeeSplitter | `0x0dd5cE18cFdc7345f0caA212B94747c052EfA947` | `0x0cc14e085989c7c95ecf1add6f8604e42fb35cf2e5bf92cff99de6b01517fd38` |
| VRFAdapter | `0x2afFca69f8aa481afDCE3c15669225246dC2008B` | `0xcd846c122f822d0548a33ec943ea0104185c915a16861b7cfba79f90a01c9676` |
| RiskEngine | `0x16700BA3141Dd73ea0139B1e6663B6b40268d99d` | `0x752f9c20610479d2a73ef71fa886dabbe8fbab43c28fa66331b688a047b89758` |

## External Addresses (verified)

- USDC (native Arbitrum): `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` — decimals()==6, symbol "USDC" verified on-chain
- LINK: `0xf97f4df75117a78c1A5a0DBb814Af92458539FB4` — **NOTE:** the address given in the task brief (`0xf97f...45d02c95`) has no code on Arbitrum; the correct address above was taken from Chainlink's official VRF v2.5 supported-networks docs and verified (symbol "LINK").
- VRF Coordinator v2.5: `0x3C0Ca683b403E37668AE3DC4FB62F4B29B6f7a3e` (Chainlink docs)
- Key hash: `0x8472ba59cf7134dfe321f4d61a430c4857e8b19cdd5230b09952a92671c24409` (30 gwei lane)

## Wiring Txs

- `vault.setEngine`: `0x24b96bee53fca0a7fa6d31dc9bc693b81026506676ab65591d38068866e531a7`
- `splitter.setEngine`: `0x5eee2df8dec4bbf6b3c5303875590717b6f87df42a178182630e2111e57e3e3f`
- `vrf.setEngine`: `0x1c409e114a8fd3fcb4c6945c26f1ccb906b623c2e144bef31927cc7ee75009aa`
- 5× `listMarket` (even-money 1.975x, 4x, 10x, 36.5375x, ~100x @ EV 0.9875):
  `0xb0a88a4d…eebdab`, `0x3b41ae78…840120a`, `0x06f12eaf…7ec48d`, `0x9212ca49…dca230`, `0x61ee9913…44b6b7`

## Chainlink VRF v2.5 Subscription

- **Sub ID:** `89227813476920981096392676009477736066099074454582175463472230943458630375262`
- createSubscription tx: `0x07692ab24e16404fb31e5ded64ba63405e1faaeb09994daff25f948f98b44a56`
- Funded with **3.0 LINK** via `LINK.transferAndCall(coordinator, 3e18, abi.encode(subId))`: `0xa4844276a326a46828cc9333fb42a637f8563f66d803533ef2acf848f2db9f39` (chose LINK funding — VRFAdapter requests with `nativePayment:false`, so LINK is what actually gets billed)
- addConsumer(VRFAdapter): `0xfe30f10106c8b14ccb60d1b54a5c5a96b84f23b32e7e0431fd5b95f23c4ea733`

## ETH→LINK Swap

- LiFi (Kyberswap route): 0.014 ETH → 3.1257 LINK, tx `0x0d816df5c47e78b787989b9127f1b44498fd16db202d357d0bf79039db77aa86`
- (First send attempt failed on a transient "max fee < base fee" race; re-quoted and retried successfully.)

## Safety Config

- RiskVault TVL cap: **2,000 USDC** (`tvlCap = 2_000_000_000`, verified on-chain)
- All governance roles: deployer wallet

## Smoke Test (read-only, all ✅)

- `vault.totalAssets()` = 0; `vault.tvlCap()` = 2,000e6
- `registry.marketCount()` = 5
- `vault.engine()` = RiskEngine; `vrfAdapter.engine()` = RiskEngine; `vrfAdapter.subId()` = sub ID
- `coordinator.getSubscription(subId)` → balance 3.0 LINK, owner = deployer, consumers = [VRFAdapter]
- No USDC deposit test performed (wallet holds no USDC, per instructions)

## Frontend

- CONFIG updated in `rain-risk-markets/app/app.js` (chainId 42161 already set; 4 protocol addresses filled in)
- Pushed to rainsandbox-xyz repo (agentbase workflow `b086e87d-01e0-4c20-9b0b-3e73df8a0cd1`), commit `cce81e24`, redeployed → **live**
- Verified https://rainsandbox.xyz/app/app.js serves the real addresses; `/app/` already existed on the site (LAUNCH APP links unchanged)

## Balances After Deployment

- ETH: **0.02007** (well above the 0.012 floor)
- LINK in wallet: **0.1257** · LINK in VRF sub: **3.0**

## Deviations & Notes

1. **LINK address correction** (see above) — task brief's LINK address was invalid on Arbitrum; used official Chainlink-docs address.
2. **Arbiscan verification skipped** — no `ARBISCAN_API_KEY` (or ETHERSCAN key) in the environment. Contracts are unverified; can be done later with `forge verify-contract` once a key is available.
3. Used the existing `Deploy.s.sol` unmodified (it is fully env-driven): `USDC`, `VRF_COORDINATOR`, `VRF_KEY_HASH`, `VRF_SUB_ID`, `TVL_CAP=2000000000` passed via env. `--skip-simulation` used to save time; all txs confirmed on-chain.
4. Agentbase repo update done via `update-code.cjs`/`redeploy.cjs` (skill's supported path) rather than a manual git clone/push — same result, verified live.

---

# DEMO MODE (Play-Money) Deployment — v1.3.0

**Date:** 2026-07-27 · **Chain:** Arbitrum One (42161) · Deployer: `0x74a435596b7C76d99320eF960FbF6e4750257aC2`

## RUSD + Demo Protocol Stack

| Contract | Address |
|---|---|
| RUSD (RISK Dollar, 6 dec, 100M supply) | `0x260c7019E760763988843Bc5b873d749b5937469` |
| MarketRegistry (demo) | `0xADf3B1270961231ff925C8885843c597502A3d44` |
| RiskVault (demo, asset=RUSD, cap 100M) | `0xEcF918c851d9259DDaEc48d51D8363CBaDe3A18F` |
| FeeSplitter (demo) | `0x46F8478aeDbE189ffa85CE06BFF767c57b7d403c` |
| VRFAdapter (demo) | `0x17fb4Fc8778aF721D28846F64A391F7D7374d47f` |
| RiskEngine (demo) | `0x4fd901d0f18d738142F86A82AC1895D4B0C6EBa5` |

- Deployed via `script/DeployDemo.s.sol` (single broadcast: token + stack + wiring + 5 canonical markets + 10M RUSD house-bankroll deposit).
- Same 5 markets as mainnet stack (1.975×, 4×, 10×, 36.5375×, ~100×).
- VRF: reuses the existing v2.5 subscription (3 LINK). addConsumer(demo VRFAdapter): `0x05460cc52296d0c3133f7613f0743bede9e8b2209dcb654326c85fd185b6fa5a`. Sub now has 2 consumers (live + demo adapters).
- Sanity checks ✅: totalAssets = 10,000,000 RUSD; nextMarketId = 5; vault.engine & vrf.engine = demo RiskEngine; consumer registered.

## Faucet (REDEEM CODE)

- Faucet wallet: `0x488F62E6e11fA7DC70bea1A4D4D642aB2ba81D5B` (key in `faucet-wallet.json`, chmod 600). Funded: 0.004 ETH (`0x63aeaf18…23a1f6`) + 5,000,000 RUSD (`0xaf16aec6…0703bc`).
- Backend: `POST https://api.rainsandbox.xyz/faucet` `{address, code}` → sends 0.0003 ETH + 1,000 RUSD. Code `RAIN2026`, 24h per-address limit (in-memory), kill switch `FAUCET_ENABLED`.
- Backend env (via agentbase redeploy): FAUCET_PRIVATE_KEY, FAUCET_CODE=RAIN2026, FAUCET_ENABLED=1, ARB_RPC, RUSD_ADDRESS. CORS: `cors()` open (allows rainsandbox.xyz).
- Test claim ✅ to `0x7B61dc9eE9875Cde4d0D9b1deE3dDa6C92FAA79F`: ETH tx `0xa9293d20fefbe21346ce45d612d4d5dcb367cf7de1a16c6d6d8fc839169adf50`, RUSD tx `0x3260ec88d5e04ab8bc82e20b19cabefbadba7d92ae020ac63b028da5ab8de089`. On-chain confirmed (0.0003 ETH + 1000 RUSD). Repeat claim → 429; wrong code → 403. ✅

## Frontend v1.3.0

- DEMO/LIVE toggle (header, defaults to DEMO, persisted in localStorage `rainMode`).
- DEMO mode auto-creates an in-browser burner wallet (ethers.Wallet, obfuscated in localStorage `rainDemoBurnerV1`), signs via public RPC — no extension needed. Banner shows address + demo-wallet note.
- 🎁 REDEEM CODE button → POSTs to the faucet, refreshes balances.
- Market game names in list + position feed: Coin Flip / Even Money, 4× Multiplier, 10× Multiplier, Roulette — Single Number, Long Shot 100×.
- Deployed via agentbase (frontend commit `86a38dfc`, backend commit `6fe26323`), both `live`; verified `v1.3.0` served at rainsandbox.xyz/app/app.js.

## Balances After Demo Deployment

- Deployer ETH: **0.01590** · Deployer RUSD: 85,000,000 (100M − 10M vault − 5M faucet)
- Faucet: 0.0037 ETH + 4,999,000 RUSD (after test claim)

## User Flow

1. Open https://rainsandbox.xyz/app/ — DEMO mode is default; a demo wallet is created automatically in the browser.
2. Click **🎁 REDEEM CODE**, enter `RAIN2026` → receive 1,000 RUSD + 0.0003 ETH gas.
3. Trade any market or deposit into the vault — real on-chain txs, VRF settlement, play money.
4. Switch to **🔴 LIVE** for the real USDC stack (browser wallet / WalletConnect / Coinbase).

---

# ROULETTE — v1.7.0

**Date:** 2026-07-27 · New page https://rainsandbox.xyz/app/roulette.html (+ `roulette.js`), full European single-zero roulette on the DEMO stack (RUSD), shared burner wallet with the main app. Deployed commit `80aa0d8f`.

## New Markets on Demo MarketRegistry (`0xADf3B1270961231ff925C8885843c597502A3d44`)

All two-outcome (win/lose), EV = 987,499 ppm (≤ 98.75% spread floor):

| ID | Bet type | prob (ppm) | payoutX | tx |
|---|---|---|---|---|
| 5 | Even-money (red/black/even/odd/1-18/19-36), 18/37 | 486,486 | 2.029863× | `0xb2e11c97b0d16ee6853a631a9671cee98c5c30fe5d9d4a206f6dc261e4b47ed0` |
| 6 | Corner (4-number quad), 4/37 | 108,108 | 9.134384× | `0xd7c0b3c2caf8eb565797a24973f289515607577de998d0573ad5109ea74a8285` |
| 7 | Dozen / Column, 12/37 | 324,324 | 3.044794× | `0xc7654f5870694088e24eebde33e52d6b279c3364bae3586893dafb37192226e7` |

- Straight bets reuse existing market **#3** (prob 27,027 = 1/37, 36.5375×) — verified via `getMarket(3)`.
- All verified on-chain via `getMarket` (active, EV 987,499). `nextMarketId` = 8.
- Mapping recorded in `app/roulette.js` `MARKETS` config.
- Wheel presentation: win/loss decided on-chain by Chainlink VRF; displayed pocket derived client-side consistent with the verified outcome (covered number on WIN, non-covered on LOSS), seeded by settlement tx hash. Honest "ⓘ provably fair" note on page.
- Deployer ETH after registrations: **0.01589**.

## Roulette v1.9.0 (2026-07-27)

**Batch betting decision — NO periphery contract deployed.** RiskEngine.placePosition
attributes positions to `msg.sender` (`positions[id].trader = msg.sender`) and
settlement pays `p.trader` directly (also void refunds → `p.trader`). There is no
owner/onBehalf/recipient parameter. A `RouletteTable` periphery calling placePosition
would become the trader: all VRF payouts and timeout refunds would flow to the
periphery, requiring escrow bookkeeping + an extra claim tx per user (worse UX, new
custody risk) — clean attribution is impossible without modifying the core engine.
Instead the frontend now broadcasts all placePosition txs **in parallel** (Promise.all
fan-out with sequential explicit nonces, single approval, no inter-send waits).

Other v1.9.0 changes: RAIN logo drawn into wheel hub (circular clip, canvas);
wheel comes to a complete stop on the result (new "stopped" phase, zero residual
drift); persistent results sidebar (last 50 spins, localStorage, colored number
chips + MULTI chip, net win/loss per spin).

## Roulette v2.0.0 — Standalone RouletteTable (2026-07-27)

**One VRF draw per spin.** New self-contained contract `protocol/src/RouletteTable.sol`
(demo stack is single-slot onlyEngine, so the table has its own house bankroll and
replicates the fee waterfall by direct transfers).

| Item | Value |
|---|---|
| RouletteTable | `0x38a01415C7171608D96aD929cd67A63a593e1D01` |
| Deploy tx | `0x79936d58a42aa6840843d39964dee2b49b407002185f813170b354668e41894f` |
| addConsumer (VRF sub) | `0xff22e7fe4d1c6960e7e45088fa1a1473106746985d552e15f6cede9f1fb56835` |
| RUSD approve 5M | `0xafa9163281b50e4f3a1c5a04d683d8f34ca079ffd085192e4e401ba8db7f3b7f` |
| depositFloat 5,000,000 RUSD | `0x798c25ac09ca1f78dbaa73cd5c4a7e51ce12b07d2c9426abcbb8cd1eb3479728` |

- Mechanics: `placeBets(BetInput[])` (max 10, single transferFrom) → one Chainlink VRF
  v2.5 request → `number = word % 37` settles all bets atomically. Events:
  SpinRequested / SpinResult / BetSettled / SpinVoided.
- Economics mirror the engine: 1.25% spread (netStake = 98.75%), fee ppm split
  4_000 burn (1_500 protocol + 2_500 unattributed brand) / 1_700 rebate / 500 standby /
  6_300 residual → demo RiskVault (`0xEcF9…3A18F`). All fee sinks = deployer (as on the
  demo FeeSplitter). Multipliers identical to registry markets #3/#5/#6/#7 (verified
  via getMarket): straight 36.5375× · even-money 2.029863× · corner 9.134384× ·
  dozen/column 3.044794×.
- Fees held in escrow until fulfillment; timeout void (1h, mirrors engine default)
  refunds FULL gross stake, anyone can call `voidSpin`.
- Exposure rule: worst-case total payout over the 37 numbers ≤ 2% of free float
  (balance − lockedEscrow). Owner deposit/withdrawFloat can never touch escrow.
- **Corner mapping (canonical, mirrored in frontend):** param k∈0..21: c=k>>1,
  dr=k&1, anchor a=3c+dr+1, quad {a, a+1, a+3, a+4}. k=0→{1,2,4,5}, k=1→{2,3,5,6},
  …, k=21→{32,33,35,36}.
- Forge tests `test/RouletteTable.t.sol`: 12/12 green (classification of all
  types/params/numbers, corner mapping, payout math, worst-case exposure,
  multi-bet settlement + value conservation, zero-loses-outside, void refund,
  escrow-protected withdrawal, pause, bad-bet validation, conservation fuzz).
- Frontend v2.0.0 (commit `e36f4302`, agentbase redeploy → live): single placeBets tx,
  ball always lands on the real drawn number, per-bet breakdown from BetSettled
  events (JS covers() mirror as fallback), no MULTI ever; roulette-logic.js
  superseded (kept, not loaded). app/tests.js now verifies JS↔contract corner &
  covers parity (7/7 pass). Headless check: zero console errors, v2.0.0 served.
- Deployer ETH after all txs: **0.012840** (above 0.005 floor). Contract unverified
  on Arbiscan (no API key, same as prior deploys).

---

# BLACKJACK — v2.1.0

**Date:** 2026-07-27 · New page https://rainsandbox.xyz/app/blackjack.html (+ `blackjack.js`), single-player blackjack vs house on the DEMO stack (RUSD), standalone bankroll contract (RouletteTable pattern), shared burner wallet.

## Contract

| Contract | Address |
|---|---|
| Blackjack | `0x38579152b8c8CAA90719A4dA1732bC5644157Da5` |

- Deploy tx: see `protocol/broadcast/DeployBlackjack.s.sol/42161/run-latest.json`
- addConsumer to VRF sub: `0xdb5ae5dc047257ff0cf9519df22b232f7d6da6934c832c003695e78324807ee3` (sub now 4 consumers)
- Bankroll funded **5,000,000 RUSD**: approve `0xf0b7515504650e66b9bbbb61dd26e727454407f8655ec9df963feffc6eb44a4e`, depositFloat `0xa919b27b73151c7d203c5b92de3d5f0fad439751b91282d342a94144531808f5`

## Rules & Economics

- S17 (dealer stands on ALL 17s), BJ pays 3:2, double any two cards, split pairs ONCE (split aces: one card each, auto-stand), dealer peeks on A/10-value up-card, no insurance/surrender.
- **Continuous-shuffle (infinite shoe):** card i = `keccak256(seed, i) % 52`, dealt with replacement. The single Chainlink VRF word (seed) commits the ENTIRE card stream before any player action — provably fair; player actions only reveal predetermined cards. (Honest ⓘ note on the page. Demo caveat: post-fulfill the seed is readable on-chain; bounded by the 2% exposure cap, play-money only.)
- **Spread 1.25%** on every gross stake unit at bet time: at-risk `netStake = stake × 98.75%`. Win returns 2×net, BJ 2.5×net, push returns net. House edge = rules edge + spread.
- Fee waterfall on total gross volume at settlement (void-safe, escrowed until then): burn 4,000 ppm → deployer(burnSink), rebate 1,700 ppm, standby 500 ppm, residual 6,300 ppm → demo RiskVault. Total = 12,500 ppm.
- Exposure cap: worst-case payout (split + double both, win both = 8×unit-net) ≤ 2% of free float. Escrow accounting; owner depositFloat/withdrawFloat can never touch escrow.
- Timeouts: VRF > 1h → `voidHand` (anyone, FULL gross refund); player inactive > 30 min mid-hand → `forceStand` (anyone, settles per current cards).

## Tests

`protocol/test/Blackjack.t.sol` — 25 tests, all green (naturals 3:2 / BJ-push / dealer-BJ, S17 soft-17 stand + hit-to-17, win/push/lose payouts, bust-loses-first, double (escrow + payout + 2-card-only), split (pair-only, once-only, aces auto-stand), fee waterfall conservation on 1× and 2× gross, exposure cap, escrow-safe float withdrawal, VRF-void full refund, forceStand, pause, auth; fuzz: 256-run invariants over seeds×strategies — payout bounded, escrow always released, token conservation; fuzz dealer-final ∈ [17,26]). Full suite 60/60 green.

## Live Sanity (mainnet hand #0)

startHand 100 RUSD `0xce10dcd1…f43b9b` → VRF dealt [3♦,5♣ v 9♦+hole] → hit `(card 10)` → stand `0x620072b6…4442ef` → settled: player 18 v dealer 22 BUST, outcome WIN, payout 197.50 RUSD (2×net ✅), fees split on-chain (vault residual 0.63 ✅), lockedEscrow back to 0 ✅.

## Frontend v2.1.0

- `app/blackjack.html` + `app/blackjack.js`: dark felt table w/ RAIN logo watermark, CSS playing cards (unicode suits, court-face frames, deal slide-in + hole-card 3D flip, 420ms suspense stagger), chip betting (1/5/25/100/500) → DEAL, contextual HIT/STAND/DOUBLE/SPLIT (contract `availableActions` + balance check), split two-hand layout w/ active-hand glow, soft totals shown as e.g. 7/17, gold "BLACKJACK! 3:2 🃏" overlay + win confetti / loss / push fx, hands history sidebar (localStorage), balance bar + faucet + tx pill + RPC pool + shared burner wallet, mobile responsive.
- Approval is `stake × 4` up front so double/split never need a second approval.
- Nav: 🃏 BLACKJACK buttons added to index.html + roulette.html (patched from LIVE copies fetched after roulette v2.0.0 was deployed — no clobber; roulette.js untouched).
- Deployed via agentbase update-code (4 files) + redeploy → live; verified 200 + v2.1.0 served; headless chromium: zero console errors (local + live).

## Balances After Blackjack Deployment

- Deployer ETH: **0.01259** · Deployer RUSD: 80,000,119 (85M − 5M blackjack bankroll + hand-#0 flows)

---

# BLACKJACK V2 — v2.2.0 (Fee-on-Loss + S17 Verification)

**Date:** 2026-07-28 · Replaces Blackjack v1 (`0x3857…7Da5`, now PAUSED and drained).

## Issue 1 — "Dealer hit on 17" bug report: verdict

- Repo source of `Blackjack.sol` is S17-correct (`if (dt >= 17) break;` in `_dealerPlay`, stands on ALL 17s incl. soft).
- **Deployed v1 bytecode verified byte-identical to repo source** (compiled `deployedBytecode` vs `cast code`, 0 non-immutable diffs). The on-chain contract could NOT have hit on 17.
- Frontend audit: dealer totals computed by the same `totalOf` mirror; hole card hidden until settlement — a plausible source of the report is the dealer's **soft total transitioning** (e.g. A+6 displayed after more cards from an earlier draw sequence) or the reveal animation showing dealer draws that happened when dealer was <17 (e.g. soft 16 → draw → 17+). Audited all 10 v1 on-chain hands: **no hand shows a dealer card drawn at ≥17**. E.g. hand 9 dealer A? sequence [24,40,14,20] = 12,2,2,8 → totals 12→14→16→(draw)→hard 24? — all draws happened below 17. No contract or display bug found; rule enforced correctly.
- New forge tests added anyway: `test_dealerStandsOnHard17`, `test_dealerStandsOnSoft17` (A+6), `test_dealerStandsOnMultiCardSoft17` (A+2+4), `test_push17_fullRefund` (player 17 v dealer 17 = push, dealer takes no card).

## Issue 2 — Fee model redesign (V2): fees ONLY on losses

- **Old (v1):** 1.25% spread deducted from every gross stake upfront → win on 100 paid 197.50, push returned 98.75. Bad UX.
- **New (v2):** exact payouts — win **2× gross** (100 → 200), push **full refund**, blackjack **2.5× gross** (100 → 250). No haircut ever on wins/pushes.
- **Fee choice (documented):** 1.25% (12,500 ppm) of the **lost gross stake** (per losing hand; a doubled losing hand contributes 2 units) routed through the same waterfall sinks and ppm split as v1 / the demo FeeSplitter: burn 4,000 / rebate 1,700 / standby 500 / residual 6,300 → demo RiskVault. Fee is funded from house winnings, never from the player's payout. House edge = rules edge (~0.6% S17 infinite shoe); fee is a revenue split of losses, not a player levy.
- **Exposure cap recomputed:** worst case = split + double both + win both = 8 × **gross** stake (was 8 × net). Cap unchanged at ≤ 2% of free float → max stake exactly 12,500 RUSD on 5M float (test `test_exposureCap`).
- Escrow / voidHand (VRF >1h, full gross refund) / forceStand (30 min) identical to v1.

## Contract

| Item | Value |
|---|---|
| Blackjack V2 | `0x096382Af1E2B6CC5D900E22eF7DCe53eeF4F2808` |
| Deploy tx | `0x96bac6dca15b3cb7385f3f69c323b4f30cc669bff4785388120aaa6b02b32a67` |
| v1 setPaused(true) | `0xbafbb0458257a6f4b227271f432a191c4316524e91315f548069475ccabf7ec2` |
| v1 withdrawFloat (4,999,876.5625 RUSD free float) | `0x0c701c07a779f4979b56a8f4ccaa7919d361acfbf01b5bdbaa36988649864094` |
| approve 5M RUSD | `0x594cce6e080a1a9d0a913f95da92297a36f01121b02bc8cf48b9ee4176c87154` |
| depositFloat 5,000,000 RUSD | `0x5338613f6f9554f19a068a9c0d714da42bd896530880a9b07a041332ffb814f0` |
| addConsumer (VRF sub) | `0x6b0a92202983eb3e81518eaab8d82c6dc5d91c93b4f847ca7b1d76ee8a036b2f` |

Same deployer, VRF sub, RUSD, sinks (deployer ×3 + demo RiskVault) as v1.

## Tests

`test/Blackjack.t.sol` updated to v2 math + new tests: 32/32 green, **full repo suite 67/67 green**. New: hard-17 stand, soft-17 (A+6) stand, multi-card soft 17 stand, 17v17 push full refund w/ zero fees, exact 2×/2.5× no-fee payouts, fee-on-lost-gross (1× and doubled 2×), split mixed-outcome fee base, exposure cap at exactly 12,500.

## Live Sanity (V2 hands #0 and #1)

- Hand 0: start `0xac03e7ba…3c08c` → player 5+5=10 v dealer 10-up + hole ACE = dealer natural → LOSS. Contract bal +98.75 RUSD net (house kept loss minus 1.25 fee routed to sinks ✅).
- Hand 1: start `0x2832de4e…a0924` → player 9+4=13, hit `0x4abcad61…81e571` (card 10 → 23 BUST) → LOSS. Contract bal +98.75 again ✅. lockedEscrow 0 after both ✅. Fee-on-loss math confirmed on-chain.

## Roulette upfront-fee check (report only, unchanged)

**Yes — RouletteTable (`0x38a0…1D01`) has the same v1 UX problem:** 1.25% spread is taken from every gross bet upfront (`netStake = 98.75%`), so wins pay on net (e.g. even-money 100 → 202.99 instead of "clean" 202.99×gross… i.e. payouts are multiplier × net, and any refund path also nets). Same fee-on-loss redesign would apply. NOT changed per instructions.

## Frontend v2.2.0

- Base = live blackjack.js (verified identical to repo copy; `makeAddrCopyable()` click-to-copy preserved).
- New contract address, removed net-stake math from result display (net = payout − gross), PUSH overlay now "full stake returned / no fee on a push", ⓘ note: "no fee on wins or pushes; the 1.25% table fee is taken only from losing stakes".
- Deployed via agentbase update-code + redeploy on workflow `b086e87d-…cd1` → **live**; verified rainsandbox.xyz serves v2.2.0, new address, and copy feature.

## Balances After V2

- Deployer ETH: **0.01674** (floor 0.005 respected) · Blackjack V2 float: 5,000,197.50 RUSD (5M + two lost hands net of fees).
- v1 (`0x3857…7Da5`): paused, ~0 free float (only residual dust from spread rounding remains).

---

# BLACKJACK V3 — v3.0.0 (TRUSTLESS Per-Action Randomness, Pyth Entropy)

**Date:** 2026-07-28 · Replaces Blackjack V2 (`0x0963…F2808`, now PAUSED and drained).

## The V2 bug fixed

V2 committed the ENTIRE card stream (dealer hole card + all future draws) to a single
Chainlink VRF word stored in `seed` the moment the deal landed — any on-chain reader
(bot) could derive every hidden card and play perfectly. V3 makes every hidden card
**unknowable to anyone (house included)** at decision time.

## Oracle — Pyth Entropy (verified)

- Entropy contract (Arbitrum One): `0x7698E925FfC29655576D0b361D75Af579e20AdAc` — verified live on-chain (`getDefaultProvider`, `getFee` calls succeed).
- Default provider: `0x52DeaA1c84233F7bb8C8A45baeDE41091c616506` (returned by `getDefaultProvider()`; matches Pyth docs mainnet default).
- Fee at deploy time: ~0.000077–0.0000937 ETH per request (dynamic; contract reads `getFee(provider)` before every request).
- Flow: `requestWithCallback` → provider commit-reveal → `_entropyCallback(uint64,address,bytes32)` (guarded `msg.sender == entropy`). Latency observed: ~1–3 s.

## Design (per-action entropy)

- `startHand(stake)`: escrow gross, entropy #1 → callback deals **player 2 cards + dealer UP card only**. The hole card does not exist yet (dealer array length 1 on-chain — verified live).
- `hit` / `doubleDown` / `split`: escrow extra stake first (double/split), then fresh entropy; callback reveals the drawn card(s) (split: 2 cards from one word — both go to the player immediately, no decision in between).
- `stand` / hand complete / player natural: one **final** entropy → callback draws hole card + all dealer S17 draws from that word and settles. Safe: player has no further decisions. All-bust hands settle instantly with NO dealer cards.
- **Rule variant: ENHC (European no-hole-card)** — documented in contract natspec + frontend ⓘ. No dealer peek; a dealer natural revealed at settlement beats ALL non-natural hands **including doubled/split additional stakes** (player natural still pushes). Other rules unchanged: S17, BJ 3:2, double any two, split once (aces one card each), infinite shoe.
- Economics EXACTLY V2: win 2× gross, push full refund, BJ 2.5×; 1.25% of LOST gross → burn 4,000 / rebate 1,700 / standby 500 / residual 6,300 ppm → demo RiskVault; sinks = deployer ×3 + `0xEcF9…3A18F`.
- **Entropy fees paid by the contract** from its own ETH float (simpler UX than msg.value; players pay zero extra). `startHand` requires float ≥ 8× current fee so a hand can never strand for lack of fees; owner tops up by plain transfer, `withdrawEth` reclaims.
- Safety rails kept: 2%-free-float exposure cap (max stake 12,500 RUSD on 5M), lockedEscrow accounting, `voidHand` after 1h pending entropy (FULL gross refund incl. double/split escrow, works in ALL pending states incl. mid-action), `forceStand` after 30 min idle (stands remaining hands, requests final entropy on house-paid fee), pause, escrow-safe withdrawFloat, nonReentrant + CEI, entropy-only callback auth, stale-callback no-op after void.

## Contract & txs

| Item | Value |
|---|---|
| BlackjackV3 | `0x2E7747E9c4c18511A4F9e79C6bD38061e1443F7A` |
| Deploy tx | `0x9639a4f9e239c236e6bb0f83a7cfd589f09fe26d474a3a1b4df479ebfb418c71` |
| V2 setPaused(true) | `0x9c2e0440c5c084451278ab5c77bd099fb29930cb3d87e61940e82ca606d15b40` |
| V2 withdrawFloat (5,000,172.1875 RUSD) | `0x443dd93215946d2b23bd57351f1f60f6802adc7b3872cf7feadd85e34ed25f1d` |
| approve 5M RUSD | `0x29e700a95946e9886f5828de131c4484a8bb756a90954cce7379884514a5a000` |
| depositFloat 5,000,000 RUSD | `0xa1079fa407c386f36f37878c9d1211745c6514f8a8aea730b7650cfbd518f22f` |
| fund entropy fee float 0.003 ETH | `0xf8c03fb7e89bd7924d24f231cd132e19761a90ab63ce2cef75733ac6871f23fe` |

## Tests

New `test/BlackjackV3.t.sol` (MockEntropy commit/fulfill): 28 tests — deal reveals only 3 cards
(no hole in storage), async hit/double/split with pending-state guards, ENHC dealer-natural beats
doubled stake, S17 (soft/hard 17 stand, hits to 17), naturals 3:2 / BJ push, push full refund,
fee-on-loss waterfall (single + doubled + split-mixed conservation), exposure cap 12,500,
escrow-safe withdrawal, FeeFloatLow guard, entropy-timeout voids (deal + mid-double, full gross
refund, stale-callback no-op), forceStand, callback auth, owner ETH withdraw, 2 fuzz invariant
suites (256 runs: settlement, escrow release, payout bound, token conservation, dealer 17–26).
**Full repo suite: 97/97 green** (V1/V2 blackjack, roulette, protocol, fuzz all kept passing).

## Live hands (Arbitrum One, real Pyth Entropy)

- **Hand #0** (100 RUSD, with hit): start `0x43afde05…d4a761` → deal callback **~3.0 s** (player 4+10=14 v dealer 3-up, hole nonexistent) → hit `0x987aba17…8273db` → draw callback **~3.0 s** (drew 6 → 20) → stand `0x504f3aa3…fda333e` → final callback **~0.9 s** (dealer 3+A+Q+Q = 24 BUST) → **WIN +100 RUSD** paid, lockedEscrow 0 ✅.
- **Hand #1** (100 RUSD): start `0xfca10f19…82fe9` → deal **~1.6 s** (K+9=19 v 8-up) → stand `0xea270059…898cc` → final **~1.6 s** (dealer 8+8+8 = 24 BUST) → **WIN +100 RUSD** ✅.
- **Observed per-action latency: ~1–3 s** (well inside the 1–4 s target).
- **Cost per action:** gas ≈ 0.0000033–0.0000058 ETH (tx) + entropy fee ≈ 0.00008–0.000094 ETH (paid by house float). **Cost per hand:** ~0.00019 ETH house entropy fees + ~0.00001 ETH player gas for a stand-only hand; ~0.00028 ETH entropy + ~0.000015 ETH gas with one hit (≈ $1 total at $3.5k ETH). Contract fee float after 5 requests: 0.00258 ETH remaining of 0.003.

## Frontend v3.0.0

- `app/blackjack.js` + `blackjack.html`: new address + V3 ABI (state machine with DrawPending/FinalPending), per-action pending pill ("fresh entropy…") + card reveal on callback, facedown hole placeholder that only materializes at settlement (removed on all-bust), player-natural async resolution, ⓘ panel rewritten (trustless per-action Pyth Entropy + ENHC rule), footer "PER-ACTION PYTH ENTROPY".
- Deployed via agentbase update-code + redeploy (workflow `b086e87d-…cd1`) → **live**; verified https://rainsandbox.xyz/app/blackjack.js serves `v3.0.0` and `0x2E77…3f7a`.

## Balances after V3

- Deployer ETH: **0.01363** (floor 0.005 respected) · V3 RUSD float **4,999,800** (5M − two 100-RUSD player wins) · V3 ETH fee float **0.002582**.
- V2 (`0x0963…F2808`): paused, drained to dust.

## Residual risks / notes

- Pyth Entropy provider liveness: if the default provider stops revealing, hands stall — mitigated by 1h `voidHand` full-refund path (works in every pending state). Contract unverified on Arbiscan (no API key, consistent with all prior deploys).
- ENHC increases house edge slightly vs peek (~+0.11%); disclosed in UI.
- userRandomNumber is derived from prevrandao/timestamp — fine because Pyth's commit-reveal makes the provider contribution unpredictable regardless.

---

# RAIN CHANNELS — v4.0.0 (State-Channel Blackjack)

**Date:** 2026-07-28 · **Chain:** Arbitrum One (42161) · Implements `research/rain-channels-spec-v1.md` with all 9 founder decisions RESOLVED (see spec §9).

## Contracts (mainnet, deployed & wired)

| Contract | Address | Deploy Tx |
|---|---|---|
| ChannelManager | `0x82698e4C14DC5D144bdA2a028aDe60d886681AFb` | `0x7a88df721f177503da40faa109bfbb650bd154f7bba59cad76ba4bd2b75e2976` |
| HouseBond | `0x37E41E1d1EE2F96cE0203af06d639448547ade84` | `0x4f3cd867a7f0e9b423258e28b0e593ac8c2f2949bda200395a17cca6b205d502` |
| BlackjackRulesVerifier | `0xFdcCE2DE2F8A28cBBdBaB01131C3bab138Eb9293` | `0x5be5d4c64e446c3832af0ee6ccbd4ad6c9f15f6ec72c5cab5f3d66113b04e1dd` |

Wiring: bond.setManager `0x6cbd84…04c50`, setOperator(deployer) `0x6f9fa2…3eac38`, 100k RUSD bond posted `0x8ec754…d569b6`, 0.01 ETH entropy fee float `0x86aa18…ac7a0d`. Second operator (production node key `0xbc3Aca2Fb930D4Ca6a7691B40e0e077B12623F29`): setOperator `0xd6c118…841392`, 100k RUSD bond, 200k RUSD float + 0.01 ETH funded.

## Design (per founder decisions)
1. **Pyth Entropy sessionSeed at open — ON.** One `requestWithCallback` per channel; fee paid from ChannelManager ETH float; `voidOpen` full-refund after 1 h if the seed never lands.
2. **180 s player grace**, then house `forceClose` → after challenge window the open hand's gross is forfeited (fee taken on it, no extra fine). UI copy: "Disconnected? You have 3 minutes to reconnect and resume."
3. **House fault:** open hand voided in player's favor + compensation `max(2× open gross, 5 RUSD)` from HouseBond **+ equal penalty BURNED to burnSink** (never vault).
4. **Generic operators:** HouseBond registry (7-day delayed withdrawals, slashable incl. pending), `approvedOperators` map; foundation-run only for now.
5. **Auto checkpoints** by house node when |session P&L| ≥ min(500 RUSD, 5% channel size); checkpointed nonce is an on-chain dispute floor.
6. **Tiered challenge windows:** ≤ 10,000 RUSD (deposit+allocation) → 10 min; above → 24 h. One shortened (5 min) restart after a successful challenge.
7. **Statistical fairness:** transparency only (house node WAL keeps full signed history; no on-chain statistical slashing in v1).
8. **Fee waterfall at close:** 1.25 % of cumulative lost gross, split burn 4000 / rebate 1700 / standby 500 / residual 6300 ppm — executed on-chain in cooperative AND forced close.
9. **Game-agnostic ChannelManager:** pluggable `IRulesVerifier`; blackjack module = exact V3 rules (ENHC/S17, win 2×, push refund, BJ 2.5×) with `card_k = keccak(pRev_k, hRev_k, sessionSeed, channelId, k) % 52`, house-reveals-first off-chain ordering, hash-chain (N=4096) pre-commitment, on-chain reveal verification (`submitReveals`/`demandReveal`) and `proveBadTransition` fraud proof.

EIP-712 states (domain "RAIN ChannelManager" v1); burner **session keys** sign states but funds always pay out to `playerOwner` wallet (open authorized by wallet-key EIP-712 signature). Exposure caps: Σ allocations ≤ 2 % vault float, single ≤ 0.25 %; `8×maxUnitStake ≤ allocation` enforced (no-stall-profit sizing).

## House node (production)
- Deployed inside **rainsandbox-api** (agentbase backend, workflow `b086e87d-…cd1`) — WS + faucet share one service: `wss://api.rainsandbox.xyz` (WS), `POST /faucet`, `GET /health` (reports openChannels/operator). Bundled via esbuild (`services/house-node/dist/house-node.cjs` + `deploy/server.js`); commit `f2a810b5`.
- Env keys (Render env vars, never in code): `OPERATOR_PRIVATE_KEY` (0xbc3A…3F29), `HOUSE_SESSION_PRIVATE_KEY` (0xDC32…3563), `CHANNEL_MANAGER`, `DEFAULT_ALLOCATION=2000 RUSD`, plus existing faucet vars.
- Guarantees implemented: write-ahead fsync persistence BEFORE countersigning, house-reveals-first, 180 s grace timers → forceClose, auto-checkpointing, reconnect/resume replay.
- ⚠️ Caveat: `DATA_DIR=/tmp/rain-channels` on Render is **ephemeral** — a node restart loses session WAL (players can always forceClose on-chain from their own signed states; funds are never at risk, but the house would rely on checkpoints). Production-grade needs a persistent disk or DB.

## Tests
- Forge: **132/132** green (97 pre-existing + 35 new in `test/Channels.t.sol`: bond slash/withdraw-delay/comp-first, open/seed/voidOpen, exposure caps, coop close + waterfall exactness, conservation enforcement, stale-state challenge, checkpoint floor, player-forfeit, house-comp+burn, comp floor, tiered windows, reveal chains, demandReveal, bad-transition fraud proof both directions, fuzz conservation + cross-channel replay). All 97 pre-existing tests stay green (via_ir enabled).
- Integration (Anvil, `services/house-node/test/integration.ts`): **14/14** — happy path 8 hands (hit/double/split-capable), waterfall verification, stale-close attack beaten by newer state, player-stall forfeit with exact payout math, checkpoint-floor rejection.

## E2E on Arbitrum One mainnet (real Pyth Entropy)
- **Happy path** (`test/e2e-mainnet.ts`): open 5.3 s (tx `0x5fe6265b…5502f8`, incl. entropy seed ~4.7 s) → 3 hands with hits + auto-checkpoints (txs by node) → cooperative close 4.6 s, gas 105,592 (tx `0x23518fbb…496d3`). **Per-action latency: avg 39 ms, max 49 ms** (local WS). Session P&L −150 RUSD settled exactly.
- **Forced-close dispute drill**: channel `0xf34576b9…`, played deal+2 hits, player `forceClose` from nonce 3 (tx `0x66036cd2…968674`, gas 197,873) → waited real 10-min window → `finalizeClose` (tx `0x08dbd039…02d9df`, gas 89,179) paid per last dual-signed state.
- **Production smoke over wss://api.rainsandbox.xyz**: open 11.3 s, 3 hands, **per-action avg 313 ms / max 420 ms** (internet RTT), close 5.0 s (tx `0xb7d35b5f…54563`), payout exact (+20 RUSD net).
- **Cost per session:** open ≈ 470k gas + Pyth fee (~0.00009 ETH, paid by mgr float) + close ≈ 105k gas ≈ **$0.10–0.15 total at typical Arbitrum prices — zero marginal cost per hand** (vs V3's ~0.0002–0.0003 ETH per hand).

## Frontend v4.0.0
- `app/channels-sdk.js` (new): burner session key, browser hash-chain, EIP-712 signing/verification, WS protocol with house-reveal verification, engine mirror, reconnect/resume, forceClose/challenge/finalize dispute helpers.
- `app/blackjack.js/html` v4.0.0: **⚡ CHANNEL mode default** (open session → instant hands → close & settle) with session bar (state + live session P&L), positive disconnect copy, rewritten ⓘ panel (commit-reveal, 3-min grace, house-bond compensation, fee-at-close); **⛓ PYTH toggle** keeps the entire V3 per-action flow as fallback. Deployed via agentbase → live; verified `v4.0.0` + SDK served at rainsandbox.xyz.

## Balances after deployment
- Deployer ETH **0.2591** · Deployer RUSD 74,600,194 (after 100k+100k bonds, 200k operator float, session flows)
- Operator node wallet 0xbc3A…3F29: 0.00999 ETH + ~200k RUSD · ChannelManager ETH float 0.00977 (~100 session seeds)
- V3 blackjack (0x2E77…3f7a) untouched & live as fallback.

## Honest caveats / remaining for production-grade
- **Unaudited** (~1,600 new SLoC; spec §7.3 estimates $60–150k audit). proveBadReveal is subsumed by submitReveals chain-verification on-chain; off-chain bad reveals are simply rejected + dispute path.
- Single house-node instance on Render starter (no hot-hot HA, ephemeral WAL, no HSM). Watchtower not built.
- Challenge/finalize automation for the HOUSE side of expired grace closes is timer-based in-process only (lost on restart until channels are re-loaded from WAL).
- 24 h tier window < Arbitrum force-inclusion delay concern only matters for real-value stakes (spec §4.4).
- Contracts unverified on Arbiscan (still no API key, consistent with all prior deploys).

---

# RAIN CHANNELS — v4.0.1 (2026-07-28, funds-recovery + session persistence)

## Incident: 1,500 RUSD abandoned in escrow
Player `0xe6baFC62c0FbBAbb8067d8da4F31F7395Fa512Da` opened 3 × 500 RUSD channels and never closed them — the v4.0.0 frontend created the burner session key + SDK chainSecret in memory only, so any page refresh made resume impossible and the escrow was abandoned.

**Recovery executed (all 3 + 1 more found in sweep):** the house WAL held the latest dual-signed states; recovered via operator-submitted `cooperativeClose` (instant exact payout, no challenge window needed since states were idle/dual-signed):
| Channel | Player payout | P&L | Tx |
|---|---|---|---|
| `0x94795018…` | 575 RUSD | +75 | `0x4dbd0511…642e837f` |
| `0xea1c98b8…` | 425 RUSD | −75 | `0xc876c5ff…fd461fb7` |
| `0xffd0e13c…` | 550 RUSD | +50 | `0xdd0da7d3…379694d84` |
| `0xc1ef2525…` (4th abandoned, found in sweep) | 850 RUSD | +350 | `0x90dca4a0…5f9e60e8` |
All verified on-chain: RUSD Transfer CM→player + CooperativeClosed events; channels now status Closed. Total returned to player: **2,400 RUSD**.

## Fixes deployed
**Frontend v4.0.1** (`app/blackjack.js`, `app/channels-sdk.js`):
- SDK `snapshot()`/`restore()` — full session material (burner session key, chainSecret, sessionSeed, houseChainRoot, hReveals, latest dual-signed state + sigs, game).
- `localStorage` persistence (`rainChSessionV1`) written at open and after every state advance; cleared on cooperative close. Burner-key-in-localStorage matches the existing `rainDemoBurnerV1` trust model (play-money demo).
- Page load auto-resume: reconnect + hello/resume from persisted snapshot; if the house answers "unknown channel" (WAL loss) but the channel is Active on-chain, the client submits `recover_close` with its dual-signed idle state.

**House node** (`services/house-node/src/server.ts`, bundled to `dist/house-node.cjs`, deployed in rainsandbox-api):
- **Abandoned-channel reconciliation**: boot-time sweep + 5-min interval — idle (phase IDLE, no pending) channels with a disconnected player > 30 min (`ABANDON_MS`) are settled by operator `cooperativeClose` from the latest dual-signed WAL state (exact payout, immediate). Open-hand stalls remain on the grace→forceClose path.
- `POST /admin/reconcile` (header `x-admin-secret`, env `ADMIN_SECRET`) — force-sweep on demand; returns per-channel report.
- `scheduleFinalize`: after any house forceClose, polls closeDeadline and auto-`finalizeClose` (previously finalize was manual/lost on restart).
- `recover_close` WS message: player-driven WAL-loss settlement — node re-verifies BOTH signatures against the on-chain channel record (its own houseSessionKey), checks conservation + idle phase, then `cooperativeClose`. Trustless for the house: only states it previously countersigned can settle.

## Tests
- Integration (Anvil): **24/24** — prior 14 + refresh simulation (snapshot → socket drop → new client restores, plays, closes exact), abandoned-channel reconciliation (force sweep pays exact balance, channel Closed on-chain), WAL-loss recovery (`recover_close` pays exact dual-signed balance).
- Forge: **132/132** still green (no contract changes).

## Honest caveats
- A 4th channel `0xdd64aa43…` (500 RUSD deposit) was opened by the same player at 18:12 UTC on the OLD v4.0.0 frontend, during this maintenance window — the backend redeploys wiped its `/tmp` WAL. It is Active on-chain with **no house-side dual-signed state available**, so the house cannot close it. `cooperativeClose`/`forceClose`/`checkpoint` all require dual signatures; `voidOpen` only applies in SeedPending. **Only the player can recover it** (their open tab still holds the signed state → SDK `forceClose`, or replaying via `recover_close` if they have the state). If the player's tab is gone, the 500 RUSD deposit + 2,000 RUSD allocation remain locked in the contract indefinitely — this is exactly the failure mode v4.0.1 persistence eliminates going forward.
- `DATA_DIR=/tmp/rain-channels` is still ephemeral on Render: node restarts lose WAL. v4.0.1's client-side persistence + `recover_close` now covers idle states, but an open-hand state at restart still relies on the player's snapshot. Production-grade needs a persistent disk/DB.
- Reconciliation `cooperativeClose` is chosen over `forceClose` for abandoned idle channels (instant, exact, no window). Force path is reserved for open-hand stalls.

---

# RAIN CHANNELS — v4.1.0 (2026-07-28, ChannelManager V2 migration + durable checkpoints + audit prep)

## ChannelManager V2 (reclaimStale) — deployed & live
- **V2:** `0xacbfCf3523DDA59419a181Afb2e17A80BE453fEb` (Arbitrum One, tx `0x65723a95…824169`, script `DeployChannelManagerV2.s.sol`). Adds `reclaimStale(channelId)`: permissionless stale-channel escrow recovery after **30 days of zero on-chain activity**, settling conservatively at the last on-chain checkpoint (or opening balances if none). This closes the "both WAL and player snapshot lost → escrow locked forever" failure mode permanently.
- Wiring (same run): `HouseBond.setManager(V2)` ✓ (verified on-chain), both operators approved ✓, 0.005 ETH entropy fee float ✓. Bonds untouched (2×100k RUSD still slashable).
- **Frontend v4.1.0** deployed: `CH.manager` → V2 (EIP-55 validated with ethers `getAddress`), verified served at rainsandbox.xyz. **Backend** redeployed with `CHANNEL_MANAGER=V2`; health OK at api.rainsandbox.xyz/health.
- **V1 (`0x82698e4C…1AFb`) drained & retired:** all 8 channels ever opened checked on-chain — 7 Closed, 1 stuck (below). No open escrow besides the stuck channel.

## Stuck V1 channel 0xdd64aa43… — final resolution
- Channel `0xdd64aa438f8d7a1b1dd8c3ec9b65fdaa7cbe7a2536d9d144b9258d68d105b720`: Active on-chain, 500 RUSD player deposit + 2,000 RUSD house allocation, **no dual-signed state exists on either side** (WAL wiped mid-maintenance; player snapshot pre-dates v4.0.1 persistence).
- Every V1 path re-audited: `cooperativeClose`/`forceClose`/`checkpoint` all require dual signatures (none exist); `voidOpen` requires SeedPending (channel is Active, seed landed); `demandReveal`/`submitReveals` only move reveal cursors, never settle; `proveBadTransition` requires a dual-signed prev; no owner/operator escape hatch exists in V1 (by design). **The 2,500 RUSD is locked in V1 permanently.** V1 has no reclaimStale — that is exactly why V2 exists.
- **Goodwill refund executed:** 500 RUSD sent from deployer treasury to player `0xe6baFC62c0FbBAbb8067d8da4F31F7395Fa512Da` — tx `0x89ff39e8d9c09511f0a41141431afccccb006b6feb12d7b6a18bb36b9fa5c19d` (status 1). Player is made whole on their deposit; the 2,000 house allocation is a treasury write-off inside V1.

## House-node durability (v4.1.0)
Bar: a backend redeploy mid-session must never destroy the only copy of a signed state. Three layers now:
1. **Per-hand on-chain checkpoints** (default ON, `CHECKPOINT_EVERY_HAND=0` to disable): every completed hand's dual-signed state anchored via `checkpoint()`. Measured **~130k gas** on Arbitrum (≈$0.01–0.02) — cheap enough to run always. Checkpointed nonce is the settlement floor for reclaimStale/forceClose.
2. **Checkpoint-on-disconnect:** idle dual-signed state anchored when a player's socket drops.
3. **SIGTERM/SIGINT flush:** before the process exits (Render redeploy sends SIGTERM, ~30 s grace) every open channel's latest un-checkpointed state is checkpointed.
Legacy ±500 RUSD threshold retained as fallback when per-hand mode is disabled. Integration tests updated (adversarial stale-state sections explicitly disable per-hand checkpoints so stale states remain closable): **24/24 green**. Forge: **140/140 green**.

## Audit prep
- **Sourcify exact_match verification (chain 42161):** ChannelManager V2 ✓, ChannelManager V1 (from commit `e7da239` source) ✓, HouseBond ✓, BlackjackRulesVerifier ✓. (Arbiscan API verification still blocked — no ETHERSCAN_API_KEY; Sourcify links render on Arbiscan/Blockscout regardless.)
- **`research/audit-scope.md`** written: contracts + SLoC, 10 invariants, threat model, accepted risks, addresses, coverage summary.
- `forge coverage` unobtainable: via_ir + coverage instrumentation → "stack too deep" even with `--ir-minimum`. Documented honestly in audit scope.
- No TODO/FIXME markers in channels contracts.

## Honest caveats
- V1 keeps 2,500 RUSD locked forever (documented above; player compensated). Do not re-point anything at V1.
- Per-hand checkpoints put hand-level P&L cadence on-chain (privacy = transparency tradeoff, fine for demo).
- Checkpoint TXs are house-node-signed; if the operator wallet runs dry of ETH the node degrades to WAL-only durability (log alerts on checkpoint failure).
- Coverage percentages unavailable (tooling), tests-to-code ≈1.9:1 by lines.
