{"assessments":[],"deployments":[{"attestationHash":"f50ddf30bd85a62744de3d7a12b9241df98b25c84de847087ea940948e7f69cf","chainId":1,"contracts":[{"address":"0x147456d867937bba20f5bb4ee85b2e6d899e780e","blockNumber":26212913,"name":"RoyaltyDistributor","role":"other","txHash":"0xb5e3b8f8d1258233c1646bba9f36a03e0279539f7594d58df7a8d233cda5c1af"},{"address":"0x71a94bba25c46d3a8cb834549d5cb738637f046c","blockNumber":26212913,"name":"SeedClock","role":"other","txHash":"0xb5e3b8f8d1258233c1646bba9f36a03e0279539f7594d58df7a8d233cda5c1af"}],"id":"b0f86ec4-dc0b-4701-b039-51b563a6df78","manifestHash":"a7d9b5f7aa738497a5857081fdf05a887b077b64501f66fd2da86e59bbdd631b","status":"live"}],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"451ea373-4ca2-4eb3-b765-217ec8752407","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"7c44c113fd28c98b45335259faaa883239e8a5da8fc8dcee19b96757883e2878","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"c5bc04be8ec7b07dd4d3e478b46d1ae5aaf61eec741b61e9ee804d92789b1e5a","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"5141a9c0bc9da4b147f828dc6d32f3e55384645c575293fc460820ed19543358","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"33ce2ca9461245f9ee1f4e45cbe2c51fad3d13b7bf210627da30ef80a79e96c7","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"67e87f85a65f256bf71ea40248bf84245f301cd789c3bbf182fe862c0138172f","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"90a3df1238b5cb74e94c918eaaa2ee3a02eb4fb4e85c35c47c40bc6d4bdf6ebe","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"d59561da3b378461b7c749f6bd5084477d44993aa94972ddea90f542535751df","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"8c46a612f24ec34546a337bd59f924f33cc7defc0edf39767ac9e06bec7ef70f","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"64702c32481cf97fa1fd29fc026e947969d3a77a8faa027d8a792ae413198eb2","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Build and deploy SeedClock (symbol SCLOCK), a fully on-chain animated generative ERC-721 collection on Ethereum mainnet (chainId 1).\n\nSUPPLY & MINT\n- Hard cap: 5555 tokens (ids 1–5555).\n- 55 tokens minted to a treasury address (constructor param or set after deploy) on deployment. Remaining 5500 public.\n- Free mint for any wallet that holds ≥1 IMD (ERC-20 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7). Max 5 per wallet. Public mint open immediately, no allowlist, no pause after open.\n- Mint function checks IMD balance via balanceOf. No ETH or other payment required.\n\nROYALTIES & DISTRIBUTION\n- ERC721C (Limit Break style) with 6.9% (690 bps) enforced royalties.\n- Royalty recipient is a dedicated RoyaltyDistributor contract (deployed in the same job).\n- On OpenSea (and any Seaport 1.6 / 721-C compatible marketplace) the 6.9% is enforced on-chain.\n- All royalties received by the distributor are claimable by SeedClock holders proportionally to their current balance (or snapshot at claim time). Anyone can call claim(); use a simple pull pattern with a claimable balance mapping updated on royalty receipt or via a claimable-per-token calculation. No owner control over the funds.\n\nART & TRAITS\n- Fully on-chain animated SVG (SMIL or CSS animation inside the SVG so it works on OpenSea).\n- Nouns-style multi-layer generative art: background, body/form, accessory, head/face, glasses/overlay, plus 1–2 animation traits (speed, direction, pulse).\n- Visual theme: clock faces, rotating hands, seed particles, lattice/PRNG patterns, and subtle 1997-era digital aesthetics. Clean, high-contrast, visually appealing pixel or vector style. Animation should feel alive (rotating elements, pulsing seeds, moving clock hands) but stay performant.\n- Seed derivation: each token’s seed is deterministically derived from a Unix timestamp in the 1997–1998 window recovered by Claude for the NIST curves (or a simple mapping of tokenId → one of the published timestamps + candidate index). Document the exact derivation in the contract so anyone can reproduce the art.\n- tokenURI returns base64 JSON with the animated SVG embedded. No external storage.\n\nCONTRACT REQUIREMENTS\n- Not upgradeable, not pausable after mint opens.\n- Owner can only set the treasury address once (or it is immutable) and cannot mint extra tokens.\n- Include a simple on-chain view that returns the current royalty recipient and the distributor address.\n- Deploy order: RoyaltyDistributor first, then the SeedClock ERC721C that points to it, then any renderer/descriptor contracts if split.\n- All source verified, Foundry tests covering mint eligibility (IMD balance), supply limits, royalty flow, and claim function.\n\nWEBSITE (separate job or follow-up)\n- Full public site explaining the collection and the October 2026 Claude NIST curve-seed discovery (lrand48 + Unix timestamps, P-256 example, why it increases confidence there is no backdoor).\n- Sections: What is SeedClock, the 1997 timestamps story, how seeds map to art, mint instructions (connect wallet, hold ≥1 IMD), royalty claim page, contract addresses, and links to the Anthropic post and reproduction code.\n- Clean, modern, dark/light mode, mobile-friendly. Host via the swarm’s site publishing if available, or provide static files ready for IPFS/ENS.\n\nDeliver: verified contracts on mainnet, deploy script, tests, and a short README with all addresses and the exact seed-to-art mapping.\n\nToken name: SeedClock","parentJobId":null,"planHash":"9e61afc638632e887b83fd593cb936a7247959c37a1aaaaa49aabf846e5d2348","previousHash":"3c1f870ec5e91edff0c592037141ee5c0485536eb88dbc7043997e6d19247f0b","projectId":"451ea373-4ca2-4eb3-b765-217ec8752407","publication":{"commit":"89b7f5edb83760e640e8d1d27f2509a1a0676f38","deliveredAt":"2026-10-17T14:35:32.497Z","repoUrl":"https://github.com/identity-md-launches/launch-2079-seedclock"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":null,"snapshotHash":"1007e8d61376135a35611361853da460a2cca05eb03c7145fcc186677268d5d6","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"723272f2e0ec6b00eaebd7b44d229daf620973b6e5df3f026504234dbe225416","device":"ca075d17c94a854b","findings":[{"description":"_pending starts at nextReceipt[asset][holder], which is 0 for every new holder, and walks up to 64 receipts per claim, doing a binary search over sharesAt for each one. A holder who mints or buys after N receipts has to send ceil(N/64) zero-value claims (about 7k gas per receipt) before they reach the first royalty they earned. Every holder also pays about 7k gas per receipt for each receipt they did earn. Each sale's ETH royalty adds one receipt, so cost grows with the collection's trading history, not with what the holder is owed. At full supply (5555 shares), a 0.05 ETH sale pays 0.00345 ETH royalty, which is about 6.2e-7 ETH per token, while processing that receipt costs about 7e-6 ETH at 1 gwei. For a holder of one to five tokens, claiming costs more than it pays, so the brief's 'claimable proportionally' is not economically true for small holders. Anyone can also add receipts at 5555 wei each (about 109k gas per receipt, measured), which raises every holder's claim cost. Funds are never lost and pagination keeps claims callable, so this is a liveness and economic defect, not theft. Possible fixes: start nextReceipt at the current receipt count when a holder's shares go from 0 to non-zero (receipts before their first checkpoint can never pay them), or replace the receipt queue with a cumulative per-share accumulator settled in updateShares, which makes claims O(1). Note: test/RoyaltyDistributor.t.sol test_zeroEntitlementBatchCanAdvanceToLaterEarnedReceipts currently asserts the paging behaviour (first claim returns 0) and would need updating with the fix.","line":209,"path":"src/RoyaltyDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SeedClock} from \"src/SeedClock.sol\";\nimport {RoyaltyDistributor} from \"src/RoyaltyDistributor.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\ncontract ProofValidator {\n    function validateTransfer(address, address, address, uint256) external pure {}\n}\n\ncontract ProofDeployer {\n    function deploy(address treasury) external returns (RoyaltyDistributor d, SeedClock c) {\n        d = new RoyaltyDistributor(treasury);\n        c = new SeedClock(treasury, address(d));\n    }\n}\n\n/// A holder who first acquires a SeedClock after N royalty receipts must not have to pay gas to step\n/// through the N receipts in which they held no shares before reaching the royalty they earned.\ncontract LateHolderClaimProof is Test {\n    SeedClock clock;\n    RoyaltyDistributor distributor;\n    address treasury = makeAddr(\"treasury\");\n    address late = makeAddr(\"late\");\n\n    function setUp() public {\n        (distributor, clock) = new ProofDeployer().deploy(treasury);\n        vm.etch(clock.IMD(), address(new ProofIMD()).code);\n        vm.etch(clock.TRANSFER_VALIDATOR(), address(new ProofValidator()).code);\n        vm.deal(address(this), 1000 ether);\n    }\n\n    function test_lateHolderReachesFirstEarnedRoyaltyInOneClaim() public {\n        for (uint256 i; i < 640; ++i) {\n            (bool ok,) = address(distributor).call{value: 0.00345 ether}(\"\");\n            require(ok);\n        }\n        ProofIMD(clock.IMD()).mint(late, 1 ether);\n        vm.prank(late);\n        clock.mint(5);\n        (bool sent,) = address(distributor).call{value: 60 ether}(\"\");\n        require(sent);\n        // Expected: the late holder's first claim pays the 5 ether earned on the only receipt after minting.\n        // Actual: it returns 0 and advances 64 receipts; ten more zero-value claims (~4.7M gas) are needed.\n        vm.prank(late);\n        uint256 amount = distributor.claim();\n        assertEq(amount, 5 ether, \"late holder must page through pre-ownership receipts\");\n    }\n}","reproduction":"Deploy the pair atomically. Send 640 payments of 0.00345 ETH to the distributor (640 receipts). A new IMD holder then mints 5 tokens, and 60 ETH is paid (one receipt, 5 ether owed to the new holder). Expected: the new holder's first claim() returns 5 ether. Actual: it returns 0 and advances nextReceipt to 64. Ten more zero-value claims (about 4.7M gas in total, measured) are needed before the 5 ether is paid.","severity":"medium","title":"Claim cost grows with every royalty receipt ever recorded, including receipts from before the holder owned any token"},{"description":"receive() runs _syncNative/_record, which writes accountedBalance, totalReceived and roundingCredit, pushes a two-word Receipt and emits an event. Measured cost: about 177k gas for the first receipt and about 109k for each later one. Seaport forwards all gas, so OpenSea sales go through, but every sale's buyer pays this extra gas. Any marketplace, splitter or contract that pays royalties with transfer()/send() (2300 gas) reverts the whole sale or payout. This is not a fund-loss bug. A cheaper receive() (accept ETH and sync lazily through syncNative, as ERC20s already are) would remove both costs, but it changes the snapshot timing.","line":116,"path":"src/RoyaltyDistributor.sol","reproduction":"address(distributor).call{value: 1 ether, gas: 2300}(\"\") returns false. The same call with unlimited gas uses 108,815 gas (second and later receipts).","severity":"low","title":"Royalty payments need about 109k gas in receive(); senders that forward only the 2300-gas stipend revert"},{"description":"_claim rejects to == address(this), and the distributor itself has no way to call claim. Tokens sent with plain transferFrom to the distributor (it does not implement onERC721Received, but transferFrom does not check) keep earning a pro-rata share of every receipt, and that share is locked forever. The same happens for tokens sent to the SeedClock contract: an ETH claim to it reverts with PaymentFailed because it has no receive function, and ERC20 sent to it cannot be moved. There is no owner sweep, by design, so these shares dilute every other holder for good. A fix could exclude the distributor's (and collection's) shares from totalShares, or reject transfers to those addresses in SeedClock._update.","line":221,"path":"src/RoyaltyDistributor.sol","reproduction":"Treasury calls transferFrom(treasury, distributor, 2), then 55 ether is paid. distributor.claimable(distributor) is 1 ether, and claimFor(address(0), address(distributor)) reverts with InvalidReceiver. Expected: those royalties go to real holders. Actual: 1 ether is permanently locked.","severity":"low","title":"Royalties for SeedClock tokens held by the distributor or the collection contract can never be claimed"}],"hash":"0479f180825dbc3c17dadc2aa1ed1f53765946da23132bcd9be50ff1a3ade16f","nodeId":"16414b85-ef59-4879-8278-d107ea4d5fb3","outcome":"completed","summary":"I added two test files. The full suite passes with the verifier's fuzz seed: `forge build` is clean and `forge test` runs 84 tests with 0 failures, up from the 56 that were there. I also found one medium and two low defects, which are in `.imd-findings.json` instead of being blessed by tests. I changed nothing outside `test/`.\n\n**`test/Adversarial.t.sol`** (27 tests, mostly failure paths):\n- **Mint checks:** the quantity check runs before the IMD lookup, `type(uint256).max` can't overflow anything, and mint fails safely when IMD's `balanceOf` reverts or returns nothing. Only the caller's own IMD counts.\n- **Per-wallet limit:** five single mints, a batch that would cross the limit (no partial fill), and the treasury gets no extra allowance.\n- **Supply cap:** fills to exactly 5550, then 5553, then shows that batches over the cap are rejected and the last 2 tokens still mint. The wallet limit is checked before the sold-out check.\n- **Royalty discovery:** the Limit Break interface id `0xad0d7f6c`, ERC2981, the validator selector `0xcaee23ea` and the validator address; nobody can call `setTransferValidator`.\n- **Royalty split:** rounding at small sale prices, dust going to whoever holds token 1, and the claim-page boundary at exactly 64 and 65 receipts.\n- **Distributor failure paths:** claiming with nothing owed, claiming the same receipt twice, `claimFor(0)`, share updates from anyone but the collection (all reverts), the binding can't be reopened later, and syncing a non-contract or zero-balance token.\n- **Fuzz tests:** share history checked against recorded balances, and a single payment split exactly and fully across holders.\n\n**`test/DeepInvariant.t.sol`** drives random call sequences with 5 holders and an approved operator: 48 runs, 160 calls each, no reverts. Unlike the existing invariant suite, sequences go past one 64-receipt claim page. They also include ETH forced in without a sync (and later bundled into a payment), operator transfers both allowed and refused by the validator, and claims paid to a different address. The checks:\n- the distributor's ETH and token balances always equal what holders are owed (plus any ETH not yet synced);\n- every successful claim pays exactly what an independent model says is owed;\n- nobody receives more than they earned;\n- shares always match NFT balances;\n- supply never goes down or past 5555;\n- the royalty settings and validator never change.\n\nBefore relying on two constants, I checked mainnet: IMD has 18 decimals (so \"1 IMD\" is `1 ether` in raw units), and the deployed validator's codehash matches the test fixture.\n\n**Findings in `.imd-findings.json`:**\n- **Medium — claim cost grows with the number of royalty payments.** A claim walks through payment records from the holder's last claim, and for a new holder that means from the very first record. Someone who mints after 640 payments needs 11 claims (about 4.7M gas) before they get the 5 ETH they actually earned. At full supply, processing one payment costs more gas than a 1–5 token holder earns from it. The finding includes a test that fails on the current code. The existing test `test_zeroEntitlementBatchCanAdvanceToLaterEarnedReceipts` asserts this behaviour and would need updating along with a fix.\n- **Low — receiving a royalty costs about 109k gas** (177k for the first one). A sender that forwards only 2300 gas, as `transfer()`/`send()` do, reverts. OpenSea's Seaport forwards all gas so its sales go through, but each buyer pays the extra.\n- **Low — royalties are locked for tokens held by the distributor or the SeedClock contract.** Those tokens keep earning a share that nobody can ever claim.\n\nStill not covered: OpenSea's actual signed royalty approvals and the other live contracts on mainnet. That would need a test against a mainnet fork, which these offline tests can't run.","treeHash":"de8c28c5c31b7faaa7e939391f0c629deccc6b86","usage":{"cachedInputTokens":1616503,"inputTokens":40,"model":"claude-opus-5-5","outputTokens":34260,"runtime":"claude","turns":23,"wallClockMs":584624}},{"artifacts":[],"attempt":1,"bundleHash":"bb624569207945b339bf29de93b4fe47d430fa637c274c344e4ec2b900dec986","device":"55ad97181c96c1ba","findings":[],"hash":"06ee04b6c74d6937a31edcdf7164757e06b5fc27ab77b9d071f2a22406333300","nodeId":"4b1c2318-f4c9-46a6-a20f-06b53fec19ee","outcome":"completed","summary":"Created [launch.json](/home/identitymd/.identitymd/work/451ea373-4ca2-4eb3-b765-217ec8752407/4b1c2318-f4c9-46a6-a20f-06b53fec19ee/launch.json) with both contracts in dependency order, matching constructor ABIs and using `$owner` for treasury.\n\n`forge build` succeeded. All 56 tests passed with the specified fuzz seed. Only `launch.json` is changed for submission.","treeHash":"1a41e5ed3d76f4615e78989dd62355114f3d51a3","usage":{"cachedInputTokens":436992,"inputTokens":35999,"model":"gpt-6-astra","outputTokens":3356,"runtime":"codex","turns":4,"wallClockMs":134436}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"62491201fe455e89","findings":[{"citation":"resolved","description":"Every ETH receipt whose amount is at least totalShares wei appends a Receipt, and _pending/_claim process a holder's receipts strictly in order, MAX_CLAIM_RECEIPTS (64) per call, with no way to skip receipts in which the holder's entitlement is negligible or zero (line 211: end = start + MAX_CLAIM_RECEIPTS). Anyone can therefore append a receipt for 56 wei today (5555 wei after mint-out). Measured on this code: 2,560 receipts cost the spammer about 143.5M gas in one loop (about 56k gas each). After that, a holder with 1 token needs 41 claimFor transactions totalling about 17.8M gas (about 434k gas each) to reach a royalty that arrived after the spam. The cost is paid by EVERY holder independently (up to 5,555 wallets), so the attacker's one-off spend is amplified roughly 700x across the collection, and any holder whose entitlement is worth less than their paging gas rationally abandons it: the brief's guarantee that all royalties are claimable by holders is broken in practice. The same cost hits late joiners without an attacker: a buyer who enters after N organic receipts must make ceil(N/64) empty claim calls per asset before their first payout (SECURITY.md acknowledges empty batches but not the magnitude). Fix options that keep the design: (a) credit sub-threshold receipts as dust instead of pushing a Receipt (e.g. require perToken * totalShares >= a minimum such as 1e12 wei, else add to roundingCredit), (b) start a holder's cursor at the first receipt whose revision is >= the holder's first checkpoint revision (receipt revisions are monotonic, so a binary search works), or (c) replace the receipt list with a cumulative per-share accumulator plus per-holder corrections on updateShares (dividend-token pattern), which makes claims O(1) regardless of receipt count.","line":204,"path":"src/RoyaltyDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SeedClock} from \"src/SeedClock.sol\";\nimport {RoyaltyDistributor} from \"src/RoyaltyDistributor.sol\";\n\ncontract ScratchIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract ScratchValidator {\n    function validateTransfer(address, address, address, uint256) external view {}\n}\n\n/// @dev Deploys both contracts in one call, like the factory.\ncontract ScratchFactory {\n    function deploy(address treasury) external returns (RoyaltyDistributor d, SeedClock c) {\n        d = new RoyaltyDistributor(treasury);\n        c = new SeedClock(treasury, address(d));\n    }\n}\n\n/// @dev Appends many minimal receipts in one transaction.\ncontract Spammer {\n    function spam(address payable target, uint256 n, uint256 each) external payable {\n        for (uint256 i; i < n; ++i) {\n            (bool ok,) = target.call{value: each}(\"\");\n            require(ok);\n        }\n    }\n}\n\ncontract ReceiptGriefingTest is Test {\n    RoyaltyDistributor distributor;\n    SeedClock clock;\n    address treasury = makeAddr(\"treasury\");\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        (distributor, clock) = new ScratchFactory().deploy(treasury);\n        vm.etch(clock.IMD(), address(new ScratchIMD()).code);\n        vm.etch(clock.TRANSFER_VALIDATOR(), address(new ScratchValidator()).code);\n        ScratchIMD(clock.IMD()).mint(alice, 1 ether);\n        vm.prank(alice);\n        clock.mint(1);\n        vm.deal(address(this), 100 ether);\n    }\n\n    /// A single 2,560-receipt spam costs the attacker one block's worth of gas, after which every\n    /// holder needs 41 claim transactions (~434k gas each, ~17.8M total) before reaching one real royalty.\n    function test_realRoyaltyClaimableInOneCallAfterDustReceiptSpam() public {\n        Spammer spammer = new Spammer();\n        uint256 n = 2560;\n        uint256 each = distributor.totalShares(); // 56 wei: smallest amount that appends a receipt.\n        uint256 gasBefore = gasleft();\n        spammer.spam{value: n * each}(payable(address(distributor)), n, each);\n        uint256 attackerGas = gasBefore - gasleft();\n        assertEq(distributor.receiptCount(address(0)), n);\n\n        // A real royalty arrives after the spam.\n        (bool ok,) = address(distributor).call{value: 56 ether}(\"\");\n        assertTrue(ok);\n\n        uint256 holderGas;\n        uint256 calls;\n        while (distributor.nextReceipt(address(0), alice) < distributor.receiptCount(address(0))) {\n            gasBefore = gasleft();\n            distributor.claimFor(address(0), alice);\n            holderGas += gasBefore - gasleft();\n            ++calls;\n        }\n        emit log_named_uint(\"attacker gas for spam\", attackerGas);\n        emit log_named_uint(\"holder claim transactions needed\", calls);\n        emit log_named_uint(\"holder total claim gas\", holderGas);\n        assertEq(alice.balance, 1 ether + n); // every unit is eventually paid ...\n        // ... but a holder should be able to reach a real royalty without paging through junk.\n        assertEq(calls, 1, \"holder forced through dust receipts before the real royalty\");\n    }\n}","reproduction":"State: fresh deployment, alice minted token 56 (totalShares = 56). 1) A contract loops 2,560 times: distributor.call{value: 56}(\"\") -> receiptCount(address(0)) == 2560, attacker gas about 143.5M. 2) Real royalty: distributor.call{value: 56 ether}(\"\"). 3) alice calls claimFor(address(0), alice) repeatedly until nextReceipt == receiptCount. Expected: alice reaches her 1 ETH in one claim (or without paying for junk). Actual: 41 claim transactions, about 17.8M gas in total, before the 1 ETH (plus 2,560 wei) is paid. Run test/scratch/ReceiptGriefing.t.sol.","severity":"medium","snippet":"        if (perToken != 0) _receipts[asset].push(Receipt(revision, perToken));","title":"Receipt queue is append-only and must be paged in order: 56-wei spam (or ordinary history) forces every holder through O(receipts) claim transactions before reaching real royalties"},{"citation":"resolved","description":"updateShares credits shares to any `to` the ERC-721 accepts. OpenZeppelin ERC721.transferFrom only rejects to == address(0), the strict registry allows owner gifts, and SeedClock._update (line 159) forwards the transfer without checking that `to` is not the distributor or the collection itself. Once a token sits at address(distributor), 1/totalShares of every subsequent ETH and ERC-20 receipt is added to shares[distributor]'s entitlement. No claim path can pay it: _claim reverts InvalidReceiver when to == address(this), claimFor pays only `holder`, the distributor never calls itself, and there is no sweep by design. accountedBalance[asset] therefore stays above zero forever and the stuck amount grows with every royalty. The same happens for a token sent to address(clock), whose share accrues to the collection contract, which cannot call claim either. Minimal fix preserving the no-owner design: in SeedClock._update revert when to == distributor || to == address(this) (mirroring the existing zero-address guard), or in updateShares revert when to == address(this) || to == collection.","line":111,"path":"src/RoyaltyDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SeedClock} from \"src/SeedClock.sol\";\nimport {RoyaltyDistributor} from \"src/RoyaltyDistributor.sol\";\n\ncontract ScratchIMD2 is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract ScratchValidator2 {\n    function validateTransfer(address, address, address, uint256) external view {}\n}\n\ncontract ScratchFactory2 {\n    function deploy(address treasury) external returns (RoyaltyDistributor d, SeedClock c) {\n        d = new RoyaltyDistributor(treasury);\n        c = new SeedClock(treasury, address(d));\n    }\n}\n\ncontract StrandedSharesTest is Test {\n    RoyaltyDistributor distributor;\n    SeedClock clock;\n    address treasury = makeAddr(\"treasury\");\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        (distributor, clock) = new ScratchFactory2().deploy(treasury);\n        vm.etch(clock.IMD(), address(new ScratchIMD2()).code);\n        vm.etch(clock.TRANSFER_VALIDATOR(), address(new ScratchValidator2()).code);\n        ScratchIMD2(clock.IMD()).mint(alice, 1 ether);\n        vm.prank(alice);\n        clock.mint(1);\n        vm.deal(address(this), 100 ether);\n    }\n\n    /// Owner gift of token 56 to the distributor itself is accepted by transferFrom and by the\n    /// registry's owner-gift path. From then on 1/56 of every receipt is credited to the distributor\n    /// address, which no claim variant can pay out (InvalidReceiver guards `to == address(this)`,\n    /// and the distributor never calls itself). The ETH is stuck forever.\n    function test_tokenSentToDistributorDoesNotStrandRoyalties() public {\n        vm.prank(alice);\n        clock.transferFrom(alice, address(distributor), 56);\n        assertEq(clock.ownerOf(56), address(distributor));\n        assertEq(distributor.shares(address(distributor)), 1);\n\n        (bool ok,) = address(distributor).call{value: 56 ether}(\"\");\n        assertTrue(ok);\n        assertEq(distributor.claimable(address(distributor)), 1 ether);\n\n        // Nobody can move that entitlement: claimFor pays `holder` and refuses address(this).\n        vm.expectRevert(RoyaltyDistributor.InvalidReceiver.selector);\n        distributor.claimFor(address(0), address(distributor));\n\n        distributor.claimFor(address(0), treasury);\n        // Expected: everything received is claimable by some wallet. Actual: 1 ether is stuck.\n        assertEq(address(distributor).balance, 0, \"royalties stranded on the distributor's own share\");\n    }\n}","reproduction":"State: fresh deployment, alice owns token 56 (totalShares = 56). 1) vm.prank(alice); clock.transferFrom(alice, address(distributor), 56) -> succeeds, shares[distributor] == 1. 2) distributor.call{value: 56 ether}(\"\") -> claimable(distributor) == 1 ether. 3) claimFor(address(0), address(distributor)) reverts InvalidReceiver; claimFor(address(0), treasury) pays 55 ether. Expected: all 56 ether claimable by some wallet. Actual: address(distributor).balance == 1 ether forever, and every later receipt strands another 1/56. Run test/scratch/StrandedShares.t.sol.","severity":"medium","snippet":"        ++shares[to];","title":"Plain transferFrom to the distributor (or collection) address is accepted; the token's share of every future receipt is credited to an address that can never claim, permanently stranding ETH/ERC-20"},{"citation":"resolved","description":"Native receipts are snapshotted in receive() at the moment of payment, and the suite asserts that new mints cannot claim earlier royalties (test_newMintsCannotClaimEarlierRoyalties). ERC-20 royalties (WETH offers on OpenSea are the common case) are only snapshotted when someone calls syncToken, at the revision current then. Because mint is free for any wallet with 1 IMD and the same IMD can be moved between wallets (SECURITY.md), a watcher who sees WETH land on the distributor can mint up to 5 tokens per wallet, then call syncToken, and receive minted/totalShares of royalties that accrued while they held nothing. Holders at sale time lose that fraction. The brief allows 'current balance or snapshot at claim time', and SECURITY.md notes ordering can affect the snapshot, so this is reported as a design asymmetry between the two asset branches rather than a bypass. If sale-time attribution is intended for WETH too, the cheapest mitigation is documentation plus a keeper that syncs promptly; a stronger one is a per-asset balance snapshot in updateShares (call syncToken-style accounting for a small allowlist of assets before shares change).","line":134,"path":"src/RoyaltyDistributor.sol","reproduction":"State: fresh deployment (supply 55, all treasury), alice holds 1 IMD and no SeedClock. 1) weth.mint(address(distributor), 60e18) (royalty from a WETH-settled sale). 2) vm.prank(alice); clock.mint(5) -> totalShares 60. 3) distributor.syncToken(weth). Expected (ETH-path semantics): claimable(weth, alice) == 0, treasury == 60e18. Actual: claimable(weth, alice) == 5e18, claimable(weth, treasury) == 55e18. With ETH instead (pay 60 ether before the mint) alice gets 0, as test_newMintsCannotClaimEarlierRoyalties shows.","severity":"low","snippet":"        _record(asset, balance - accountedBalance[asset]);","title":"ERC-20 royalties snapshot at sync time, so free mints or transfers made after the royalty arrived capture it; the ETH path explicitly prevents this"},{"citation":"resolved","description":"The 'indivisible dust' comment assumes wei-scale units. amount % totalShares is bounded by totalShares - 1 base units, which is up to 5,554 units once minted out. For 18-decimal assets that is negligible, but for an 8-decimal asset (WBTC) 5,554 sats is about 5 USD at 100k USD/BTC per receipt, for a 6-decimal stablecoin it is 0.0055 USD, and for a 0 or 2 decimal token it can be the whole receipt. Any receipt smaller than totalShares units is allocated 100 percent to firstTokenOwner, who is the treasury unless token 1 is sold, so a stream of small low-decimal receipts is systematically redirected from proportional holders to one wallet. Fix: scale perToken by a magnifier (e.g. 1e18 or 2**128) before dividing, and only credit the sub-unit remainder of the magnified value, or accumulate remainders into the next receipt instead of crediting one holder.","line":202,"path":"src/RoyaltyDistributor.sol","reproduction":"State: minted out, totalShares = 5555, treasury holds token 1. An 8-decimal token sends 5,554 units (0.00005554 WBTC, about 5.5 USD) and anyone calls syncToken(token). Expected: proportional split. Actual: perToken = 5554 / 5555 = 0, no receipt is pushed, roundingCredit[token][treasury] += 5554; the other 5,499 holders receive 0. Repeating with 11,109 units gives 1 unit per token and 5,554 units to treasury (50 percent of the receipt).","severity":"low","snippet":"        roundingCredit[asset][firstTokenOwner] += amount % totalShares;","title":"Remainder of each receipt goes entirely to token 1's holder; for low-decimal ERC-20s that remainder is up to 5,554 base units per receipt, which is not dust"},{"citation":"resolved","description":"index is read as _receipts[asset].length before the conditional push. When perToken == 0 nothing is pushed, but the event still reports that index as the receipt. The next receipt that is pushed reuses the same index, so an indexer keyed by (asset, receipt) sees two different amounts for one index and cannot reconstruct the queue that claim() will walk. Fix: emit type(uint256).max (or a separate RoyaltyDust event) when no receipt is pushed, or emit index only inside the branch.","line":205,"path":"src/RoyaltyDistributor.sol","reproduction":"State: fresh deployment, totalShares = 55. 1) distributor.call{value: 1}(\"\") -> event RoyaltyReceived(address(0), 1, 1, 0) while receiptCount(address(0)) stays 0. 2) distributor.call{value: 55}(\"\") -> event RoyaltyReceived(address(0), 55, 1, 0) and receipt(address(0), 0).perToken == 1. Expected: receipt index unique per stored receipt. Actual: index 0 is announced twice for different amounts.","severity":"low","snippet":"        emit RoyaltyReceived(asset, amount, revision, index);","title":"RoyaltyReceived emits a receipt index for receipts that are not stored, so the same index is later reused by a real receipt"},{"citation":"resolved","description":"Math.mulDiv truncates, so the fee leaks at most 1 wei per sale to the seller. Marketplaces compute the enforced fee from their own order data and ERC-2981 implementations commonly floor, so impact is negligible; recorded for completeness of the rounding-direction pass. Math.mulDiv also means no overflow for any salePrice.","line":88,"path":"src/SeedClock.sol","reproduction":"clock.royaltyInfo(1, 14) returns 0; clock.royaltyInfo(1, 1e18 + 1) returns 69000000000000000 (floor of 69000000000000000.069).","severity":"info","snippet":"        return (distributor, Math.mulDiv(salePrice, ROYALTY_BPS, 10_000));","title":"royaltyInfo rounds the fee down; the auditor checklist expects fees to round up (sale prices below 15 wei pay 0 royalty)"}],"hash":"33ce2ca9461245f9ee1f4e45cbe2c51fad3d13b7bf210627da30ef80a79e96c7","nodeId":"20b3f20a-457d-43c9-ba42-e9f3fefe1e37","outcome":"completed","summary":"The review is complete. The findings file is written and validated, both proof tests fail on the current code for the stated reasons, and no repository file outside the scratch area and `.imd-findings.json` was touched.\n\n**What I covered**\nI applied the Math Precision, Boundary and Numerical Gap guides to `RoyaltyDistributor`, `SeedClock`, `SeedData` and `SeedArt`. I traced every division, remainder, cast and coordinate computation, the checkpoint binary search, the receipt conservation invariant, every external call and sentinel branch, and verified on mainnet via RPC that IMD has 18 decimals so the one-whole-IMD mint threshold is correct. The lrand48 skip-ahead, the published seed vectors and the SVG coordinate bounds all hold. The existing 56-test suite passes.\n\n**Findings written to `.imd-findings.json`** (6 findings, 20 coverage rows, all 16 entry points answered)\n\n1. **Medium, with proof.** Receipt queue griefing in `RoyaltyDistributor._record`. Anyone can append a receipt for 56 wei. Receipts must be paged in order, 64 per claim. Measured: 2,560 spam receipts cost the spammer about 143M gas once, then every holder needs 41 claim transactions and about 17.8M gas to reach a real royalty. Late joiners pay the same cost even without an attacker.\n2. **Medium, with proof.** A plain `transferFrom` to the distributor's own address is accepted, and the token's share of every future receipt is credited to an address no claim variant can pay. ETH is stranded permanently and grows with each royalty.\n3. **Low.** ERC-20 royalties snapshot at sync time, so free mints after a WETH royalty lands capture a share of it. The ETH path explicitly prevents this. Documented as an ordering caveat, so reported as an asymmetry.\n4. **Low.** The per-receipt remainder goes entirely to token 1's holder. For low-decimal assets that is up to 5,554 base units per receipt, which for an 8-decimal token is real money and can be the whole receipt.\n5. **Low.** `RoyaltyReceived` emits a receipt index for dust receipts that are never stored, so the index is reused by the next real receipt.\n6. **Info.** `royaltyInfo` floors the fee where the checklist expects fees to round up. Impact is at most 1 wei per sale.\n\n**Not reached in depth**\nThe transfer-validator policy semantics and OpenSea zone behaviour are outside my area and were only checked for math and boundary effects.","treeHash":null,"usage":{"cachedInputTokens":1367651,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":33718,"runtime":"claude","turns":34,"wallClockMs":529563}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ce6eaff570c608ab","findings":[],"hash":"5141a9c0bc9da4b147f828dc6d32f3e55384645c575293fc460820ed19543358","nodeId":"b3ae5e4b-2708-4ac0-8007-9851f76bf691","outcome":"completed","summary":"The review is complete and the findings file is written. Nothing is outstanding.\n\n**Outcome:** every finding from the previous round is settled, and no new defect reproduced. `.imd-findings.json` holds an empty findings list and a coverage record answering all 15 entry points plus the six prior findings and the checked invariants.\n\n**Prior findings, settled:**\n\n- **Claim cursor O(all receipts) [medium]:** fixed. Receipts now store a cumulative per-token total and claims walk constant-balance segments with binary searches. The attached proof passes. A holder who mints after 2,000 spam receipts gets 1 ETH in a single claim of about 130k gas.\n- **Shares stranded on distributor or collection [medium]:** fixed. SeedClock rejects both sinks on every transfer path. The attached proof passes through its \"transfer rejected\" branch. The audit_math proof fails only because it demanded the other fix shape.\n- **WETH snapshotted at sync time [low]:** fixed. WETH is synced inside every share change before balances move. I confirmed WETH arriving before a transfer goes to the seller and before a mint goes to prior holders.\n- **Remainder to token 1 holder [low]:** fixed with a per-asset carry. Conservation holds as balance equals entitlements plus carry.\n- **Event index reuse [low]:** fixed with a sentinel index. The reordered event keeps both runtimes free of the forbidden opcode bytes under the floor's linear scan.\n- **Receive gas stipend and registry trust dependency:** author disputed both as intended design. I accept both as documented trade-offs rather than defects.\n\n**Independent verification this round:** a scratch differential test ran 400 randomized sequences of mints, transfers and payments with partial claims and pagination against a brute-force per-receipt model, with exact agreement and full conservation. The full project suite passes (96 tests including stateful invariants). The protected floor test could not run locally because it needs the launch environment variables, so I replicated its size and opcode scan on the atomically deployed pair, and it passes. Slither and aderyn leads were checked and none reproduce as defects. Scratch files were removed and the tree is clean apart from the findings file.","treeHash":null,"usage":{"cachedInputTokens":1344872,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":20389,"runtime":"claude","turns":21,"wallClockMs":398960}},{"artifacts":[],"attempt":1,"bundleHash":"98e5a4e4c2ba45447972d8caac38473dc32bfec6eeedb82867ad10c02b6a2d8a","device":"fcb71e606c933181","findings":[],"hash":"64702c32481cf97fa1fd29fc026e947969d3a77a8faa027d8a792ae413198eb2","nodeId":"16414b85-ef59-4879-8278-d107ea4d5fb3","outcome":"completed","summary":"My tests had stopped compiling because the distributor was rewritten. I've updated them to the new accounting, and the full `forge test` now passes with the verifier's fuzz seed: 92 tests across 8 suites, none failing. I found no defect in the new code, so I didn't write `.imd-findings.json`.\n\n**What broke:** the old distributor gave each payment's leftover wei (the part that doesn't divide evenly across tokens) to whoever held token 1, and exposed `firstTokenOwner()`. The new one keeps that remainder in `undistributed[asset]` and adds it to the next payment. My tests assumed the old rule, and one called the removed function.\n\n**`test/DeepInvariant.t.sol`** (random-sequence invariant tests)\n- The expected-payout model now carries the leftover forward for ETH and the test token separately, the same way the contract does.\n- The balance checks now say: what the contract has recorded = unclaimed payouts + the carried leftover, and its actual ETH balance also includes ETH that was force-sent and not yet synced.\n- New checks: the contract's carried leftover always matches the model and stays below the token supply.\n- I removed the `firstTokenOwner` check.\n\n**`test/Adversarial.t.sol`** (failure-path tests)\n- I replaced the old leftover test with one showing a payment too small to split is carried forward. Token 1's holder gets nothing extra, and a claim before the next payment fails with \"nothing to claim\".\n- The single-payment fuzz test now expects the treasury to get only its per-token share, with all holders' shares plus the carried leftover adding up exactly to the payment.\n- New: a claim now stops after 64 stretches of unchanged balance, rather than 64 payments. The test changes a holder's balance 80 times; the first claim stops at 64, the second finishes, and the two add up to exactly what's owed.\n- New: a wallet that mints after 300 earlier payments collects its payout in one call.\n- New: a token payment too large for the contract's per-token counter is rejected, and nothing is recorded.\n\n**Minor behaviour, not a defect:** the carried leftover isn't tied to who held tokens when it arrived. Anyone who mints before the next payment shares in it. It is always less than one wei per token in existence, so I left it out of the findings file.\n\nI changed only those two files and haven't committed them.","treeHash":"8246edab97e9458ed1d4e9e4f18d0c0011fbc0c8","usage":{"cachedInputTokens":560096,"inputTokens":22,"model":"claude-opus-5-5","outputTokens":7712,"runtime":"claude","turns":11,"wallClockMs":161213}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8472a2af9a9cf974","findings":[{"citation":"resolved","description":"Asymmetry between the share ledger and the claim path. SeedClock.transferFrom (not the safe variants, which fail the receiver check) lets any owner move a token to the RoyaltyDistributor or to SeedClock itself: the strict registry allows owner-initiated gifts (caller == from) and plain transferFrom performs no receiver check. updateShares then credits shares[distributor] (or shares[clock]) and every later receipt allocates perToken * shares to that address. But _claim rejects to == address(this) for the distributor, and SeedClock has no receive(), so claimFor(asset, address(distributor)) reverts InvalidReceiver and claimFor(asset, address(clock)) reverts PaymentFailed. Nothing else can move those units: there is no sweep, and the dust rule only covers amount % totalShares. The SECURITY.md guarantee 'every unit is claimable' and the Stateful invariant 'all funds claimable' are broken for the lifetime of the collection by one mistaken (or malicious, since anyone can mint 5 and gift them) transfer. The same holds for the firstTokenOwner dust if token 1 is ever gifted to the distributor. Minimal fix that preserves the design: in SeedClock._update revert when to == distributor || to == address(this); alternatively, in _record treat shares held by address(this)/collection as dust credited to firstTokenOwner.","line":221,"path":"src/RoyaltyDistributor.sol","reproduction":"Fixture: deploy RoyaltyDistributor+SeedClock atomically (treasury T), etch an IMD mock at SeedClock.IMD and a permissive validator at SeedClock.TRANSFER_VALIDATOR. 1) alice holds 1 IMD, alice.mint(1) -> token 56, totalShares 56. 2) vm.prank(alice); clock.transferFrom(alice, address(distributor), 56) succeeds; distributor.shares(address(distributor)) == 1. 3) send 56 ether to distributor -> perToken = 1 ether. 4) distributor.claimFor(address(0), T) pays 55 ether. 5) distributor.claimFor(address(0), address(distributor)) reverts InvalidReceiver; claim()/claimTo cannot be called by the distributor either. Actual: accountedBalance(address(0)) == 1 ether and address(distributor).balance == 1 ether forever, and 1/56 (later 1/5555) of every future receipt is stuck likewise. Expected: either the transfer is rejected or the entitlement is claimable by someone. Same with token 57 gifted to address(clock): claimFor(address(0), address(clock)) reverts PaymentFailed. Verified in test/scratch/Probe.t.sol test_sharesHeldByDistributorAreUnclaimable.","severity":"low","snippet":"        if (to == address(0) || to == address(this)) revert InvalidReceiver();","title":"Shares held by the distributor or the collection contract itself accrue royalties nobody can ever claim"},{"citation":"resolved","description":"Native-vs-ERC20 branch asymmetry (economics x asymmetry seam). receive() snapshots ETH royalties at the revision of receipt, so a wallet that mints or buys afterwards gets nothing from them (test_newMintsCannotClaimEarlierRoyalties). WETH royalties, which is what every accepted OpenSea offer pays, sit unaccounted until anyone calls syncToken, and are then snapshotted at the current revision. Whoever controls ordering picks the snapshot: a wallet holding >= 1 IMD can mint 5 tokens for free immediately before calling syncToken(WETH) and take 5/totalShares of all WETH accumulated before it held anything; a buyer whose own purchase paid the WETH royalty can sync right after the sale and receive part of the fee back, while the seller who held the tokens when the fee was earned gets nothing. The brief tolerates either receipt-time or claim-time snapshots, so this is reported as a broken internal guarantee ('A new mint cannot collect pre-mint royalties', docs/SECURITY.md) that holds for ETH but not for the dominant WETH path, rather than a brief violation. Minimal fix preserving the design: make updateShares snapshot the tracked WETH asset (constant 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 on mainnet, code-checked on first use) by calling the syncToken logic before shares change, so WETH received between two share changes is attributed to the holders of record at the time it arrived.","line":132,"path":"src/RoyaltyDistributor.sol","reproduction":"Same fixture, plus a mock ERC20 'weth'. 1) alice.mint(5) -> tokens 56..60, totalShares 60. 2) weth.mint(address(distributor), 6 ether) and send 6 ether of ETH to the distributor (both royalties earned while bob holds nothing). 3) bob holds 1 IMD, bob.mint(5) -> 61..65, totalShares 65. 4) vm.prank(bob); distributor.syncToken(address(weth)). Actual: claimable(weth, bob) == 461538461538461535 (5 * 6e18/65) while claimable(bob) for ETH == 0; alice gets 461538461538461535 WETH but 500000000000000000 ETH. Expected (if the ETH rule is the intended one): bob 0 WETH, alice 5 * 6e18/60 = 0.5 WETH. Verified in test/scratch/Probe.t.sol test_erc20RoyaltyCapturedByLaterMinter.","severity":"low","snippet":"        uint256 balance = IERC20(asset).balanceOf(address(this));\n        if (balance < accountedBalance[asset]) revert UnsupportedBalanceDecrease();\n        _record(asset, balance - accountedBalance[asset]);","title":"ERC-20 royalties (OpenSea WETH offers) are attributed at sync time, not receipt time, unlike ETH: later minters and buyers capture royalties earned by earlier holders"},{"citation":"resolved","description":"nextReceipt[asset][holder] defaults to 0 and _pending walks receipts [start, start+64) regardless of whether the holder had any shares at those revisions, so a wallet that mints or buys after N receipts must send ceil(N/64) claim transactions that each do 64 receipt reads plus binary searches and pay out 0, before reaching the first receipt it is entitled to. The queue is permissionless to grow: any amount >= totalShares wei (5555 wei at full supply) pushes a receipt, and the attacker pays ~100k gas per receipt once while every late holder pays ~6.3k gas per receipt, so with thousands of holders the griefing is amplified several-hundred-fold. The design is documented ('Long histories may need repeated claims') but the amplification makes small entitlements uneconomic to claim. Minimal fix preserving the design: on the first claim for a holder with nextReceipt == 0 (or whenever sharesAt(holder, receipts[start].revision) == 0), binary-search _receipts[asset] for the first receipt whose revision >= the holder's first checkpoint revision and start there; this is O(log n) and changes no entitlement.","line":209,"path":"src/RoyaltyDistributor.sol","reproduction":"Same fixture. 1) alice.mint(1) -> totalShares 56. 2) loop 640 times: send 5555 wei to the distributor (perToken = 99, so each creates a receipt); receiptCount(address(0)) == 640. 3) bob.mint(1). 4) bob calls claim() until nextReceipt(address(0), bob) == 640. Actual: 10 claim() transactions, 4,009,490 gas in total, every one returning 0 (bob had no shares at any of those revisions). Expected: one call that starts at the first receipt after bob's first checkpoint. Scaling: 10,000 organic or spam receipts -> 157 transactions (~63M gas) for a late holder entitled to 1/5555 of future receipts. Verified in test/scratch/Probe.t.sol test_lateHolderPagesThroughHistory.","severity":"low","snippet":"        uint256 start = nextReceipt[asset][holder];","title":"A holder's claim cursor starts at receipt 0, so late holders must page through every receipt that predates them; anyone can inflate that queue for 5555 wei per receipt"},{"citation":"resolved","description":"Not a permission bypass; documented here as the trust gap the brief's 'enforced royalties' guarantee rests on. Every non-owner transfer (every marketplace fill) staticcalls the fixed mainnet StrictAuthorizedTransferSecurityRegistry, whose authorizer list and policy are controlled by OpenSea, and setTransferValidator always reverts. Actors and preconditions: (a) OpenSea rotates the authorizer to a new zone or retires the registry (as happened when its earlier validator was superseded): every approved-operator transfer reverts StrictAuthorizedTransferSecurityRegistry__UnauthorizedTransfer forever, so secondary trading exists only as owner-to-owner gifts and OTC sales that pay no royalty; (b) the registry administrator can block or allow transfers for this collection at will. The owner-gift path (caller == from) also means a seller using a smart account or EIP-7702-delegated EOA can execute transferFrom itself inside a paid batch, so 'enforced on-chain' holds only for operator-executed fills. The design intentionally has no owner; if the requester wants an escape hatch, the scope decision is a one-time, treasury-only validator rotation, which trades immutability for recoverability.","line":112,"path":"src/SeedClock.sol","reproduction":"Fixture with the mainnet registry bytecode and storage (test/fixtures/MainnetValidator.sol) installed. 1) alice.mint(1); vm.prank(alice); clock.setApprovalForAll(operator, true). 2) Baseline inside one transaction (external self-call): vm.prank(SIGNED_ZONE 0x000056F7000000EcE9003ca63978907a00FFD100); registry.beforeAuthorizedTransfer(clock, 56); vm.prank(operator); clock.transferFrom(alice, bob, 56); registry.afterAuthorizedTransfer(clock, 56) -> ownerOf(56) == bob. 3) Model OpenSea rotating its authorizer: vm.store(registry, 0x08ad42b8c5cc6483fc6f741635eaa19fc52c5290bb801ae007ad7830329c2a0d, 0) and vm.store(registry, 0x729e8fd76847aff373d216f4a70fd90c2fca9a1c85c1439cb490880b9313796a, 0) (the authorizer set's value and index slots the fixture installs). 4) vm.prank(bob); clock.setApprovalForAll(operator, true); then in one transaction vm.prank(SIGNED_ZONE); registry.beforeAuthorizedTransfer(clock, 56) reverts StrictAuthorizedTransferSecurityRegistry__CallerIsNotValidAuthorizer() and vm.prank(operator); clock.transferFrom(bob, alice, 56) reverts StrictAuthorizedTransferSecurityRegistry__UnauthorizedTransfer(). 5) vm.prank(treasury); clock.setTransferValidator(address(0)) reverts RoyaltiesArePermanent(): no caller can restore marketplace transfers. 6) vm.prank(bob); clock.transferFrom(bob, alice, 56) succeeds with no royalty, so OTC sales continue. Actual: every operator-executed (marketplace) transfer is blocked for the life of the collection by an external policy change. Expected per brief: royalties enforced on compatible marketplaces, which requires the external registry to keep this collection authorized. Verified in test/scratch/Registry.t.sol test_authorizerRotationBricksOperatorTransfersForever.","severity":"info","snippet":"    function setTransferValidator(address) external pure override {\n        revert RoyaltiesArePermanent();\n    }","title":"Trust assumption: marketplace transfers depend permanently on OpenSea's registry and its authorizer, with no on-chain fallback"},{"citation":"resolved","description":"Access x asymmetry note rather than a defect for ETH: amount % totalShares is at most 5554 base units per receipt, negligible in wei but up to 55.54 units of a 2-decimal token (EURS/GUSD style) or 0.0055 USDC. With 2-decimal royalties of 100.00 the treasury (holder of token 1) receives 44.45 while all 5555 holders share 55.55. Since the brief says holders are paid proportionally and token 1 is permanently minted to the treasury, this is a small structural advantage to the launch owner; it only matters if a marketplace ever pays royalties in a low-decimal asset. Document it, or carry the remainder forward into the next receipt instead of crediting one holder.","line":202,"path":"src/RoyaltyDistributor.sol","reproduction":"Same fixture with a mock ERC20 'eurs' (2 decimals). 1) Mint until totalShares == 5555 (or use 60 shares for a smaller example: alice.mint(5), totalShares 60). 2) eurs.mint(address(distributor), 10_000) (100.00 EURS); anyone calls syncToken(eurs). Actual with 60 shares: perToken = 166, remainder 40 -> treasury claimable 55*166+40 = 9170, alice 830; with 5555 shares: perToken = 1, remainder 4445 -> treasury 55+4445 = 4500 (45.00 EURS) of a 100.00 royalty for holding 55/5555 of the supply. Expected: 100 * 55/5555 = 0.99 EURS.","severity":"info","snippet":"        roundingCredit[asset][firstTokenOwner] += amount % totalShares;","title":"Per-receipt remainder goes entirely to token 1's holder (the treasury), which is non-proportional and material for low-decimal ERC-20 royalties"}],"hash":"67e87f85a65f256bf71ea40248bf84245f301cd789c3bbf182fe862c0138172f","nodeId":"f8567a6c-40b4-4110-97e5-9056e6eab352","outcome":"completed","summary":"Review complete. The findings file is written and validated, the scratch tests are removed, and the working tree has no other changes.\n\n**Outcome:** no critical, high or medium defects in the access-control, trust-gap and asymmetry areas. The permission model is tight: neither contract has an owner, the distributor binds once inside the deployment transaction via a transient-storage window, share updates are collection-only, and every claim path pays only the entitled holder. I confirmed on mainnet that IMD has 18 decimals and that the validator codehash matches the fixture, so the hard-coded assumptions in the mint gate and transfer hook hold today.\n\n**Findings written to `.imd-findings.json`** (3 low, 2 info), each with a reproduction I ran against the code:\n\n- **Low, stuck shares.** A plain `transferFrom` gift (which the registry allows) can park a token in the distributor or the collection itself. Those shares accrue royalties nobody can claim, since claimFor to the distributor reverts and SeedClock cannot receive ETH. Verified: 1 ETH of a 56 ETH receipt locked forever.\n- **Low, ETH versus WETH asymmetry.** ETH royalties snapshot at receipt, but WETH royalties (every accepted OpenSea offer) snapshot when anyone calls syncToken. A wallet can mint five tokens for free then sync and take a share of WETH earned before it held anything. Verified: later minter gets 0.46 WETH and 0 ETH from identical 6-unit royalties.\n- **Low, cursor griefing.** New holders page through every receipt from index 0, and anyone can push a receipt for 5555 wei. Verified: 640 spam receipts cost a late holder 10 claim calls and about 4M gas for zero payout.\n- **Info, registry trust.** Marketplace transfers depend permanently on OpenSea's authorizer with no fallback, since setTransferValidator always reverts. Verified with real registry bytecode: after an authorizer rotation, operator transfers revert forever while royalty-free owner gifts continue.\n- **Info, dust rule.** The per-receipt remainder goes to token 1's holder (the treasury), which is non-proportional and only material for low-decimal royalty assets.\n\n**Coverage:** all 16 listed entry points have a row (12 hold, 4 reference findings), plus three invariant rows. Nothing was left unreached. Art rendering and seed derivation were outside my area and not reviewed beyond the static-analysis leads, which produced no reportable defect.","treeHash":null,"usage":{"cachedInputTokens":1565070,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":48230,"runtime":"claude","turns":39,"wallClockMs":781711}},{"artifacts":[],"attempt":1,"bundleHash":"ac363cead7b926c0cd71dd0f1d306002153659a183581a152ac99a78db143528","device":"8b35536ee4821baa","findings":[],"hash":"6ceeaab670df83d2b4ab75d17449a57f023d556b2be3f49d5465fcb7403b92b9","nodeId":"93f5f449-c532-4a4d-9e21-b058b2510fed","outcome":"completed","summary":"Implemented SeedClock, royalty distribution, animated on-chain art, deployment script, tests, and documentation.\n\nPassed `forge build`, all **56 tests** with the supplied fuzz seed, and `forge fmt --check`.\n\n[README and deployment handoff](/home/imd-worker/.identitymd/work/451ea373-4ca2-4eb3-b765-217ec8752407/93f5f449-c532-4a4d-9e21-b058b2510fed/README.md)\n\nMainnet deployment, final addresses, and explorer verification remain with IdentityMD’s launch service.","treeHash":"579354ac5164b6c4cd5a29f1653f354e24ab3e6b","usage":{"cachedInputTokens":4851968,"inputTokens":149888,"model":"gpt-6-astra","outputTokens":73985,"runtime":"codex","turns":9,"wallClockMs":2288958}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"23617eab0c5242d6","findings":[{"citation":"resolved","description":"Every royalty payment of >= totalShares wei pushes a Receipt (line 204), and a holder's claim must iterate receipts one by one (64 per tx) starting at nextReceipt, which defaults to 0 for every address, including addresses that held no share when those receipts were recorded. Economically: (1) a buyer/minter who first acquires a token after N receipts must send ceil(N/64) claim transactions that pay 0 (each ~410k gas measured) before reaching any receipt in which they hold a share; (2) for any holder, each receipt costs ~6.4k gas to process while it pays perToken*shares = royalty/totalShares per token. At 20 gwei a 1-token holder only breaks even on receipts from sales above ~10 ETH (0.069*price/5555 = 6400*20e-9 ETH), so most royalty income is uneconomic to claim and stays in the contract forever (no sweep by design); (3) anyone can inflate the queue for every holder at once by sending totalShares wei (5555 at full supply) per call (~102k gas per receipt measured), costing each of thousands of holders ~6.4k gas per pushed receipt. This breaks the brief's requirement that all royalties be claimable proportionally by holders; the brief suggested a claimable-per-token (accumulator) calculation, which is O(1) per claim. Minimal fix preserving the design: switch to a cumulative magnified perShare accumulator with per-holder corrections updated in updateShares (O(1) claims), or at least start a holder's cursor at the first receipt whose revision >= the holder's first checkpoint revision (receipts' revisions are monotonic, so a binary search over _receipts finds it) and coalesce receipts recorded at the same revision.","line":209,"path":"src/RoyaltyDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SeedClock} from \"src/SeedClock.sol\";\nimport {RoyaltyDistributor} from \"src/RoyaltyDistributor.sol\";\n\ncontract ScratchIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract AtomicDeployer {\n    function deploy(address treasury) external returns (RoyaltyDistributor d, SeedClock c) {\n        d = new RoyaltyDistributor(treasury);\n        c = new SeedClock(treasury, address(d));\n    }\n}\n\n/// A holder who first acquires a token after N receipts must pay for ceil(N/64) claim transactions\n/// that walk receipts in which they never held a share before reaching a single wei of their own.\ncontract ReceiptWalkTest is Test {\n    RoyaltyDistributor d;\n    SeedClock c;\n    ScratchIMD imd;\n    address treasury = makeAddr(\"treasury\");\n    address late = makeAddr(\"late\");\n\n    function setUp() public {\n        (d, c) = new AtomicDeployer().deploy(treasury);\n        vm.etch(c.IMD(), address(new ScratchIMD()).code);\n        imd = ScratchIMD(c.IMD());\n        vm.deal(address(this), 1000 ether);\n    }\n\n    function test_lateHolderMustWalkReceiptsFromBeforeTheyHeld() public {\n        // 640 ordinary royalty receipts (e.g. 640 secondary sales, or a griefer paying 55 wei each).\n        uint256 g = gasleft();\n        for (uint256 i; i < 640; ++i) {\n            (bool ok,) = address(d).call{value: 55}(\"\");\n            assertTrue(ok);\n        }\n        emit log_named_uint(\"gas per griefing receipt\", (g - gasleft()) / 640);\n        assertEq(d.receiptCount(address(0)), 640);\n\n        // A new holder appears, then a 1 ETH royalty arrives in which they hold 1/56.\n        imd.mint(late, 1 ether);\n        vm.prank(late);\n        c.mint(1);\n        (bool ok2,) = address(d).call{value: 1 ether}(\"\");\n        assertTrue(ok2);\n        uint256 entitled = uint256(1 ether) / 56;\n\n        // Expected: one claim pays the late holder their share.\n        g = gasleft();\n        vm.prank(late);\n        uint256 paid = d.claim();\n        emit log_named_uint(\"gas of first claim (zero payout)\", g - gasleft());\n        emit log_named_uint(\"paid on first claim\", paid);\n        assertEq(paid, entitled, \"late holder must first pay for 10 empty claim txs\");\n    }\n}","reproduction":"Atomic deploy (treasury holds 55 shares). Send 55 wei to the distributor 640 times -> receiptCount(0)=640. A fresh wallet `late` holding 1 IMD calls SeedClock.mint(1) (token 56). Send 1 ETH to the distributor. late calls claim(). Expected: pays 1 ether/56 = 17857142857142857 wei. Actual: pays 0, uses ~409,515 gas, advances nextReceipt to 64; late must repeat 9 more zero-paying claims (~4.1M gas total) before receiving anything. Test below fails with `0 != 17857142857142857`.","severity":"medium","snippet":"        uint256 start = nextReceipt[asset][holder];\n        uint256 length = _receipts[asset].length;\n        end = start + MAX_CLAIM_RECEIPTS;\n        if (end > length) end = length;\n        amount = roundingCredit[asset][holder];\n        for (uint256 i = start; i < end; ++i) {\n            Receipt storage item = _receipts[asset][i];\n            amount += item.perToken * sharesAt(holder, item.revision);\n        }","title":"Claim cost is O(all receipts ever recorded): new holders start their cursor at receipt 0 and every sale/griefer push adds per-holder claim gas that dwarfs per-token payouts"},{"citation":"resolved","description":"ETH royalties are recorded in receive() at the revision current when paid, so tokens minted later cannot share them (test_newMintsCannotClaimEarlierRoyalties). ERC20 royalties (WETH from accepted OpenSea offers/collection offers, which pay creator fees in WETH) cannot trigger a hook and are only recorded when someone calls syncToken, at whatever share distribution exists then. While the public mint is open, minting is free and the IMD gate is per-balance-at-call (one IMD can be passed through many wallets), so a backrunner can mint new shares at zero cost after every WETH royalty lands and then call syncToken in the same transaction, diluting the holders who held when the royalty was earned. Honest holders cannot prevent it: the royalty arrives inside the marketplace tx and the attacker backruns it. The brief allows 'current balance (or snapshot at claim time)', so severity depends on the requester's intent, but it gives a different, exploitable rule for WETH than for ETH, and new mints taking earlier royalties is exactly what the ETH path was designed to stop. Fix options that keep the design: snapshot ERC20 receipts at the last revision observed before the token's arrival is not knowable on-chain, so instead (a) make SeedClock._update call syncToken for a configured WETH constant before changing shares (so any share change first records pending WETH at the old distribution), or (b) document the race and treat WETH as current-balance-at-sync.","line":129,"path":"src/RoyaltyDistributor.sol","reproduction":"Fixture deploy; alice mints 2 (57 shares). 6.9 WETH (mock ERC20) is transferred to the distributor as an offer-sale royalty. Attacker with 1 IMD loops over 20 wallets: transfer IMD in, mint(5), transfer IMD out (100 new tokens, zero cost). Then syncToken(weth). Expected: the 6.9 WETH is split among the 57 tokens that existed when it was paid (treasury ~6.658 WETH). Actual: fresh mints can claim 4.3949 WETH in total; treasury gets only 2.4172 WETH. Verified with a Foundry test (claimable values logged).","severity":"medium","snippet":"    function syncToken(address asset) external nonReentrant {\n        if (collection == address(0)) revert NotBound();\n        if (asset == address(0)) revert NativeAsset();\n        uint256 balance = IERC20(asset).balanceOf(address(this));\n        if (balance < accountedBalance[asset]) revert UnsupportedBalanceDecrease();\n        _record(asset, balance - accountedBalance[asset]);","title":"ERC20/WETH royalties are snapshotted at syncToken time, so free mints (or any share inflow) after a royalty arrives capture royalties earned by earlier holders"}],"hash":"7c44c113fd28c98b45335259faaa883239e8a5da8fc8dcee19b96757883e2878","nodeId":"d9a7ac93-6831-49f2-b789-f91a57d0a0f5","outcome":"completed","summary":"I found two medium-severity problems in how the royalty money is shared out. Both are reproduced with Foundry tests and written to `.imd-findings.json`, along with a coverage row for all 15 entry points and two invariants I checked. The working tree is unchanged; I deleted my scratch tests and embedded the one that proves finding 1 in the JSON.\n\n**1. Claiming gets more expensive with every receipt ever recorded** (`src/RoyaltyDistributor.sol:209`, medium)\n- **Mechanism:** every payment of at least one wei per token adds a receipt. Each claim reads receipts one by one, 64 per transaction, and every address starts at receipt 0.\n- **New holders pay for history:** someone who buys or mints after 640 receipts gets 0 from their first claim and spends about 410k gas. They need 10 claims before any payout. The embedded test fails on this.\n- **Small holders can't afford to claim:** each receipt costs about 6.4k gas to process. At 20 gwei, a one-token holder only breaks even on royalties from sales above roughly 10 ETH. Below that, the gas costs more than the payout, so most of the money stays in the contract for good (there is no sweep, by design).\n- **Anyone can make it worse:** sending 5,555 wei at full supply (about 102k gas) adds a receipt that every holder then has to process.\n- **Fix:** the brief suggested a running per-token total, which makes each claim cost the same regardless of history. At minimum, start each holder's position at the first receipt after they first held a token.\n\n**2. WETH royalties are split by who holds tokens when someone syncs, not when they were earned** (`src/RoyaltyDistributor.sol:129`, medium)\n- ETH royalties are recorded the moment they arrive. WETH royalties (for example from accepted OpenSea offers) are only recorded when someone calls `syncToken`.\n- While the public mint is open, minting is free, and one IMD can be passed through many wallets. So someone can mint new tokens right after a WETH royalty lands, then sync in the same transaction.\n- In the test, 57 tokens existed when 6.9 WETH arrived. 20 wallets sharing one IMD minted 100 more and then synced. They could claim 4.39 WETH, and the treasury (55 tokens) got 2.42 WETH instead of about 6.66.\n- The brief does allow \"current balance (or snapshot at claim time)\", so whether this counts as a defect depends on what was intended. Even so, the ETH path was deliberately built to stop new mints taking earlier royalties, and WETH doesn't get that protection.\n\n**Checked and holding:**\n- **Royalty accounting:** money in equals what's owed, and receipts line up with the ownership snapshots. Leftover wei from each payment goes to whoever holds token 1. ETH sent back into the contract during a claim is accounted correctly, and a holder that refuses ETH can't block anyone else. The binding window only opens during the one-transaction deployment.\n- **SeedClock:** 690 bps always goes to the distributor, and royalty enforcement can't be switched off. The mint keeps the supply cap, the five-per-wallet limit and the IMD check, and it can't be re-entered.\n\n**Not reviewed:** the art libraries, including the static-analysis leads in `SeedArt` and `SeedData`. I also didn't check on mainnet that OpenSea's registry and signing zone enforce the 6.9%; the project's own tests only run the registry's code offline.","treeHash":null,"usage":{"cachedInputTokens":567571,"inputTokens":22,"model":"claude-opus-5-5","outputTokens":16999,"runtime":"claude","turns":11,"wallClockMs":200475}},{"artifacts":[],"attempt":1,"bundleHash":"53ba96d00bacef07f75ed52b332bd8807aa52b1b1e9030d727e271344c69fa54","device":"c4617da5f1fd5ce8","findings":[],"hash":"8c46a612f24ec34546a337bd59f924f33cc7defc0edf39767ac9e06bec7ef70f","nodeId":"4b1c2318-f4c9-46a6-a20f-06b53fec19ee","outcome":"completed","summary":"Done. Only launch.json changed.\n\n**What I checked**\n\n- The accepted source's constructors still match the manifest. RoyaltyDistributor takes one address, the treasury. SeedClock takes the treasury and the distributor address. The brief names no treasury wallet, so both receive `$owner`, and SeedClock's second argument is `$contract:RoyaltyDistributor`, keeping constructor dependency order.\n- Neither contract exposes an owner role. The distributor records the factory only as the expected deployment caller for its one-time constructor binding, which is not an ownership or admin power.\n- The protected deployment probe deploys both contracts in one call, so the constructor handshake between SeedClock and the distributor holds there as it does on chain.\n- The build passes and all 61 tests pass with the verifier's fuzz seed, including the three stateful invariants.\n- A local schema check confirms the manifest has only the three allowed keys, unique identifiers within 32 characters, string arguments with backward-only contract references, and notes under 4000 characters.\n\n**What I changed**\n\nThe latest accepted source commit reworked the distributor's accounting, so the previous notes described behavior that no longer exists. I rewrote the notes to match the current implementation: rounding remainders are now carried into the asset's next receipt instead of paid to token 1's holder, mainnet WETH is synced before every mint or transfer, other ERC-20s snapshot on a public sync call, claims settle at most 64 balance segments per call, and the binding handshake now also verifies the deployment caller. The contract entries and constructor arguments are unchanged.","treeHash":"227582472232feccffb3bc337a078d215f9fcbaf","usage":{"cachedInputTokens":318077,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":4627,"runtime":"claude","turns":8,"wallClockMs":117258}},{"artifacts":[],"attempt":1,"bundleHash":"5be4ef571bcfe6d82f886c81e7a55b2978610e3fb49fe5d30507abaa2012f8b3","device":"1c29c7c6e4cf57eb","findings":[],"hash":"90a3df1238b5cb74e94c918eaaa2ee3a02eb4fb4e85c35c47c40bc6d4bdf6ebe","nodeId":"93f5f449-c532-4a4d-9e21-b058b2510fed","outcome":"completed","summary":"Both medium findings reproduced and are fixed. `forge build`, `forge test` and `forge fmt --check` all pass, using the verifier's fuzz seed. The tests are 64 of 64 green, counting the two reviewer proofs and one gas-measurement test of mine in `test/scratch/`. The stateful invariants also pass at a heavier 512 runs × depth 128. Every finding has an answer in `.imd-responses.json`. I didn't commit anything.\n\n**Medium findings**\n- **Claim cost grew with every receipt (proof `175459b5f1ac`):** each receipt now stores a running per-token total. A claim works through each stretch where the holder's balance didn't change, with two binary searches per stretch, so its cost no longer depends on how many receipts exist.\n  - The proof's late holder gets the 5 ether on the first claim.\n  - A new test with 1,000 earlier receipts pays out in one claim of under 200k gas. Spamming tiny receipts no longer costs holders anything to claim through.\n  - The per-call limit is now 64 balance changes instead of 64 receipts. Only a holder whose own balance changed more than 64 times needs more than one call.\n- **Shares stranded on the distributor or SeedClock (proof `6c7c6cbaaef8`):** transfers to either contract now revert with `ERC721InvalidReceiver`. This covers both `transferFrom` and `safeTransferFrom`, and a new test checks both.\n\n**Advisory findings**\n- **WETH royalties captured by later minters:** fixed. Before any mint or transfer, the distributor first records any mainnet WETH that has arrived. A mint or purchase made after WETH lands can no longer claim part of it. I confirmed the WETH address against the reference and on chain.\n- **Remainder sent to token 1's holder:** fixed. The leftover from each split now carries into that asset's next receipt instead of going to one wallet. It is always less than one unit per token (`undistributed(asset)`), and the invariant tests cover it.\n- **Same event index reported twice:** fixed. An amount too small to create a receipt now reports `NO_RECEIPT` as its index.\n- **`receive()` fails with the 2,300-gas stipend:** disputed. Recording ETH immediately is what stops later minters from claiming earlier royalties, and Seaport forwards all gas. The cost did drop, from about 104k to about 83k gas per receipt.\n- **Permanent dependence on OpenSea's validator:** disputed as a defect. It is the intended trade-off of having no owner, pause or upgrade, and `docs/SECURITY.md` now spells it out.\n\n**Things to know about this revision**\n- **Event signature changed:** `RoyaltyReceived` fields are now ordered `(asset, amount, receipt, revision)`. With the extra WETH call, the compiler moved the old event's hash into a data section at the end of the code. That hash contains a `0xff` byte, which the launch's opcode scan reads as SELFDESTRUCT and refuses. The new order's hash has no such bytes. This is fragile: a future code change could push another constant there, so re-run the opcode scan test after any edit.\n- **Public interface changes:** `roundingCredit`, `firstTokenOwner` and `MAX_CLAIM_RECEIPTS` are gone. `MAX_CLAIM_SEGMENTS`, `NO_RECEIPT`, `WETH` and `undistributed` are new. `receipt()` now returns `(revision, perToken, cumulative)`. Any website or indexer built against the old names will need updating.\n- **Docs and tests:** `README.md`, `docs/SECURITY.md` and `docs/SITE-HANDOFF.md` describe the new accounting. I rewrote the existing tests that asserted the old rounding and pagination behaviour.","treeHash":"45eda8f5871e92f0ce1630c652fad10cfc15274a","usage":{"cachedInputTokens":1947506,"inputTokens":44,"model":"claude-opus-5-5","outputTokens":39464,"runtime":"claude","turns":26,"wallClockMs":521260}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa99051b60a858d6","findings":[{"citation":"resolved","description":"Area: First Principles / Periphery (gas complexity). Each ETH royalty payment of at least totalShares wei appends one Receipt (one per OpenSea sale). A holder's entitlement is computed by walking receipts from nextReceipt[asset][holder], which is 0 for every address that has never claimed, 64 per call, with a binary search per receipt. A holder who buys in after N receipts is owed nothing from those receipts yet must still spend ceil(N/64) transactions and ~6,200 gas per receipt before the first receipt they actually earn is reachable. Measured in test/scratch/Probe.t.sol (test_lateHolderCatchUpGas): after 6,400 receipts a fresh holder needed 101 claimFor calls and 39.4M gas (plus 21k base per transaction) to receive a 1 ETH entitlement; a holder present for the whole history pays ~463k gas per 64-receipt batch (~7.2k per receipt). Because per-token royalty per sale is salePrice*0.069/totalShares (a 1 ETH sale yields ~1.24e-5 ETH per token at full supply) while walking one receipt costs ~7.2k gas (~3.6e-5 ETH at 5 gwei), a one-token holder loses money claiming the proceeds of any sale under roughly 3 ETH, and the gap widens with gas price. The log is also inflatable by anyone: receive() appends a receipt for any payment >= totalShares wei; the same test created 6,400 receipts from a contract loop for 0.036 ETH of value and ~56k gas each (359M gas total), i.e. the attacker pays 56k gas per receipt once while every one of up to 5,555 holders pays ~6.2k gas per receipt to get past it. The brief asked for a claimable-per-token calculation; a cumulative accRoyaltyPerShare accumulator settled on updateShares makes claims O(1) regardless of history. A minimal fix that keeps the receipt design is to record the receipt index at a holder's first checkpoint (or the revision, then binary-search _receipts by revision) and start _pending from there, and to raise the minimum receipt size above totalShares wei so dust cannot append receipts.","line":214,"path":"src/RoyaltyDistributor.sol","reproduction":"State: deployed pair with validator and IMD etched (test/scratch/Probe.t.sol). 1) bob mints 1 token. 2) A contract sends 5,555 wei to the distributor 6,400 times in one transaction: receiptCount(address(0)) == 6400, cost 358,714,939 gas, 0.036 ETH. 3) alice mints 1 token, then 57 ETH of royalties arrive; claimable(alice) == 0 until she pages. 4) Loop distributor.claimFor(address(0), alice) until it returns nonzero: expected a single call paying 1 ETH; actual 101 calls and 39,408,840 gas before the 1 ETH is paid (100 zero-payout batches). 5) distributor.claimFor(address(0), bob) for one 64-receipt batch costs 463,384 gas and pays 64*5555/57 wei-scale amounts. Expected: a claim costs gas proportional to the holder's own activity, not to the global sale count; actual: cost grows with every receipt ever recorded and anyone can append receipts for 5,555 wei each.","severity":"medium","snippet":"        for (uint256 i = start; i < end; ++i) {\n            Receipt storage item = _receipts[asset][i];\n            amount += item.perToken * sharesAt(holder, item.revision);\n        }","title":"Claim cost is linear in the receipt log and every new holder starts at receipt 0, so royalties become uneconomical to claim and the queue can be inflated by anyone"},{"citation":"resolved","description":"Area: Execution Trace (sentinel/self address path). updateShares accepts any `to`, including address(this). SeedClock.transferFrom(owner, distributor, id) passes the strict validator (caller == from) and OZ ERC721 does not call onERC721Received for plain transferFrom, so the distributor becomes a holder with shares[distributor] == 1 and a checkpoint. Every later receipt allocates perToken * 1 to the distributor itself, and because _claim rejects to == address(this) (InvalidReceiver) no claim variant can ever pay that entitlement, nor can the token leave the distributor (it has no transfer function). accountedBalance keeps the locked amount forever, so address(distributor).balance can never reach zero again and the pro-rata slice is dead for the collection's lifetime. The same happens for a token sent to the SeedClock contract (ETH claim fails with PaymentFailed because SeedClock has no receive). The distributor is the published royalty recipient, so it is a plausible mis-send target. Minimal fix: in updateShares revert when to == address(this) || to == collection, or have SeedClock._update reject to == distributor || to == address(this).","line":111,"path":"src/RoyaltyDistributor.sol","reproduction":"test/scratch/Probe.t.sol test_tokenSentToDistributorLocksRoyaltySlice: alice mints id 56; vm.prank(alice); clock.transferFrom(alice, address(distributor), 56) succeeds; distributor.shares(address(distributor)) == 1. Send 56 ether to the distributor: claimable(address(distributor)) == 1 ether. distributor.claimFor(address(0), address(distributor)) reverts InvalidReceiver; after treasury claims its 55 ether, address(distributor).balance == 1 ether and no call sequence can withdraw it. Expected: the transfer to the royalty contract is rejected, or the entitlement is reallocated; actual: 1/56 of every future royalty is unrecoverable.","severity":"low","snippet":"        ++shares[to];\n        _checkpoint(to);","title":"A SeedClock token transferred to the distributor (or to SeedClock) becomes a share whose royalties can never be claimed, locking that slice of every future receipt"},{"citation":"resolved","description":"Area: Execution Trace (event vs state). index is read as _receipts[asset].length before the conditional push. When amount < totalShares no receipt is pushed, but the event still reports index == length, which is the index the next real receipt will take. Off-chain indexers keyed by (asset, receipt) will attribute the dust receipt and the next real receipt to the same index. Fix: emit type(uint256).max (or a separate RoyaltyDust event) when perToken == 0.","line":205,"path":"src/RoyaltyDistributor.sol","reproduction":"Fresh deployment (totalShares == 55). Send 1 wei to the distributor: event RoyaltyReceived(address(0), 1, 1, 0) is emitted while receiptCount(address(0)) stays 0. Send 55 wei next: event RoyaltyReceived(address(0), 55, 1, 0) again, and now receipt(address(0), 0) exists. Expected: distinct identifiers or a sentinel for non-receipts; actual: two different payments share receipt index 0.","severity":"info","snippet":"        emit RoyaltyReceived(asset, amount, revision, index);","title":"RoyaltyReceived emits a receipt index that does not exist for sub-share receipts"}],"hash":"c5bc04be8ec7b07dd4d3e478b46d1ae5aaf61eec741b61e9ee804d92789b1e5a","nodeId":"15aef837-f0fe-4da7-892d-79d69226aa99","outcome":"completed","summary":"The review is complete and written to `.imd-findings.json` at the repository root. No source or config files were changed.\n\n**Scope covered.** I traced the Execution Trace, Periphery and First Principles guides against SeedClock, RoyaltyDistributor, the two art libraries, the deploy helper and the test suite. Build and all 56 existing tests pass. I confirmed on mainnet via a public RPC that IMD has 18 decimals and that the strict validator's codehash and default OpenSea SignedZone authorizer match the offline fixture. The published lrand48 seed vectors from the Anthropic report were not independently verified.\n\n**Findings (3), all reproduced with scratch tests:**\n\n1. **Medium. Claim cost is linear in the global receipt log.** Every OpenSea sale appends a receipt, and every new holder starts paging from receipt 0 even though they are owed nothing from the past. Measured: after 6,400 receipts a fresh holder needed 101 claim calls and 39M gas before their first payout. Anyone can inflate the queue for 5,555 wei and ~56k gas per receipt. Per-token royalties per sale are small enough that a one-token holder loses money claiming on sales under roughly 3 ETH at 5 gwei. Fix: an accumulator settled in updateShares, or at minimum a per-holder starting cursor.\n2. **Low. A token transferred to the distributor locks its royalty slice forever.** Plain transferFrom to the distributor passes the owner-gift rule and skips the receiver hook. The distributor then holds a share it can never claim, since claims to self are rejected, and the token cannot leave.\n3. **Info. The RoyaltyReceived event reports a receipt index that does not exist for dust receipts**, colliding with the next real receipt's index.\n\n**Coverage.** All 16 listed entry points have a row, plus four invariant and periphery rows. Mint, both safeTransferFrom overloads, approvals, bindCollection, syncNative and syncToken hold. The claim variants, receive, updateShares and transferFrom carry finding references. No critical or high findings, so no proof files were attached.","treeHash":null,"usage":{"cachedInputTokens":1390536,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":41329,"runtime":"claude","turns":38,"wallClockMs":595147}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"2d027bc56749d95c","findings":[{"citation":"resolved","description":"Merged from audit_math, audit_flow, audit_permissions, audit_economics and write_foundry_tests (same root cause). _record pushes one Receipt for every ETH payment of at least totalShares wei (line 204) and _pending/_claim walk a holder's receipts strictly in order from nextReceipt[asset][holder], 64 per call, with a binary search over the holder's checkpoints for each. nextReceipt defaults to 0 for every address, so a wallet that mints or buys after N receipts must send ceil(N/64) claim transactions that pay 0 (about 410k-460k gas each, about 6.5-7k gas per receipt) before reaching the first receipt it is entitled to. Every holder also pays about 7k gas per receipt it does earn, while a receipt from a 1 ETH sale pays about 1.24e-5 ETH per token at full supply, so for one-to-five-token holders claiming costs more than it returns for most sales. The queue is also growable by anyone: receive() accepts any payment >= totalShares wei (56 wei in the proof, 5,555 wei at full supply) and appends a receipt for about 56k-109k gas, a cost that is then paid again by each of up to 5,555 holders. Funds are never lost and pagination keeps claims callable, so this is a liveness/economic defect, but the brief's guarantee that all royalties are claimable by holders proportionally is not economically true, and SECURITY.md's note about 'empty batches' does not describe the magnitude. Fix options that preserve the design: start a holder's cursor at the first receipt whose revision >= the holder's first checkpoint revision (receipt revisions are monotonic, so a binary search over _receipts works and changes no entitlement), or replace the receipt list with a cumulative per-share accumulator settled in updateShares (O(1) claims), optionally with a minimum receipt size above totalShares wei. Note: test/RoyaltyDistributor.t.sol test_zeroEntitlementBatchCanAdvanceToLaterEarnedReceipts asserts the current paging behaviour and would need updating.","line":209,"path":"src/RoyaltyDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SeedClock} from \"src/SeedClock.sol\";\nimport {RoyaltyDistributor} from \"src/RoyaltyDistributor.sol\";\n\ncontract ProofIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n}\n\ncontract ProofValidator {\n    function validateTransfer(address, address, address, uint256) external pure {}\n}\n\ncontract ProofDeployer {\n    function deploy(address treasury) external returns (RoyaltyDistributor d, SeedClock c) {\n        d = new RoyaltyDistributor(treasury);\n        c = new SeedClock(treasury, address(d));\n    }\n}\n\n/// A holder who first acquires a SeedClock after N royalty receipts must not have to pay gas to step\n/// through the N receipts in which they held no shares before reaching the royalty they earned.\ncontract LateHolderClaimProof is Test {\n    SeedClock clock;\n    RoyaltyDistributor distributor;\n    address treasury = makeAddr(\"treasury\");\n    address late = makeAddr(\"late\");\n\n    function setUp() public {\n        (distributor, clock) = new ProofDeployer().deploy(treasury);\n        vm.etch(clock.IMD(), address(new ProofIMD()).code);\n        vm.etch(clock.TRANSFER_VALIDATOR(), address(new ProofValidator()).code);\n        vm.deal(address(this), 1000 ether);\n    }\n\n    function test_lateHolderReachesFirstEarnedRoyaltyInOneClaim() public {\n        for (uint256 i; i < 640; ++i) {\n            (bool ok,) = address(distributor).call{value: 0.00345 ether}(\"\");\n            require(ok);\n        }\n        ProofIMD(clock.IMD()).mint(late, 1 ether);\n        vm.prank(late);\n        clock.mint(5);\n        (bool sent,) = address(distributor).call{value: 60 ether}(\"\");\n        require(sent);\n        // Expected: the late holder's first claim pays the 5 ether earned on the only receipt after minting.\n        // Actual: it returns 0 and advances 64 receipts; ten more zero-value claims (~4.7M gas) are needed.\n        vm.prank(late);\n        uint256 amount = distributor.claim();\n        assertEq(amount, 5 ether, \"late holder must page through pre-ownership receipts\");\n    }\n}","reproduction":"Atomic deploy (treasury holds tokens 1-55, totalShares 55), IMD and validator etched. 1) Send 640 payments of 0.00345 ether to the distributor: receiptCount(address(0)) == 640. 2) A new wallet holding 1 IMD calls mint(5) (tokens 56-60, totalShares 60). 3) Send 60 ether to the distributor (one receipt, perToken 1 ether). 4) The new wallet calls claim(). Expected: returns 5 ether. Actual: returns 0 and sets nextReceipt to 64; ten more zero-value claims (about 4.1-4.7M gas in total, measured) are needed before the 5 ether is paid. Griefing variant (specialist proof, reproduced): with totalShares 56, a contract sends 56 wei 2,560 times in one call (about 143M gas); a holder of one token then needs 41 claimFor calls (about 17.8M gas) to reach a 56 ether royalty that arrived after the spam. Run forge test --match-path test/scratch/LateHolderClaim.t.sol (attached proof): fails with '0 != 5000000000000000000'.","severity":"medium","snippet":"        uint256 start = nextReceipt[asset][holder];\n        uint256 length = _receipts[asset].length;\n        end = start + MAX_CLAIM_RECEIPTS;","title":"Claim cursor starts at receipt 0 and the receipt queue is append-only and permissionlessly growable, so claims cost O(all receipts) and late or small holders cannot economically reach their royalties"},{"citation":"resolved","description":"Merged from audit_math, audit_flow, audit_permissions and write_foundry_tests (same root cause). SeedClock.transferFrom performs no receiver check (only safeTransferFrom does), the strict registry allows owner-initiated gifts (caller == from), and SeedClock._update (line 159) forwards any `to` to updateShares, which increments shares[to] for any address. If `to` is the RoyaltyDistributor, every later receipt allocates perToken * 1 to the distributor's own address, but _claim reverts InvalidReceiver when to == address(this) (line 221), claimFor pays only the named holder, the distributor never calls itself and has no ERC-721 transfer function, and there is no sweep by design. If `to` is the SeedClock contract, the ETH claim reverts PaymentFailed (no receive) and ERC-20 claims transfer tokens to a contract that cannot move them. accountedBalance[asset] therefore never returns to zero and 1/totalShares (per stranded token) of every future ETH and ERC-20 receipt is lost for the collection's lifetime. The distributor is the published royalty recipient, so it is a plausible mis-send target, and anyone with 1 IMD can mint 5 tokens for free and gift them to lock 5/5555 of all future royalties on purpose. Minimal fix preserving the no-owner design: in SeedClock._update revert when to == distributor || to == address(this) (mirroring the zero-address guard), or in updateShares revert when to == address(this) || to == collection.","line":111,"path":"src/RoyaltyDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {SeedClock} from \"src/SeedClock.sol\";\nimport {RoyaltyDistributor} from \"src/RoyaltyDistributor.sol\";\n\ncontract StrandIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract StrandValidator {\n    function validateTransfer(address, address, address, uint256) external pure {}\n}\n\n/// @dev Deploys both contracts in one call, like the launch factory.\ncontract StrandFactory {\n    function deploy(address treasury) external returns (RoyaltyDistributor d, SeedClock c) {\n        d = new RoyaltyDistributor(treasury);\n        c = new SeedClock(treasury, address(d));\n    }\n}\n\n/// A SeedClock token moved with plain transferFrom to the RoyaltyDistributor (or to SeedClock itself)\n/// becomes a share whose entitlement no claim path can pay. Either the transfer must be rejected or\n/// every unit received must remain claimable by some wallet. On the current code the transfer is\n/// accepted and 1/56 of every receipt is locked forever.\ncontract StrandedSharesProof is Test {\n    RoyaltyDistributor distributor;\n    SeedClock clock;\n    address treasury = makeAddr(\"treasury\");\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        (distributor, clock) = new StrandFactory().deploy(treasury);\n        vm.etch(clock.IMD(), address(new StrandIMD()).code);\n        vm.etch(clock.TRANSFER_VALIDATOR(), address(new StrandValidator()).code);\n        StrandIMD(clock.IMD()).mint(alice, 1 ether);\n        vm.prank(alice);\n        clock.mint(1); // token 56, totalShares 56\n        vm.deal(address(this), 100 ether);\n    }\n\n    function test_royaltiesForTokenSentToDistributorRemainClaimable() public {\n        vm.prank(alice);\n        (bool accepted,) =\n            address(clock).call(abi.encodeCall(clock.transferFrom, (alice, address(distributor), 56)));\n        if (!accepted) return; // Fixed by rejecting the transfer.\n\n        (bool ok,) = address(distributor).call{value: 56 ether}(\"\");\n        assertTrue(ok);\n\n        // Pay out everything any real wallet can claim.\n        if (distributor.claimable(treasury) != 0) distributor.claimFor(address(0), treasury);\n        if (distributor.claimable(alice) != 0) distributor.claimFor(address(0), alice);\n        // The distributor cannot be paid its own entitlement by any claim variant.\n        (bool selfOk,) =\n            address(distributor).call(abi.encodeCall(distributor.claimFor, (address(0), address(distributor))));\n        assertFalse(selfOk, \"claimFor(distributor) is expected to revert InvalidReceiver\");\n\n        // Expected: all 56 ether reached holders. Actual: 1 ether is locked in the distributor forever.\n        assertEq(address(distributor).balance, 0, \"royalty slice stranded on the distributor's own share\");\n    }\n}","reproduction":"Atomic deploy; alice holds 1 IMD and mints token 56 (totalShares 56). 1) vm.prank(alice); clock.transferFrom(alice, address(distributor), 56) succeeds; distributor.shares(address(distributor)) == 1. 2) Send 56 ether to the distributor: claimable(address(distributor)) == 1 ether. 3) claimFor(address(0), address(distributor)) reverts InvalidReceiver; claimFor(address(0), treasury) pays 55 ether. Expected: all 56 ether claimable by some wallet (address(distributor).balance == 0). Actual: address(distributor).balance == 1 ether forever, and every later receipt strands another 1/56. Same with transferFrom(alice, address(clock), 56): claimable(address(clock)) == 1 ether and claimFor(address(0), address(clock)) reverts PaymentFailed. Run forge test --match-path test/scratch/StrandedShares.t.sol (attached proof): fails with '1000000000000000000 != 0'. The proof also passes if the fix rejects the transfer.","severity":"medium","snippet":"        ++shares[to];\n        _checkpoint(to);","title":"updateShares credits shares to the distributor or the collection itself when a token is gifted there with plain transferFrom; that slice of every future royalty is permanently unclaimable"},{"citation":"resolved","description":"Merged from audit_math, audit_permissions and audit_economics. Native receipts are recorded in receive() at the revision current when paid, and the suite asserts new mints cannot claim earlier royalties (test_newMintsCannotClaimEarlierRoyalties). ERC-20 royalties, which is what every accepted OpenSea WETH offer pays, sit unaccounted until anyone calls syncToken and are then recorded at the revision current at that moment. Because minting is free for any wallet with 1 IMD (and the same IMD can be moved between wallets), a watcher who sees WETH land on the distributor can mint up to 5 tokens per wallet and call syncToken in the same transaction, taking minted/totalShares of royalties earned while they held nothing; a buyer whose purchase paid the fee can likewise sync right after the sale. The brief tolerates 'current balance or snapshot at claim time' and SECURITY.md documents that ordering affects the snapshot, so this is reported as a broken internal guarantee ('a new mint cannot collect pre-mint royalties') that holds for ETH but not for the dominant WETH path, not as a brief violation. Mitigation keeping the design: have SeedClock._update (or updateShares) run the syncToken logic for a constant mainnet WETH address (code-checked on first use) before shares change, so WETH received between two share changes is attributed to the holders of record when it arrived; or document and run a prompt keeper.","line":132,"path":"src/RoyaltyDistributor.sol","reproduction":"Atomic deploy (totalShares 55, all treasury); mock ERC-20 `weth`; alice holds 1 IMD and no SeedClock. 1) weth.mint(address(distributor), 60e18) (royalty from a WETH-settled sale). 2) vm.prank(alice); clock.mint(5) -> totalShares 60. 3) distributor.syncToken(address(weth)). Expected (ETH-path semantics): claimable(weth, alice) == 0 and claimable(weth, treasury) == 60e18. Actual: claimable(weth, alice) == 5e18, claimable(weth, treasury) == 55e18. Control: paying 60 ether of ETH before a later mint(5) by bob gives claimable(bob) == 0. Reproduced in test/scratch/Review.t.sol test_erc20SyncTimeCapture.","severity":"low","snippet":"        uint256 balance = IERC20(asset).balanceOf(address(this));\n        if (balance < accountedBalance[asset]) revert UnsupportedBalanceDecrease();\n        _record(asset, balance - accountedBalance[asset]);","title":"ERC-20 (WETH) royalties are snapshotted at syncToken time, not at receipt, so free mints or transfers made after the royalty arrives capture it; the ETH path prevents exactly this"},{"citation":"resolved","description":"From write_foundry_tests, reproduced. receive() runs _syncNative/_record, which writes accountedBalance, totalReceived and roundingCredit, pushes a two-word Receipt and emits an event. Measured: about 104k gas for the second and later receipts (about 177k for the first). Seaport forwards all remaining gas, so OpenSea sales succeed, but every sale's buyer pays this extra gas, and any marketplace, payment splitter or wallet that pays royalties with Solidity transfer()/send() or an equivalent 2,300-gas stipend reverts the whole payout. SECURITY.md documents the requirement. Not a fund-loss defect; a lazy receive() that only accepts ETH and relies on syncNative (as ERC-20s already do) would remove the cost but changes snapshot timing, which interacts with finding 3.","line":116,"path":"src/RoyaltyDistributor.sol","reproduction":"Atomic deploy. 1) address(distributor).call{value: 1 ether}(\"\") succeeds. 2) Second call with 1 ether: measured 104,357 gas. 3) address(distributor).call{value: 1 ether, gas: 2300}(\"\") returns false. Expected per common royalty-paying integrations: the payment is accepted. Actual: it reverts. Reproduced in test/scratch/Review.t.sol test_receiveGas.","severity":"low","snippet":"    receive() external payable {\n        // Counterfactual / pre-binding deposits wait for the initial 55 treasury shares.\n        if (collection != address(0)) _syncNative();\n    }","title":"receive() performs full receipt accounting (about 104k gas), so any payer forwarding only the 2,300-gas stipend reverts and every marketplace buyer pays the accounting gas"},{"citation":"resolved","description":"Merged from audit_math and audit_permissions, reproduced. The 'indivisible dust' comment assumes wei-scale units. The remainder is bounded by totalShares - 1 base units, up to 5,554 units once minted out: negligible for 18-decimal assets, but 5,554 sats of an 8-decimal token, 0.0055 of a 6-decimal stablecoin, or up to 55.54 of a 2-decimal token per receipt. Any receipt smaller than totalShares units is allocated 100 percent to firstTokenOwner, who is the treasury unless token 1 is sold, so a stream of small low-decimal receipts is systematically redirected from proportional holders to one wallet, contrary to the brief's 'proportionally to their current balance'. Fix: scale perToken by a magnifier (1e18 or 2**128) before dividing and credit only the sub-unit remainder of the magnified value, or carry remainders into the next receipt instead of crediting one holder.","line":202,"path":"src/RoyaltyDistributor.sol","reproduction":"Atomic deploy; alice mints 5 (totalShares 60, treasury holds token 1); mock 2-decimal token `eurs`. 1) eurs.mint(address(distributor), 10_000) (100.00 units). 2) distributor.syncToken(address(eurs)). Expected proportional split: treasury 55/60 * 10000 = 9166.67, alice 833.33. Actual: perToken = 166, remainder 40 -> claimable(eurs, treasury) == 9170, claimable(eurs, alice) == 830. At full supply (5555 shares) a 10,000-unit receipt gives perToken 1 and remainder 4,445: the treasury receives 4,500 of 10,000 for holding 55/5555 of the supply. Reproduced in test/scratch/Review.t.sol test_lowDecimalRemainder.","severity":"low","snippet":"        roundingCredit[asset][firstTokenOwner] += amount % totalShares;","title":"Per-receipt remainder amount % totalShares goes entirely to token 1's holder, which for low-decimal ERC-20 royalties is a material non-proportional transfer to the treasury"},{"citation":"resolved","description":"Merged from audit_math and audit_flow, reproduced. index is read as _receipts[asset].length before the conditional push. When perToken == 0 nothing is pushed, but the event still reports that index as the receipt. The next stored receipt reuses the same index, so an indexer keyed by (asset, receipt) sees two different amounts for one index and cannot reconstruct the queue claim() walks. No on-chain accounting is affected. Fix: emit type(uint256).max (or a separate RoyaltyDust event) when no receipt is pushed, or emit the index only inside the push branch.","line":203,"path":"src/RoyaltyDistributor.sol","reproduction":"Atomic deploy (totalShares 55). 1) address(distributor).call{value: 1}(\"\") emits RoyaltyReceived(address(0), 1, 1, 0) while receiptCount(address(0)) stays 0. 2) address(distributor).call{value: 55}(\"\") emits RoyaltyReceived(address(0), 55, 1, 0) and now receipt(address(0), 0).perToken == 1. Expected: a unique identifier per stored receipt. Actual: index 0 is announced twice for different payments. Reproduced with vm.expectEmit in test/scratch/Review.t.sol test_eventIndexReuse.","severity":"low","snippet":"        uint256 index = _receipts[asset].length;\n        if (perToken != 0) _receipts[asset].push(Receipt(revision, perToken));\n        emit RoyaltyReceived(asset, amount, revision, index);","title":"RoyaltyReceived emits a receipt index for sub-share receipts that are never stored, so the same index is later reused by a real receipt"},{"citation":"resolved","description":"From audit_permissions; recorded as the trust dependency the brief's 'enforced royalties' rests on, not as a permission bypass. Every transfer where caller != from (every marketplace fill) staticcalls the fixed mainnet StrictAuthorizedTransferSecurityRegistry (line 157), whose authorizer list and policy OpenSea controls, and setTransferValidator always reverts. If OpenSea rotates or retires the authorizer, or the registry's administrator blocks the collection, every approved-operator transfer reverts for the life of the collection and only owner-to-owner gifts (which pay no royalty) remain; if the registry address ever has no code, _update reverts ValidatorUnavailable for all secondary transfers. The owner-gift path (caller == from) also means a seller using a smart account or EIP-7702 delegation can execute transferFrom itself inside a paid batch, so 'enforced on-chain' holds only for operator-executed fills. The design intentionally has no owner, matching the brief's 'not upgradeable, not pausable'; if the requester wants recoverability, the scope decision is a one-time, treasury-only validator rotation.","line":112,"path":"src/SeedClock.sol","reproduction":"Fixture with mainnet registry bytecode (test/fixtures/MainnetValidator.sol). 1) alice mints 56 and approves operator. 2) Within one transaction: prank SignedZone 0x000056F7000000EcE9003ca63978907a00FFD100, registry.beforeAuthorizedTransfer(clock, 56); prank operator, clock.transferFrom(alice, bob, 56) succeeds (test/Marketplace.t.sol). 3) Clear the authorizer set in registry storage (model of OpenSea rotating its zone): the same sequence reverts StrictAuthorizedTransferSecurityRegistry__CallerIsNotValidAuthorizer / __UnauthorizedTransfer. 4) vm.prank(treasury); clock.setTransferValidator(address(0)) reverts RoyaltiesArePermanent(). 5) vm.prank(bob); clock.transferFrom(bob, alice, 56) still succeeds with no royalty. Expected per brief: royalties enforced on compatible marketplaces, which requires the external registry to keep this collection authorized.","severity":"info","snippet":"    function setTransferValidator(address) external pure override {\n        revert RoyaltiesArePermanent();\n    }","title":"Trust assumption: all marketplace transfers depend permanently on OpenSea's strict registry and its authorizer, with no on-chain fallback or rotation"}],"hash":"c7a15f1a1f4826bee75d84ad75dd2670c753476a7d0643685a56626d2076435f","nodeId":"b3ae5e4b-2708-4ac0-8007-9851f76bf691","outcome":"completed","summary":"Review complete. The findings file is written at `.imd-findings.json` with 7 findings and 17 coverage rows, and no repository file was changed (the findings file is gitignored and `test/scratch/` is my scratch area).\n\n**What I verified**\n\n- The project builds and all 84 of its own tests pass. Runtime sizes are 21,081 and 5,595 bytes, under the 24,576 limit, with no forbidden opcodes.\n- All four specialist proof tests fail on the current code for the stated reasons. I reran each one.\n- Every non-proof specialist claim I kept was reproduced in a scratch test: ERC-20 sync-time capture, event index reuse, the 2,300-gas receive failure, the low-decimal remainder, and stranding via the collection address.\n- My own pass over the entry points found nothing new. The constructor handshake, mint checks, authorization ordering, checkpoint/revision consistency, claim reentrancy, and manifest arguments all hold.\n\n**Findings reported, after merging duplicates**\n\n1. **Medium.** Claim cursor starts at receipt 0 and the queue is append-only and permissionlessly growable, so claims cost gas proportional to all receipts ever recorded. Five specialists reported this. Proof attached: a late minter's first claim returns 0 instead of 5 ether.\n2. **Medium.** Plain transferFrom to the distributor or the collection credits shares no claim path can pay, permanently locking that slice of all future royalties. Four specialists reported this. I attached a proof that also passes if the fix rejects the transfer.\n3. **Low.** ERC-20 royalties snapshot at sync time while ETH snapshots at receipt, so free mints after a WETH royalty lands capture it.\n4. **Low.** The receive function costs about 104k gas and reverts for stipend-only payers.\n5. **Low.** The per-receipt remainder goes entirely to token 1's holder, which is material for low-decimal tokens.\n6. **Low.** The RoyaltyReceived event reuses receipt indexes for unstored dust receipts.\n7. **Info.** Trust assumption: marketplace transfers depend permanently on OpenSea's registry with no rotation path.\n\nDropped: the royaltyInfo floor-rounding note, since the impact is at most 1 wei per sale. The slither and aderyn leads were all benign on inspection (intentional integer grid math, zero-initialized locals, a constructor call to the trusted distributor).","treeHash":null,"usage":{"cachedInputTokens":886688,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":32247,"runtime":"claude","turns":34,"wallClockMs":509664}}],"verification":[{"checks":[{"durationMs":29101,"exitCode":0,"name":"build","output":"Compiling 64 files with Solc 0.8.26\nSolc 0.8.26 finished in 29.00s\nCompiler run successful!\n","passed":true},{"durationMs":5065,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_badConstructorAddressesRevert() (gas: 4169)\n[PASS] test_bindingWindowExpiresAtTransactionBoundary() (gas: 241707)\n[PASS] test_counterfactualFundingIsClaimableAfterAtomicDeployment() (gas: 8113723)\n[PASS] test_deployScriptUsesExplicitTreasuryAndAtomicPairing() (gas: 7889168)\n[PASS] test_noFactoryOrTreasuryOwnershipPrivileges() (gas: 23826)\n[PASS] test_runtimeSizesAndForbiddenOpcodesMatchProtectedFloor() (gas: 8448391)\n[PASS] test_unboundDistributorRejectsSyncAndDefersAllocation() (gas: 320631)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 49.57ms (27.62ms CPU time)\n\nRan 7 tests for test/Art.t.sol:ArtTest\n[PASS] testFuzz_skipAheadMatchesIndependentLinearRecurrence(uint32,uint8) (runs: 256, μ: 342309, ~: 183350)\n[PASS] testFuzz_traitRangesAndExactMapping(uint16) (runs: 256, μ: 42983, ~: 43328)\nLogs:\n  Bound result 3280\n\n[PASS] test_allFivePublishedSeedsReproduceExactly() (gas: 95985)\n[PASS] test_candidateMappingAndTraitsRemainStableAcrossTimeAndOwnership() (gas: 2397346)\n[PASS] test_contractMetadataHasPermanentRoyaltyRecipient() (gas: 161853)\n[PASS] test_metadataIsValidJSONWithEmbeddedAnimatedLayeredSVG() (gas: 26860073)\n[PASS] test_uriForNonexistentTokenRevertsAndPreviewIsBounded() (gas: 50512)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 55.13ms (123.84ms CPU time)\n\nRan 5 tests for test/Marketplace.t.sol:MarketplaceTest\n[PASS] test_arbitraryCallerCannotImpersonateRoyaltyAuthorizer() (gas: 39500)\n[PASS] test_attestedTransferAndERC20RoyaltyFlow() (gas: 1573069)\n[PASS] test_attestedTransferAndNativeRoyaltyFlow() (gas: 1138649)\n[PASS] test_mainnetRegistryRecognizesOpenSeaZoneAndAllowsOwnerGifts() (gas: 481096)\n[PASS] test_mainnetRegistryRejectsUnattestedApprovedOperator() (gas: 369785)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 102.66ms (4.16ms CPU time)\n\nRan 20 tests for test/RoyaltyDistributor.t.sol:RoyaltyDistributorTest\n[PASS] testFuzz_conservationIncludingDustAndTransfers(uint96,uint96,uint8) (runs: 256, μ: 1454849, ~: 1457139)\nLogs:\n  Bound result 1\n  Bound result 41006547630623895780\n  Bound result 1\n\n[PASS] test_allRoundingDustClaimableByTokenOneHolderAtReceipt() (gas: 707732)\n[PASS] test_anyCallerCanAssistButCannotRedirectAnotherHoldersClaim() (gas: 350293)\n[PASS] test_claimPaginationProgressesWithoutUnboundedLoops() (gas: 14772648)\n[PASS] test_claimReentrancyCannotDrainOtherHolders() (gas: 1499781)\n[PASS] test_erc20OwnershipChangesBeforeSyncUseSyncTimeBalances() (gas: 1254725)\n[PASS] test_erc20RoyaltiesSnapshotOnPermissionlessSyncAndCannotBeCountedTwice() (gas: 1871482)\n[PASS] test_falseReturningERC20RollsBackClaimAndCannotAffectETH() (gas: 1159855)\n[PASS] test_forcedETHCanBeSyncedByAnyone() (gas: 231806)\n[PASS] test_invalidDestinationsAndNativeTokenSyncRejected() (gas: 250304)\n[PASS] test_negativeRebaseRejectedAndCannotBlockOtherAssets() (gas: 1119252)\n[PASS] test_newMintsCannotClaimEarlierRoyalties() (gas: 835572)\n[PASS] test_receiptDuringClaimCallbackIsAccountedAndNotOverwritten() (gas: 595213)\n[PASS] test_receiptProportionalToAllCurrentBalances() (gas: 1223232)\n[PASS] test_rejectingReceiverCannotBlockOthersAndCanChooseClaimDestination() (gas: 1513833)\n[PASS] test_tinyAndZeroReceiptsDoNotGrowQueue() (gas: 5898442)\n[PASS] test_tokenCyclingAndSelfTransferCannotDoubleClaim() (gas: 1863868)\n[PASS] test_transferPreservesEarnedRoyaltiesAndChangesFutureEntitlement() (gas: 1227627)\n[PASS] test_unauthorizedLedgerMutationRebindingAndSweepFail() (gas: 83285)\n[PASS] test_zeroEntitlementBatchCanAdvanceToLaterEarnedReceipts() (gas: 7575231)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 102.79ms (49.74ms CPU time)\n\nRan 16 tests for test/SeedClock.t.sol:SeedClockTest\n[PASS] testFuzz_mintEligibility(uint96,uint8) (runs: 256, μ: 186166, ~: 130956)\nLogs:\n  Bound result 2\n\n[PASS] testFuzz_royaltyIs690BpsWithoutOverflow(uint256) (runs: 256, μ: 12391, ~: 12386)\n[PASS] test_all5500PublicTokensAndHardCap() (gas: 493599839)\n[PASS] test_deploymentMintsExactly55AndHasNoAdministrativeMint() (gas: 573351)\n[PASS] test_interfacesAndFixedRoyaltyConfiguration() (gas: 73915)\n[PASS] test_lifetimeWalletLimitDoesNotResetAfterTransfer() (gas: 1179594)\n[PASS] test_mintRequiresOneWholeIMDAndRechecksEveryTime() (gas: 557324)\n[PASS] test_missingIMDContractFailsClosed() (gas: 51734)\n[PASS] test_missingValidatorFailsClosedButDoesNotPauseMint() (gas: 322668)\n[PASS] test_noCallerCanDisableRoyaltiesOrMintAsAdmin() (gas: 93058)\n[PASS] test_operatorTransferRequiresValidatorAuthorization() (gas: 759085)\n[PASS] test_receiverCannotReenterMintAndSeesConsistentShares() (gas: 1245573)\n[PASS] test_receiverRejectionRollsBackAllMintAccounting() (gas: 1011093)\n[PASS] test_rejectsZeroOverlargeAndPaidMints() (gas: 176223)\n[PASS] test_selfTransfersAndDirectGiftsCannotChangeMintHistoryOrCreateShares() (gas: 548018)\n[PASS] test_unapprovedTransfersRejectedEvenWithValidatorPermission() (gas: 362181)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 568.43ms (580.45ms CPU time)\n\nRan 27 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] testFuzz_royaltyRecipientIsDistributorForAnyIdAndPrice(uint256,uint256) (runs: 256, μ: 12072, ~: 12065)\n[PASS] testFuzz_sharesAtMatchesRecordedHistory(uint256) (runs: 200, μ: 3472559, ~: 3466267)\n[PASS] testFuzz_singleReceiptSplitIsExactAndConserved(uint256,uint8,uint8) (runs: 300, μ: 1002787, ~: 1000703)\nLogs:\n  Bound result 2\n  Bound result 1\n  Bound result 388615863\n\n[PASS] test_IMDOnlyCountsCallersOwnBalance() (gas: 336653)\n[PASS] test_IMDReturningNoDataFailsClosed() (gas: 149004)\n[PASS] test_bindingCannotBeReopenedFromAConstructorInALaterTransaction() (gas: 8287)\n[PASS] test_claimForZeroHolderRejected() (gas: 188811)\n[PASS] test_creatorTokenInterfaceIdMatchesLimitBreakConstant() (gas: 28257)\n[PASS] test_dustBelowTotalSharesGoesEntirelyToTokenOneHolder() (gas: 1027290)\n[PASS] test_freshHolderWithNoReceiptsCannotClaim() (gas: 655578)\n[PASS] test_hugeQuantityCannotOverflowWalletOrSupplyChecks() (gas: 128475)\n[PASS] test_lastTokensMintableExactlyAndBatchOverCapRejected() (gas: 493958552)\n[PASS] test_mintedIdsAreSequentialAcrossWallets() (gas: 722272)\n[PASS] test_onlyCollectionMayUpdateSharesIncludingTreasuryAndDistributor() (gas: 135715)\n[PASS] test_operatorTransferMovesFutureRoyaltiesOnly() (gas: 884353)\n[PASS] test_paginationBoundaryAtExactly64And65Receipts() (gas: 7315473)\n[PASS] test_quantityIsValidatedBeforeAnyIMDLookup() (gas: 73990)\n[PASS] test_revertingIMDBalanceOfFailsClosedWithoutState() (gas: 193729)\n[PASS] test_royaltyRoundsDownAtSmallPrices() (gas: 22963)\n[PASS] test_secondClaimOfSameReceiptReverts() (gas: 362785)\n[PASS] test_setTransferValidatorRevertsForEveryCaller() (gas: 45892)\n[PASS] test_syncNativeWithNothingNewIsNoop() (gas: 236639)\n[PASS] test_syncTokenOnNonContractAssetReverts() (gas: 49386)\n[PASS] test_syncTokenWithZeroBalanceRecordsNothing() (gas: 592603)\n[PASS] test_treasuryHasNoExtraAllowanceBeyondPublicFive() (gas: 474479)\n[PASS] test_walletLimitAcrossSeveralSmallMints() (gas: 821863)\n[PASS] test_walletLimitRejectsWholeBatchThatWouldCrossIt() (gas: 604284)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 614.09ms (764.44ms CPU time)\n\nRan 1 test for test/Stateful.t.sol:StatefulTest\n[PASS]\nStatefulTest invariants:\n[PASS] invariant_allSharesMatchERC721Balances\n[PASS] invariant_erc20AssetsConservedAndAllFundsClaimable\n[PASS] invariant_nativeAssetsConservedAndAllFundsClaimable\n StatefulTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭----------------+---------------+-------+---------+----------╮\n| Contract       | Selector      | Calls | Reverts | Discards |\n+=============================================================+\n| RoyaltyHandler | claimNative   | 663   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | claimToken    | 694   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | mint          | 666   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveNative | 701   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveToken  | 681   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | transfer      | 691   | 0       | 0        |\n╰----------------+---------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.66s (1.66s CPU time)\n\nRan 1 test for test/DeepInvariant.t.sol:DeepInvariantTest\n[PASS]\nDeepInvariantTest invariants:\n[PASS] invariant_nativeBalanceCoversEveryOutstandingEntitlement\n[PASS] invariant_supplySharesAndConfigurationNeverDrift\n[PASS] invariant_tokenBalanceCoversEveryOutstandingEntitlement\n DeepInvariantTest invariants (runs: 48, calls: 7680, reverts: 0)\n\n╭-------------+------------------+-------+---------+----------╮\n| Contract    | Selector         | Calls | Reverts | Discards |\n+=============================================================+\n| DeepHandler | burstPay         | 762   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | claimNative      | 776   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | claimToken       | 807   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | forceNative      | 768   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | mint             | 763   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | operatorTransfer | 768   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | ownerTransfer    | 789   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | payNative        | 1491  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | payToken         | 756   | 0       | 0        |\n╰-------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 18455103\n  Bound result 15\n  Bound result 4\n  Bound result 4\n  Bound result 18\n  Bound result 999999999999994479\n  Bound result 1\n  Bound result 17\n  Bound result 8\n  Bound result 10475\n  Bound result 80\n  Bound result 574494618087274907\n  Bound result 2505\n  Bound result 14806\n  Bound result 18007\n  Bound result 164\n  Bound result 16\n  Bound result 112615453540185250\n  Bound result 15463\n  Bound result 11792\n  Bound result 60\n  Bound result 5\n  Bound result 28653984114512402184\n  Bound result 7\n  Bound result 4723697784776388461\n  Bound result 29\n  Bound result 251532841023550897\n  Bound result 49152\n  Bound result 10\n  Bound result 63\n  Bound result 97\n  Bound result 63\n  Bound result 531681528107670231\n  Bound result 4390704299370265643\n  Bound result 1101\n  Bound result 5000000000000000000\n  Bound result 20\n  Bound result 62\n  Bound result 1028\n  Bound result 5473\n  Bound result 21\n  Bound result 999999999999999997\n  Bound result 12\n  Bound result 20\n  Bound result 5\n  Bound result 7\n  Bound result 5554000\n  Bound result 10977\n  Bound result 3734\n  Bound result 57\n  Bound result 5\n  Bound result 69000000000000000\n  Bound result 4283495456250914893\n  Bound result 25107887083726237491\n  Bound result 7\n  Bound result 65\n  Bound result 12000\n  Bound result 55\n  Bound result 336997991386460937\n  Bound result 65\n  Bound result 40\n  Bound result 29977876690682644591\n  Bound result 21\n  Bound result 695090299867954037\n  Bound result 40\n  Bound result 75\n  Bound result 75268072107875103\n  Bound result 1644\n  Bound result 3538\n  Bound result 999999999999999999999997\n  Bound result 2323\n  Bound result 79\n  Bound result 999999999999994690\n  Bound result 12\n  Bound result 65\n  Bound result 52\n  Bound result 14\n  Bound result 999999999999994509\n  Bound result 5555\n  Bound result 36\n  Bound result 7951574286951218567\n  Bound result 4582\n  Bound result 12315\n  Bound result 2688\n  Bound result 14\n  Bound result 1099511627776\n  Bound result 64\n  Bound result 40\n  Bound result 4928571428571592\n  Bound result 30\n  Bound result 700\n  Bound result 47\n  Bound result 1049675\n  Bound result 137401426669\n  Bound result 42\n  Bound result 633546235031585825\n  Bound result 43989932371716382154\n  Bound result 13608\n  Bound result 11897\n  Bound result 258530244968570803393588\n  Bound result 31\n  Bound result 15\n  Bound result 4\n  Bound result 46\n  Bound result 999999999999994457\n  Bound result 10909\n  Bound result 18\n  Bound result 10342\n  Bound result 361720958946104542572837\n  Bound result 57\n  Bound result 5600\n  Bound result 281876990568215880058890\n  Bound result 1\n  Bound result 58\n  Bound result 12721\n  Bound result 26425\n  Bound result 9678\n  Bound result 4283495456250915193\n  Bound result 74\n  Bound result 88431579554571898\n  Bound result 43\n  Bound result 50\n  Bound result 3404604394\n  Bound result 13\n  Bound result 19308\n  Bound result 4\n  Bound result 18719\n  Bound result 1\n  Bound result 796023611727083337\n  Bound result 710221914\n  Bound result 493745728708118190909506\n  Bound result 29757\n  Bound result 1837070393995458515\n  Bound result 18919\n  Bound result 31\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 4.94s (4.93s CPU time)\n\nRan 8 test suites in 4.94s (8.09s CPU time): 84 tests passed, 0 failed, 0 skipped (84 total tests)\n","passed":true},{"durationMs":52,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"RoyaltyDistributor.bindCollection(address,address)\",\"RoyaltyDistributor.claim()\",\"RoyaltyDistributor.claim(address)\",\"RoyaltyDistributor.claimFor(address,address)\",\"RoyaltyDistributor.claimTo(address,address)\",\"RoyaltyDistributor.receive()\",\"RoyaltyDistributor.syncNative()\",\"RoyaltyDistributor.syncToken(address)\",\"RoyaltyDistributor.updateShares(address,address,uint256)\",\"SeedClock.approve(address,uint256)\",\"SeedClock.mint(uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256,bytes)\",\"SeedClock.setApprovalForAll(address,bool)\",\"SeedClock.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":6,\"DEPENDENCIES.md\":14,\"LICENSE\":21,\"README.md\":60,\"docs/DEPLOYMENT.md\":43,\"docs/GALLERY.html\":39,\"docs/SECURITY.md\":32,\"docs/SITE-HANDOFF.md\":14,\"docs/previews/seedclock-1.svg\":1,\"docs/previews/seedclock-13.svg\":1,\"docs/previews/seedclock-25.svg\":1,\"docs/previews/seedclock-3.svg\":1,\"docs/previews/seedclock-4.svg\":1,\"docs/previews/seedclock-6.svg\":1,\"foundry.toml\":22,\"remappings.txt\":2,\"script/DeploySeedClock.s.sol\":52,\"src/RoyaltyDistributor.sol\":238,\"src/SeedClock.sol\":162,\"src/art/SeedArt.sol\":402,\"src/art/SeedData.sol\":70,\"src/interfaces/ICreatorToken.sol\":15,\"src/interfaces/IRoyaltyDistributor.sol\":7,\"test/Adversarial.t.sol\":360,\"test/Art.t.sol\":160,\"test/DeepInvariant.t.sol\":293,\"test/Deployment.t.sol\":104,\"test/Marketplace.t.sol\":122,\"test/RoyaltyDistributor.t.sol\":312,\"test/SeedClock.t.sol\":224,\"test/Stateful.t.sol\":162,\"test/fixtures/MainnetValidator.sol\":50,\"test/fixtures/MainnetValidatorCode.sol\":681,\"test/fixtures/mainnet-validator.json\":39,\"test/helpers/Fixtures.sol\":140,\"tools/reproduce.py\":83},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"0479f180825dbc3c17dadc2aa1ed1f53765946da23132bcd9be50ff1a3ade16f","verifiedTreeHash":"de8c28c5c31b7faaa7e939391f0c629deccc6b86","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":22549,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 22.43s\nCompiler run successful!\n","passed":true},{"durationMs":2063,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_badConstructorAddressesRevert() (gas: 4169)\n[PASS] test_bindingWindowExpiresAtTransactionBoundary() (gas: 241707)\n[PASS] test_counterfactualFundingIsClaimableAfterAtomicDeployment() (gas: 8113723)\n[PASS] test_deployScriptUsesExplicitTreasuryAndAtomicPairing() (gas: 7889168)\n[PASS] test_noFactoryOrTreasuryOwnershipPrivileges() (gas: 23826)\n[PASS] test_runtimeSizesAndForbiddenOpcodesMatchProtectedFloor() (gas: 8448391)\n[PASS] test_unboundDistributorRejectsSyncAndDefersAllocation() (gas: 320631)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 16.54ms (17.25ms CPU time)\n\nRan 5 tests for test/Marketplace.t.sol:MarketplaceTest\n[PASS] test_arbitraryCallerCannotImpersonateRoyaltyAuthorizer() (gas: 39500)\n[PASS] test_attestedTransferAndERC20RoyaltyFlow() (gas: 1573069)\n[PASS] test_attestedTransferAndNativeRoyaltyFlow() (gas: 1138649)\n[PASS] test_mainnetRegistryRecognizesOpenSeaZoneAndAllowsOwnerGifts() (gas: 481096)\n[PASS] test_mainnetRegistryRejectsUnattestedApprovedOperator() (gas: 369785)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 73.51ms (1.61ms CPU time)\n\nRan 7 tests for test/Art.t.sol:ArtTest\n[PASS] testFuzz_skipAheadMatchesIndependentLinearRecurrence(uint32,uint8) (runs: 256, μ: 333940, ~: 175882)\n[PASS] testFuzz_traitRangesAndExactMapping(uint16) (runs: 256, μ: 43054, ~: 43494)\nLogs:\n  Bound result 446\n\n[PASS] test_allFivePublishedSeedsReproduceExactly() (gas: 95985)\n[PASS] test_candidateMappingAndTraitsRemainStableAcrossTimeAndOwnership() (gas: 2397346)\n[PASS] test_contractMetadataHasPermanentRoyaltyRecipient() (gas: 161853)\n[PASS] test_metadataIsValidJSONWithEmbeddedAnimatedLayeredSVG() (gas: 26860073)\n[PASS] test_uriForNonexistentTokenRevertsAndPreviewIsBounded() (gas: 50512)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 73.55ms (142.47ms CPU time)\n\nRan 20 tests for test/RoyaltyDistributor.t.sol:RoyaltyDistributorTest\n[PASS] testFuzz_conservationIncludingDustAndTransfers(uint96,uint96,uint8) (runs: 256, μ: 1459654, ~: 1457139)\nLogs:\n  Bound result 1\n  Bound result 107290\n  Bound result 760702591659071020\n\n[PASS] test_allRoundingDustClaimableByTokenOneHolderAtReceipt() (gas: 707732)\n[PASS] test_anyCallerCanAssistButCannotRedirectAnotherHoldersClaim() (gas: 350293)\n[PASS] test_claimPaginationProgressesWithoutUnboundedLoops() (gas: 14772648)\n[PASS] test_claimReentrancyCannotDrainOtherHolders() (gas: 1499781)\n[PASS] test_erc20OwnershipChangesBeforeSyncUseSyncTimeBalances() (gas: 1254725)\n[PASS] test_erc20RoyaltiesSnapshotOnPermissionlessSyncAndCannotBeCountedTwice() (gas: 1871482)\n[PASS] test_falseReturningERC20RollsBackClaimAndCannotAffectETH() (gas: 1159855)\n[PASS] test_forcedETHCanBeSyncedByAnyone() (gas: 231806)\n[PASS] test_invalidDestinationsAndNativeTokenSyncRejected() (gas: 250304)\n[PASS] test_negativeRebaseRejectedAndCannotBlockOtherAssets() (gas: 1119252)\n[PASS] test_newMintsCannotClaimEarlierRoyalties() (gas: 835572)\n[PASS] test_receiptDuringClaimCallbackIsAccountedAndNotOverwritten() (gas: 595213)\n[PASS] test_receiptProportionalToAllCurrentBalances() (gas: 1223232)\n[PASS] test_rejectingReceiverCannotBlockOthersAndCanChooseClaimDestination() (gas: 1513833)\n[PASS] test_tinyAndZeroReceiptsDoNotGrowQueue() (gas: 5898442)\n[PASS] test_tokenCyclingAndSelfTransferCannotDoubleClaim() (gas: 1863868)\n[PASS] test_transferPreservesEarnedRoyaltiesAndChangesFutureEntitlement() (gas: 1227627)\n[PASS] test_unauthorizedLedgerMutationRebindingAndSweepFail() (gas: 83285)\n[PASS] test_zeroEntitlementBatchCanAdvanceToLaterEarnedReceipts() (gas: 7575231)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 73.60ms (66.21ms CPU time)\n\nRan 16 tests for test/SeedClock.t.sol:SeedClockTest\n[PASS] testFuzz_mintEligibility(uint96,uint8) (runs: 256, μ: 185766, ~: 130956)\nLogs:\n  Bound result 2\n\n[PASS] testFuzz_royaltyIs690BpsWithoutOverflow(uint256) (runs: 256, μ: 12391, ~: 12386)\n[PASS] test_all5500PublicTokensAndHardCap() (gas: 493599839)\n[PASS] test_deploymentMintsExactly55AndHasNoAdministrativeMint() (gas: 573351)\n[PASS] test_interfacesAndFixedRoyaltyConfiguration() (gas: 73915)\n[PASS] test_lifetimeWalletLimitDoesNotResetAfterTransfer() (gas: 1179594)\n[PASS] test_mintRequiresOneWholeIMDAndRechecksEveryTime() (gas: 557324)\n[PASS] test_missingIMDContractFailsClosed() (gas: 51734)\n[PASS] test_missingValidatorFailsClosedButDoesNotPauseMint() (gas: 322668)\n[PASS] test_noCallerCanDisableRoyaltiesOrMintAsAdmin() (gas: 93058)\n[PASS] test_operatorTransferRequiresValidatorAuthorization() (gas: 759085)\n[PASS] test_receiverCannotReenterMintAndSeesConsistentShares() (gas: 1245573)\n[PASS] test_receiverRejectionRollsBackAllMintAccounting() (gas: 1011093)\n[PASS] test_rejectsZeroOverlargeAndPaidMints() (gas: 176223)\n[PASS] test_selfTransfersAndDirectGiftsCannotChangeMintHistoryOrCreateShares() (gas: 548018)\n[PASS] test_unapprovedTransfersRejectedEvenWithValidatorPermission() (gas: 362181)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 554.72ms (570.05ms CPU time)\n\nRan 1 test for test/Stateful.t.sol:StatefulTest\n[PASS]\nStatefulTest invariants:\n[PASS] invariant_allSharesMatchERC721Balances\n[PASS] invariant_erc20AssetsConservedAndAllFundsClaimable\n[PASS] invariant_nativeAssetsConservedAndAllFundsClaimable\n StatefulTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭----------------+---------------+-------+---------+----------╮\n| Contract       | Selector      | Calls | Reverts | Discards |\n+=============================================================+\n| RoyaltyHandler | claimNative   | 663   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | claimToken    | 694   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | mint          | 666   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveNative | 701   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveToken  | 681   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | transfer      | 691   | 0       | 0        |\n╰----------------+---------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.95s (1.95s CPU time)\n\nRan 6 test suites in 1.95s (2.74s CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":82,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"RoyaltyDistributor.bindCollection(address,address)\",\"RoyaltyDistributor.claim()\",\"RoyaltyDistributor.claim(address)\",\"RoyaltyDistributor.claimFor(address,address)\",\"RoyaltyDistributor.claimTo(address,address)\",\"RoyaltyDistributor.receive()\",\"RoyaltyDistributor.syncNative()\",\"RoyaltyDistributor.syncToken(address)\",\"RoyaltyDistributor.updateShares(address,address,uint256)\",\"SeedClock.approve(address,uint256)\",\"SeedClock.mint(uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256,bytes)\",\"SeedClock.setApprovalForAll(address,bool)\",\"SeedClock.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":6,\"DEPENDENCIES.md\":14,\"LICENSE\":21,\"README.md\":60,\"docs/DEPLOYMENT.md\":43,\"docs/GALLERY.html\":39,\"docs/SECURITY.md\":32,\"docs/SITE-HANDOFF.md\":14,\"docs/previews/seedclock-1.svg\":1,\"docs/previews/seedclock-13.svg\":1,\"docs/previews/seedclock-25.svg\":1,\"docs/previews/seedclock-3.svg\":1,\"docs/previews/seedclock-4.svg\":1,\"docs/previews/seedclock-6.svg\":1,\"foundry.toml\":22,\"launch.json\":14,\"remappings.txt\":2,\"script/DeploySeedClock.s.sol\":52,\"src/RoyaltyDistributor.sol\":238,\"src/SeedClock.sol\":162,\"src/art/SeedArt.sol\":402,\"src/art/SeedData.sol\":70,\"src/interfaces/ICreatorToken.sol\":15,\"src/interfaces/IRoyaltyDistributor.sol\":7,\"test/Art.t.sol\":160,\"test/Deployment.t.sol\":104,\"test/Marketplace.t.sol\":122,\"test/RoyaltyDistributor.t.sol\":312,\"test/SeedClock.t.sol\":224,\"test/Stateful.t.sol\":162,\"test/fixtures/MainnetValidator.sol\":50,\"test/fixtures/MainnetValidatorCode.sol\":681,\"test/fixtures/mainnet-validator.json\":39,\"test/helpers/Fixtures.sol\":140,\"tools/reproduce.py\":83},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"06ee04b6c74d6937a31edcdf7164757e06b5fc27ab77b9d071f2a22406333300","verifiedTreeHash":"1a41e5ed3d76f4615e78989dd62355114f3d51a3","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":28058,"exitCode":0,"name":"build","output":"Compiling 64 files with Solc 0.8.26\nSolc 0.8.26 finished in 27.97s\nCompiler run successful!\n","passed":true},{"durationMs":4081,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Marketplace.t.sol:MarketplaceTest\n[PASS] test_arbitraryCallerCannotImpersonateRoyaltyAuthorizer() (gas: 39500)\n[PASS] test_attestedTransferAndERC20RoyaltyFlow() (gas: 1566882)\n[PASS] test_attestedTransferAndNativeRoyaltyFlow() (gas: 1114218)\n[PASS] test_mainnetRegistryRecognizesOpenSeaZoneAndAllowsOwnerGifts() (gas: 486589)\n[PASS] test_mainnetRegistryRejectsUnattestedApprovedOperator() (gas: 372606)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 3.14ms (1.74ms CPU time)\n\nRan 7 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_badConstructorAddressesRevert() (gas: 4169)\n[PASS] test_bindingWindowExpiresAtTransactionBoundary() (gas: 241743)\n[PASS] test_counterfactualFundingIsClaimableAfterAtomicDeployment() (gas: 8302975)\n[PASS] test_deployScriptUsesExplicitTreasuryAndAtomicPairing() (gas: 8088026)\n[PASS] test_noFactoryOrTreasuryOwnershipPrivileges() (gas: 23825)\n[PASS] test_runtimeSizesAndForbiddenOpcodesMatchProtectedFloor() (gas: 8734103)\n[PASS] test_unboundDistributorRejectsSyncAndDefersAllocation() (gas: 320624)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 19.49ms (18.32ms CPU time)\n\nRan 7 tests for test/Art.t.sol:ArtTest\n[PASS] testFuzz_skipAheadMatchesIndependentLinearRecurrence(uint32,uint8) (runs: 256, μ: 314084, ~: 175882)\n[PASS] testFuzz_traitRangesAndExactMapping(uint16) (runs: 256, μ: 43069, ~: 43408)\nLogs:\n  Bound result 446\n\n[PASS] test_allFivePublishedSeedsReproduceExactly() (gas: 95985)\n[PASS] test_candidateMappingAndTraitsRemainStableAcrossTimeAndOwnership() (gas: 2400118)\n[PASS] test_contractMetadataHasPermanentRoyaltyRecipient() (gas: 161646)\n[PASS] test_metadataIsValidJSONWithEmbeddedAnimatedLayeredSVG() (gas: 26860073)\n[PASS] test_uriForNonexistentTokenRevertsAndPreviewIsBounded() (gas: 50512)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 47.79ms (137.29ms CPU time)\n\nRan 17 tests for test/SeedClock.t.sol:SeedClockTest\n[PASS] testFuzz_mintEligibility(uint96,uint8) (runs: 256, μ: 189320, ~: 130956)\nLogs:\n  Bound result 3\n\n[PASS] testFuzz_royaltyIs690BpsWithoutOverflow(uint256) (runs: 256, μ: 12390, ~: 12386)\n[PASS] test_all5500PublicTokensAndHardCap() (gas: 497367331)\n[PASS] test_deploymentMintsExactly55AndHasNoAdministrativeMint() (gas: 573363)\n[PASS] test_interfacesAndFixedRoyaltyConfiguration() (gas: 73915)\n[PASS] test_lifetimeWalletLimitDoesNotResetAfterTransfer() (gas: 1191761)\n[PASS] test_mintRequiresOneWholeIMDAndRechecksEveryTime() (gas: 560257)\n[PASS] test_missingIMDContractFailsClosed() (gas: 51734)\n[PASS] test_missingValidatorFailsClosedButDoesNotPauseMint() (gas: 325489)\n[PASS] test_noCallerCanDisableRoyaltiesOrMintAsAdmin() (gas: 93080)\n[PASS] test_operatorTransferRequiresValidatorAuthorization() (gas: 765156)\n[PASS] test_receiverCannotReenterMintAndSeesConsistentShares() (gas: 1249138)\n[PASS] test_receiverRejectionRollsBackAllMintAccounting() (gas: 1013842)\n[PASS] test_rejectsZeroOverlargeAndPaidMints() (gas: 176245)\n[PASS] test_selfTransfersAndDirectGiftsCannotChangeMintHistoryOrCreateShares() (gas: 553711)\n[PASS] test_transfersToDistributorOrCollectionRejectedSoNoShareIsStranded() (gas: 489848)\n[PASS] test_unapprovedTransfersRejectedEvenWithValidatorPermission() (gas: 364921)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 507.71ms (593.22ms CPU time)\n\nRan 30 tests for test/Adversarial.t.sol:AdversarialTest\n[PASS] testFuzz_royaltyRecipientIsDistributorForAnyIdAndPrice(uint256,uint256) (runs: 256, μ: 12072, ~: 12065)\n[PASS] testFuzz_sharesAtMatchesRecordedHistory(uint256) (runs: 200, μ: 3505177, ~: 3496896)\n[PASS] testFuzz_singleReceiptSplitIsExactAndConserved(uint256,uint8,uint8) (runs: 300, μ: 973108, ~: 974721)\nLogs:\n  Bound result 1\n  Bound result 5\n  Bound result 19449\n\n[PASS] test_IMDOnlyCountsCallersOwnBalance() (gas: 339396)\n[PASS] test_IMDReturningNoDataFailsClosed() (gas: 149004)\n[PASS] test_bindingCannotBeReopenedFromAConstructorInALaterTransaction() (gas: 8287)\n[PASS] test_claimForZeroHolderRejected() (gas: 164718)\n[PASS] test_claimSettlesAtMost64SegmentsAndResumes() (gas: 21866164)\n[PASS] test_creatorTokenInterfaceIdMatchesLimitBreakConstant() (gas: 28301)\n[PASS] test_dustBelowTotalSharesIsCarriedNotGivenToTokenOneHolder() (gas: 1061859)\n[PASS] test_freshHolderWithNoReceiptsCannotClaim() (gas: 655596)\n[PASS] test_hugeQuantityCannotOverflowWalletOrSupplyChecks() (gas: 128475)\n[PASS] test_lastTokensMintableExactlyAndBatchOverCapRejected() (gas: 497728611)\n[PASS] test_lateJoinerSkipsAnyNumberOfEarlierReceiptsInOneCall() (gas: 24720717)\n[PASS] test_mintedIdsAreSequentialAcrossWallets() (gas: 728286)\n[PASS] test_onlyCollectionMayUpdateSharesIncludingTreasuryAndDistributor() (gas: 135807)\n[PASS] test_operatorTransferMovesFutureRoyaltiesOnly() (gas: 835897)\n[PASS] test_paginationBoundaryAtExactly64And65Receipts() (gas: 5525112)\n[PASS] test_quantityIsValidatedBeforeAnyIMDLookup() (gas: 74034)\n[PASS] test_receiptBeyondUint192PerShareRevertsWithoutAccounting() (gas: 736261)\n[PASS] test_revertingIMDBalanceOfFailsClosedWithoutState() (gas: 193721)\n[PASS] test_royaltyRoundsDownAtSmallPrices() (gas: 23007)\n[PASS] test_secondClaimOfSameReceiptReverts() (gas: 339508)\n[PASS] test_setTransferValidatorRevertsForEveryCaller() (gas: 45914)\n[PASS] test_syncNativeWithNothingNewIsNoop() (gas: 212547)\n[PASS] test_syncTokenOnNonContractAssetReverts() (gas: 49458)\n[PASS] test_syncTokenWithZeroBalanceRecordsNothing() (gas: 592689)\n[PASS] test_treasuryHasNoExtraAllowanceBeyondPublicFive() (gas: 477904)\n[PASS] test_walletLimitAcrossSeveralSmallMints() (gas: 835468)\n[PASS] test_walletLimitRejectsWholeBatchThatWouldCrossIt() (gas: 610276)\nSuite result: ok. 30 passed; 0 failed; 0 skipped; finished in 517.44ms (760.76ms CPU time)\n\nRan 24 tests for test/RoyaltyDistributor.t.sol:RoyaltyDistributorTest\n[PASS] testFuzz_conservationIncludingDustAndTransfers(uint96,uint96,uint8) (runs: 256, μ: 1381588, ~: 1389371)\nLogs:\n  Bound result 1\n  Bound result 107290\n  Bound result 760702591659071020\n\n[PASS] test_anyCallerCanAssistButCannotRedirectAnotherHoldersClaim() (gas: 318195)\n[PASS] test_claimPaginatesOverBalanceSegments() (gas: 36499832)\n[PASS] test_claimReentrancyCannotDrainOtherHolders() (gas: 1477758)\n[PASS] test_erc20OwnershipChangesBeforeSyncUseSyncTimeBalances() (gas: 1228962)\n[PASS] test_erc20RoyaltiesSnapshotOnPermissionlessSyncAndCannotBeCountedTwice() (gas: 1816490)\n[PASS] test_falseReturningERC20RollsBackClaimAndCannotAffectETH() (gas: 1104054)\n[PASS] test_forcedETHCanBeSyncedByAnyone() (gas: 204015)\n[PASS] test_holderReturningAfterSellingSkipsGapInOneClaim() (gas: 41236684)\n[PASS] test_invalidDestinationsAndNativeTokenSyncRejected() (gas: 226228)\n[PASS] test_lateHolderSkipsEarlierReceiptsInOneCheapClaim() (gas: 81120468)\n[PASS] test_lowDecimalRemainderIsNotRedirectedToTokenOneHolder() (gas: 1293170)\n[PASS] test_negativeRebaseRejectedAndCannotBlockOtherAssets() (gas: 1067202)\n[PASS] test_newMintsCannotClaimEarlierRoyalties() (gas: 778466)\n[PASS] test_receiptDuringClaimCallbackIsAccountedAndNotOverwritten() (gas: 542629)\n[PASS] test_receiptProportionalToAllCurrentBalances() (gas: 1182606)\n[PASS] test_rejectingReceiverCannotBlockOthersAndCanChooseClaimDestination() (gas: 1477168)\n[PASS] test_remainderCarriesIntoNextReceiptInsteadOfOneHolder() (gas: 756611)\n[PASS] test_subShareAmountEmitsNoReceiptIndex() (gas: 314065)\n[PASS] test_tinyAndZeroReceiptsDoNotGrowQueue() (gas: 3207754)\n[PASS] test_tokenCyclingAndSelfTransferCannotDoubleClaim() (gas: 1860252)\n[PASS] test_transferPreservesEarnedRoyaltiesAndChangesFutureEntitlement() (gas: 1176730)\n[PASS] test_unauthorizedLedgerMutationRebindingAndSweepFail() (gas: 83417)\n[PASS] test_wethReceivedBeforeShareChangeBelongsToHoldersOfRecord() (gas: 1877416)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 517.44ms (122.83ms CPU time)\n\nRan 1 test for test/Stateful.t.sol:StatefulTest\n[PASS]\nStatefulTest invariants:\n[PASS] invariant_allSharesMatchERC721Balances\n[PASS] invariant_erc20AssetsConservedAndAllFundsClaimable\n[PASS] invariant_nativeAssetsConservedAndAllFundsClaimable\n StatefulTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭----------------+---------------+-------+---------+----------╮\n| Contract       | Selector      | Calls | Reverts | Discards |\n+=============================================================+\n| RoyaltyHandler | claimNative   | 663   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | claimToken    | 694   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | mint          | 666   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveNative | 701   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveToken  | 681   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | transfer      | 691   | 0       | 0        |\n╰----------------+---------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.58s (1.58s CPU time)\n\nRan 1 test for test/DeepInvariant.t.sol:DeepInvariantTest\n[PASS]\nDeepInvariantTest invariants:\n[PASS] invariant_nativeBalanceCoversEveryOutstandingEntitlement\n[PASS] invariant_supplySharesAndConfigurationNeverDrift\n[PASS] invariant_tokenBalanceCoversEveryOutstandingEntitlement\n DeepInvariantTest invariants (runs: 48, calls: 7680, reverts: 0)\n\n╭-------------+------------------+-------+---------+----------╮\n| Contract    | Selector         | Calls | Reverts | Discards |\n+=============================================================+\n| DeepHandler | burstPay         | 762   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | claimNative      | 776   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | claimToken       | 807   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | forceNative      | 768   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | mint             | 763   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | operatorTransfer | 768   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | ownerTransfer    | 789   | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | payNative        | 1491  | 0       | 0        |\n|-------------+------------------+-------+---------+----------|\n| DeepHandler | payToken         | 756   | 0       | 0        |\n╰-------------+------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 4183\n  Bound result 12\n  Bound result 4\n  Bound result 46\n  Bound result 62\n  Bound result 999999999999994460\n  Bound result 1\n  Bound result 52\n  Bound result 20\n  Bound result 2865\n  Bound result 6\n  Bound result 176522667684735184\n  Bound result 6878\n  Bound result 14806\n  Bound result 241765366154146664572019\n  Bound result 2903\n  Bound result 16\n  Bound result 112615453540185250\n  Bound result 15463\n  Bound result 6550\n  Bound result 60\n  Bound result 375\n  Bound result 7\n  Bound result 1467851867814521196\n  Bound result 29\n  Bound result 251532841023550897\n  Bound result 244\n  Bound result 51\n  Bound result 13\n  Bound result 43\n  Bound result 20\n  Bound result 531681528107670231\n  Bound result 4390704299370265643\n  Bound result 833163407\n  Bound result 49152\n  Bound result 22\n  Bound result 20\n  Bound result 3107\n  Bound result 281474976710655\n  Bound result 21\n  Bound result 999999999999999997\n  Bound result 5\n  Bound result 30\n  Bound result 1\n  Bound result 7\n  Bound result 6600\n  Bound result 109\n  Bound result 10362\n  Bound result 5553\n  Bound result 4\n  Bound result 112000000000000000000\n  Bound result 949152542372549713\n  Bound result 25107887083726237491\n  Bound result 127\n  Bound result 24\n  Bound result 5556\n  Bound result 55\n  Bound result 10392\n  Bound result 301\n  Bound result 39\n  Bound result 11960796970885325840\n  Bound result 21\n  Bound result 695090299867954037\n  Bound result 24\n  Bound result 75\n  Bound result 75268072107875103\n  Bound result 6449\n  Bound result 3538\n  Bound result 2097152\n  Bound result 6770\n  Bound result 4\n  Bound result 15\n  Bound result 88570027242723297\n  Bound result 48\n  Bound result 301\n  Bound result 7\n  Bound result 70\n  Bound result 999999999999994453\n  Bound result 5555\n  Bound result 70\n  Bound result 7951574286951218567\n  Bound result 5019\n  Bound result 17597\n  Bound result 871002724\n  Bound result 60\n  Bound result 852076800\n  Bound result 5\n  Bound result 80\n  Bound result 999999999999994809\n  Bound result 35\n  Bound result 7700\n  Bound result 36\n  Bound result 8524\n  Bound result 137401426669\n  Bound result 67\n  Bound result 18211\n  Bound result 4642\n  Bound result 13608\n  Bound result 985398024232127577937602\n  Bound result 325807200394525307574571\n  Bound result 61\n  Bound result 80\n  Bound result 2\n  Bound result 1\n  Bound result 24\n  Bound result 999999999999994486\n  Bound result 49545954823536484480\n  Bound result 17\n  Bound result 7824\n  Bound result 361720958946104542572837\n  Bound result 67\n  Bound result 2720838756\n  Bound result 281876990568215880058890\n  Bound result 5\n  Bound result 16\n  Bound result 439746354623586608\n  Bound result 26425\n  Bound result 2866\n  Bound result 2170379362620669968\n  Bound result 74\n  Bound result 88431579554571898\n  Bound result 20000\n  Bound result 28\n  Bound result 119\n  Bound result 1\n  Bound result 70\n  Bound result 6473\n  Bound result 1\n  Bound result 18719\n  Bound result 55\n  Bound result 641325791469426146\n  Bound result 59\n  Bound result 493745728708118190909506\n  Bound result 75268072107875105\n  Bound result 36893488147419103245\n  Bound result 5834\n  Bound result 16\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.99s (3.98s CPU time)\n\nRan 8 test suites in 3.99s (7.18s CPU time): 92 tests passed, 0 failed, 0 skipped (92 total tests)\n","passed":true},{"durationMs":47,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"RoyaltyDistributor.bindCollection(address,address)\",\"RoyaltyDistributor.claim()\",\"RoyaltyDistributor.claim(address)\",\"RoyaltyDistributor.claimFor(address,address)\",\"RoyaltyDistributor.claimTo(address,address)\",\"RoyaltyDistributor.receive()\",\"RoyaltyDistributor.syncNative()\",\"RoyaltyDistributor.syncToken(address)\",\"RoyaltyDistributor.updateShares(address,address,uint256)\",\"SeedClock.approve(address,uint256)\",\"SeedClock.mint(uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256,bytes)\",\"SeedClock.setApprovalForAll(address,bool)\",\"SeedClock.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":6,\"DEPENDENCIES.md\":14,\"LICENSE\":21,\"README.md\":62,\"docs/DEPLOYMENT.md\":43,\"docs/GALLERY.html\":39,\"docs/SECURITY.md\":33,\"docs/SITE-HANDOFF.md\":14,\"docs/previews/seedclock-1.svg\":1,\"docs/previews/seedclock-13.svg\":1,\"docs/previews/seedclock-25.svg\":1,\"docs/previews/seedclock-3.svg\":1,\"docs/previews/seedclock-4.svg\":1,\"docs/previews/seedclock-6.svg\":1,\"foundry.toml\":22,\"remappings.txt\":2,\"script/DeploySeedClock.s.sol\":52,\"src/RoyaltyDistributor.sol\":307,\"src/SeedClock.sol\":164,\"src/art/SeedArt.sol\":402,\"src/art/SeedData.sol\":70,\"src/interfaces/ICreatorToken.sol\":15,\"src/interfaces/IRoyaltyDistributor.sol\":7,\"test/Adversarial.t.sol\":413,\"test/Art.t.sol\":160,\"test/DeepInvariant.t.sol\":306,\"test/Deployment.t.sol\":104,\"test/Marketplace.t.sol\":124,\"test/RoyaltyDistributor.t.sol\":401,\"test/SeedClock.t.sol\":240,\"test/Stateful.t.sol\":169,\"test/fixtures/MainnetValidator.sol\":50,\"test/fixtures/MainnetValidatorCode.sol\":681,\"test/fixtures/mainnet-validator.json\":39,\"test/helpers/Fixtures.sol\":140,\"tools/reproduce.py\":83},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"64702c32481cf97fa1fd29fc026e947969d3a77a8faa027d8a792ae413198eb2","verifiedTreeHash":"8246edab97e9458ed1d4e9e4f18d0c0011fbc0c8","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":40701,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 40.51s\nCompiler run successful!\n","passed":true},{"durationMs":3133,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Marketplace.t.sol:MarketplaceTest\n[PASS] test_arbitraryCallerCannotImpersonateRoyaltyAuthorizer() (gas: 39500)\n[PASS] test_attestedTransferAndERC20RoyaltyFlow() (gas: 1573069)\n[PASS] test_attestedTransferAndNativeRoyaltyFlow() (gas: 1138649)\n[PASS] test_mainnetRegistryRecognizesOpenSeaZoneAndAllowsOwnerGifts() (gas: 481096)\n[PASS] test_mainnetRegistryRejectsUnattestedApprovedOperator() (gas: 369785)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 10.08ms (3.01ms CPU time)\n\nRan 7 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_badConstructorAddressesRevert() (gas: 4169)\n[PASS] test_bindingWindowExpiresAtTransactionBoundary() (gas: 241707)\n[PASS] test_counterfactualFundingIsClaimableAfterAtomicDeployment() (gas: 8113723)\n[PASS] test_deployScriptUsesExplicitTreasuryAndAtomicPairing() (gas: 7889168)\n[PASS] test_noFactoryOrTreasuryOwnershipPrivileges() (gas: 23826)\n[PASS] test_runtimeSizesAndForbiddenOpcodesMatchProtectedFloor() (gas: 8448391)\n[PASS] test_unboundDistributorRejectsSyncAndDefersAllocation() (gas: 320631)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 49.93ms (35.77ms CPU time)\n\nRan 7 tests for test/Art.t.sol:ArtTest\n[PASS] testFuzz_skipAheadMatchesIndependentLinearRecurrence(uint32,uint8) (runs: 256, μ: 333940, ~: 175882)\n[PASS] testFuzz_traitRangesAndExactMapping(uint16) (runs: 256, μ: 43054, ~: 43494)\nLogs:\n  Bound result 14\n\n[PASS] test_allFivePublishedSeedsReproduceExactly() (gas: 95985)\n[PASS] test_candidateMappingAndTraitsRemainStableAcrossTimeAndOwnership() (gas: 2397346)\n[PASS] test_contractMetadataHasPermanentRoyaltyRecipient() (gas: 161853)\n[PASS] test_metadataIsValidJSONWithEmbeddedAnimatedLayeredSVG() (gas: 26860073)\n[PASS] test_uriForNonexistentTokenRevertsAndPreviewIsBounded() (gas: 50512)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 96.98ms (194.81ms CPU time)\n\nRan 20 tests for test/RoyaltyDistributor.t.sol:RoyaltyDistributorTest\n[PASS] testFuzz_conservationIncludingDustAndTransfers(uint96,uint96,uint8) (runs: 256, μ: 1459654, ~: 1457139)\nLogs:\n  Bound result 3\n  Bound result 3714461160904546703\n  Bound result 7839\n\n[PASS] test_allRoundingDustClaimableByTokenOneHolderAtReceipt() (gas: 707732)\n[PASS] test_anyCallerCanAssistButCannotRedirectAnotherHoldersClaim() (gas: 350293)\n[PASS] test_claimPaginationProgressesWithoutUnboundedLoops() (gas: 14772648)\n[PASS] test_claimReentrancyCannotDrainOtherHolders() (gas: 1499781)\n[PASS] test_erc20OwnershipChangesBeforeSyncUseSyncTimeBalances() (gas: 1254725)\n[PASS] test_erc20RoyaltiesSnapshotOnPermissionlessSyncAndCannotBeCountedTwice() (gas: 1871482)\n[PASS] test_falseReturningERC20RollsBackClaimAndCannotAffectETH() (gas: 1159855)\n[PASS] test_forcedETHCanBeSyncedByAnyone() (gas: 231806)\n[PASS] test_invalidDestinationsAndNativeTokenSyncRejected() (gas: 250304)\n[PASS] test_negativeRebaseRejectedAndCannotBlockOtherAssets() (gas: 1119252)\n[PASS] test_newMintsCannotClaimEarlierRoyalties() (gas: 835572)\n[PASS] test_receiptDuringClaimCallbackIsAccountedAndNotOverwritten() (gas: 595213)\n[PASS] test_receiptProportionalToAllCurrentBalances() (gas: 1223232)\n[PASS] test_rejectingReceiverCannotBlockOthersAndCanChooseClaimDestination() (gas: 1513833)\n[PASS] test_tinyAndZeroReceiptsDoNotGrowQueue() (gas: 5898442)\n[PASS] test_tokenCyclingAndSelfTransferCannotDoubleClaim() (gas: 1863868)\n[PASS] test_transferPreservesEarnedRoyaltiesAndChangesFutureEntitlement() (gas: 1227627)\n[PASS] test_unauthorizedLedgerMutationRebindingAndSweepFail() (gas: 83285)\n[PASS] test_zeroEntitlementBatchCanAdvanceToLaterEarnedReceipts() (gas: 7575231)\nSuite result: ok. 20 passed; 0 failed; 0 skipped; finished in 97.29ms (93.02ms CPU time)\n\nRan 16 tests for test/SeedClock.t.sol:SeedClockTest\n[PASS] testFuzz_mintEligibility(uint96,uint8) (runs: 256, μ: 185766, ~: 130956)\nLogs:\n  Bound result 2\n\n[PASS] testFuzz_royaltyIs690BpsWithoutOverflow(uint256) (runs: 256, μ: 12391, ~: 12386)\n[PASS] test_all5500PublicTokensAndHardCap() (gas: 493599839)\n[PASS] test_deploymentMintsExactly55AndHasNoAdministrativeMint() (gas: 573351)\n[PASS] test_interfacesAndFixedRoyaltyConfiguration() (gas: 73915)\n[PASS] test_lifetimeWalletLimitDoesNotResetAfterTransfer() (gas: 1179594)\n[PASS] test_mintRequiresOneWholeIMDAndRechecksEveryTime() (gas: 557324)\n[PASS] test_missingIMDContractFailsClosed() (gas: 51734)\n[PASS] test_missingValidatorFailsClosedButDoesNotPauseMint() (gas: 322668)\n[PASS] test_noCallerCanDisableRoyaltiesOrMintAsAdmin() (gas: 93058)\n[PASS] test_operatorTransferRequiresValidatorAuthorization() (gas: 759085)\n[PASS] test_receiverCannotReenterMintAndSeesConsistentShares() (gas: 1245573)\n[PASS] test_receiverRejectionRollsBackAllMintAccounting() (gas: 1011093)\n[PASS] test_rejectsZeroOverlargeAndPaidMints() (gas: 176223)\n[PASS] test_selfTransfersAndDirectGiftsCannotChangeMintHistoryOrCreateShares() (gas: 548018)\n[PASS] test_unapprovedTransfersRejectedEvenWithValidatorPermission() (gas: 362181)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 1.25s (1.26s CPU time)\n\nRan 1 test for test/Stateful.t.sol:StatefulTest\n[PASS]\nStatefulTest invariants:\n[PASS] invariant_allSharesMatchERC721Balances\n[PASS] invariant_erc20AssetsConservedAndAllFundsClaimable\n[PASS] invariant_nativeAssetsConservedAndAllFundsClaimable\n StatefulTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭----------------+---------------+-------+---------+----------╮\n| Contract       | Selector      | Calls | Reverts | Discards |\n+=============================================================+\n| RoyaltyHandler | claimNative   | 663   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | claimToken    | 694   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | mint          | 666   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveNative | 701   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveToken  | 681   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | transfer      | 691   | 0       | 0        |\n╰----------------+---------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.92s (2.91s CPU time)\n\nRan 6 test suites in 2.93s (4.42s CPU time): 56 tests passed, 0 failed, 0 skipped (56 total tests)\n","passed":true},{"durationMs":114,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"RoyaltyDistributor.bindCollection(address,address)\",\"RoyaltyDistributor.claim()\",\"RoyaltyDistributor.claim(address)\",\"RoyaltyDistributor.claimFor(address,address)\",\"RoyaltyDistributor.claimTo(address,address)\",\"RoyaltyDistributor.receive()\",\"RoyaltyDistributor.syncNative()\",\"RoyaltyDistributor.syncToken(address)\",\"RoyaltyDistributor.updateShares(address,address,uint256)\",\"SeedClock.approve(address,uint256)\",\"SeedClock.mint(uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256,bytes)\",\"SeedClock.setApprovalForAll(address,bool)\",\"SeedClock.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":6,\"DEPENDENCIES.md\":14,\"LICENSE\":21,\"README.md\":60,\"docs/DEPLOYMENT.md\":43,\"docs/GALLERY.html\":39,\"docs/SECURITY.md\":32,\"docs/SITE-HANDOFF.md\":14,\"docs/previews/seedclock-1.svg\":1,\"docs/previews/seedclock-13.svg\":1,\"docs/previews/seedclock-25.svg\":1,\"docs/previews/seedclock-3.svg\":1,\"docs/previews/seedclock-4.svg\":1,\"docs/previews/seedclock-6.svg\":1,\"foundry.toml\":22,\"remappings.txt\":2,\"script/DeploySeedClock.s.sol\":52,\"src/RoyaltyDistributor.sol\":238,\"src/SeedClock.sol\":162,\"src/art/SeedArt.sol\":402,\"src/art/SeedData.sol\":70,\"src/interfaces/ICreatorToken.sol\":15,\"src/interfaces/IRoyaltyDistributor.sol\":7,\"test/Art.t.sol\":160,\"test/Deployment.t.sol\":104,\"test/Marketplace.t.sol\":122,\"test/RoyaltyDistributor.t.sol\":312,\"test/SeedClock.t.sol\":224,\"test/Stateful.t.sol\":162,\"test/fixtures/MainnetValidator.sol\":50,\"test/fixtures/MainnetValidatorCode.sol\":681,\"test/fixtures/mainnet-validator.json\":39,\"test/helpers/Fixtures.sol\":140,\"tools/reproduce.py\":83},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":9992,"exitCode":0,"name":"slither","output":"[medium/medium] divide-before-multiply at src/art/SeedArt.sol:138: SeedArt.lattice(bytes32,string) (src/art/SeedArt.sol#138-148) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/RoyaltyDistributor.sol:194: RoyaltyDistributor._record(address,uint256) (src/RoyaltyDistributor.sol#194-206) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/art/SeedData.sol:55: SeedData.candidateSeed(uint32,uint32).seed (src/art/SeedData.sol#55) is a local variable never initialized\n[medium/medium] uninitialized-local at src/RoyaltyDistributor.sol:176: RoyaltyDistributor.sharesAt(address,uint64).low (src/RoyaltyDistributor.sol#176) is a local variable never initialized\n[low/medium] calls-loop at src/SeedClock.sol:151: SeedClock._update(address,uint256,address) (src/SeedClock.sol#151-161) has external calls inside a loop: IRoyaltyDistributor(distributor).updateShares(from,to,tokenId) (src/SeedClock.sol#159)\n[low/medium] calls-loop at src/SeedClock.sol:151: SeedClock._update(address,uint256,address) (src/SeedClock.sol#151-161) has external calls inside a loop: IRoyaltyDistributor(distributor).updateShares(from,to,tokenId) (src/SeedClock.sol#159)\n[low/medium] calls-loop at src/SeedClock.sol:151: SeedClock._update(address,uint256,address) (src/SeedClock.sol#151-161) has external calls inside a loop: ITransferValidator(TRANSFER_VALIDATOR).validateTransfer(msg.sender,from,to,tokenId) (src/SeedClock.sol#157)\n[low/medium] calls-loop at src/SeedClock.sol:151: SeedClock._update(address,uint256,address) (src/SeedClock.sol#151-161) has external calls inside a loop: ITransferValidator(TRANSFER_VALIDATOR).validateTransfer(msg.sender,from,to,tokenId) (src/SeedClock.sol#157)","passed":true},{"durationMs":1073,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/SeedClock.sol:56: Reentrancy: State change after external call (2 places)\n[low] costly-loop at src/SeedClock.sol:75: Costly operations inside loop\n[low] internal-function-used-once at src/art/SeedArt.sol:25: Internal Function Used Only Once (7 places)\n[low] large-numeric-literal at src/SeedClock.sol:88: Large Numeric Literal\n[low] literal-instead-of-constant at src/RoyaltyDistributor.sol:91: Literal Instead of Constant (53 places)\n[low] local-variable-shadowing at src/RoyaltyDistributor.sol:45: Local Variable Shadows State Variable (3 places)\n[low] require-revert-in-loop at src/SeedClock.sol:75: Loop Contains `require`/`revert`\n[low] state-change-without-event at src/RoyaltyDistributor.sol:101: State Change Without Event\n[low] state-variable-could-be-constant at src/SeedClock.sol:46: State Variable Could Be Constant\n[low] unchecked-return at src/SeedClock.sol:122: Unchecked Return\n[low] uninitialized-local-variable at src/art/SeedArt.sol:140: Uninitialized Local Variable (4 places)\n[low] unsafe-oz-erc721-mint at src/SeedClock.sol:58: Unsafe `ERC721::_mint()`","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"6ceeaab670df83d2b4ab75d17449a57f023d556b2be3f49d5465fcb7403b92b9","verifiedTreeHash":"579354ac5164b6c4cd5a29f1653f354e24ab3e6b","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":20259,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 20.17s\nCompiler run successful!\n","passed":true},{"durationMs":1802,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_badConstructorAddressesRevert() (gas: 4169)\n[PASS] test_bindingWindowExpiresAtTransactionBoundary() (gas: 241743)\n[PASS] test_counterfactualFundingIsClaimableAfterAtomicDeployment() (gas: 8302975)\n[PASS] test_deployScriptUsesExplicitTreasuryAndAtomicPairing() (gas: 8088026)\n[PASS] test_noFactoryOrTreasuryOwnershipPrivileges() (gas: 23825)\n[PASS] test_runtimeSizesAndForbiddenOpcodesMatchProtectedFloor() (gas: 8734103)\n[PASS] test_unboundDistributorRejectsSyncAndDefersAllocation() (gas: 320624)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 20.24ms (14.97ms CPU time)\n\nRan 5 tests for test/Marketplace.t.sol:MarketplaceTest\n[PASS] test_arbitraryCallerCannotImpersonateRoyaltyAuthorizer() (gas: 39500)\n[PASS] test_attestedTransferAndERC20RoyaltyFlow() (gas: 1566882)\n[PASS] test_attestedTransferAndNativeRoyaltyFlow() (gas: 1114218)\n[PASS] test_mainnetRegistryRecognizesOpenSeaZoneAndAllowsOwnerGifts() (gas: 486589)\n[PASS] test_mainnetRegistryRejectsUnattestedApprovedOperator() (gas: 372606)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 44.28ms (1.20ms CPU time)\n\nRan 24 tests for test/RoyaltyDistributor.t.sol:RoyaltyDistributorTest\n[PASS] testFuzz_conservationIncludingDustAndTransfers(uint96,uint96,uint8) (runs: 256, μ: 1384656, ~: 1389371)\nLogs:\n  Bound result 1\n  Bound result 41006547630623895780\n  Bound result 1\n\n[PASS] test_anyCallerCanAssistButCannotRedirectAnotherHoldersClaim() (gas: 318195)\n[PASS] test_claimPaginatesOverBalanceSegments() (gas: 36499832)\n[PASS] test_claimReentrancyCannotDrainOtherHolders() (gas: 1477758)\n[PASS] test_erc20OwnershipChangesBeforeSyncUseSyncTimeBalances() (gas: 1228962)\n[PASS] test_erc20RoyaltiesSnapshotOnPermissionlessSyncAndCannotBeCountedTwice() (gas: 1816490)\n[PASS] test_falseReturningERC20RollsBackClaimAndCannotAffectETH() (gas: 1104054)\n[PASS] test_forcedETHCanBeSyncedByAnyone() (gas: 204015)\n[PASS] test_holderReturningAfterSellingSkipsGapInOneClaim() (gas: 41236684)\n[PASS] test_invalidDestinationsAndNativeTokenSyncRejected() (gas: 226228)\n[PASS] test_lateHolderSkipsEarlierReceiptsInOneCheapClaim() (gas: 81120468)\n[PASS] test_lowDecimalRemainderIsNotRedirectedToTokenOneHolder() (gas: 1293170)\n[PASS] test_negativeRebaseRejectedAndCannotBlockOtherAssets() (gas: 1067202)\n[PASS] test_newMintsCannotClaimEarlierRoyalties() (gas: 778466)\n[PASS] test_receiptDuringClaimCallbackIsAccountedAndNotOverwritten() (gas: 542629)\n[PASS] test_receiptProportionalToAllCurrentBalances() (gas: 1182606)\n[PASS] test_rejectingReceiverCannotBlockOthersAndCanChooseClaimDestination() (gas: 1477168)\n[PASS] test_remainderCarriesIntoNextReceiptInsteadOfOneHolder() (gas: 756611)\n[PASS] test_subShareAmountEmitsNoReceiptIndex() (gas: 314065)\n[PASS] test_tinyAndZeroReceiptsDoNotGrowQueue() (gas: 3207754)\n[PASS] test_tokenCyclingAndSelfTransferCannotDoubleClaim() (gas: 1860252)\n[PASS] test_transferPreservesEarnedRoyaltiesAndChangesFutureEntitlement() (gas: 1176730)\n[PASS] test_unauthorizedLedgerMutationRebindingAndSweepFail() (gas: 83417)\n[PASS] test_wethReceivedBeforeShareChangeBelongsToHoldersOfRecord() (gas: 1877416)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 74.84ms (124.60ms CPU time)\n\nRan 7 tests for test/Art.t.sol:ArtTest\n[PASS] testFuzz_skipAheadMatchesIndependentLinearRecurrence(uint32,uint8) (runs: 256, μ: 322174, ~: 168414)\n[PASS] testFuzz_traitRangesAndExactMapping(uint16) (runs: 256, μ: 43057, ~: 43328)\nLogs:\n  Bound result 14\n\n[PASS] test_allFivePublishedSeedsReproduceExactly() (gas: 95985)\n[PASS] test_candidateMappingAndTraitsRemainStableAcrossTimeAndOwnership() (gas: 2400118)\n[PASS] test_contractMetadataHasPermanentRoyaltyRecipient() (gas: 161646)\n[PASS] test_metadataIsValidJSONWithEmbeddedAnimatedLayeredSVG() (gas: 26860073)\n[PASS] test_uriForNonexistentTokenRevertsAndPreviewIsBounded() (gas: 50512)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 75.69ms (172.90ms CPU time)\n\nRan 17 tests for test/SeedClock.t.sol:SeedClockTest\n[PASS] testFuzz_mintEligibility(uint96,uint8) (runs: 256, μ: 188920, ~: 130956)\nLogs:\n  Bound result 2\n\n[PASS] testFuzz_royaltyIs690BpsWithoutOverflow(uint256) (runs: 256, μ: 12391, ~: 12386)\n[PASS] test_all5500PublicTokensAndHardCap() (gas: 497367331)\n[PASS] test_deploymentMintsExactly55AndHasNoAdministrativeMint() (gas: 573363)\n[PASS] test_interfacesAndFixedRoyaltyConfiguration() (gas: 73915)\n[PASS] test_lifetimeWalletLimitDoesNotResetAfterTransfer() (gas: 1191761)\n[PASS] test_mintRequiresOneWholeIMDAndRechecksEveryTime() (gas: 560257)\n[PASS] test_missingIMDContractFailsClosed() (gas: 51734)\n[PASS] test_missingValidatorFailsClosedButDoesNotPauseMint() (gas: 325489)\n[PASS] test_noCallerCanDisableRoyaltiesOrMintAsAdmin() (gas: 93080)\n[PASS] test_operatorTransferRequiresValidatorAuthorization() (gas: 765156)\n[PASS] test_receiverCannotReenterMintAndSeesConsistentShares() (gas: 1249138)\n[PASS] test_receiverRejectionRollsBackAllMintAccounting() (gas: 1013842)\n[PASS] test_rejectsZeroOverlargeAndPaidMints() (gas: 176245)\n[PASS] test_selfTransfersAndDirectGiftsCannotChangeMintHistoryOrCreateShares() (gas: 553711)\n[PASS] test_transfersToDistributorOrCollectionRejectedSoNoShareIsStranded() (gas: 489848)\n[PASS] test_unapprovedTransfersRejectedEvenWithValidatorPermission() (gas: 364921)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 507.88ms (535.14ms CPU time)\n\nRan 1 test for test/Stateful.t.sol:StatefulTest\n[PASS]\nStatefulTest invariants:\n[PASS] invariant_allSharesMatchERC721Balances\n[PASS] invariant_erc20AssetsConservedAndAllFundsClaimable\n[PASS] invariant_nativeAssetsConservedAndAllFundsClaimable\n StatefulTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭----------------+---------------+-------+---------+----------╮\n| Contract       | Selector      | Calls | Reverts | Discards |\n+=============================================================+\n| RoyaltyHandler | claimNative   | 663   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | claimToken    | 694   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | mint          | 666   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveNative | 701   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveToken  | 681   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | transfer      | 691   | 0       | 0        |\n╰----------------+---------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.71s (1.71s CPU time)\n\nRan 6 test suites in 1.72s (2.44s CPU time): 61 tests passed, 0 failed, 0 skipped (61 total tests)\n","passed":true},{"durationMs":50,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"RoyaltyDistributor.bindCollection(address,address)\",\"RoyaltyDistributor.claim()\",\"RoyaltyDistributor.claim(address)\",\"RoyaltyDistributor.claimFor(address,address)\",\"RoyaltyDistributor.claimTo(address,address)\",\"RoyaltyDistributor.receive()\",\"RoyaltyDistributor.syncNative()\",\"RoyaltyDistributor.syncToken(address)\",\"RoyaltyDistributor.updateShares(address,address,uint256)\",\"SeedClock.approve(address,uint256)\",\"SeedClock.mint(uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256,bytes)\",\"SeedClock.setApprovalForAll(address,bool)\",\"SeedClock.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":6,\"DEPENDENCIES.md\":14,\"LICENSE\":21,\"README.md\":62,\"docs/DEPLOYMENT.md\":43,\"docs/GALLERY.html\":39,\"docs/SECURITY.md\":33,\"docs/SITE-HANDOFF.md\":14,\"docs/previews/seedclock-1.svg\":1,\"docs/previews/seedclock-13.svg\":1,\"docs/previews/seedclock-25.svg\":1,\"docs/previews/seedclock-3.svg\":1,\"docs/previews/seedclock-4.svg\":1,\"docs/previews/seedclock-6.svg\":1,\"foundry.toml\":22,\"launch.json\":14,\"remappings.txt\":2,\"script/DeploySeedClock.s.sol\":52,\"src/RoyaltyDistributor.sol\":307,\"src/SeedClock.sol\":164,\"src/art/SeedArt.sol\":402,\"src/art/SeedData.sol\":70,\"src/interfaces/ICreatorToken.sol\":15,\"src/interfaces/IRoyaltyDistributor.sol\":7,\"test/Art.t.sol\":160,\"test/Deployment.t.sol\":104,\"test/Marketplace.t.sol\":124,\"test/RoyaltyDistributor.t.sol\":401,\"test/SeedClock.t.sol\":240,\"test/Stateful.t.sol\":169,\"test/fixtures/MainnetValidator.sol\":50,\"test/fixtures/MainnetValidatorCode.sol\":681,\"test/fixtures/mainnet-validator.json\":39,\"test/helpers/Fixtures.sol\":140,\"tools/reproduce.py\":83},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"8c46a612f24ec34546a337bd59f924f33cc7defc0edf39767ac9e06bec7ef70f","verifiedTreeHash":"227582472232feccffb3bc337a078d215f9fcbaf","verifierVersion":"0.1.0+c3fba9eb"},{"checks":[{"durationMs":31762,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 31.61s\nCompiler run successful!\n","passed":true},{"durationMs":2218,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Marketplace.t.sol:MarketplaceTest\n[PASS] test_arbitraryCallerCannotImpersonateRoyaltyAuthorizer() (gas: 39500)\n[PASS] test_attestedTransferAndERC20RoyaltyFlow() (gas: 1566882)\n[PASS] test_attestedTransferAndNativeRoyaltyFlow() (gas: 1114218)\n[PASS] test_mainnetRegistryRecognizesOpenSeaZoneAndAllowsOwnerGifts() (gas: 486589)\n[PASS] test_mainnetRegistryRejectsUnattestedApprovedOperator() (gas: 372606)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 9.67ms (1.86ms CPU time)\n\nRan 7 tests for test/Deployment.t.sol:DeploymentTest\n[PASS] test_badConstructorAddressesRevert() (gas: 4169)\n[PASS] test_bindingWindowExpiresAtTransactionBoundary() (gas: 241743)\n[PASS] test_counterfactualFundingIsClaimableAfterAtomicDeployment() (gas: 8302975)\n[PASS] test_deployScriptUsesExplicitTreasuryAndAtomicPairing() (gas: 8088026)\n[PASS] test_noFactoryOrTreasuryOwnershipPrivileges() (gas: 23825)\n[PASS] test_runtimeSizesAndForbiddenOpcodesMatchProtectedFloor() (gas: 8734103)\n[PASS] test_unboundDistributorRejectsSyncAndDefersAllocation() (gas: 320624)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 62.67ms (31.36ms CPU time)\n\nRan 24 tests for test/RoyaltyDistributor.t.sol:RoyaltyDistributorTest\n[PASS] testFuzz_conservationIncludingDustAndTransfers(uint96,uint96,uint8) (runs: 256, μ: 1384656, ~: 1389371)\nLogs:\n  Bound result 1\n  Bound result 41006547630623895780\n  Bound result 1\n\n[PASS] test_anyCallerCanAssistButCannotRedirectAnotherHoldersClaim() (gas: 318195)\n[PASS] test_claimPaginatesOverBalanceSegments() (gas: 36499832)\n[PASS] test_claimReentrancyCannotDrainOtherHolders() (gas: 1477758)\n[PASS] test_erc20OwnershipChangesBeforeSyncUseSyncTimeBalances() (gas: 1228962)\n[PASS] test_erc20RoyaltiesSnapshotOnPermissionlessSyncAndCannotBeCountedTwice() (gas: 1816490)\n[PASS] test_falseReturningERC20RollsBackClaimAndCannotAffectETH() (gas: 1104054)\n[PASS] test_forcedETHCanBeSyncedByAnyone() (gas: 204015)\n[PASS] test_holderReturningAfterSellingSkipsGapInOneClaim() (gas: 41236684)\n[PASS] test_invalidDestinationsAndNativeTokenSyncRejected() (gas: 226228)\n[PASS] test_lateHolderSkipsEarlierReceiptsInOneCheapClaim() (gas: 81120468)\n[PASS] test_lowDecimalRemainderIsNotRedirectedToTokenOneHolder() (gas: 1293170)\n[PASS] test_negativeRebaseRejectedAndCannotBlockOtherAssets() (gas: 1067202)\n[PASS] test_newMintsCannotClaimEarlierRoyalties() (gas: 778466)\n[PASS] test_receiptDuringClaimCallbackIsAccountedAndNotOverwritten() (gas: 542629)\n[PASS] test_receiptProportionalToAllCurrentBalances() (gas: 1182606)\n[PASS] test_rejectingReceiverCannotBlockOthersAndCanChooseClaimDestination() (gas: 1477168)\n[PASS] test_remainderCarriesIntoNextReceiptInsteadOfOneHolder() (gas: 756611)\n[PASS] test_subShareAmountEmitsNoReceiptIndex() (gas: 314065)\n[PASS] test_tinyAndZeroReceiptsDoNotGrowQueue() (gas: 3207754)\n[PASS] test_tokenCyclingAndSelfTransferCannotDoubleClaim() (gas: 1860252)\n[PASS] test_transferPreservesEarnedRoyaltiesAndChangesFutureEntitlement() (gas: 1176730)\n[PASS] test_unauthorizedLedgerMutationRebindingAndSweepFail() (gas: 83417)\n[PASS] test_wethReceivedBeforeShareChangeBelongsToHoldersOfRecord() (gas: 1877416)\nSuite result: ok. 24 passed; 0 failed; 0 skipped; finished in 78.14ms (162.54ms CPU time)\n\nRan 7 tests for test/Art.t.sol:ArtTest\n[PASS] testFuzz_skipAheadMatchesIndependentLinearRecurrence(uint32,uint8) (runs: 256, μ: 322174, ~: 168414)\n[PASS] testFuzz_traitRangesAndExactMapping(uint16) (runs: 256, μ: 43057, ~: 43328)\nLogs:\n  Bound result 14\n\n[PASS] test_allFivePublishedSeedsReproduceExactly() (gas: 95985)\n[PASS] test_candidateMappingAndTraitsRemainStableAcrossTimeAndOwnership() (gas: 2400118)\n[PASS] test_contractMetadataHasPermanentRoyaltyRecipient() (gas: 161646)\n[PASS] test_metadataIsValidJSONWithEmbeddedAnimatedLayeredSVG() (gas: 26860073)\n[PASS] test_uriForNonexistentTokenRevertsAndPreviewIsBounded() (gas: 50512)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 108.84ms (220.05ms CPU time)\n\nRan 17 tests for test/SeedClock.t.sol:SeedClockTest\n[PASS] testFuzz_mintEligibility(uint96,uint8) (runs: 256, μ: 188920, ~: 130956)\nLogs:\n  Bound result 3\n\n[PASS] testFuzz_royaltyIs690BpsWithoutOverflow(uint256) (runs: 256, μ: 12391, ~: 12386)\n[PASS] test_all5500PublicTokensAndHardCap() (gas: 497367331)\n[PASS] test_deploymentMintsExactly55AndHasNoAdministrativeMint() (gas: 573363)\n[PASS] test_interfacesAndFixedRoyaltyConfiguration() (gas: 73915)\n[PASS] test_lifetimeWalletLimitDoesNotResetAfterTransfer() (gas: 1191761)\n[PASS] test_mintRequiresOneWholeIMDAndRechecksEveryTime() (gas: 560257)\n[PASS] test_missingIMDContractFailsClosed() (gas: 51734)\n[PASS] test_missingValidatorFailsClosedButDoesNotPauseMint() (gas: 325489)\n[PASS] test_noCallerCanDisableRoyaltiesOrMintAsAdmin() (gas: 93080)\n[PASS] test_operatorTransferRequiresValidatorAuthorization() (gas: 765156)\n[PASS] test_receiverCannotReenterMintAndSeesConsistentShares() (gas: 1249138)\n[PASS] test_receiverRejectionRollsBackAllMintAccounting() (gas: 1013842)\n[PASS] test_rejectsZeroOverlargeAndPaidMints() (gas: 176245)\n[PASS] test_selfTransfersAndDirectGiftsCannotChangeMintHistoryOrCreateShares() (gas: 553711)\n[PASS] test_transfersToDistributorOrCollectionRejectedSoNoShareIsStranded() (gas: 489848)\n[PASS] test_unapprovedTransfersRejectedEvenWithValidatorPermission() (gas: 364921)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 1.36s (1.38s CPU time)\n\nRan 1 test for test/Stateful.t.sol:StatefulTest\n[PASS]\nStatefulTest invariants:\n[PASS] invariant_allSharesMatchERC721Balances\n[PASS] invariant_erc20AssetsConservedAndAllFundsClaimable\n[PASS] invariant_nativeAssetsConservedAndAllFundsClaimable\n StatefulTest invariants (runs: 64, calls: 4096, reverts: 0)\n\n╭----------------+---------------+-------+---------+----------╮\n| Contract       | Selector      | Calls | Reverts | Discards |\n+=============================================================+\n| RoyaltyHandler | claimNative   | 663   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | claimToken    | 694   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | mint          | 666   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveNative | 701   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | receiveToken  | 681   | 0       | 0        |\n|----------------+---------------+-------+---------+----------|\n| RoyaltyHandler | transfer      | 691   | 0       | 0        |\n╰----------------+---------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.07s (2.07s CPU time)\n\nRan 6 test suites in 2.08s (3.69s CPU time): 61 tests passed, 0 failed, 0 skipped (61 total tests)\n","passed":true},{"durationMs":98,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"RoyaltyDistributor.bindCollection(address,address)\",\"RoyaltyDistributor.claim()\",\"RoyaltyDistributor.claim(address)\",\"RoyaltyDistributor.claimFor(address,address)\",\"RoyaltyDistributor.claimTo(address,address)\",\"RoyaltyDistributor.receive()\",\"RoyaltyDistributor.syncNative()\",\"RoyaltyDistributor.syncToken(address)\",\"RoyaltyDistributor.updateShares(address,address,uint256)\",\"SeedClock.approve(address,uint256)\",\"SeedClock.mint(uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256)\",\"SeedClock.safeTransferFrom(address,address,uint256,bytes)\",\"SeedClock.setApprovalForAll(address,bool)\",\"SeedClock.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":6,\"DEPENDENCIES.md\":14,\"LICENSE\":21,\"README.md\":62,\"docs/DEPLOYMENT.md\":43,\"docs/GALLERY.html\":39,\"docs/SECURITY.md\":33,\"docs/SITE-HANDOFF.md\":14,\"docs/previews/seedclock-1.svg\":1,\"docs/previews/seedclock-13.svg\":1,\"docs/previews/seedclock-25.svg\":1,\"docs/previews/seedclock-3.svg\":1,\"docs/previews/seedclock-4.svg\":1,\"docs/previews/seedclock-6.svg\":1,\"foundry.toml\":22,\"remappings.txt\":2,\"script/DeploySeedClock.s.sol\":52,\"src/RoyaltyDistributor.sol\":307,\"src/SeedClock.sol\":164,\"src/art/SeedArt.sol\":402,\"src/art/SeedData.sol\":70,\"src/interfaces/ICreatorToken.sol\":15,\"src/interfaces/IRoyaltyDistributor.sol\":7,\"test/Art.t.sol\":160,\"test/Deployment.t.sol\":104,\"test/Marketplace.t.sol\":124,\"test/RoyaltyDistributor.t.sol\":401,\"test/SeedClock.t.sol\":240,\"test/Stateful.t.sol\":169,\"test/fixtures/MainnetValidator.sol\":50,\"test/fixtures/MainnetValidatorCode.sol\":681,\"test/fixtures/mainnet-validator.json\":39,\"test/helpers/Fixtures.sol\":140,\"tools/reproduce.py\":83},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":7104,"exitCode":0,"name":"slither","output":"[high/high] uninitialized-state at src/RoyaltyDistributor.sol:177: RoyaltyDistributor._receipts (src/RoyaltyDistributor.sol#66) is never initialized. It is used in:\n[medium/medium] divide-before-multiply at src/art/SeedArt.sol:138: SeedArt.lattice(bytes32,string) (src/art/SeedArt.sol#138-148) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/RoyaltyDistributor.sol:215: RoyaltyDistributor._record(address,uint256) (src/RoyaltyDistributor.sol#215-234) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/art/SeedData.sol:55: SeedData.candidateSeed(uint32,uint32).seed (src/art/SeedData.sol#55) is a local variable never initialized\n[low/medium] calls-loop at src/SeedClock.sol:151: SeedClock._update(address,uint256,address) (src/SeedClock.sol#151-163) has external calls inside a loop: ITransferValidator(TRANSFER_VALIDATOR).validateTransfer(msg.sender,from,to,tokenId) (src/SeedClock.sol#159)\n[low/medium] calls-loop at src/SeedClock.sol:151: SeedClock._update(address,uint256,address) (src/SeedClock.sol#151-163) has external calls inside a loop: ITransferValidator(TRANSFER_VALIDATOR).validateTransfer(msg.sender,from,to,tokenId) (src/SeedClock.sol#159)\n[low/medium] calls-loop at src/SeedClock.sol:151: SeedClock._update(address,uint256,address) (src/SeedClock.sol#151-163) has external calls inside a loop: IRoyaltyDistributor(distributor).updateShares(from,to,tokenId) (src/SeedClock.sol#161)\n[low/medium] calls-loop at src/SeedClock.sol:151: SeedClock._update(address,uint256,address) (src/SeedClock.sol#151-163) has external calls inside a loop: IRoyaltyDistributor(distributor).updateShares(from,to,tokenId) (src/SeedClock.sol#161)","passed":true},{"durationMs":1174,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/SeedClock.sol:56: Reentrancy: State change after external call (2 places)\n[low] costly-loop at src/SeedClock.sol:75: Costly operations inside loop\n[low] internal-function-used-once at src/art/SeedArt.sol:25: Internal Function Used Only Once (7 places)\n[low] large-numeric-literal at src/SeedClock.sol:88: Large Numeric Literal\n[low] literal-instead-of-constant at src/RoyaltyDistributor.sol:102: Literal Instead of Constant (53 places)\n[low] local-variable-shadowing at src/RoyaltyDistributor.sol:54: Local Variable Shadows State Variable (3 places)\n[low] require-revert-in-loop at src/SeedClock.sol:75: Loop Contains `require`/`revert`\n[low] state-variable-could-be-constant at src/SeedClock.sol:46: State Variable Could Be Constant\n[low] unchecked-return at src/SeedClock.sol:122: Unchecked Return\n[low] uninitialized-local-variable at src/RoyaltyDistributor.sol:246: Uninitialized Local Variable (5 places)\n[low] unsafe-oz-erc721-mint at src/SeedClock.sol:58: Unsafe `ERC721::_mint()`\n[low] unused-public-function at src/RoyaltyDistributor.sol:192: Public Function Not Used Internally","passed":true},{"durationMs":9060,"exitCode":0,"name":"proof 175459b5f1ac","output":"Compiling 57 files with Solc 0.8.26\nSolc 0.8.26 finished in 8.15s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-e7096866/Proof_175459b5f1ac.t.sol:LateHolderClaimProof\n[PASS] test_lateHolderReachesFirstEarnedRoyaltyInOneClaim() (gas: 54322278)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.35ms (13.10ms CPU time)\n\nRan 1 test suite in 16.57ms (14.35ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":7186,"exitCode":0,"name":"proof 6c7c6cbaaef8","output":"2026-10-17T14:23:13.756442Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-BjwxrK/repo/test/imd-proof-e7096866/Proof_175459b5f1ac.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 6.37s\nCompiler run successful!\n\nRan 1 test for test/imd-proof-a6693c08/Proof_6c7c6cbaaef8.t.sol:StrandedSharesProof\n[PASS] test_royaltiesForTokenSentToDistributorRemainClaimable() (gas: 37863)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.65ms (103.98µs CPU time)\n\nRan 1 test suite in 4.42ms (1.65ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"90a3df1238b5cb74e94c918eaaa2ee3a02eb4fb4e85c35c47c40bc6d4bdf6ebe","verifiedTreeHash":"45eda8f5871e92f0ce1630c652fad10cfc15274a","verifierVersion":"0.1.0+c3fba9eb"}]}