# Audit report

> Adversarial security review of PvP Pad v3, already DEPLOYED and IMMUTABLE on Robinhood Chain (4663). This job changes no contract and deploys nothing; produce a findings report. No RPC, fork or broadcast are available here: static review plus the existing forge suite and new adversarial tests to prove findings.
>
> REPO: github.com/identity-md-launches/launch-1764-pvp-pad-v3-robinhood @c3a2fac8312fac1dabbd8deca78a987e180523c8. Confirm the deployed bytecode matches the verified source at these addresses on robin.etherscan.io: PvPPad 0xf425c5130208847f2e8422e35bcd37dcb7540bc5, PadGame template 0x82c032a933bf351c3b14d356b694e63f3cbb51eb, PadToken template 0x6870b7777e30a3f152a5bb6476d1152211a017f0, PadLiquidity 0x3ebbe5148eb9a2c45c5d2206dd9b8d9bf7ef7700, KingOfThePad 0xafb61b177e7522c748f9b3e138843a39ac189b37. The pad is still closed (openPad not yet called), so the central question is: is it safe to open?
>
> CONTEXT: v3 fixes the v2 external audit (job 5c4ec689, report api.imd.fun/jobs/5c4ec689-085b-4d72-8807-668b284d3222/report.md). docs/AUDIT.md maps each finding to its fix. Robinhood Chain runs Arbitrum's stack at about 10 L2 blocks/s; RNG uses ArbSys (0x64). Uniswap v4 PoolManager 0x8366a39cc670b4001a1121b8f6a443a643e40951.
>
> PRIORITIES (fund safety first):
> - Can anyone drain, strand or permanently lock ETH or tokens? Above all graduation: any input or state that makes graduate() revert permanently on the REAL PoolManager would lock a game forever.
> - King settlement, rewritten twice: the exttload unlock check, the EIP-150 gas precondition, settleCrown's gas-bounded walk with the 16-pool floor, broken-pool flagging across blocks, and the reign stamps (reignKing, settledReign, AUDIT.md #29). Can a bidder still take fees from the outgoing king, starve or steer the walk, brick takeCrown, or make a broken pool pay the wrong reign? Check fees paid in kind cannot fail or be redirected.
> - Resolver tips (90/7/2/1): tip accounting, double-claim, tips routed through upkeepFor to the right caller, expiry still penalised, ETH conservation.
> - Draw: binding commit, deterministic fallback seed, the bounty paid exactly once; re-examine disputed finding 779d74d2 and say if you agree.
> - Upkeep gas caps: can any step still make enter() or roll() revert, or be skipped forever so a game never finalizes or graduates?
> - Carry pulled at finalize under fundedReleaseCap(): can it be drained, double-pulled or made profitable for a sybil?
> - Everything new since v2: reentrancy, CEI, access control, invariants.
> Do NOT simply accept docs/AUDIT.md: independently re-test its accepted risks and say where you disagree.
>
> DELIVER: findings ranked by severity (critical/high/medium/low/info). Each finding: impact, exact file:line, a failing forge test or concrete step-by-step scenario, whether it is exploitable before or only after openPad, and a recommended fix (the contracts cannot be patched without a v4 redeploy). Add proof tests under test/ where possible. End with a clear verdict: is it safe to call openPad, or should a redeploy fix listed issues first. Note this remains a swarm review; an independent human audit and legal review are still recommended before a public launch.

| | |
|---|---|
| Repository | https://github.com/identity-md-launches/launch-1764-pvp-pad-v3-robinhood.git |
| Commit | `c3a2fac8312fac1dabbd8deca78a987e180523c8` |
| Job | `ea823664-6ab6-4f1a-9702-1bd102eca1e6` |
| Judged | 2026-10-15 08:50 UTC |
| Findings | 1 medium · 4 low · 6 info |

Four agents audited the code as it is at `c3a2fac`, each in one area (math, permissions, economics, control flow),
and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the repository was changed or deployed.

## Findings

