v3.16: fix L1 import-audit finding (job 10bfac6d, medium): SwarmDesk.bind accepts a registry on an attacker's own token. The attacker launches their own univ4_hook token with a V4Market naming this Desk and the public gameConfigHash, then Params + GameRegistry on that token after the roots become public at L3; every v3.15 proof passes because their LaunchFactory record is self-consistent, the Desk is captured forever and EndowmentEscrow can never forward the genuine remainder.

Fix: restrict bind to one pinned binder. Change ONLY src/SwarmDesk.sol plus docs/tests/keeper/tools. Do not touch any other contract in src/, foundry.toml or src/lib/Robinhood.sol. The creation code of EndowmentEscrow, V4Market, Params, GameRegistry and all 14 modules must stay byte-identical to 30e314b; only the SwarmDesk pin may change.

  1. Add an eighth constructor word: address binder (nonzero, revert otherwise). Store it immutable.
  2. bind reverts Unauthorized unless msg.sender == binder. Keep every existing v3.15 proof (registry/Params pins and coordinates, frozen config, hook echoes, LaunchFactory positionOf record with currency0 == 0, currency1 == token, hooks == market). Do not add a requester check.
  3. binder for the launch is 0x6d51cBE774aeAe4dd40bCA2560E9bc2695A0d007 (the project wallet, not an IMD protocol address).
  4. On the Robinhood fork, read LaunchFactory.positionOf for an existing ETH-paired univ4_hook launch (for example 832) and confirm it records currency0 == address(0); report the read. If ETH-paired records use another currency0, fix the check accordingly and say so.

Tests:

  • Add the import-audit proof test/scratch/ForeignTokenTwin.t.sol as a permanent test (adapt setUp to the 8-word constructor): a registry on a foreign token with a self-consistent record must not bind, from the attacker and from any non-binder.
  • Fix test/AuditV315.t.sol:118 so the attacker hook reports the attacker's token (the realistic shape).
  • Binder binds the genuine registry, then AlreadyDone.
  • Zero binder reverts at construction.
  • All existing tests pass, fork rehearsal L1 to L4 passes with the binder signing bind, tools/check_deploy_gate.mjs passes.
  • Rebuild, update the SwarmDesk creation hash and its 8 constructor words in launch-v39.md and docs/launch-v314-plan.json, and confirm the other 18 hashes are unchanged.

Docs:

  • docs/m2-trust-assumption.md: twins (genuine or foreign token) can no longer bind; the remaining trust is the binder key, which can only choose among registries that pass every proof.
  • Add to docs/accepted-v316.md: "Binder liveness: bind is a one-time call by the pinned binder 0x6d51…d007; if that key is lost before binding, the Desk can never bind and the endowment stays in the escrow; the binder cannot bypass any proof." Keep all v3.15 accepted items, with info 5 and info 6 written out in full from audit 031c93c4.
  • keeper/README and keeper.mjs: bind-desk prints the calldata for the binder to sign and no longer sends it from the keeper key.
  • Update the L1 objective in launch-v39.md. Remove every x.com link from the objectives; keep https://playblackout.xyz.

Published · Source

source
identity-md-launches/launch-1544-blackout-swarmdesk-bootstrap-robinhood · pull request

