Job
Swarmopoly v2: an onchain Monopoly-style daily game on Robinhood Chain (4663), plus a website. Currency: IMD 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127.\n\nContracts, in deploy order:\n1 SeasonPot($owner, currency): holds the pot; only the bound game moves it.\n2 DeedVault($owner, currency, poolManager): per-tile token vaults and deeds. poolManager is the Uniswap v4 PoolManager on 4663: read poolManager() on hook 0x19bec7c2e1b2aadaf67b259744751a9960d66000. Never guess it.\n3 …
Published · Contracts
- app
- DeedVault 0x5a3dc16c23447f70c414145118ee2e9984260482 · Robinhood Chain
- app
- SeasonPot 0xd77f768d634328bdfa826051457c974f4028a6dc · Robinhood Chain
- app
- SwarmopolyGame 0xa5ae5282aa914a4ce689f1609462e2a5eaa2490d · Robinhood Chain
- github
- identity-md-launches/launch-895-swarmopoly-v2-onchain-monopoly-style
Work
- posted32 minto the first attempt
- built
#464Build contract projectCodex69 files changedrevised
Implemented the three contracts, static website, deployment manifest, tests, and documentation. PoolManager was verified through the supplied hook.
Passed
forge build,forge test(29 tests),forge fmt --check, and site checks.Deployment, IPFS publication, and independent review remain outstanding. The site awaits confirmed contract addresses.
See README for rules, assumptions, and deployment instructions.
ran oncodex · gpt-6-astra · 8 turns · 31m 42s · 119.8K in · 56.1K out · 1.7M cachedsubmissiond1a55f5ac2c077677b32dbca20311c2fa2045452f3c269a60e54ef4074c2ee5bdevice84a29c6575331b775ad71766f3d9e013aebe78b33a4a5fad32c907fb60ed7636started from0243d7da4a4337ae8b16bcdf15bb4ead736fd68fbundle9e101245e99a3d06af9c45e1da38d5cf0e758df8da810c37f3c78a50bbc3fa76 · 299 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 69 files.gitignoreREADME.mddocs/DEPLOYMENT.mddocs/REVIEW.mddocs/RULES.mddocs/TESTING.mddocs/check-results.jsondocs/network-verification.jsonfoundry.tomllaunch.jsonlib/forge-std/LICENSE-APACHElib/forge-std/LICENSE-MITlib/forge-std/src/Base.sollib/forge-std/src/Script.sollib/forge-std/src/StdAssertions.sollib/forge-std/src/StdChains.sollib/forge-std/src/StdCheats.sollib/forge-std/src/StdConstants.sollib/forge-std/src/StdError.sollib/forge-std/src/StdInvariant.sollib/forge-std/src/StdJson.sollib/forge-std/src/StdMath.sollib/forge-std/src/StdStorage.sollib/forge-std/src/StdStyle.sollib/forge-std/src/StdToml.sollib/forge-std/src/StdUtils.sollib/forge-std/src/Test.sollib/forge-std/src/Vm.sollib/forge-std/src/console.sollib/forge-std/src/console2.sollib/forge-std/src/interfaces/IERC1155.sollib/forge-std/src/interfaces/IERC165.sollib/forge-std/src/interfaces/IERC20.sollib/forge-std/src/interfaces/IERC4626.sollib/forge-std/src/interfaces/IERC6909.sollib/forge-std/src/interfaces/IERC721.sollib/forge-std/src/interfaces/IERC7540.sollib/forge-std/src/interfaces/IERC7575.sollib/forge-std/src/interfaces/IMulticall3.sollib/forge-std/src/safeconsole.sollib/openzeppelin-contracts/LICENSElib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sollib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Permit.sollib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sollib/openzeppelin-contracts/contracts/utils/Address.sollib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sollib/openzeppelin-contracts/contracts/utils/math/Math.solscripts/configure-site.mjsscripts/export-abi.pyscripts/verify-network.pysite/abi.jsonsite/app.jssite/config.jsonsite/index.htmlsite/package.jsonsite/rules.htmlsite/style.csssite/vendor/ethers.LICENSE.mdsite/vendor/ethers.min.jssrc/DeedVault.solsrc/Owned.solsrc/SeasonPot.solsrc/SwarmopolyGame.solsrc/interfaces/IV4.soltest/Invariants.t.soltest/Swarmopoly.t.soltest/mocks/MockERC20.soltest/mocks/MockPoolManager.soltest/site.test.mjs - integrated
#961ManifestCodex1 file changedrevised
afterBuild contract projectwrites tolaunch.jsonUpdated launch.json with verified PoolManager evidence and accepted constructor arguments.
Schema/ABI checks,
forge build, andforge testpassed: 30 tests, zero failures, including the protected deployment probe.No other submitted files changed.
ran oncodex · gpt-6-astra · 6 turns · 9m 50s · 51.4K in · 9.8K out · 857.2K cachedsubmission6d3092bfce8b31685ed2625f7ff14f1d6cab573d71bcc3eecf3e580d29d983e7devicefdeac05268aded13c073eb33b093dc797dc06252faf595120f17f270d1e717a8started fromcbc90c9558c472934eecf35107fc955c36d05ba4bundlef53e2f74ab2d0c5e4a6de62914573bad74ae3f2a0ee50b494e95eab23de12d82 · 300 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549changed · 1 filelaunch.json - reviewed
#13Audit permissionsClaude2 findings · 2 low
afterBuild contract projectReview complete. The findings file is written and validated, and the working tree is clean apart from it.
Outcome. The permission model of SeasonPot, DeedVault and SwarmopolyGame holds. Every privileged function is owner-only or game-only, both bindings are one-shot, no owner path touches user funds, and the swap callback authenticates the PoolManager plus a single-use context. On-chain checks confirmed the PoolManager in launch.json matches the hook's
poolManager()read, and the IMD token exposes no pause, blacklist, mint or proxy selectors.Findings (both low, no fund loss):
- Sponsor slot squatting in
DeedVault.sponsorTile. The slot is first-come with no minimum, so anyone can lock every tile's sponsorship for the season with 1 wei of tile token and reclaim it afterwards. Reproduced with a scratch test. - Cooldown reset on join in
SwarmopolyGame.joinSeason. The function zeroesnextRoll, so a player who rolls in the last allowed hour of a season can roll again right after joining the next one. Reproduced with a scratch test.
Coverage. All 36 listed entry points have a row. Two are marked
finding, the restholdswith a one-line trace reason, plus three invariant rows for owner custody, mixed-guard storage writers and callback reentrancy. Leads I checked and rejected include expire-roll underflow, reveal/expire window overlap, claim-rent scoring across the season boundary, and confused-deputy approval spending.Out of my area, noted only. Blockhash predictability under a sequencer and Sybil scoring are documented trust assumptions, not defects in access control.
ran onclaude · claude-fable-5-1 · 34 turns · 14m 12s · 418 in · 57.2K out · 1.5M cachedsubmission1c8b82ac113782ca664e130edd1f2121520dc0644e855ceae7c883b1fa31b1a6device0238a59bba7222372009ab205c0c51a5a37380b7e12f07c8a62b5f2a0dc30ae4started fromcbc90c9558c472934eecf35107fc955c36d05ba4bundlenoneapplied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549changed · 0 filesnothingAnyone can squat a tile's single sponsor slot for a whole season with 1 weisrc/DeedVault.sol:281
joinSeason resets nextRoll, so the roll cooldown is skipped across a season boundarysrc/SwarmopolyGame.sol:162
- Sponsor slot squatting in
- tested
#167Write foundry testsCodex4 files changedrevised
afterBuild contract projectwrites totesttest/**Added 25 adversarial/fuzz tests and 12-player stateful invariants covering custody, rent, sponsorships, prizes, and withheld rolls.
Validation passed:
forge buildforge test: 56 passed, zero failures- New invariant campaign: 24,576 calls, zero reverts
Deliverable changes are confined to
test/. No reproducible contract defects found. Live-chain integration remains unverified.ran oncodex · gpt-6-astra · 7 turns · 13m 51s · 114.8K in · 21.4K out · 2.3M cachedsubmissiond3fb034cbcfbce8728e12df1e1a9dac0dc95ff4c39c91f197001d4b5a2105ea8device19e9c602f9934c6542c4832e1e06f69bcb7f1bffcc3ff7a7507f038d3bc3be35started fromcbc90c9558c472934eecf35107fc955c36d05ba4bundlecd04f258834d53e94d96afb24d5ab6c6c953b21d4d2dfbf8f5284001ef1b1141 · 311 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549changed · 4 filestest/Adversarial.t.soltest/CONTRIBUTOR_TESTS.mdtest/StatefulAccounting.t.soltest/helpers/AccountingHandler.sol - reviewed
#3Audit flowClaude2 findings · 1 high
afterBuild contract projectThe review is complete. Findings are in
.imd-findings.jsonat the repository root, with 2 findings and 39 coverage rows (all 36 listed entry points plus 3 invariants). No source files were changed; the only extra file is the scratch proof undertest/scratch/, which is gitignored.What I found
- High: zero-score joiners are paid prize money.
joinSeasoninserts the caller into the leaderboard with score 0 andfinalizeSeasonpays every listed address. Ten fresh accounts that only called joinSeason under a zero buy-in received the full 60% prize budget (600 of 1000 IMD) and could claim it. With a positive buy-in it stays profitable whenever a rank share exceeds the buy-in. The embedded Foundry proof fails on the current code and passes once zero-score entries are excluded. - Info: missed-reveal penalty exceeds the brief's wording. The brief says an expired roll is "resolved as jail"; the code also bankrupts the player for the season and forfeits the reserved exposure. The author documents this as the mechanism that keeps the "unrevealed never better" invariant true, so I recorded it for confirmation rather than as a bug.
What I verified
- Full suite builds and passes (29 tests, including invariants).
- Robinhood Chain RPC: the hook's
poolManager()returns the address in launch.json. The EVMblock.numberis the Ethereum L1 height (ArbOS 116), so the 200-block reveal window is about 40 minutes and sits inside the 1-hour deadline.blockhashis nonzero within 256 blocks and is L2-derived, so the commit-reveal path works as designed. - Traced the balance-versus-exposure invariant across every balance-reducing path, the v4 unlock/swap/settle/take sequence and delta decoding, the rent accumulator and season baselines, the leaderboard insertion, and the pause and wrong-state behavior of each entry point. All held.
Not covered: the site beyond the commit/reveal flow, and the live hook's swap behavior against a real pool, which no local test can reach.
ran onclaude · claude-fable-5-1 · 23 turns · 14m 8s · 418 in · 58K out · 1.5M cachedsubmission1593d866f6e0991f2c093a980b82297d803cd7b2e88578fcdf3ed69a5decc6f5device077d2937780a81bc63aca73b54616f949b3566a81a7a59abda7b8245765661d9started fromcbc90c9558c472934eecf35107fc955c36d05ba4bundlenoneapplied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549changed · 0 filesnothinghighjoinSeason ranks zero-score accounts, so finalizeSeason pays prize money to addresses that never playedsrc/SwarmopolyGame.sol:173
proof · a Foundry test the fix has to passMissed reveal is resolved as forfeiture plus season bankruptcy, not only jail as the brief statessrc/SwarmopolyGame.sol:259
The brief says a reveal missed past the window 'can be resolved by anyone as jail'. expireRoll additionally sets bankrupt (ending the player's season) and moves r.exposure (max(TAX, max tierRent) capped at balance, 30 IMD at defaults) to the pot via _toPot.
The author documents this as the mechanism that keeps 'unrevealed rolls never better' true, and the trace confirms the penalty is always at least the worst reveal outcome, so this is a deliberate, defensible deviation rather than a bug; it is recorded so the requester can confirm the stricter rule is accepted (a jail-only resolution would make withholding a bad roll strictly profitable once rent exceeds the cost of a lost turn).
Player with balance 100 IMD commits a roll and never reveals.
After the 200-block / 1-hour window anyone calls expireRoll(player).
Brief-literal expectation: player is jailed.
Actual: position=10, jailed=true, bankrupt=true (no further commit/buy/bail this season), balance 70 IMD, 30 IMD credited to the pot.
- High: zero-score joiners are paid prize money.
- reviewed
#2Audit mathClaude1 finding · 1 medium
afterBuild contract projectThe review is complete.
.imd-findings.jsonis written at the repository root with one finding and 39 coverage rows (all 36 listed entry points plus three invariants). Scratch tests were removed, and no source, test or config file was changed.What I checked (Math Precision, Boundary, Numerical Gap guides). All three contracts were traced end to end, the existing suite was re-run with a second fuzz seed and 1024 runs, and six scratch probes exercised the arithmetic edges. The tree builds and all 29 tests pass.
Result: the arithmetic holds.
- Tile shares are pinned 1:1 to assets for every tile, so there is no share-price drift, inflation or zero-share rounding. Redeem returns exactly the lot's shares.
- The 1e27 rent accumulator rounds down at credit and claim. With a 2-wei-weight lot beside a 200e18-weight lot, claims summed to one wei under the credited rent.
- The commit-time exposure equals the exact worst-case landing cost, and no balance-reducing path exists while a roll is pending, so withdraw can never underflow and expiry can never fail.
- Prize weights sum to 100, each prize rounds down, and reserve never exceeds 60% of the available pot.
- The v4 callback's delta unpacking, exact-input check, price limits and settle check match the vendored v4-core. The 36-byte quote revert bubbles through PoolManager.unlock unchanged.
- Robinhood Chain's
block.numbertracks Ethereum L1 (RPC header shows l1BlockNumber near 26.1M), so the 200-block reveal window is about 40 minutes and fits inside the 1-hour deadline.
One finding, medium, outside the pure math area but a brief deviation with permanent effect. The brief says only "a tile holding tokens can't change", but
listTilerefuses any second listing on a slot once a token is set, even with zero assets. A mistyped fee or tick spacing yields an uninitialized pool: every buy on that slot reverts forever, yet landing still charges full rent to the pot. Reproduced with a scratch test: re-listing a freshly listed, empty tile reverts with Invalid for both a corrected key and a corrected tier. Suggested fix keeps the design: allow re-listing only when shares, assets and sponsor liability for the slot are all zero.Not reproduced as defects. The Slither weak-PRNG and timestamp lines describe the commissioned commit-reveal design and are documented in REVIEW.md. The reentrancy and uninitialized-local lines were traced and do not lead to harm. The IMD token being standard, the owner listing honest tokens, and sequencer influence over blockhash remain the documented trust assumptions.
ran onclaude · claude-fable-5-1 · 33 turns · 15m 5s · 546 in · 51.2K out · 2.1M cachedsubmission6d3968fd67f67c6aa1976b381c250dbba0278340cc4fdcebb66cb9d8d5ad0260device468e82a89b9bfe188ef46b055e2ea554bde626cc162630c20effb0fcc7e13fedstarted fromcbc90c9558c472934eecf35107fc955c36d05ba4bundlenoneapplied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549changed · 0 filesnothingmediumListed tile with no tokens cannot be re-listed; a wrong PoolKey or tier permanently bricks one of the 28 property slots while it keeps charging rentsrc/DeedVault.sol:105
- reviewed
#1082Audit economicsClaude7 findings · 1 medium
afterBuild contract projectThe review is complete. Findings are written to
.imd-findings.jsonat the repository root, no tracked file was changed, and the full suite including my scratch tests passes (38 of 38).Outcome. The core accounting is sound. I traced pot solvency, vault custody, the rent accumulator, the commit/reveal exposure scheme and the leaderboard, and found no fund-loss bug. The defects are in the economic design, where the game's own mechanisms can be turned against the pot. Seven findings, one medium, four low, two info, each with a reproduction run in
test/scratch/Leads.t.sol.Findings, most severe first:
- Medium, pot-proportional rewards versus flat entry cost. Salary, parking and chance pay a percentage of the pot, while an account's downside is capped at the balance it chooses to hold. At the suggested parameters on a vacant board, a Sybil account becomes profitable once the pot passes roughly 1,100 IMD and takes about 1.15% of the pot per season. Reproduced with a 1 IMD balance collecting 50.1 IMD from parking while a 1 IMD balance on a tier-8 tile loses only 1 IMD.
- Low, zero-score joiners are paid prizes. The ascending-address tie-break lets eight low addresses collect 607 IMD of a 660 IMD budget for 80 IMD of buy-ins in a sparse season.
- Low, dust deed captures 80% of tile rent. A 1 wei purchase redirected 2.4 IMD of the next 3 IMD rent from the pot to the dust holder.
- Low, 1 wei sponsorship locks a tile's sponsor slot for the whole season.
- Low, cooldown bypass at season boundaries. Joining resets the roll timer, so two rolls were accepted 3,601 seconds apart.
- Info, lock boosts never decay after maturity, so a matured 90-day lot earns double against new unlocked buyers while fully liquid.
- Info, unverified lead on Nitro blockhash semantics. The future blockhash of parent-chain block N+1 may be the hash of the committer's own L2 block. This cannot be reproduced in Foundry and is recorded as a lead only.
Coverage. All 36 listed entry points have rows, plus four invariant rows. None are unreached. The invariant and flow-gap guides were fully applied. Pool-level behaviour on the live PoolManager and the IMD token's transfer semantics were outside what I could verify offline and remain trust assumptions, as the repository's own review states.
ran onclaude · claude-fable-5-1 · 30 turns · 19m 35s · 450 in · 77.7K out · 1.8M cachedsubmission2a0db6c2fda8d6be4f029a10c64c14cc04108c26d187ff96cd4b60a11ec11016device5739ce0d803a43cdf1c1f07f89068041652b5527d38c46f74bacb730a95973e7started fromcbc90c9558c472934eecf35107fc955c36d05ba4bundlenoneapplied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549changed · 0 filesnothingmediumPer-roll rewards scale with the pot while entry cost is flat and downside is capped at balance, so Sybil rolling becomes +EV once the pot is modestsrc/SwarmopolyGame.sol:343
Zero-score joiners occupy prize ranks and the ascending-address tie-break lets low vanity addresses capture 92% of the prize budget in a sparse seasonsrc/SwarmopolyGame.sol:444
joinSeason calls _score(msg.sender, 0) (line 173), inserting every joiner into the top ten while there is room, and _better() breaks equal scores by lower numeric address (line 432). finalizeSeason only skips empty slots, so a joiner with score 0 and a low address is paid 25/18/13/... percent of the 60% budget ahead of real players with score 0 and even evicts them.
Because 40% of each pot carries over, a quiet season following an active one (or any season where fewer than ten players score) can be farmed by joining with cheap low-address accounts (vanity addresses with leading zero bytes are trivial to generate).
Suggested fix preserving the brief ('Score = salary + parking + chance + rent claimed'): skip leaders with scores[season][winner] == 0 in finalizeSeason (and/or do not insert zero-score joiners), leaving unpaid weights in the pot as already done for empty slots.
A dust deed (1 wei of IMD) redirects 80% of all rent on a tile from the pot to its holdersrc/SwarmopolyGame.sol:308
The holder share is switched on by weight != 0 regardless of how much value backs that weight, and DeedVault.buy accepts any amount whose swap output is >= 1 unit (lines 139-141). The first buyer on a tile, with 1 wei of IMD, therefore receives the entire 80% holder share of every subsequent rent payment until someone else buys, and the pot (which funds every player's salary and prizes) drops from 100% to 20% of that tile's rent.
There is no minimum purchase, no owner parameter for one, and the brief only bounds amountIn from above (maxBuy). Rational play is to dust-buy every tile landed on, which hollows out the pot.
Suggested fix: add a minBuy season parameter (checked in _buy next to maxBuy) or require out/shares >= a per-tile minimum.
One-wei sponsorship locks a tile's single sponsor slot for the whole seasonsrc/DeedVault.sol:281
sponsorTile allows exactly one sponsor address per (season, tile) and rejects anyone else, with no minimum amount or perLanding. A griefer can call sponsorTile(slot, 1, 1) on every listed tile at season start for 1 wei of each tile token plus gas, after which no genuine sponsor can fund any tile until the next season; the griefer's 1 wei is awarded on the first landing and the slot stays dead (remaining == 0, sponsor set).
Suggested fix preserving the O(1) landing path: let a new sponsor take over when remaining == 0, or let any address top up as long as perLanding matches, or require a minimum amount >= perLanding x some count.
State: listTile(6,key,3); startSeason(10e18, 30 days, 1e18, 100e18).
Griefer (0x6) calls token.approve(vault, 1) then vault.sponsorTile(6, 1, 1). alice approves 100e18 and calls vault.sponsorTile(6, 100e18, 1e18).
Expected: a real sponsorship should be acceptable.
Actual: revert Invalid() because s.sponsor != msg.sender; tile 6 cannot be sponsored by anyone else for season 1.
Scratch test: test/scratch/Leads.t.sol::testSponsorSlotGriefing.
joinSeason resets nextRoll to zero, bypassing the roll cooldown across a season boundarysrc/SwarmopolyGame.sol:162
The 20h (owner-configurable 1-48h) cooldown is a per-player timer stored in nextRoll, but joinSeason unconditionally zeroes it. A player who commits at the last permitted moment of season N (end - 1h - 1s) can reveal, then join season N+1 as soon as the owner starts it and commit again immediately.
With back-to-back seasons this yields two full rolls (two chances at salary/parking/chance rewards, and a second parking payout if the UTC day flipped since parkingDay is keyed by day, line 277) about one hour apart. docs/RULES.md lists what joining resets and says nonces and parking day persist, but does not mention the cooldown.
Suggested fix: do not touch nextRoll in joinSeason (or set it to max(nextRoll, 0)).
Lock weight boost persists after the lock matures, so a liquid matured 90-day lot earns 2x against new unlocked buyers foreversrc/DeedVault.sol:141
The 1.5x/2x weight is compensation for forgoing redemption for 30/90 days, but the weight never decays: after redeemAt passes the lot is fully redeemable at any time yet keeps its boosted share of rent.
Since there is no cost to keeping the lot and the boost is permanent, the dominant strategy is always to pick the 90-day lock, and early lockers hold a permanent structural advantage over later entrants that the brief's wording ('Optional lock: 30d = 1.5x weight, 90d = 2x') does not clearly intend. docs/RULES.md documents the behaviour as a choice, so this is reported for the author's confirmation rather than as a defect.
If undesired, re-weight to 2x shares on first _accrue after redeemAt (updating t.weight) or require re-locking.
State: listTile(6,key,3); 365-day season; alice buys 10 IMD on tile 6 with lockDays = 90 (weight 80 units); warp 91 days (lock matured, redeemable); bob buys 10 IMD unlocked (weight 40 units); charlie lands and pays 3 IMD rent.
Expected (if boost is tied to being locked): equal 1.2 IMD each.
Actual: alice pending 1.6 IMD, bob 0.8 IMD.
Scratch test: test/scratch/Leads.t.sol::testMaturedLockKeepsWeight.
Unverified lead: on Arbitrum Nitro the 'future blockhash' of L1 block N+1 is the hash of the last L2 block before the L1 number advanced, which can be the committer's own blocksrc/SwarmopolyGame.sol:236
Not reproducible in the local EVM.
Concrete state to check on chain: send commitRoll in an otherwise empty L2 block whose L1 number is N, immediately before the sequencer observes L1 block N+1; after reveal, compare blockhash(N+1) as seen by the contract with the hash of the L2 block that contained the commit.
If they are equal, the committer's own block determined the entropy.
- reviewed
#244Audit judgeClaude7 findings · 1 highrevised
afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flowReview complete. The report is in
.imd-findings.jsonat the repository root, with seven findings and a coverage row for all 36 entry points plus four invariants. No source files were changed;test/scratch/holds only my throwaway reproductions, and git shows no tracked changes.What I reproduced and kept
- High: zero-score joiners are paid prizes. Joining inserts an address into the top ten at score 0, and finalization pays every non-empty rank. Ten fresh addresses that only call
joinSeasontake 60% of the pot. The attached proof from the flow specialist fails on the current tree for exactly that reason, so I kept it as the proof. Merged with the economics specialist's lower-severity report of the same root cause. - Medium: a vacant listed tile can never be corrected. The brief allows changing a tile that holds no tokens. A wrong fee or tier makes the slot unbuyable for the life of the contracts while it keeps charging rent.
- Low: one-wei sponsorship squats a tile for the season. Reported by two specialists, merged into one.
- Low: joining resets the roll cooldown, giving one extra roll per season boundary. Reported by two specialists, merged.
- Low: a one-wei deed captures 80% of a tile's rent from the pot. No minimum purchase exists.
- Info: expiry is forfeiture plus bankruptcy, stricter than the brief's "resolved as jail". Deliberate and documented, recorded for confirmation.
- Info: rewards scale with the pot while entry cost is flat, and parking pays each player once per UTC day. This is the brief's own economics, recorded as a trust assumption.
What I dropped
- The matured-lock weight note is documented behavior and does not contradict the brief.
- The Arbitrum Nitro blockhash-grinding lead does not apply here. I sampled the chain: it produces roughly ten L2 blocks per second with several transactions each, so a committer's block can never be the last block of an L1 slot.
My own pass found nothing beyond the specialists' leads. I traced the v4 callback (context hash, signed deltas, sync/settle/take), the share and rent accumulator math, exposure versus balance, season boundaries, pause semantics and the leaderboard insertion. The project's 56 tests pass. On chain I confirmed the hook returns the PoolManager address written in the manifest, IMD is a non-proxy 18-decimal token, and
block.numbertracks Ethereum L1 height, so the 200-block reveal window is about 40 minutes as documented.ran onclaude · claude-fable-5-1 · 35 turns · 16m 39s · 386 in · 57.2K out · 1.5M cachedsubmissioncf8762353ef81c0adfbf6a8f9a2b57da5632c303e181b6621299407f37daa2c8device3a40eaafbd83a6bc57b859dab02a7e0ae1fcd12ca7afee73c0d6c380b94178e9started fromd809b2a34d8c33e377d6f8e5ffc4087f624b67cfbundlenoneapplied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549, fcadbae2e9d68bf482f3e4c349dc5b655d2cb4e1ef7cc7b3e7e288ae66e1bdaa, 84c048c9b9842c56fcbf6d31079f513dafa365521ffad90d3652b4f1de2ca503changed · 0 filesnothinghighjoinSeason ranks zero-score accounts and finalizeSeason pays them, so 60% of the pot can be claimed by addresses that never rolledsrc/SwarmopolyGame.sol:444
proof · a Foundry test the fix has to passmediumA listed tile holding no tokens cannot be re-listed, so a wrong PoolKey or tier permanently bricks a property slot that still charges rentsrc/DeedVault.sol:105
Anyone can lock a tile's single sponsor slot for the whole season with 1 weisrc/DeedVault.sol:281
sponsorTile is permissionless and first-come: the first caller for (season, slot) becomes the only address allowed to fund that tile until seasonEnd, pinned to its first perLanding. There is no minimum amount or perLanding and no way to displace a sponsorship whose remaining is 0.
A griefer can call sponsorTile(slot, 1, 1) on every listed tile at season start for 28 wei of tile tokens plus gas, after which no real sponsor can fund any tile that season; the 1 wei is awarded on the first landing and the slot stays dead, and the griefer recovers nothing of value but denies the feature. No funds are lost. Reported identically by audit_economics and audit_permissions; merged.
Fix preserving the O(1) landing path: let a new sponsor take over when s.remaining == 0, or require amount >= perLanding with a minimum perLanding.
State: tile 6 listed, season 1 active.
(1) mallory: token.approve(vault, 1); vault.sponsorTile(6, 1, 1) succeeds.
(2) alice: token.approve(vault, 100e18); vault.sponsorTile(6, 100e18, 1e18) reverts Invalid() because s.sponsor == mallory.
Expected: a real sponsorship can be placed or the dust one displaced.
Actual: tile 6 cannot be sponsored by anyone but mallory, at 1 wei per landing, until seasonEnd[1].
Verified with test/scratch/Judge.t.sol::testSponsorSquat on the current tree.
joinSeason resets nextRoll, so the roll cooldown is skipped across a season boundarysrc/SwarmopolyGame.sol:162
The 20h (owner-configurable 1-48h) cooldown is stored per player in nextRoll, but joinSeason zeroes it unconditionally, while the comment two lines later says nonce and parkingDay are deliberately preserved to prevent resets at season boundaries.
A player who commits and reveals at the last permitted moment of season N (end - 1h - 1s) can join season N+1 as soon as it is started and commit again immediately, giving two full rolls (two chances at salary, chance and, if the UTC day changed, a second parking payout) about an hour apart. Bounded to one extra roll per season per player; no fund loss. Reported identically by audit_economics and audit_permissions; merged.
Fix: do not touch p.nextRoll in joinSeason.
A 1-wei deed redirects 80% of a tile's rent from the pot to its holdersrc/SwarmopolyGame.sol:308
The holder share switches on at weight != 0 regardless of how much value backs that weight, and DeedVault.buy accepts any amountIn whose swap output is at least minOut >= 1. The first buyer on a tile, with 1 wei of IMD, therefore receives the entire 80% holder share of every later rent payment on that tile until someone else buys, and the pot (which funds every salary, parking payout and prize) drops from 100% to 20% of that tile's rent.
The brief bounds amountIn only from above (maxBuy); there is no minimum purchase and no owner parameter for one, so the rational play is to dust-buy every tile landed on, hollowing out the pot's rent income.
Fix: add a minBuy season parameter checked in _buy next to maxBuy, or require a per-tile minimum of shares.
A missed reveal is resolved as forfeiture plus season bankruptcy, not only jail as the brief statessrc/SwarmopolyGame.sol:259
The brief says a reveal missed past the window 'can be resolved by anyone as jail'. expireRoll additionally marks the player bankrupt for the rest of the season and moves r.exposure (max(TAX, max tierRent) capped at balance, 30 IMD at defaults) to the pot.
The author documents this as the mechanism that keeps the 'unrevealed rolls never better' invariant true, and the trace confirms the penalty is at least the worst reveal outcome, so this is a deliberate deviation rather than a bug. It also means an honest player whose auto-reveal fails (wallet rejection, lost localStorage, sequencer outage longer than 40 minutes of L1 blocks) loses up to 30 IMD and their season. Recorded for the requester to confirm the stricter rule.
Player with 100 IMD balance commits a roll and never reveals.
After 200 L1 blocks (or 1 hour) anyone calls expireRoll(player).
Brief-literal expectation: player is jailed.
Actual: position == 10, jailed == true, bankrupt == true, balance 70 IMD, pot.available() up by 30 IMD.
Verified with test/scratch/Judge.t.sol::testExpireRollSemantics.
Per-roll rewards scale with the pot while entry cost is flat and downside is capped at the play balance; parking pays every player once per UTC daysrc/SwarmopolyGame.sol:343
Fresh season with only tile 9 listed, startSeason(10e18, 30 days, 1e18, 100e18), pot funded to 10,010 IMD.
Alice joins (10 IMD) and deposits exactly 1 IMD, rolls 10 then 10 (square 20): _reward(alice, 50) pays 50 bps of the pot (about 50.1 IMD) into her withdrawable balance.
Bob joins with 1 IMD and lands on tile 9 (rent 30): pays 1 IMD and is bankrupt.
Maximum loss per account is the 1 IMD bond plus buy-in; upside per roll is 0.1 to 0.5% of the whole pot.
- High: zero-score joiners are paid prizes. Joining inserts an address into the top ten at score 0, and finalization pays every non-empty rank. Ten fresh addresses that only call
- updated
#1838Build contract projectCodex12 files changed
Fixed four defects: zero-score prizes, empty-tile relisting, exhausted-sponsor replacement, and season-boundary cooldown resets.
Recorded all seven verdicts in
.imd-responses.json; three economic/timeout advisories were reproduced and disputed with explanations.Validation passed:
forge build, all 40 tests,forge fmt --check, the unchanged reviewer proof, and website smoke checks.ran oncodex · gpt-6-astra · 6 turns · 8m 51s · 112.9K in · 15K out · 1.3M cachedsubmissiond77c76c03b6faf451deada73682a8f1b43806cf2f5bed348ccc8f8b792571a68device24b3b94660bddda1f83b2583d1fc5e67036b7df4ccf69111a296df82de2610a5started fromcbc90c9558c472934eecf35107fc955c36d05ba4bundled05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549 · 305 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 12 filesdocs/DEPLOYMENT.mddocs/REVIEW.mddocs/RULES.mddocs/TESTING.mddocs/check-results.jsonsite/index.htmlsite/rules.htmlsrc/DeedVault.solsrc/SwarmopolyGame.soltest/Invariants.t.soltest/RevisionRegression.t.soltest/Swarmopoly.t.sol - updated
#946ManifestCodex1 file changed
afterBuild contract projectwrites tolaunch.jsonUpdated launch.json with fresh PoolManager verification evidence. Constructor arguments remain correct.
Schema/ABI checks,
forge build, and all 41 tests passed, including the protected deployment check. Onlylaunch.jsonchanged outside scratch space.ran oncodex · gpt-6-astra · 4 turns · 3m 32s · 61.1K in · 6.6K out · 373.9K cachedsubmission0e62e57cb09a06dd7f4c9bd2141960ea91103b2a4886d6d9e09e8c912859d924device269d07167a05c2058ee08777dab51e85a0ca01bad82bf8d5e4974754a3cd4b1cstarted fromb6333ad4b791c9f00d5687d34874fa667aa2e846bundle84c048c9b9842c56fcbf6d31079f513dafa365521ffad90d3652b4f1de2ca503 · 307 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549changed · 1 filelaunch.json - updated
#973Write foundry testsCodex4 files changed
afterBuild contract projectwrites totesttest/**Updated prize fixtures and leaderboard invariants, and added season-rollover regressions.
forge buildandforge testpass: 70 passed, 0 failed, including randomized custody invariants.Only
test/files changed. No contract defects found; live-chain integration remains unverified.ran oncodex · gpt-6-astra · 7 turns · 9m 39s · 92.7K in · 8K out · 2.6M cachedsubmissionf7c98dfbdcfa13a5cf556857e8111c7478da465d37b1f2a280d1f24c757ab94bdevice2c736d2f8963f8a507c1f105407e5ba312f42ab34716c5f81b163e655e340023started fromae71377be4057e2b20044ab1e73eae9947c322cdbundlefcadbae2e9d68bf482f3e4c349dc5b655d2cb4e1ef7cc7b3e7e288ae66e1bdaa · 321 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549changed · 4 filestest/Adversarial.t.soltest/CONTRIBUTOR_TESTS.mdtest/RevisionRegression.t.soltest/StatefulAccounting.t.sol - reviewed
#1023Audit judgeClaude3 findings
afterBuild contract project, Write foundry tests, Manifest, Audit math, Audit permissions, Audit economics, Audit flowThe second-round review is complete. All four round-one defects are fixed, and the only items left are three informational notes the author disputed on design grounds, which I accepted and downgraded.
Settled as fixed
- Zero-score joiners paid prizes (high). The score update now returns early when the resulting score is zero, and finalizeSeason skips any winner with a zero score. The attached proof, copied unchanged into test/scratch and run against the revised code, passes. My own check with fifteen join-only accounts left the leaderboard empty while a real scorer took rank one.
- Empty listed tile could not be relisted (medium). listTile now refuses only when the tile has assets, shares or outstanding sponsor liability. Fee and tier corrections on an empty tile succeed. I confirmed the refusal still holds when the only remaining liability is an awarded but unclaimed lander token with sponsor remaining already zero.
- 1-wei sponsor squat (low). A new sponsor or rate is blocked only while remaining is nonzero. I verified a partial last award followed by replacement: the old sponsor's withdrawal reverts with nothing owed, earlier awards stay reserved, and liability reconciles to zero at season end.
- joinSeason cleared the cooldown (low). The reset line is gone. nextRoll persists for both normal and bankrupt rejoiners.
Kept as info, non-blocking
- Dust deed takes the full holder share. Downgraded from low. The brief specifies only maxBuy and an 80/20 split by weight, so the author's dispute holds.
- Missed reveal resolves as forfeiture plus bankruptcy, stricter than "as jail". Deliberate, needed for the never-better invariant, disclosed in rules and site.
- Pot-scaled rewards against a flat buy-in and per-player daily parking. Specified economics, now documented as an operator responsibility.
Verification
Check Result Project suite (no scratch) 70 passed, 0 failed Round-one proofs on revised code both pass My scratch edge tests 4 passed The findings file holds three info findings and 44 coverage rows: all 36 entry points marked holds or finding, plus rows for each settled round-one item and the full-suite invariant. The scratch directory was removed, so the tree is unchanged apart from the findings file. Nothing new that reproduces as a defect was found in the changed code.
ran onclaude · claude-fable-5-1 · 19 turns · 7m 44s · 226 in · 22.4K out · 667.8K cachedsubmission9af0f782e92dafa6d1db74b38e079a9ec1a25f3c8c6846bb39f0b1390ba3105fdevice6b16b4979d22724158b8c5c0f0cc9ce06382153e865034e462fc689651022adastarted from748199e0a257eeca193b33a5d5b9f6482c03098dbundlenoneapplied ond05207a67d3e3c2c6bfcf80e7d990d6b507eb3713cb5548fda68bf97397ac549, fcadbae2e9d68bf482f3e4c349dc5b655d2cb4e1ef7cc7b3e7e288ae66e1bdaa, 84c048c9b9842c56fcbf6d31079f513dafa365521ffad90d3652b4f1de2ca503changed · 0 filesnothingA 1-wei deed receives the full 80% holder share of a tile's rent while it is the only holder (accepted design, non-blocking)src/SwarmopolyGame.sol:307
A missed reveal is resolved as forfeiture of the reserved exposure plus season bankruptcy, stricter than the brief's 'as jail' (accepted deviation)src/SwarmopolyGame.sol:260
Settlement of round-1 finding f2daf55d (info, author disputed). expireRoll jails the player and additionally sets bankrupt for the rest of the season and moves r.exposure (min(balance, max(TAX, max tierRent)) = 30 IMD at defaults with a 100 IMD balance) to the pot.
The author's reasoning is correct: a jail-only resolution would let a player withhold a reveal that would have charged rent or bankrupted them, breaking the brief's 'unrevealed rolls never better' invariant, which test/Swarmopoly.t.sol::testFuzzWithholdingNeverImprovesBalance checks differentially. The behaviour is unchanged this round and disclosed in RULES.md and the site.
Recorded so the requester explicitly accepts that an honest player whose auto-reveal fails (wallet rejection, lost local secret, sequencer outage beyond 200 L1 blocks or 1 hour) loses up to the max tier rent and their season.
Player alice with 100 IMD play balance calls commitRoll(commitment) and never reveals. vm.roll(+202); bob calls expireRoll(alice).
Brief-literal expectation: alice jailed.
Actual: position == 10, jailed == true, bankrupt == true, balance == 70e18, pot.available() + 30e18.
Re-ran test/RevisionRegression.t.sol::testObservedMissedRevealPenalty on the current tree: PASS with these values.
Per-roll rewards scale with the pot while entry cost is flat and downside is capped at the play balance; parking pays each player once per UTC day (economic assumption)src/SwarmopolyGame.sol:342
Settlement of round-1 finding f61b4270 (info, author disputed). GO salary (salaryBps, default 10), free parking (50 bps per player per UTC day via parkingDay) and the chance card (5 bps) are all percentages of pot.available(), while joining costs a flat buyIn and rolling needs only balance >= rollBond, and the worst a roll can cost is min(balance, max tierRent or 5 IMD).
Multi-account play therefore becomes profitable once the pot is large relative to buyIn + rollBond; the owner's only levers are buyIn and rollBond per season, and startSeason accepts buyIn = 0. This is the economics the brief specifies and not a code defect. The author expanded RULES.md and REVIEW.md to state the farming exposure and the operator's duty to size buy-in and bond against the pot.
Unchanged this round; recorded as the main economic trust assumption the requester accepts by launching.
- publishedidentity-md-launches/launch-895-swarmopoly-v2-onchain-monopoly-stylepull request
- onchain
- deployed
3 contractson Robinhood Chain, 7 gates passedtransaction
- rebuilt
- DeedVault, SeasonPot, SwarmopolyGame · verifier 0.1.0 · solc 0.8.26
- gates
- provenance
- findings
- independent review
- bytecode
- manifest
- protected invariants
- economics
- proof
commit, attestation, manifest, tree, per-contract hashes
- repository
- identity-md-launches/launch-895-swarmopoly-v2-onchain-monopoly-style
- commit
- f6823f17c10356eba369977b89c00e56577e5308
- attestation
- 9faedec7444f471c9b47ece76e99b962845b6c23fa506e580b690aac4ab9e1e0
- manifest
- ebc6995f28b5d11e1580951bbebdc18f3403ea55f97c56ee34b3da05fdcb2f7f
- constructor
- SeasonPot: $owner, 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127
- constructor
- DeedVault: $owner, 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127, 0x8366a39CC670B4001A1121B8F6A443A643e40951
- constructor
- SwarmopolyGame: $owner, 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127, $contract:DeedVault, $contract:SeasonPot
- tree
- ae9a830ebce0f6918263a0698ddca260d2e02951
- compiler
- solc 0.8.26, optimizer 200 runs, via-ir, reproducible
- contract
- DeedVault
src/DeedVault.sol · 9997 bytes
creation 1b402d9025d0b859e4e3e7702b6be430adb92eb2b054ae1df0b3de02fbf00eff
abi 6874e0e9f6b9684135984c7986e88b2935078971f0883c79c6b088d7518a6680
metadata 20c40196dd9e23b70c7e364bfd4a918e88765832eefb840450c8141cedebb89b
onchain at 0x5a3d…0482, block 82,454,371 · creation code matches - contract
- SeasonPot
src/SeasonPot.sol · 2074 bytes
creation 6a0852478ea112bb0bb67c47e090e90e8b388d07c33e85d3b39eabcf718d3371
abi 7af39e324d984ce950c934349cf3fd2bfb04d390a606c841f5536dd656bea927
metadata 464250f6e9ca60f7ea7ec37eda97d3f29362fc7a8bf3c6a3865d7570a343a561
onchain at 0xd77f…a6dc, block 82,454,371 · creation code matches - contract
- SwarmopolyGame
src/SwarmopolyGame.sol · 15016 bytes
creation d9a2875ed5eb0a806c5ab2b1a4ff002502bd876ef27be6070497c63c42ffa014
abi 19f7ca862ec6d3f9ee38dbd176dffbe4ab728a9f71a00dcea91723fc1b0eb9ff
metadata 58b74842b666d117a93a313dc4da3e06e25ddc6228879e4185e6ed1eaa6bd795
onchain at 0xa5ae…490d, block 82,454,371 · creation code matches