### 1. Medium: King's 50% games share is diverted to carry by ten one-entry launches: the last-10 window is by entry order, not liveness

`src/PvPPad.sol:190`

```
        uint256 from = n > KING_MAX_GAMES ? n - KING_MAX_GAMES : 0;
```

fundGames() only looks at the last KING_MAX_GAMES (10) ids of `entered`, and a launch joins that list on its first 0.001 ETH entry. launch() is free and unlimited once genesis has graduated. An attacker therefore fills the whole window with ten dead one-entry launches for 0.01 ETH plus gas before (or right after) a bid; no honest live game is examined, count == 0 and the entire games share of the bid is parked in carryPot instead of funding the live game it was meant for. Carry then only drips out at finalization under the 15%-of-raise cap, so the honest players of the live game lose that share (not stolen, mis-routed). Each honest first entry re-enters the window, so the attacker must keep ten dead launches ahead of it; cost 0.01 ETH per refresh. Merged from audit_economics; reproduced. Exploitable only after openPad and after genesis graduates (king open). Recommended fix (v4 redeploy): select the king window by liveness (walk `entered` backwards collecting up to 10 games whose isLiveForKing() is true, bounded), or only push a launch to `entered` once it has KING_MIN_IMPS paid entries. Operational mitigation now: monitor dead launches; a bidder can also wait until the honest game is in the window.

**Reproduction**

test/scratch/Judge.t.sol::test_kingShareDivertedToCarryByTenDeadLaunches (passes = demonstrates) on the project's real-v4 BaseTest: graduatePvp(); launch 'Honest' H and enter it from five wallets (H.isLiveForKing() == true); then ten times pad.launch() + one 0.001 ETH enter() from a fresh wallet; players[9] calls king.takeCrown{value: 0.1 ether}(). Expected (docs/AUDIT.md #13): H.fundedPot() == 0.05 ether, carryPot unchanged. Actual: H.fundedPot() == 0 and pad.carryPot() grew by exactly 0.05 ether. Control test test_control_honestLiveGameReceivesKingShare without the ten dead launches: H.fundedPot() == 0.05 ether, carry delta 0.

### 2. Low: Reign stamp pays every fee of an unreached pool to the outgoing king, including fees earned after the crown changed; the outgoing king can steer which pools go unreached

`src/PadLiquidity.sol:264`

```
        address payee = settled < reign ? reignKing[settled] : k;
```

The stamp is per pool, not per fee. A pool the pre-crown walk did not reach (or a broken pool) keeps settledReign below `reign`, and its NEXT collection pays reignKing[stamp] the whole accrued amount, including all trading fees earned under the new king until someone collects it. Two aggravations: the walk starts at the public distributeCursor, so the outgoing king can call distribute(1) before an expected bid to choose which pools fall outside a minimum-gas walk; and the bid's trailing upkeep only runs distribute(2), so on a quiet pad the remaining pools stay stamped to the old reign for a long time. docs/AUDIT.md #29 accepts this as an imprecision; re-tested, the transfer is real and old-king influenceable, so it is reported as low, not as a note. Not exploitable for the bidder, and the walk cannot be starved below 16 pools or bricked by an outsider (re-verified: collectPool cannot be made to revert by outsiders, ETH push falls back to claimable, token transfer has no hooks). Merged from audit_math and audit_economics (same mechanism). Exploitable only after openPad, after at least two crowns, with more pools than the bid's gas settles (about 40 at the minimum stipend). Fix (v4): snapshot the position's feeGrowthInside for unreached pools at settleCrown so the next collection splits pre- and post-crown fees, or require a bid's gas to cover every pool. Operational now: a new king should call distribute(16) in a loop right after being crowned until every pool's settledReign == reign.

**Reproduction**