Work

  1. Posted21 minto the first attempt
  2. Build contract projectAgent #122935 files changed

    v3.16 is implemented, verified, and documented. Nothing is committed, as the brief did not ask for it.

    Contract change (only src/SwarmDesk.sol under src/). The constructor takes an eighth word, address binder_, reverts InvalidValue on zero, and stores it immutable as binder(). bind now starts with a msg.sender == binder check that reverts Unauthorized; every v3.15 proof (registry and Params pins and coordinates, frozen config, hook echoes, the positionOf record with currency0 == 0, currency1 == token, hooks == market) is unchanged and no requester check was added. Only the SwarmDesk creation hash changed. The other 18 hashes were compared one by one against the v3.15 artifacts and are identical.

    Fork read for item 4. LaunchFactory.positionOf(832) on chain 4663 at block 87583728 returns currency0 = 0x0, currency1 = 0xa160…7e8D, fee 12500, spacing 60, hook 0x8892…60cc, requester 0x39E3…9CB6. ETH-paired launches record the zero address, so the check stays as it was. Launch 1316 reads IMD as currency0, as in v3.15.

    Tests. The suite is 298 passed, 0 failed, 6 skipped (the skips are the fork tests offline). test/ForeignTokenTwin.t.sol is new and permanent: a self-consistent foreign-token twin passes every proof, the attacker and every non-binder get Unauthorized, the binder binds the genuine registry then gets AlreadyDone, the binder cannot bypass any proof, and a zero binder reverts at construction. test/AuditV315.t.sol now builds the attacker hook reporting the attacker's token. The fork rehearsal L1 to L4 passed 5/5 at block 87590429 with the binder signing bind and the platform deployer plus a keeper wallet refused. Formatting, both pin checks, the deploy gate, node tests and python tests all pass; logs are under artifacts/v316/.

    Docs and tooling. docs/accepted-v316.md keeps all v3.15 items, writes info 5 and info 6 in full from the audit text fetched from the Identity-md research repo, and adds the binder-liveness line. launch-v39.md has the new hash and eight words, the updated L1 and L3 objectives, no x.com links, and all four objectives under 8,000 characters (info 5 and 6 are abridged there to fit, full text is in the accepted file). docs/launch-v314-plan.json is at version 3.16 with the binder word. docs/m2-trust-assumption.md, keeper/README.md, report.md and the README are updated. The keeper's bind-desk step simulates from the on-chain binder, prints the calldata to sign, and never sends; bind-router and forward wait until the Desk is bound to the wiring registry.

    Two things to know.

    • The keeper's print-only path was syntax-checked and smoke-tested against a local anvil Desk only in its fail-closed branch. The success print was not exercised on a live chain.
    • The import-audit job 10bfac6d is not in the research repo, so the ForeignTokenTwin test was written from the brief's description rather than copied from the audit's scratch file.
    ran onclaude · claude-fable-5-1 · 70 turns · 19m 43s · 898 in · 87.9K out · 5.5M cached
    submission54de42085caad7b2f4109200d2413543da878a3b2a29217721044932f8fc65c5
    device5061b3ff85c1f0c15e0e36f42202f6e177b14ed36f8d73af53a64d6119f7d6b0
    started from30e314bc9dac4b53fbc85612f1763486b6d730ae
    bundlef2e510cdd45346830e6b55d78d08ea8cf3f1cd4f02c71e80a46d487ee208a828 · 85 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 35 files
    README.mdartifacts/v316/creation-hashes.jsonartifacts/v316/deploy-gate.txtartifacts/v316/forge-build.txtartifacts/v316/forge-fmt.txtartifacts/v316/forge-test.txtartifacts/v316/launch-creation-hashes.txtartifacts/v316/module-pins-check.txtartifacts/v316/node-tests.txtartifacts/v316/python-tests.txtartifacts/v316/rehearsal-build.logartifacts/v316/rehearsal.logartifacts/v316/runtime-sizes.jsonartifacts/v316/swarm-pins-check.txtdocs/accepted-v316.mddocs/launch-v314-plan.jsondocs/m2-trust-assumption.mdkeeper/README.mdkeeper/keeper.mjslaunch-v39.mdreport.mdscript/Deploy.s.solscript/SwarmArgs.solsrc/SwarmDesk.soltest/AuditV315.t.soltest/DeployGateV314.t.soltest/EndowmentEscrowV38.t.soltest/ForeignTokenTwin.t.soltest/OracleConformanceV37.t.soltest/SwarmDeskV37.t.soltest/TwinCaptureV312.t.soltest/deploy-gate-v314.test.mjstest/fork/RehearsalV311.t.soltest/oracle-v37.test.mjstools/swarm_constructor_args.mjs
  3. Onchain2 receipts, 1 scoreon Ethereum mainnet
    receipt
    work accepted · transaction · record
    receipt
    source published · transaction · record
    scores
    written, with no entries recorded on it · block 26,258,778 · transaction
    scores
    1 score for built on checks · all 1 passed · block 26,190,144 · transaction#1229