test/scratch/Judge.t.sol::test_unreachedPoolPaysPostCrownFeesToOldKing (passes = demonstrates) on the real PoolManager: graduatePvp(); add 59 more real pools; oldKing takeCrown{0.01 ETH}; warp to challengeOpensAt; donate 0.001 ETH fees to all 60 pools; bidder takeCrown{value: 0.011 ether, gas: 5_800_000} -> reign == 1, 19 pools still have settledReign == 0 after the bid's trailing upkeep; donate 0.05 ETH fees to one of those pools AFTER the crown; anyone calls retryPool(idx). Expected: post-crown fees belong to the new king. Actual: oldKing receives 50,999,999,999,999,999 wei (0.001 pre-crown + the whole 0.05 post-crown), bidder receives 0. Logs: 'pools unreached by the minimum-gas bid: 19', 'post-crown fees paid to OLD king: 50999999999999999', 'post-crown fees paid to NEW king: 0'.

### 3. Low: First crown closes no reign: every LP fee accrued before any king is captured by whoever pays the 0.01 ETH minimum bid, and that bid is refunded on dethronement

`src/KingOfThePad.sol:66`

```
        if (previous == address(0)) {
```

The first-bid branch runs no settleCrown and _distribute returns while no king exists (PadLiquidity.sol:224), so all fees accrued on the genesis pool (and on any pool graduated before the first crown) between graduation and the first bid belong to the first king, who collects them in the same transaction through the bid's trailing upkeep or a distribute(16) call. The price is fixed at MIN_BID = 0.01 ETH, and that bid is not even spent: KingOfThePad refunds `previousBid` in full on the next crown (the 0.01 ETH is split 50/25/25 at bid time and repaid out of the next bidder's value), so the first king's only cost is the opportunity cost of 0.01 ETH for one hour. docs/AUDIT.md #29 states this as intended; disagreement: the captured amount is unbounded in time and the pad can only be opened once, so the window should be closed operationally. Merged from audit_permissions; reproduced by the suite's own test. Exploitable only after openPad and after PVP graduates. Fix (v4): treat the pre-crown period as a reign owed to the protocol (route reign-0-before-any-king collections to the burn bucket or carry) or require the first bid to be at least the ETH fees already collectable. Mitigation now: the owner takes the first crown immediately after genesis graduation so the pre-crown window is seconds, not days.

**Reproduction**

Existing test test/CrownSettlement.t.sol::test_firstCrownOpensNoReignAndPaysPreCrownFeesToTheFirstKing: donate 0.05 ETH fees to the PVP pool with no king, distribute(16) leaves ethFees == 0 (fees stay in the position); players[8] takeCrown{0.01 ether}; distribute(16): players[8] balance rises by >= 0.05 ether - 0.01 ether in the same block and reign stays 0. Replace the 0.05 ETH donation by any amount of real swap fees accrued over the pre-crown period to see the unbounded capture. The refund of the 0.01 ETH on the next crown is KingOfThePad.sol:87-91 (previousBid pushed to previous).

### 4. Low: Carry can be pulled by a sybil-filled genesis game at about 2% loss, not the 4%+ floor docs/AUDIT.md #13 claims: the genesis burn share is credited to the roll pot and comes back to the sybil

`src/PadGame.sol:261`

```
        if (genesis) potEth += burnEth;
```

docs/AUDIT.md #13 argues a sybil that fills a launch to extract funded ETH loses at least 4% of the curve ETH plus the 2% burn share and gas. On the genesis game (and every restarted genesis) the 2% burn share is added to the pot instead of burned, so the roll-earned pot is 9% of rolls (measured: 180,000,000,000,000 wei per 0.002 ETH roll) and is paid back to the sybil's own top-half Imps; the 1% tip returns when the sybil resolves its own rolls with a second wallet, and the finalizer fee goes to the sybil finalizer. The irrecoverable cost is only the share of curve ETH a full dump cannot pull out of the pool (about 17% of e at the 4 ETH sell-out) minus the 15% release, i.e. about 2% of e plus entry fees and gas, while up to min(3 x roll pot, 0.6 ETH) leaves the carry pot. Still a loss, so griefing with leverage (about 5x the attacker's loss leaves carry) rather than profit; honest players lose carry that would have been released to real games. Only after openPad and only while the carry pot holds ETH (donations, returned funded pots, king shares diverted by the window finding). Merged from audit_flow; arithmetic re-derived and the 9% genesis roll pot plus the carry pull reproduced. Fix (v4): exclude the genesis burn share from the roll-earned pot used by _releaseCap, or lower FUNDED_RELEASE_CURVE_BPS to about 1_000. Operational: keep the carry pot small and watch genesis games that end with exactly five living Imps.

**Reproduction**

test/scratch/Judge.t.sol::test_genesisBurnShareLiftsReleaseCap (passes = demonstrates): openPad(); fundCarry 1 ETH; five wallets enter genesis and roll every 61 s for 49 rounds; before finalization pvp.rollPot() == rolls x 0.002 ether x 9 / 100 (not 7/100) and fundedReleaseCap() == min(3 x rollPot, 15% x ethRaised); after finalize with livingAtEnd == 5 the carry pot dropped by 66,149,999,999,999,999 wei (15% of the 0.441 ETH raised). Full-scale arithmetic at the 4 ETH sell-out: 45 wallets, about 2,223 rolls = 4.446 ETH; ethRaised 4.0 ETH; roll pot 0.400 ETH; tips 0.044 ETH; release 0.6 ETH; dump of 800M tokens into the 1% pool returns 4 x 0.99 x 800/(160 + 0.99 x 800) = 3.327 ETH; spent 4.491 ETH, recovered about 4.371 ETH, net loss about 0.12 ETH while 0.6 ETH left the carry pot. Expected per AUDIT.md: net loss >= 0.16 ETH plus burn.

### 5. Low: Pad upkeep rotation can be starved by cheap dead launches: the newest-entered slot is attacker-refreshable and the round-robin slot reaches an honest game once per n-1 pad actions

`src/PvPPad.sol:238`

```
            _tend(launches[entered[n - 1] - 1].game, caller);
```

_upkeep always tends the newest entered launch and one older entered launch chosen round-robin over the unbounded `entered` list, which anyone grows for 0.001 ETH per launch. After k dead launches, the automatic draw / resolve / finalize / graduate step that every enter(), roll(), launch() and takeCrown() on the pad was meant to give an honest game arrives once every n-1 pad actions, and the newest-entered slot is spent on the attacker's latest dead launch. Nothing is lost: draw(), resolvePending(), poke(), finalize() and graduate() stay permissionless on the game itself with full caller gas, and resolver tips still reward direct callers; but docs/AUDIT.md #3 and the README rely on pad-driven upkeep for abandoned games (expired-roll resolution, finalization, the carry pull, graduation). Merged from audit_economics; reproduced. Only after openPad. Fix (v4): have upkeepFor(caller) tend the calling game first, or keep a small queue of games with pending lifecycle work instead of the entry-ordered list. Operational: run a keeper that calls poke() on every game with pending work.

**Reproduction**

test/scratch/Judge.t.sol::test_padUpkeepReachesHonestGameOncePerCycle (passes = demonstrates): graduatePvp(); launch honest game H with five entries; 30 dead launches entered once each (0.03 ETH); warp to H.signupEnd(); repeatedly call pad.upkeep() with 4 L2 blocks between calls. Measured: 32 entered launches; H's draw committed on the 3rd call; exactly 31 (= n-1) further pad.upkeep() calls elapse before H is tended again. Expected by docs/AUDIT.md #3: pad actions tend pending lifecycle work promptly. Each extra dead entry lengthens the cycle by one.

### 6. Info: Disputed finding 779d74d2 (fallback seed chosen by the first draw caller): agree it is low and needs no redeploy; an independent seed-capture keeper is a pre-open requirement

`src/PadGame.sol:193`

```
                drawSeed = keccak256(abi.encode(address(this), drawCommitBlock, entrants.length));
```

Re-examined. The first post-signup draw() caller fixes drawCommitBlock, and the fallback seed is fully determined by (game, commitBlock, entryCount), so that caller can precompute the fallback seating for candidate blocks and then hope nobody captures the hash seed for 258 L2 blocks (about 26 s at 10 blocks/s). Agreement with docs/AUDIT.md #30 and all three specialists: the attack needs 201+ paid entries (below that the seed only orders seats, the last tie-break key), the attacker must be the first committer, and any draw(1)/poke() by anyone (including the victims) in blocks commit+4..commit+257 takes the hash path and earns the first-entry bounty; the hash path is itself sequencer-influenceable, so no commit-time input is better. One nuance the dispute text overstates: pad upkeep tends only the newest entered game and one round-robin game, so a game's seed capture cannot be assumed to ride on unrelated pad traffic (see the upkeep starvation finding); it relies on direct draw()/poke() calls. Verdict: low, disclosed, not a blocker; run an independent seed-capture keeper from open. A v4 could remove the committer's choice (two-step capture of blockhash(c+1), or VRF).

**Reproduction**

State: launch with N >= 201 paid entries, signup over, no draw yet. Steps: attacker precomputes seating(keccak256(abi.encode(game, c, N))) for candidate blocks c and sends the first draw(1) at the best c; nobody touches the game for 258 L2 blocks; attacker calls draw(32) repeatedly. Actual: seating equals the precomputed fallback (mechanism covered by test/Raffle.t.sol test_lapsedCommitFallsBackOnceAndNeverRecommits). Any single draw/poke by anyone in c+4..c+257 instead seeds from arbBlockHash(c+1..c+3) and pays that caller the 0.001 ETH bounty.

### 7. Info: Deployment verified on chain against the pinned source: runtime bytecode, bindings, closed state and the real PoolManager match what the test suite assumes; pre-open checklist

`src/PadLiquidity.sol:72`

```
    IPoolManager public immutable poolManager;
```

Verified from this review over public Robinhood Chain RPC (chain id 4663, block 89,090,985) against the local forge build of commit c3a2fac (solc 0.8.26, via-IR, 200 runs, no metadata): runtime code at PvPPad 0xf425..0bc5, PadGame 0x82c0..51eb, PadToken 0x6870..17f0, PadLiquidity 0x3ebb..7700 and KingOfThePad 0xafb6..9b37 equals the artifact deployedBytecode with zero differing bytes outside the immutable references (PvPPad 512 immutable bytes, PadLiquidity 512, KingOfThePad 224; PadGame and PadToken have none and are identical). Bindings read on chain: pad.liquidity == 0x3ebb..7700, pad.king == 0xafb6..9b37, liq.pad == pad, liq.king == king, liq.poolManager == 0x8366..0951, king.pad/king.liquidity/king.dev as expected, owner 0x5b95..0d06, tokenTemplate/gameTemplate as expected; pad.opened == false, launchCount 0, hook 0, poolCount 0, all three ETH balances 0, deployedAt 1792015720 (open deadline 2026-11-13, after which anyone can open). PoolManager 0x8366..0951 is byte-identical (24,009 bytes) to the canonical Ethereum mainnet v4 PoolManager 0x0000..8A90 except the 20-byte NoDelegateCall immutable, so the vendored v4-core v4.0.0 Lock.IS_UNLOCKED_SLOT, pools slot 6, initialize(key, sqrtPrice) and hook flag mask apply; exttload(IS_UNLOCKED_SLOT) == 0 at rest; ArbSys at 0x64 answers arbBlockNumber. This resolves the audit_permissions and audit_math 'could not verify' items and the graduation-compatibility concern: graduate() has no input or state found that reverts permanently on this manager (currency ordering, bounded sqrtP, tick multiples, liquidity far below maxLiquidityPerTick, 1 ppm haircut, hook flag bit 13). Note: a protocol fee controller is set (0x6d00..314c) and 0.268 ETH of protocol fees are already accrued on this manager, so the pad's pools may pay up to v4's protocol fee share out of swap fees, lowering king income; it cannot block graduation, collection or burns. Remaining pre-open items: rehearse one initialize + modifyLiquidity with a hook of the same flag shape is optional given the byte-identical manager; mine the open salt so hookAddressFor(salt) & 0x3FFF == 0x2000; have keepers (resolver, seed capture, distribute) running before openPad.

**Reproduction**

cast code of each address compared with out/<Name>.sol/<Name>.json deployedBytecode with immutableReferences masked: zero differing bytes. cast call pad liquidity()/king()/opened()/launchCount()/hook(); liq king()/hook()/poolManager()/poolCount(); king pad()/liquidity()/dev()/king(); cast call 0x8366..0951 exttload(bytes32) 0xc090fc46...ab23 -> 0x0; cast code 0x8366..0951 versus mainnet 0x000000000004444c5dc75cB358380D2e3dE08A90: 20 differing bytes at offset 13618 only. Expected and actual match; nothing to fix.

### 8. Info: Keeper requirements the open decision depends on: at 10 L2 blocks/s a roller can never resolve its own roll on its next roll, and finalize() needs three more L2 blocks after the last roll

`src/PadGame.sol:518`

```
        if (commit + 1 + HASH_WINDOW < cur) {
```

Two timing facts, both disclosed in docs/AUDIT.md #1 and agreed as design limits, not fund-safety defects, but they decide whether the game is fair after openPad. (1) The resolution window is L2 blocks commit+4..commit+257 (about 0.4 s to 25.7 s at 10 blocks/s) while COOLDOWN is 60 s, so the own-previous-roll path in roll() always finds the previous roll expired: height moves up to 4 toward zero and the 0.00002 ETH tip goes to the pot. Without independent resolvers every honest roll is penalised. (2) A roll committed in block c resolves only when arbBlockNumber() > c + 3; Arbitrum-stack chains produce blocks only with transactions, so finalize()/poke() revert 'not finalizable' until three further L2 blocks exist, and a keeper that tries once and gives up leaves the pot and carry pull pending. Re-tested the other upkeep questions from the brief: enter() and roll() cannot be reverted by upkeep (every pad step is behind _canSpend and try/catch), _resolve has a 200k gas floor, and no step can be skipped forever because draw/resolvePending/finalize/graduate are permissionless with full caller gas. Merged from audit_flow and audit_economics. Recommendation: at least two independent resolver keepers and a finalize/graduate keeper before openPad; a v4 could anchor expiry to block.timestamp or shorten the cooldown.

**Reproduction**

(1) MockArbSys at 10 blocks/s: genesis draw completes, Imp 0 rolls at L2 block B, nobody resolves, the same wallet rolls again 60 s later at block B+600: getRoll(0).expired == true, claimable(roller) == 0, pot grew by the expired tip (existing test test_tenBlocksPerSecondSixtySecondRollerNeverExpiresWithResolver shows the resolver side). (2) Game in play, one roll at L2 block c, warp past endTime(), call finalize() while arbBlockNumber() == c+1: reverts 'not finalizable' (pendingCount == 1); advance to c+4 and call again: succeeds.

### 9. Info: Carry pot has a single capped exit: ETH parked there drips out at most 15% of each qualifying raise and can sit indefinitely

`src/PvPPad.sol:175`

```
        uint256 carry = carryPot < maxAmount ? carryPot : maxAmount;
```

Re-tested the carry pull at finalize: pulled once (finalized is set right after, fundPot requires !finalized), only by the registered game, only with >= 5 living Imps, only up to fundedReleaseCap() - fundedPot with the cap min(3 x roll pot, 15% of ethRaised), so at most 0.6 ETH per sold-out game. The flip side is that carry is sticky: king shares diverted by the window finding, funding returned from games that ended with fewer than five living Imps, and ranking dust accumulate with no other release path. On a quiet pad that ETH is effectively locked until honest well-raised games finalize. Design trade-off to document for users; a v4 could add a time-based decay of carry into the burn bucket. Merged from audit_economics.

**Reproduction**

State: carryPot = 5 ETH. A launch raising 0.1 ETH that ends with 5 living Imps pulls at most min(3 x rollPot, 0.015 ETH) - fundedPot; a sold-out 4 ETH launch at most 0.6 ETH. Existing test test_fiveWalletCarryChainOnlyPullsUnusedCapAtFinalize shows the cap; start it with a large carryPot to see the remainder stay.

### 10. Info: PvPPad.KING_MIN_IMPS is declared but never read; the five-Imp rule lives only as the literal 5 in PadGame.isLiveForKing and FUNDED_MIN_LIVING

`src/PvPPad.sol:15`

```
    uint256 public constant KING_MIN_IMPS = 5;
```

Dead public constant. Harmless today because both values are 5, but an explorer reader or a front end may take the pad constant as the source of truth while the enforced threshold is `>= 5` at src/PadGame.sol:384 and PadGame.FUNDED_MIN_LIVING. Not exploitable before or after openPad. Merged from audit_flow and audit_math. Fix (v4): drop the constant or have PadGame read it from the pad.

**Reproduction**

grep KING_MIN_IMPS src/ test/: only the declaration at src/PvPPad.sol:15. pad.KING_MIN_IMPS() returns 5; the enforcing code is src/PadGame.sol:384 `(drawComplete ? imps.length : entrants.length) >= 5`. Expected: one definition used by the enforcing code. Actual: unused.

### 11. Info: Verdict: no critical or high defect found; openPad is acceptable with operational prerequisites, and the medium/low items above are v4 candidates, not blockers

`src/PvPPad.sol:108`

```
    function openPad(bytes32 salt) external {
```

Scope reviewed: all six contracts in src/, the 193-test suite (all pass on this tree), the specialists' 21 findings (reproduced 5 defects and merged 7 duplicates; dropped nothing that reproduced), and the deployed state on chain. Fund safety: no path was found that drains, strands or permanently locks ETH or tokens; graduate() has no permanent-revert input on the real PoolManager (byte-identical to canonical v4.0.0); the king settlement cannot be bricked or starved below 16 pools by outsiders, in-kind fees cannot fail; resolver tips are paid once to the real resolver through upkeepFor; the draw bounty is paid exactly once; carry is pulled once under the cap. Agreed accepted risks with corrections: the reign stamp leaks post-crown fees of unreached pools to the old king (low), the first crown captures all pre-crown fees for a refundable 0.01 ETH (low), the genesis sybil loss is about 2% not 4% (low), and the king window and pad upkeep can be starved by 0.001 ETH dead launches (medium/low). None of these lock funds; all are griefing or mis-routing that a v4 should fix. Recommendation: calling openPad is safe from a fund-safety standpoint provided (1) the owner takes the first crown immediately after genesis graduates, (2) at least two independent resolver keepers, a seed-capture keeper and a distribute/finalize keeper run from the moment of opening, (3) the new king's UI loops distribute(16) after each crown, (4) dead-launch spam of the king window is monitored and the carry pot kept small, (5) the dev wallet is an address that accepts 30k-gas pushes or claims regularly. If the requester wants the king-window and reign-stamp semantics fixed, that needs a v4 redeploy before opening, since nothing can be patched afterwards. This remains a swarm review; an independent human audit and legal review are still recommended before a public launch.

**Reproduction**

forge test: 193 passed, 0 failed on commit c3a2fac. test/scratch/Judge.t.sol: 5 reproductions pass (king window diversion with control, unreached-pool reign leak on 60 real pools, upkeep starvation cycle of n-1, genesis 9% roll pot and carry pull). On-chain reads listed in the deployment finding.

---

Judge's submission `931fa74612afcd5327e472559900ddacd20c4ffa192f6929312bf31d4e44432b`, accepted on the IdentityMD network. Acceptance means the report met the job's checks;
it is not a guarantee that the code has no other defects.
