{"assessments":[],"deployments":[{"attestationHash":"0bc9b733a08a10d2ff9bffe3b5c73f5de7eba33b9c195a8a2a4c4e16ae4a2d70","chainId":1,"contracts":[{"address":"0x68e80c90caf73000f3af1378316eb410fa0758d9","blockNumber":26189921,"name":"KeelNoteArt","role":"other","txHash":"0x4849fa6061d1e98c26d8ae90aae2a22996c6b5ff476cd779a4c01e78258daaa8"},{"address":"0xd9822f2878edb457ec6d04ff3b2c8311136ea9ff","blockNumber":26189921,"name":"KeelNotes","role":"other","txHash":"0x4849fa6061d1e98c26d8ae90aae2a22996c6b5ff476cd779a4c01e78258daaa8"}],"id":"5141d99f-dd20-46d7-bd61-106b9b3b1c42","manifestHash":"340e68733da6b6ec2d1ed802333522951057668f705c24385b1a3abd56564215","status":"live"}],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"9d75a813-d486-45db-8185-43a905230f9e","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"693e402c29f6d4191b0c366538248110232e981d03b1a54d420380eee944e012","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":"6975cbc4cb73231470801697c7f9402145e246fda3fee3bca27d19c392f2c9ee","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":"0860ea41cf3d28a7acb6e6c58296068ed49d980f6c2a59a81a8f3d4770f60f6a","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":"21b146d828a56b145b3175e9231709a62bc29c24eceebf592dd326f71b5bfb7a","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":"a1d556b7b6f54b020552381f254cf40fda051a10d63c49514aefc90dba3f8f27","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":"87c8374dde0f78d059147b08f6017cf8f024915b1f29f1c5a5b64f30def53fdd","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"d46c044a386968a9d629afd3684f4ede4376b75b1d24639fe28435f5c6c52215","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"c0fb4ac9562c9141cd676dfd53eb07f16c465f14ec3e4959a237c84c0db2a252","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":"Keel Notes: ERC-721 banknote NFTs for the Keel launchpad. NFT only: do NOT create any ERC-20, pool or liquidity.\n\nContracts (Solidity 0.8.26, OZ 5, Foundry, no proxy/initialize/upgrades):\n- KeelNotes: ERC-721 \"Keel Notes\"/\"KNOTE\", Ownable2Step, constructor(owner=$owner, art=$contract:KeelNoteArt).\n- KeelNoteArt: on-chain SVG for tokenURI.\n\nConstants: IMD 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7. KeelFactory 0x39dfa7d3ce9364db50433fe9cc2f308c6a219f5f: isLaunched(address) bool. KeelVault 0xe2fb1c0e5ea5342cd8607efa8e7c44f06b47013c: project(address) returns a struct whose first field is address creator (then uint8,bool,uint16,uint64,uint64, 6x uint256, 2x bytes32).\n\nMINT: max 1111, ids 1..1111, free, one per wallet ever. mint(minKeel, deadline, sig): EIP-712 (\"KeelNotes\",\"1\") MintPass(address to,uint256 minKeel,uint256 deadline) signed by passSigner (SignatureChecker); to==msg.sender, not expired, minKeel>0, IERC20(keelToken).balanceOf(sender)>=minKeel now. Use _mint. Owner: setKeelToken once (then permanent), setPassSigner/revokePassSigner, setMintOpen (starts closed), setArt.\n\nLEVELS: Standard weight 1, Gold weight 5, max 111 Gold ever (burn never frees a slot). upgradeToGold(noteId, token): caller owns note, note is Standard, isLaunched(token), project(token).creator==caller, token never used before. Settle rewards first. Engrave token address and symbol read once (try/catch, A-Za-z0-9 only, max 16, else \"?\").\n\nREWARDS, IMD only: fund(amount) anyone, credit actually received, needs totalWeight>0, accPerWeight += amount*1e36/totalWeight. creditSurplus() credits IMD sent directly. Rewards stay with the note when sold/transferred; later notes get no earlier rewards. redeem(noteId): only current owner, reverts if pending==0, burns note, removes weight, pays pending IMD; nonReentrant, CEI. Invariant: IMD balance >= liabilities.\n\nSAFETY: redeem is the only burn path (no ERC721Burnable); a failed payment reverts the whole call. rescueToken(token,to,amount) owner-only, reverts for IMD. No receive/fallback.\n\nViews: noteInfo(id) level, weight, backedToken, symbol, pending, mintedAt; totalWeight, goldCount, totalMinted, totalBurned, hasMinted. Events for mint, upgrade, fund, redeem, config.\n\nART: dark engraved banknote, guilloche from keccak(noteId), chrome forest with silver leaves, big metallic \"K\". Standard silver, Gold gold + \"BACKS $SYMBOL\". Text: KEEL NOTE, \"No. 0421 / 1111\", level, \"HOLDS 12.34 IMD\" (live), microtext \"ASSEMBLED BY THE SWARM\". Monospace, no external resources, XML-escape. Each contract under 24 KB.\n\nTESTS (offline mocks at the constant addresses): pass checks, double mint, cap, Gold rules and cap, 1:5 split, upgrade settle, transfer keeps rewards, redeem, no other burn, rescueToken, ETH reverts, liability invariant, tokenURI.\n\nTotal supply: 1111\nToken symbol: KNOTE","parentJobId":null,"planHash":"966b2e4affb72ebb85cea74cb133ae376803aa668d0e772751f41c0e0af7fbbb","previousHash":"f0e4829a2d416f653ddeb4adea11a847f7d620dcde1cdd4fe1238d5fe055a3de","projectId":"9d75a813-d486-45db-8185-43a905230f9e","publication":{"commit":"406ca66d4b43aa3f2b61ede7189e6e83e3725814","deliveredAt":"2026-10-14T09:42:29.308Z","repoUrl":"https://github.com/identity-md-launches/launch-1716-keel-notes-erc-721-banknote"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":null,"snapshotHash":"2419010b13266d047439746c3fc4eadfa44d89c41cc8f256e151479ad57fc458","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"4b1cd06f5981d80f13fd8fc19b0fc8f13c8e8408c888900d10b2102aa5274b41","device":"b6e7758e5e0349d3","findings":[],"hash":"074b50dc3049a40e86d4bde0b7971ade311bc9f71912432ca7e4300d51d4540e","nodeId":"2ee35e67-7015-4768-9a81-9d367a321081","outcome":"completed","summary":"Created `launch.json` with `KeelNoteArt` first, then `KeelNotes` using `[\"$owner\", \"$contract:KeelNoteArt\"]`.\n\nVerified manifest structure and constructor ABIs. `forge build` succeeded; all 60 tests passed with the supplied fuzz seed.\n\nOnly `launch.json` changed.","treeHash":"0797f7e3eb2a38f68a44be4b9ac5a343e455caa8","usage":{"cachedInputTokens":372352,"inputTokens":41027,"model":"gpt-6-astra","outputTokens":3214,"runtime":"codex","turns":4,"wallClockMs":174105}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ddc5542bddcad225","findings":[{"citation":"resolved","description":"Second-round note, not a defect that reopens the work. The fix for my medium finding ad8e44fd (stale accumulator across weight changes) is confirmed: _syncSurplus() now runs at the start of mint() (line 162, before ++totalWeight), upgradeToGold() (line 187, before settling) and redeem() (line 268, before computing the payout), and creditSurplus() is a no-op when nothing is uncredited. Both attached proofs (Proof_ad8e44fd7c07, Proof_fe0f10129dc6) pass on the revised code, the author's test/SurplusSync.t.sol covers all four reported cases, and my own mixed sequence (donate, Gold upgrade, donate, transfer, redeem, fund, redeem all) pays every unit to the weights live at arrival and leaves exactly one unit of rounding dust in reserve. For the pre-mint case the author chose the second of the two options I offered: when totalWeight == 0 the surplus is folded into liabilities and never allocated. The consequence, stated so the operator knows it: IMD sent before the first mint or after every note is redeemed is locked in the contract forever, since the brief forbids rescuing IMD. README line 80 and docs/SECURITY.md document this and tell operators not to send IMD before the first mint; fund() already reverts NoWeight in that state. No holder can be harmed by it and the balance >= liabilities invariant holds, so this is informational. The other four earlier findings are also confirmed fixed: _update (line 337-340) rejects to == address(this) on every ERC-721 transfer path including approved operators and safeTransferFrom with data; setKeelToken (lines 109-111) refuses an address without code or without a decodable balanceOf and leaves keelToken unset so the owner can retry; upgradeMany in test/AdversarialInvariant.t.sol line 162 wraps the increment in unchecked and the full suite (82 tests, both invariant campaigns, zero reverts) passes with an unpinned fuzz seed; IKeelVault.Project carries the comment not to tighten decoding and test/Gold.t.sol exercises a 15-word mock vault. No new defect was found in the changed lines.","line":258,"path":"src/KeelNotes.sol","reproduction":"State: KeelNotes configured, mintOpen, no notes minted. 1000e18 IMD transferred directly to the contract (fund(1000e18) reverts NoWeight). alice mints note 1: SurplusStranded(1000e18) is emitted, liabilities == 1000e18, accPerWeight == 0, pending(1) == 0. creditSurplus() is a no-op; alice redeem(1) reverts NoRewards; owner rescueToken(IMD, owner, 1000e18) reverts IMDRescueForbidden. After fund(10e18) with two notes, pending == 5e18 each and liabilities == 1010e18. Expected per the README: exactly this; the IMD is never paid to anyone. Recorded because the funds are irrecoverable by design. Reproduced by the author's test/SurplusSync.t.sol::test_preMintDepositIsReservedForNobody, which passes on the current tree.","severity":"info","snippet":"        if (totalWeight == 0) {\n            liabilities = balance;\n            emit SurplusStranded(surplus);\n            return;\n        }","title":"IMD that arrives while no note is live is reserved permanently at the next mint; the lock is deliberate and documented, but unrecoverable because rescueToken refuses IMD"}],"hash":"0860ea41cf3d28a7acb6e6c58296068ed49d980f6c2a59a81a8f3d4770f60f6a","nodeId":"674002bb-7d72-4410-a5cb-68b7ce5f8c5c","outcome":"completed","summary":"The review is complete. The findings file is written and validated, the scratch directory is removed, and the working tree is clean apart from the excluded findings file.\n\n**Outcome: every earlier finding is confirmed fixed, and nothing new blocks the launch.**\n\n- **Medium, stale accumulator across weight changes (fixed).** The private sync now runs at the start of mint, upgradeToGold and redeem, before any weight or payout is computed, and creditSurplus is a no-op when nothing is uncredited. Both attached proofs pass on the revised code. The author's SurplusSync tests cover all four cases I reported, and my own mixed sequence of donations, a Gold upgrade, a transfer, a fund and three redemptions paid every unit to the weights live at arrival and left exactly one unit of rounding dust in reserve.\n- **Low, note transferred to the collection (fixed).** The `_update` override rejects a recipient equal to the collection on transferFrom and both safeTransferFrom overloads, including by an approved operator. Verified with my own scratch test.\n- **Low, setKeelToken bricking mint (fixed).** The setter refuses an address without code or whose static balanceOf fails or returns under 32 bytes, and leaves the token unset so the owner can retry. Once-then-permanent is preserved.\n- **Low, invariant handler overflow (fixed).** The increment is wrapped in unchecked. The full suite of 82 tests, including both invariant campaigns with zero reverts, passes under an unpinned fuzz seed.\n- **Info, 15-word vault layout (addressed).** The interface carries a comment not to tighten decoding, and a 15-word mock vault test exercises the creator check.\n\n**One informational note recorded.** The pre-mint path now strands IMD into liabilities permanently instead of handing it to the first minter. This is the option I offered, it is documented in the README and security notes, and no holder can be harmed by it, but the locked IMD is unrecoverable because the brief forbids rescuing IMD. It does not reopen the work.\n\nThe coverage record answers all 19 entry points as holding, with mint pointing at the informational note, plus rows for the solvency invariant and the launch manifest. Both contracts are well under the runtime size limit and the manifest's constructor arguments match the declarations in order.","treeHash":null,"usage":{"cachedInputTokens":962905,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":16611,"runtime":"claude","turns":23,"wallClockMs":341034}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"b57ae3a96321a0e1","findings":[{"citation":"resolved","description":"Boundary guide, 'no code at receiver': mint() calls IERC20(keelToken).balanceOf(msg.sender) at line 153. A high-level interface call to an address with no code reverts (extcodesize check), and a contract that is not an ERC-20 reverts on decode. setKeelToken only rejects address(0) and can never be called again (KeelTokenAlreadySet), and there is no other path that writes keelToken. So a single mistaken owner call (EOA, typo, a pre-computed address whose deployment never happens, or a non-ERC20 contract) leaves every mint() reverting forever with no recovery; the whole 1111-note collection can never be minted. The spec asks for 'once (then permanent)', which is preserved by validating the value at set time (e.g. require token.code.length > 0 and that balanceOf(address(this)) returns) rather than by allowing a correction later.","line":103,"path":"src/KeelNotes.sol","reproduction":"State: fresh KeelNotes(owner, art). Calls: owner -> setKeelToken(0x1234...EOA with no code); owner -> setPassSigner(signer); owner -> setMintOpen(true) (succeeds: it only checks keelToken != 0). Then alice with a valid MintPass -> mint(1, block.timestamp, sig). Expected: a mint with a valid pass succeeds or the owner can repair the configuration. Actual: mint reverts (empty revert from the no-code call at line 153), totalMinted stays 0, and owner -> setKeelToken(realToken) reverts KeelTokenAlreadySet. Verified with test/scratch/Boundaries.t.sol::test_keelTokenWithoutCodeBricksMintPermanently (passes on current code, i.e. the brick is reproducible).","severity":"low","snippet":"        if (keelToken != address(0)) revert KeelTokenAlreadySet();\n        if (token == address(0)) revert ZeroAddress();\n        keelToken = token;","title":"setKeelToken is one-shot but never checks the address has code, so one wrong value permanently bricks mint"},{"citation":"resolved","description":"Math Precision guide, 'overflow intermediates'. mulDiv is 512-bit internally so received*1e36 cannot overflow, but the quotient and the checked += both revert with Panic(0x11) once cumulative sum(received_i * 1e36 / totalWeight_i) reaches 2^256. At totalWeight = 1 the ceiling is 115792089237316195423570985008687907853269984665640564039457 base units (~1.158e41). After that point every fund() and creditSurplus() reverts permanently while redeem keeps working, so the note set could never receive rewards again. Live IMD (mainnet 0xD34a99Bc...) has 18 decimals and totalSupply 4,173,863,886,515,000,000,000,000 (~4.17e24), so the ceiling is ~2.8e16 times the entire supply and cannot be reached; recorded as information only. The existing test test_fullPrecisionProductsAndAccumulatorOverflowRollback already codifies the revert.","line":235,"path":"src/KeelNotes.sol","reproduction":"State: one Standard note (totalWeight = 1). Call fund(115792089237316195423570985008687907853269984665640564039457): succeeds, pending(1) equals that amount. Call fund(1): reverts Panic(0x11). Verified with test/scratch/MathEdges.t.sol::test_accumulatorBoundary. Not reachable: requires ~2.8e16 x IMD total supply.","severity":"info","snippet":"        accPerWeight += Math.mulDiv(received, PRECISION, totalWeight);","title":"Reward accumulator has a hard ceiling of floor(2^256/1e36) weighted base units after which fund/creditSurplus revert forever (unreachable with IMD supply)"},{"citation":"resolved","description":"Boundary guide, 'return-value handling'. cast call on Ethereum mainnet 0xe2fb1c0e5ea5342cd8607efa8e7c44f06b47013c project(address) with an unknown token returns 960 hex characters = 15 ABI words, all zero, whereas src/interfaces/IKeel.sol declares a 14-field static struct. Solidity's ABI decoder only requires returndata to be at least the expected size, so the extra trailing word is ignored and the call does not revert (confirmed by test/scratch/VaultWords.t.sol with a mock vault returning 15 words). The all-zero reply rules out a dynamic (offset-first) encoding. What could not be verified without archive log access is that word 0 of the live layout is the creator address, as the brief states; if a field precedes it, upgradeToGold would always revert NotProjectCreator. No failing input is demonstrated, so this is a note for the judge: compare the deployed vault's ABI against IKeelVault.Project before launch.","line":177,"path":"src/KeelNotes.sol","reproduction":"cast call 0xe2fb1c0e5ea5342cd8607efa8e7c44f06b47013c 'project(address)' 0x0000000000000000000000000000000000000001 --rpc-url https://ethereum-rpc.publicnode.com -> 0x followed by 960 hex chars (15 words). Expected per interface: 14 words (896 hex chars). On this code the discrepancy is harmless because the decoder ignores trailing data; the risk only materialises if the live field order differs from the brief.","severity":"info","snippet":"        if (IKeelVault(KEEL_VAULT).project(token).creator != msg.sender) revert NotProjectCreator();","title":"Live KeelVault.project(address) returns 15 words while IKeelVault.Project declares 14 fields; decode works but the creator-at-word-0 assumption is unverified"}],"hash":"21b146d828a56b145b3175e9231709a62bc29c24eceebf592dd326f71b5bfb7a","nodeId":"93cf9293-43e7-47b1-a47f-f034bffdfaf0","outcome":"completed","summary":"The review is complete and the findings file is written and validated. Nothing tracked in the tree was changed; only `.imd-findings.json` and the scratch tests under `test/scratch/` were created.\n\n**Result: the reward math holds.** I traced every division, carry and boundary in the accumulator against the Math Precision, Boundary and Numerical Gap guides and found no value-leaking defect.\n\n- **Accumulator and fraction carry are exact.** Each credit adds `floor(received·1e36 / totalWeight)`, each note takes `floor(delta·weight / 1e36)` plus a sub-wei fraction that is carried across the Standard-to-Gold upgrade and across transfers. A scratch fuzz compares every note's payout to the exact rational entitlement and matches to the wei, and the project's own invariant suite passed at 512 runs by depth 200.\n- **Solvency is guaranteed by construction.** Per credit, the sum of weighted increments never exceeds the received amount, so liabilities always cover the sum of pending claims. Locked rounding dust is bounded at under 1 wei per credit or redemption, which the code documents as intended.\n- **Boundaries behave.** The 1111 and 111 caps use strict equality but only ever increment by one, the deadline is inclusive, and the art formatter truncates correctly at decimals 0, 1, 2 and 77 through 80 where the power-of-ten arithmetic sits at the edge of uint256.\n- **Live dependencies checked on mainnet.** IMD has 18 decimals and exposes `decimals()`, so tokenURI works; the factory and vault have code.\n\n**Reported findings** (all in `.imd-findings.json` with reproductions):\n\n1. **Low.** `setKeelToken` is one-shot but accepts any non-zero address. Pointing it at an address without code makes every mint revert forever with no repair path. Reproduced in a scratch test.\n2. **Info.** The accumulator reverts permanently once cumulative weighted credits reach about 1.16e41 base units. With IMD's supply near 4.17e24 this is roughly 16 orders of magnitude out of reach, so it is recorded only for completeness.\n3. **Info.** The live vault's `project()` returns 15 ABI words against the 14-field interface. The decoder tolerates the extra word (confirmed with a mock), but without archive log access I could not confirm that the creator really sits in word 0. This is a note for the judge, not a defect.\n\n**Coverage:** all 19 listed entry points have rows, plus four invariant rows. No entry point was left unreached. Out of area but worth noting for the economics specialist: a project creator can front-run a visible `fund` or `creditSurplus` with `upgradeToGold` to capture a 5x share of that credit, which the brief's rules permit.","treeHash":null,"usage":{"cachedInputTokens":1411918,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":36618,"runtime":"claude","turns":34,"wallClockMs":626370}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d0653dc91b6e2259","findings":[{"citation":"resolved","description":"fund() reverts NoWeight while no note exists (line 214), so an operator who wants rewards in place before mint opens can only transfer IMD directly. That balance sits as uncredited surplus until anyone calls creditSurplus(), which allocates it entirely to the weight that is live at that moment. The first minter can mint, credit and redeem in one transaction, taking 100% of the pre-mint deposit, and every later note receives nothing from it. The same mechanism lets any pass holder front-run a visible fund() or creditSurplus() with a mint (or a Gold upgrade) to take a pro-rata slice they did not hold a note for before the transaction. This is consistent with the spec's 'credit to live weight' wording, so it is reported as a flow gap rather than an accounting bug: there is no path to place IMD for the collection as a whole, and permissionless creditSurplus hands timing to the first mover. Suggested fix preserving the design: keep pre-mint IMD out of surplus accounting (e.g. an owner-only reserve that is credited by fund-like calls once a minimum number of notes exist), or gate creditSurplus to the owner, and document never to deposit before minting.","line":229,"path":"src/KeelNotes.sol","reproduction":"State: KeelNotes configured, mintOpen=true, no notes yet. 1) Operator calls fund(1000e18) -> reverts NoWeight. 2) Operator does IMD.transfer(notes, 1000e18) instead. 3) Attacker contract with a valid pass calls in one tx: mint(1, deadline, sig) -> id 1, creditSurplus() -> accPerWeight = 1000e18*1e36/1, redeem(1) -> receives 1000e18 IMD. 4) Alice mints id 2: pending(2) == 0, contract balance 0. Expected: a collection-level deposit benefits the notes it was meant for or is refused; actual: one wallet took all of it. Reproduced in test/scratch/EconReview.t.sol::test_preMintDonationSweptByFirstMinter (passes = behaviour confirmed).","severity":"low","snippet":"        _credit(balance - liabilities, true);","title":"IMD deposited before the first mint (the only way to seed rewards while totalWeight is 0) is swept in full by whichever note mints first via creditSurplus"},{"citation":"resolved","description":"ERC721.transferFrom(from, address(notes), id) is accepted (only safeTransferFrom rejects it, because KeelNotes has no onERC721Received). The contract can never call redeem on itself, so the note's weight stays in totalWeight forever and it keeps earning a 1/W (or 5/W) share of every later fund()/creditSurplus(). That IMD is counted in liabilities, cannot be redeemed by anyone, and cannot be rescued because rescueToken rejects IMD (line 260). The sender loses their own rewards, and the locked share grows with every future funding for the life of the collection; a Gold note stranded this way locks 5 weights. A one-line guard in an _update override rejecting to == address(this) closes the common-mistake path without changing any economics.","line":240,"path":"src/KeelNotes.sol","reproduction":"1) alice mints id 1, bob mints id 2 (totalWeight 2). 2) alice: notes.transferFrom(alice, address(notes), 1) -> succeeds, ownerOf(1) == address(notes). 3) fund(100e18): pending(1) == 50e18, pending(2) == 50e18. 4) bob redeems 2 and gets 50e18. 5) alice: redeem(1) -> NotNoteOwner; owner: rescueToken(IMD, owner, 50e18) -> IMDRescueForbidden. Final: IMD balance 50e18, liabilities 50e18, totalWeight 1, nobody can ever withdraw it, and every future fund() sends 1/totalWeight of itself into the same hole. Reproduced in test/scratch/EconReview.t.sol::test_noteTransferredToCollectionIsPermanentDeadWeight.","severity":"low","snippet":"        if (_requireOwned(noteId) != msg.sender) revert NotNoteOwner();","title":"A note moved with transferFrom to the KeelNotes contract itself is permanently irredeemable yet keeps its weight, locking its share of every future funding"},{"citation":"resolved","description":"Queried on mainnet (block ~26189633) for the two launched tokens PXL 0x3E6805e68c9F0DB1e6928f04a10144357B1E5FEE and AGENT 0x35a697c7cBFEf619ecf2928D37558a0f61DA9517, KeelVault 0xe2fb...013c returns 480 bytes: word0 creator (matches the factory's Launched event), uint8, bool, uint16 (8000), uint64 timestamp, uint64 0, then SEVEN uint256 words, then two bytes32. The brief and the vendored interface list six uint256. Solidity's return decoder only requires returndatasize >= 448, so the creator in word 0 is still decoded correctly and upgradeToGold behaves as specified (verified with a mock returning the exact live layout in test/scratch/LiveVaultLayout.t.sol). Recorded so the interface/README are corrected and so nobody later 'tightens' decoding (e.g. a returndatasize equality check) and breaks Gold upgrades. The factory's isLaunched(address)(bool) matches its verified ABI, and IMD is a verified LayerZero OFT ERC-20 with plain transfers (no fee, pause or blacklist), so the reward-asset assumptions in fund/redeem hold against the live token.","line":10,"path":"src/interfaces/IKeel.sol","reproduction":"cast call 0xe2fb1c0e5ea5342cd8607efa8e7c44f06b47013c 'project(address)' 0x3E6805e68c9F0DB1e6928f04a10144357B1E5FEE --rpc-url <mainnet> returns 15 32-byte words; cast abi-decode with the 14-field signature fails ('no data was decoded') while the Solidity external call in upgradeToGold succeeds and reads creator 0x3ca38ab1e4feB2df1675c2b148544d3B8881DC30. Expected per brief: 14 words. Actual: 15.","severity":"info","snippet":"    struct Project {","title":"Live KeelVault.project() returns 15 words (seven uint256), not the 14-field struct the brief and IKeel.sol declare; it works only because Solidity ignores trailing return data"},{"citation":"resolved","description":"Implemented exactly as the brief specifies ('balanceOf(sender) >= minKeel now'), so this is a documented trust assumption rather than a defect: nothing is locked or spent, and the check can be met by moving the same KEEL between wallets or by a flash loan inside a contract minter (mint uses _mint, so contracts qualify). Because every note is free and immediately earns a pro-rata share of all later IMD funding, the economic sybil resistance of the collection rests entirely on how the backend issues passes (one per real participant). Operators should treat minKeel as cosmetic and not as a cap on how many notes one actor can accumulate.","line":153,"path":"src/KeelNotes.sol","reproduction":"alice holds exactly minKeel; alice mints with her pass; alice transfers all KEEL to bob; bob mints with his own pass; bob transfers to carol; ... n wallets controlled by one actor mint n notes with one KEEL balance. Each note then receives 1/totalWeight of every fund(). Expected (if minKeel were meant as a stake): one note per minKeel held; actual: unbounded notes per minKeel held, bounded only by passes issued.","severity":"info","snippet":"        if (IERC20(keelToken).balanceOf(msg.sender) < minKeel) revert InsufficientKeel();","title":"minKeel is a same-transaction balance snapshot: one KEEL balance can satisfy any number of passes and a flash-borrowed balance qualifies, so the pass signer is the only real economic gate on free rewa"}],"hash":"693e402c29f6d4191b0c366538248110232e981d03b1a54d420380eee944e012","nodeId":"02156ceb-ae56-4b5d-b454-b4f3bc905247","outcome":"completed","summary":"The review is complete and the findings file is written and verified against the tree. Nothing tracked in the repository was modified.\n\n**Outcome: no accounting defect found in the reward economics.** The accumulator math, upgrade settlement, redemption ordering and the `balance >= liabilities >= sum(pending)` invariant all hold under the project suite, my scratch tests, and the live token behaviour. Four items are recorded in `.imd-findings.json`, two low and two informational:\n\n- **Low. Pre-mint IMD is swept by the first minter.** `fund()` refuses to run while no note exists, so the only way to seed rewards early is a direct transfer. That surplus goes to whoever holds weight when `creditSurplus()` is called, and the first minter can mint, credit and redeem in one transaction and take all of it. Reproduced in a scratch test.\n- **Low. A note sent to the collection contract with `transferFrom` is dead weight forever.** It can never be redeemed, keeps earning its share of every later funding, and that IMD cannot be rescued. Reproduced in a scratch test. A one-line `_update` guard fixes it.\n- **Info. The live KeelVault returns 15 words from `project()`, not the 14 the brief and interface declare.** I queried mainnet for the two launched tokens. The creator is still word 0 and Solidity tolerates the extra trailing word, so Gold upgrades work. The interface and README should be corrected before anyone tightens the decode.\n- **Info. `minKeel` is a same-block balance snapshot.** One KEEL balance or a flash loan satisfies any number of passes, so the pass signer is the only real sybil gate on free reward-earning mints. This matches the brief and is recorded as a trust assumption.\n\n**Live dependency checks.** IMD is a verified LayerZero OFT ERC-20 with plain transfers and no fee, pause or blacklist. The factory's `isLaunched` matches its verified ABI. The vault is unverified, so its layout was inferred from on-chain calls against launched tokens.\n\n**Coverage.** All 19 entry points have a row, plus rows for the three economic invariants, locked dust, and accumulator overflow. No entry point was left unreached. Static-analysis leads were all checked: the strict equalities are correct sentinel checks, and the flagged reentrancy in `mint` is a static balance read before state changes under a guard.","treeHash":null,"usage":{"cachedInputTokens":1893160,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":41845,"runtime":"claude","turns":35,"wallClockMs":658128}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1c29c7c6e4cf57eb","findings":[{"citation":"resolved","description":"The brief says later notes get no earlier rewards, and creditSurplus() exists to credit IMD that was sent to the contract directly. But the surplus is only added to accPerWeight when someone calls creditSurplus(), and it is then divided by the totalWeight at that moment. mint() (line 159-160: note.checkpoint = accPerWeight; ++totalWeight;), upgradeToGold() (line 187: totalWeight += 4) and redeem() (line 246) change totalWeight without first crediting the IMD balance that is above liabilities. So a note minted after a direct transfer gets a share of it, and a Standard note upgraded to Gold after a direct transfer collects it at weight 5 instead of 1. Holders whose notes existed when the IMD arrived get less (a holder who redeems before anyone calls creditSurplus loses their share the same way). Anyone watching for a large direct IMD transfer, such as a protocol pushing revenue, can mint with their pass or upgrade first and only then call creditSurplus. The global invariant (balance >= liabilities) still holds, but the IMD goes to the wrong notes. Execution-trace / first-principles class: a stale accumulator read across a weight change.","line":225,"path":"src/KeelNotes.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {KeelNotes} from \"src/KeelNotes.sol\";\nimport {KeelNoteArt} from \"src/KeelNoteArt.sol\";\nimport {MockToken, MockFactory, MockVault} from \"test/mocks/Mocks.sol\";\n\ncontract SurplusTimingTest is Test {\n    KeelNotes notes;\n    MockToken imd;\n    MockToken keel;\n    MockFactory factory;\n    MockVault vault;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n    uint256 constant KEY = 0xA11CE;\n\n    function setUp() public {\n        notes = new KeelNotes(owner, address(new KeelNoteArt()));\n        vm.etch(notes.IMD(), address(new MockToken()).code);\n        imd = MockToken(notes.IMD());\n        imd.setDecimals(18);\n        vm.etch(notes.KEEL_FACTORY(), address(new MockFactory()).code);\n        factory = MockFactory(notes.KEEL_FACTORY());\n        vm.etch(notes.KEEL_VAULT(), address(new MockVault()).code);\n        vault = MockVault(notes.KEEL_VAULT());\n        keel = new MockToken();\n        vm.startPrank(owner);\n        notes.setKeelToken(address(keel));\n        notes.setPassSigner(vm.addr(KEY));\n        notes.setMintOpen(true);\n        vm.stopPrank();\n    }\n\n    function _mint(address to) internal returns (uint256) {\n        keel.mint(to, 1);\n        uint256 deadline = block.timestamp + 1;\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(KEY, notes.mintPassDigest(to, 1, deadline));\n        vm.prank(to);\n        return notes.mint(1, deadline, abi.encodePacked(r, s, v));\n    }\n\n    /// IMD sent directly while only Alice's note exists must not be shared with a note minted afterwards.\n    function test_laterMintSharesEarlierDirectIMD() public {\n        uint256 a = _mint(alice);\n        imd.mint(address(notes), 100 ether); // direct reward transfer, Bob's note does not exist yet\n        uint256 b = _mint(bob);\n        notes.creditSurplus();\n        assertEq(notes.pending(b), 0, \"later note received earlier rewards\");\n        assertEq(notes.pending(a), 100 ether, \"earlier note lost rewards\");\n    }\n\n    /// IMD sent directly while a note is Standard must be split at weight 1, not at Gold weight 5.\n    function test_goldUpgradeTakesEarlierDirectIMDAtWeightFive() public {\n        uint256 a = _mint(alice);\n        uint256 b = _mint(bob);\n        imd.mint(address(notes), 120 ether); // arrives while both notes are Standard (1:1)\n        MockToken t = new MockToken();\n        factory.setLaunched(address(t), true);\n        vault.setCreator(address(t), bob);\n        vm.prank(bob);\n        notes.upgradeToGold(b, address(t));\n        notes.creditSurplus();\n        assertEq(notes.pending(a), 60 ether, \"alice should keep half of rewards that arrived at 1:1\");\n        assertEq(notes.pending(b), 60 ether, \"bob's Gold weight applied retroactively\");\n    }\n}","reproduction":"Case 1 (mint): Alice mints note 1 (totalWeight=1). 100 IMD is transferred directly to KeelNotes. Bob mints note 2 (totalWeight=2). Anyone calls creditSurplus(). Expected: pending(1)=100e18 and pending(2)=0, because Bob's note did not exist when the IMD arrived. Actual: pending(1)=50e18 and pending(2)=50e18. Case 2 (Gold): Alice has note 1 and Bob has note 2, both Standard. 120 IMD is sent directly. Bob, a launched token's creator, calls upgradeToGold(2, token) and then creditSurplus(). Expected: 60/60, the 1:1 weights in force when the IMD arrived. Actual: Alice gets 20e18 and Bob gets 100e18. Both are confirmed by the proof test, which fails with 'later note received earlier rewards: 50000000000000000000 != 0' and 'alice should keep half ...: 20000000000000000000 != 60000000000000000000'. Fix: add a private _syncSurplus() that does nothing when totalWeight==0 or balance<=liabilities and otherwise calls _credit(balance - liabilities, true). Call it at the start of mint() (before ++totalWeight), upgradeToGold() (before settling and before totalWeight += 4) and redeem() (before computing amount).","severity":"medium","snippet":"    function creditSurplus() external nonReentrant {\n        if (totalWeight == 0) revert NoWeight();\n        uint256 balance = IERC20(IMD).balanceOf(address(this));\n        if (balance < liabilities) revert Insolvent();\n        _credit(balance - liabilities, true);\n    }","title":"IMD sent directly is shared by the weights at creditSurplus time, so a note minted or upgraded to Gold later takes rewards that arrived before it"}],"hash":"6975cbc4cb73231470801697c7f9402145e246fda3fee3bca27d19c392f2c9ee","nodeId":"17a5bdd3-fc08-4f5c-8588-cd7bb18cbbba","outcome":"completed","summary":"I found one defect (medium) in the control-flow review of `KeelNotes` and `KeelNoteArt`. I didn't modify any project file. The results are in `.imd-findings.json`: 1 finding and 22 coverage rows, one for each of the 19 entry points plus 3 for the invariant and the helper code.\n\n**Finding 1 (medium): IMD sent straight to the contract can go to notes that didn't exist when it arrived** (`src/KeelNotes.sol:225`, `creditSurplus`)\n- **Cause:** IMD transferred directly only counts as rewards when someone calls `creditSurplus()`. It is then split by the total weight at that moment. `mint`, `upgradeToGold` and `redeem` all change the total weight without crediting that waiting IMD first.\n- **Result:** a note minted later shares in IMD that arrived before it existed, which breaks \"later notes get no earlier rewards\". A note upgraded to Gold collects that IMD at weight 5 instead of 1. Anyone who sees a large direct transfer can mint or upgrade first and then call `creditSurplus()`.\n- **Reproduction, confirmed by a test that fails on the current code:**\n  - Alice mints, 100 IMD is sent directly, Bob mints, then `creditSurplus()` is called. Expected 100/0; actual 50/50.\n  - Alice and Bob both hold Standard notes, 120 IMD is sent directly, Bob upgrades to Gold, then `creditSurplus()` is called. Expected 60/60; actual 20/100.\n- **Proposed fix:** a private step that credits any IMD above `liabilities` to the current notes, called before the weight changes in `mint`, `upgradeToGold` and `redeem`. It does nothing when no notes exist or nothing is waiting.\n- The full test file is in the finding's `proof` field.\n\n**Checked and holding:**\n- **`mint`:** the signed pass binds the caller, the deadline, a non-zero `minKeel` and the live Keel balance. One mint per wallet and the 1111 cap hold.\n- **`upgradeToGold`:** the owner, creator, launched-token, used-token and 111-Gold checks hold, and rewards are settled before the weight changes.\n- **`fund`:** it credits the IMD actually received, and rounding favours the contract.\n- **`redeem`:** only the owner can call it, it reverts when nothing is pending, state is updated before payment, and solvency is re-checked after paying.\n- **Other entry points:** `rescueToken` refuses IMD. The owner setters and the transfer and approval functions behave as intended.\n- **Invariant:** IMD balance ≥ liabilities ≥ total pending holds on every path that changes liabilities.\n- **Helper code:**\n  - The symbol reader returns \"?\" for reverting, malformed, too-long or gas-heavy symbols.\n  - The decoded `tokenURI` JSON and SVG both parse. The text matches the brief (\"No. 0001 / 1111\", \"BACKS $ABC123\", \"HOLDS 12.34 IMD\").\n\n**Static-analysis leads I rejected:**\n- **Strict equality:** each counter only moves by 1, so `==` can't be skipped.\n- **State change after an external call:** those calls are read-only and the functions are `nonReentrant`.\n\n**Trust assumptions, not defects:** the owner can replace the art contract or renounce ownership, which freezes configuration and means minting can never be reopened.\n\n**Left out:** an upgrader who supplies too little gas gets \"?\" as their engraved symbol. Only the note's owner, who is also the token's creator, can trigger that, so it only harms themselves.\n\nThe scratch test folder has been removed.","treeHash":null,"usage":{"cachedInputTokens":521897,"inputTokens":18,"model":"claude-opus-5-5","outputTokens":16845,"runtime":"claude","turns":12,"wallClockMs":206376}},{"artifacts":[],"attempt":1,"bundleHash":"fa0bf7d8d29c39b983cb6a92b53d975db49191b1002c7d28cfa1706debe7ff14","device":"fea57d3e9d0ca7bf","findings":[],"hash":"87c8374dde0f78d059147b08f6017cf8f024915b1f29f1c5a5b64f30def53fdd","nodeId":"5f8743eb-c8ee-4833-90cd-0b8cf62716ea","outcome":"completed","summary":"All four findings reproduced and are fixed. The reviewer's proof now passes, the full suite passes with the verifier's fuzz seed, and a clean offline copy builds, tests and formats cleanly.\n\n**Medium, surplus timing (held to the proof).** Reproduced with the reported values. The accumulator was stale across weight changes: direct IMD was divided by whatever weights existed when someone called `creditSurplus()`. Fix in `src/KeelNotes.sol`:\n- A private surplus sync credits the uncredited balance to the live weights. Mint calls it before adding weight, upgrade before settling, redeem before computing the payout.\n- The public credit function runs the same sync and is now a harmless no-op when nothing is uncredited, which the proof requires.\n- IMD that arrives while no note is live is folded into the reserve and emits `SurplusStranded`. It is never paid to the first minter nor to anyone. The README says not to send IMD before the first mint.\n- New file `test/SurplusSync.t.sol` covers the four reported cases, both stranding cases, shortfall behaviour and a seeded fuzz. The invariant handler now credits donations at the same moments and tracks stranded IMD.\n\n**Low, transfer to the collection.** Reproduced. An `_update` override now rejects any transfer whose recipient is the collection. Tested for both transfer paths and an approved operator.\n\n**Low, one-shot Keel token with no validation.** Reproduced. The setter now refuses an address without code or whose static `balanceOf` probe fails, and stays unset so the owner can retry. Tested with a no-code address and a contract lacking the function.\n\n**Info, 15-word vault answer.** Confirmed with a live mainnet call. The 14-word struct was kept on purpose because it decodes both layouts, and lengthening it would break upgrades against a vault answering exactly 14 words. Added a comment on the struct, a 15-word mock vault and a Gold upgrade test against it.\n\nResults:\n\n| Check | Result |\n| --- | --- |\n| `forge build`, `forge fmt --check` | Pass |\n| Seeded `forge test` | 74 passed, including the 3 proof tests |\n| Clean offline copy, empty environment | 71 passed |\n| KeelNotes runtime | 12,742 bytes |\n\nThe answers to all four findings are in `.imd-responses.json`. README, `docs/SECURITY.md` and `docs/VALIDATION.md` are updated. Nothing is committed, and no launch or blocked file was written.","treeHash":"4a562f0d3e7ab29280a59ce2890c80b32bba8c46","usage":{"cachedInputTokens":1771529,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":48395,"runtime":"claude","turns":81,"wallClockMs":1110497}},{"artifacts":[],"attempt":1,"bundleHash":"ba5cc72d43f4ae83c456fe933fab1e6ab791db20efe13fa17d71c3f598f15cc7","device":"2b9b0095482c54e6","findings":[],"hash":"9f5912c994b6a7f62c29bf67f25a39b9d6a1b1ea05218444360864b56404ebf8","nodeId":"6709860b-8885-4ef4-ae6f-d5e8db0bbc07","outcome":"completed","summary":"I added two test files and found no defects in KeelNotes, so I wrote no `.imd-findings.json`. `forge build` succeeds, and the full `forge test` run with the verifier's fuzz seed passes: 71 tests in 7 suites, 0 failed. The two files are not committed, since that wasn't asked for.\n\nThe existing suite already covered the brief's checklist. Its one invariant suite only made valid calls, and with 16 wallets it could never reach the 111-Gold cap. My additions close those gaps:\n\n**`test/AdversarialInvariant.t.sol`** is an invariant suite over random call sequences.\n- **Setup:** it mints 140 notes and upgrades 100 to Gold, so runs cross the Gold cap. I checked with a throwaway probe that runs do reach 111 Gold and do include redemptions, so the cap and redeem checks aren't empty.\n- **Valid actions:** mint, fund, direct IMD transfer plus `creditSurplus`, upgrade (singly and in batches), transfer, redeem, time jumps, and depositing and rescuing an unrelated token. Redeem must pay exactly the pending amount; upgrade and transfer must leave pending unchanged.\n- **Hostile actions:** each must revert with the specific expected error:\n  - redeem by a stranger, by an approved operator, or of a burned note\n  - a second mint by the same wallet, or a pass used by a different wallet\n  - upgrades with an already-used backing token, by a non-creator, or with an unlaunched token\n  - upgrades past the cap, or of a note already Gold\n  - rescuing IMD, a rescue by a non-owner, or sending ETH\n  - funding more than the caller has\n- **Invariants checked after every call:**\n  - The IMD balance equals everything credited plus uncredited, minus everything paid.\n  - The contract's recorded debt equals credits minus payouts, and covers every note's pending amount.\n  - Rounding loss stays at most one wei per credit, per live note and per redemption.\n  - Gold count is at most 111 and never drops when a Gold note burns.\n  - Total weight equals one per live Standard note plus five per live Gold note.\n  - Supply figures stay consistent, the contract holds no ETH, and rescue moves only the amount it states.\n\n**`test/EdgeFuzz.t.sol`** adds stateless fuzz and unit tests:\n- **Mint pass:** passes signed by any other key are refused; changing any signed field is caught; the wallet's current Keel balance must meet the minimum and minting takes none of it; expired passes are refused.\n- **Rewards:** the 1:5 split holds exactly for every mix of Gold among 8 notes, paying out at most the amount funded and losing at most one wei per note. A note minted later earns nothing from earlier funding and can't redeem zero.\n- **Edges:** the zero address or the note contract itself can't be a backing token. Redeeming a Gold note removes 5 weight but keeps its cap slot. Closing minting blocks a valid pass. Minting can't reopen once the pass signer is revoked.\n\nOne harness quirk to know about: with `via_ir` on, `block.timestamp` can be re-read after `vm.warp`, which made an early version of the expiry test fail for no real reason. That test now uses fixed timestamps; it was never a contract bug.","treeHash":"613ee6db5a0f13b9ea411b6f055ef7ccd5aed395","usage":{"cachedInputTokens":874819,"inputTokens":32,"model":"claude-opus-5-5","outputTokens":22083,"runtime":"claude","turns":22,"wallClockMs":482680}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"bc1e515ca5c78ac4","findings":[],"hash":"a1d556b7b6f54b020552381f254cf40fda051a10d63c49514aefc90dba3f8f27","nodeId":"1381d84e-9b54-4a6d-bef6-0a118dd81b59","outcome":"completed","summary":"I found no defects I can back with a concrete failing input in the permission model of `KeelNotes`. `.imd-findings.json` holds an empty findings list and one coverage row for each of the 19 listed entry points, all marked `holds`, plus a row for the liability invariant. I changed no project files, and the existing 60 tests all pass.\n\nI went through the Access Control, Asymmetry and Trust Gap guides in that order:\n\n- **Access control:**\n  - Every owner function is `onlyOwner`, and `setKeelToken` can only be called once.\n  - Ownership is handed over in two steps, and the constructor takes the owner explicitly, so it isn't trapped at the factory.\n  - `redeem` and `upgradeToGold` require the caller to be the note's owner, not just an approved operator.\n  - `readTokenSymbol` can only be called by the contract itself.\n  - There is no `receive`, `fallback` or initializer, and no public burn: transfers to the zero address revert, so `redeem` is the only way to burn a note.\n- **Asymmetry:**\n  - Mint sets the note's reward starting point (its checkpoint); upgrade settles earnings first, including sub-wei remainders.\n  - Redeem clears the note before paying and removes its weight.\n  - Burning never decrements `goldCount` or clears `usedBackingToken`, so Gold slots and backing tokens stay used.\n- **Trust gap:**\n  - Mint passes are tied to the sender, the chain and this contract.\n  - `rescueToken` refuses IMD, so the owner can't take funds owed to notes.\n  - The owner's other powers (`setArt`, `setPassSigner`, renouncing ownership) don't touch IMD. I've noted them as trust assumptions rather than findings.\n  - Rounding in reward accounting always favours the contract, so the IMD balance stays at or above what it owes.\n\n**Static-analysis leads I checked and dropped:**\n- **Strict equalities** (`goldCount == MAX_GOLD`, `amount == 0`, `received == 0`): these are intended cap and zero checks.\n- **Calling the KEEL token before updating state in `mint`:** that token is set by the owner, and every function that moves value shares one reentrancy lock.\n- **`_mint` instead of a safe mint:** the brief asks for `_mint`.\n\n**What I didn't check:**\n- I couldn't confirm that the real KeelVault's `project()` return layout matches the interface. It only matches what the brief describes.\n- If the caller of `upgradeToGold` sends too little gas, the symbol read fails and \"?\" is engraved permanently. Only the note owner, who is also the creator, can trigger this, so I didn't report it.\n- I didn't review the art contract beyond reading it once.","treeHash":null,"usage":{"cachedInputTokens":320760,"inputTokens":14,"model":"claude-opus-5-5","outputTokens":9237,"runtime":"claude","turns":7,"wallClockMs":147286}},{"artifacts":[],"attempt":1,"bundleHash":"a9c1c28478a76a5ed2c728eba5293c12117518454cff209be8bc880178fa449b","device":"d11ea2b5e05fa7a8","findings":[],"hash":"c0fb4ac9562c9141cd676dfd53eb07f16c465f14ec3e4959a237c84c0db2a252","nodeId":"6709860b-8885-4ef4-ae6f-d5e8db0bbc07","outcome":"completed","summary":"The finding reproduced and is fixed. The only changed file is the adversarial invariant handler, and the response file answers the reviewer.\n\n**What I found**\n\n- **The reported overflow is real.** The handler's `upgradeMany` looped over `seed + i` with checked arithmetic, so a max-uint256 seed panicked before any contract call, and the suite only passed under the pinned seed.\n- **The handler was also stale against the merged contract.** The other contributor's accepted revision made mint, upgrade and redeem credit directly sent IMD first, and turned creditSurplus into a silent no-op. A random seed shrank to a donate followed by a redeem, where my handler expected a NoRewards revert but the contract correctly paid the note's share. The contract behaves as the README now documents, so this was a harness defect, not a contract bug.\n\n**What I changed in test/AdversarialInvariant.t.sol**\n\n- Wrapped the seed increment in `unchecked`, since the seed only picks a note id by modulo.\n- Added a ghost surplus sync with credited, stranded and uncredited counters, applied at the same moments the contract syncs.\n- The creditSurplus action now asserts a no-op changes neither liabilities nor the accumulator when nothing is uncredited.\n- The redeem action handles the donate-only case with a try/catch that accepts only a NoRewards revert, checks the note survives a revert, and otherwise requires a positive payout at least equal to the previously visible pending amount.\n- The upgrade action requires exact settlement when nothing is uncredited and no loss otherwise.\n- The conservation invariant now includes stranded IMD on both sides.\n\n**Verification**\n\n| Run | Result |\n| --- | --- |\n| Adversarial suite, pinned seed | pass |\n| Adversarial suite, reviewer's failing seed and two random seeds | pass |\n| Full suite with pinned seed, 84 tests | pass |\n| Full suite with random seed, 84 tests | pass |\n| Scratch test calling upgradeMany with max-uint seed | pass |\n\nNo contract defects surfaced, so no findings file was written. The reviewer's finding is answered as fixed in the responses file.","treeHash":"2cb676801edde4096d84c5abd9f8d23110bd5464","usage":{"cachedInputTokens":804466,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":14242,"runtime":"claude","turns":24,"wallClockMs":676891}},{"artifacts":[],"attempt":1,"bundleHash":"b47870a3d78b6f0c469818ca80cb7e7d1f82bfcaa500a84f80a88355243a4dc5","device":"9676188abd3783f2","findings":[],"hash":"d3c21801af53327a192341da4b6cccb1269d5bb387729d03a80b0a6b741fad11","nodeId":"5f8743eb-c8ee-4833-90cd-0b8cf62716ea","outcome":"completed","summary":"Implemented both contracts, on-chain SVG artwork, vendored dependencies, and deployment documentation.\n\nVerified: **60 tests pass** with the supplied seed, including clean offline checks. `forge build` and `forge fmt --check` pass. Both contracts are under 24 KB.\n\nOwner configuration and operational responsibilities are documented in [README.md](/root/.identitymd/work/9d75a813-d486-45db-8185-43a905230f9e/5f8743eb-c8ee-4833-90cd-0b8cf62716ea/README.md).","treeHash":"0140c8a29cd29f029e67ad2b2bbf3f34acf180b3","usage":{"cachedInputTokens":1736832,"inputTokens":135658,"model":"gpt-6-astra","outputTokens":59400,"runtime":"codex","turns":9,"wallClockMs":1690867}},{"artifacts":[],"attempt":1,"bundleHash":"e71e7f0016a7ca4f34d5c08e56e6e537ed18f8bfe3d2d33d8c78e3fb0954c347","device":"523b06a3dd921f5d","findings":[],"hash":"d46c044a386968a9d629afd3684f4ede4376b75b1d24639fe28435f5c6c52215","nodeId":"2ee35e67-7015-4768-9a81-9d367a321081","outcome":"completed","summary":"The manifest in the working tree is correct and complete, so no change is needed.\n\n**What I verified**\n\n- `launch.json` has exactly the keys kind, contracts and notes, with kind set to evm_contracts.\n- The contract order matches constructor dependency: KeelNoteArt first with no arguments, then KeelNotes with `$owner` and `$contract:KeelNoteArt`. That matches the KeelNotes constructor signature `(address owner_, address art_)` at `src/KeelNotes.sol:99`, and KeelNoteArt has no constructor.\n- Both names are unique Solidity identifiers under 32 characters. Notes are a single string of 591 characters.\n- Ownership goes to `$owner` through Ownable2Step, never to the factory or IMD, which satisfies the protected owner check.\n- Both runtimes stay under the EIP-170 limit the protected deployment test enforces.\n\n| Contract | Runtime size |\n|---|---|\n| KeelNoteArt | 10,747 B |\n| KeelNotes | 12,742 B |\n\n**Checks run**\n\n`forge build` succeeds with only lint warnings. `forge test` with the verifier's fuzz seed passed all 71 tests across 6 suites. The working tree is clean, so no file other than the previously accepted `launch.json` is touched.\n\nThe revision brief listed no concrete findings against the manifest, and rewriting an accepted, correct file would only give the reviewer a different artifact to read. The deliverable is the existing `launch.json`.","treeHash":"871d4fd2455a4f428e5018b990e3adcdf8259d7b","usage":{"cachedInputTokens":172948,"inputTokens":162,"model":"claude-fable-5-1","outputTokens":1873,"runtime":"claude","turns":6,"wallClockMs":80560}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7e64d4917fa814e6","findings":[{"citation":"resolved","description":"creditSurplus() (lines 225-230) divides the uncredited balance (balance - liabilities) by the totalWeight at the moment of the call, but mint() (line 159-160: note.checkpoint = accPerWeight; ++totalWeight;), upgradeToGold() (line 187: totalWeight += 4) and redeem() (line 246: totalWeight -= _weight(note.level)) all change the weight set without first crediting IMD that already sits in the contract. The brief guarantees that rewards stay with the note and that later notes get no earlier rewards; for IMD transferred directly (the only way to seed rewards before the first mint, since fund() reverts NoWeight at line 214, and the path a payer such as a protocol pushing revenue would use) that guarantee does not hold: (a) a note minted after the IMD arrived takes a pro-rata share; (b) a Standard note upgraded to Gold after arrival collects it at weight 5 instead of 1; (c) a holder who redeems before anyone calls creditSurplus() forfeits their share to the remaining notes; (d) a pass holder or launched-token creator watching the mempool or the IMD balance can mint/upgrade first and then call creditSurplus() in the same transaction; and in the degenerate pre-mint case the first minter can mint, creditSurplus() and redeem in one transaction and take 100% of a deposit made for the collection. The global invariant balance >= liabilities still holds; the IMD goes to the wrong notes. This merges the audit_flow finding (fe0f1012...) and the audit_economics pre-mint finding (eb53571b...), which share the root cause: a stale accumulator across a weight change. Minimal fix that preserves the design: a private _syncSurplus() that returns when totalWeight == 0 or balance <= liabilities and otherwise calls _credit(balance - liabilities, true); call it at the start of mint() (before ++totalWeight), upgradeToGold() (before settling) and redeem() (before computing amount). The totalWeight == 0 case has no live note to hold the IMD, so document that IMD must not be deposited before the first mint, or keep a pre-mint deposit out of surplus accounting.","line":229,"path":"src/KeelNotes.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {KeelNotes} from \"src/KeelNotes.sol\";\nimport {KeelNoteArt} from \"src/KeelNoteArt.sol\";\nimport {IKeelFactory, IKeelVault} from \"src/interfaces/IKeel.sol\";\n\ncontract ProofToken {\n    string public name = \"Proof\";\n    string public symbol = \"PRF\";\n    uint8 public decimals = 18;\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    }\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\ncontract ProofFactory is IKeelFactory {\n    mapping(address => bool) public isLaunched;\n\n    function setLaunched(address token, bool value) external {\n        isLaunched[token] = value;\n    }\n}\n\ncontract ProofVault is IKeelVault {\n    mapping(address => Project) private _projects;\n\n    function setCreator(address token, address creator) external {\n        _projects[token] = Project(creator, 0, false, 0, 0, 0, 0, 0, 0, 0, 0, 0, bytes32(0), bytes32(0));\n    }\n\n    function project(address token) external view returns (Project memory) {\n        return _projects[token];\n    }\n}\n\n/// Direct IMD transfers must be shared by the weights that were live when the IMD arrived,\n/// not by whichever weights exist when someone later calls creditSurplus().\ncontract SurplusTimingProof is Test {\n    KeelNotes notes;\n    ProofToken imd;\n    ProofToken keel;\n    ProofFactory factory;\n    ProofVault vault;\n    address owner = makeAddr(\"owner\");\n    address alice = makeAddr(\"alice\");\n    address bob = makeAddr(\"bob\");\n    uint256 constant KEY = 0xA11CE;\n\n    function setUp() public {\n        notes = new KeelNotes(owner, address(new KeelNoteArt()));\n        vm.etch(notes.IMD(), address(new ProofToken()).code);\n        imd = ProofToken(notes.IMD());\n        vm.etch(notes.KEEL_FACTORY(), address(new ProofFactory()).code);\n        factory = ProofFactory(notes.KEEL_FACTORY());\n        vm.etch(notes.KEEL_VAULT(), address(new ProofVault()).code);\n        vault = ProofVault(notes.KEEL_VAULT());\n        keel = new ProofToken();\n        vm.startPrank(owner);\n        notes.setKeelToken(address(keel));\n        notes.setPassSigner(vm.addr(KEY));\n        notes.setMintOpen(true);\n        vm.stopPrank();\n    }\n\n    function _mint(address to) internal returns (uint256) {\n        keel.mint(to, 1);\n        uint256 deadline = block.timestamp + 1;\n        (uint8 v, bytes32 r, bytes32 s) = vm.sign(KEY, notes.mintPassDigest(to, 1, deadline));\n        vm.prank(to);\n        return notes.mint(1, deadline, abi.encodePacked(r, s, v));\n    }\n\n    /// 100 IMD arrives while only Alice's note exists; Bob mints afterwards and must get none of it.\n    function test_laterMintSharesEarlierDirectIMD() public {\n        uint256 a = _mint(alice);\n        imd.mint(address(notes), 100 ether);\n        uint256 b = _mint(bob);\n        notes.creditSurplus();\n        assertEq(notes.pending(b), 0, \"later note received earlier rewards\");\n        assertEq(notes.pending(a), 100 ether, \"earlier note lost rewards\");\n    }\n\n    /// 120 IMD arrives while both notes are Standard (1:1); Bob upgrades to Gold afterwards and must not\n    /// collect it at weight 5.\n    function test_goldUpgradeTakesEarlierDirectIMDAtWeightFive() public {\n        uint256 a = _mint(alice);\n        uint256 b = _mint(bob);\n        imd.mint(address(notes), 120 ether);\n        ProofToken t = new ProofToken();\n        factory.setLaunched(address(t), true);\n        vault.setCreator(address(t), bob);\n        vm.prank(bob);\n        notes.upgradeToGold(b, address(t));\n        notes.creditSurplus();\n        assertEq(notes.pending(a), 60 ether, \"alice should keep half of rewards that arrived at 1:1\");\n        assertEq(notes.pending(b), 60 ether, \"bob's Gold weight applied retroactively\");\n    }\n\n    /// Redeeming before anyone calls creditSurplus() forfeits the redeemer's share of IMD that arrived\n    /// while the note was live; it goes to the remaining notes instead.\n    function test_redeemBeforeCreditForfeitsShare() public {\n        uint256 a = _mint(alice);\n        uint256 b = _mint(bob);\n        imd.mint(address(this), 10 ether);\n        imd.approve(address(notes), 10 ether);\n        notes.fund(10 ether);\n        imd.mint(address(notes), 100 ether);\n        vm.prank(alice);\n        notes.redeem(a);\n        assertEq(imd.balanceOf(alice), 55 ether, \"alice should receive her half of the direct IMD\");\n        notes.creditSurplus();\n        assertEq(notes.pending(b), 55 ether, \"bob should only receive his half of the direct IMD\");\n    }\n}","reproduction":"State: KeelNotes configured with a mock IMD at 0xD34a...63B7, mintOpen. Case 1: alice mints note 1 (totalWeight 1); 100e18 IMD is transferred directly to KeelNotes; bob mints note 2; anyone calls creditSurplus(). Expected pending(1)=100e18, pending(2)=0. Actual pending(1)=50e18, pending(2)=50e18. Case 2: alice note 1 and bob note 2 both Standard; 120e18 IMD transferred directly; bob (creator of a launched token) calls upgradeToGold(2, token) then creditSurplus(). Expected 60e18/60e18. Actual pending(1)=20e18, pending(2)=100e18. Case 3: notes 1 and 2, fund(10e18), then 100e18 transferred directly; alice redeem(1) before creditSurplus(). Expected alice receives 55e18. Actual alice receives 5e18 and after creditSurplus() pending(2)=105e18. Case 4 (pre-mint): 1000e18 transferred directly while totalWeight==0 (fund reverts NoWeight); alice mint(), creditSurplus(), redeem(1) in one transaction receives 1000e18; bob mints next, pending=0. The attached proof fails on the current code with 'later note received earlier rewards: 50000000000000000000 != 0', '20000000000000000000 != 60000000000000000000' and '5000000000000000000 != 55000000000000000000' and passes once surplus is synced before each weight change.","severity":"medium","snippet":"        _credit(balance - liabilities, true);","title":"Direct IMD is credited to the weights live at creditSurplus() time, so notes minted, upgraded or redeemed between arrival and credit take or lose rewards they were not entitled to"},{"citation":"resolved","description":"OpenZeppelin ERC721.transferFrom does not check the receiver, so transferFrom(from, address(notes), id) succeeds (only safeTransferFrom reverts because KeelNotes has no onERC721Received). The contract can never be msg.sender of its own redeem(), so the note's weight stays in totalWeight for the life of the collection, it keeps earning 1/totalWeight (5/totalWeight if Gold) of every later fund()/creditSurplus(), that IMD is counted in liabilities, and rescueToken() refuses IMD (line 260). The sender loses only their own note, but the locked share reduces every other holder's rewards from then on, and the IMD can never leave the contract. An _update override that rejects to == address(this) closes the path without changing any economics.","line":240,"path":"src/KeelNotes.sol","reproduction":"alice mints note 1, bob mints note 2 (totalWeight 2). alice: notes.transferFrom(alice, address(notes), 1) succeeds; ownerOf(1) == address(notes). fund(100e18): pending(1) == 50e18, pending(2) == 50e18. bob redeem(2) receives 50e18. alice redeem(1) reverts NotNoteOwner; owner rescueToken(IMD, owner, 50e18) reverts IMDRescueForbidden. Final state: IMD balance 50e18, liabilities 50e18, totalWeight 1, no path withdraws it, and every future fund() sends 1/totalWeight of itself to the same note. Expected: a transfer to the collection is refused or the stranded weight can be cleared. Confirmed with test/scratch/Review.t.sol::test_noteTransferredToCollectionIsDeadWeight.","severity":"low","snippet":"        if (_requireOwned(noteId) != msg.sender) revert NotNoteOwner();","title":"A note transferred with transferFrom to the KeelNotes contract itself can never be redeemed but keeps its weight, locking its share of every future funding forever"},{"citation":"resolved","description":"mint() calls IERC20(keelToken).balanceOf(msg.sender) at line 153. A high-level call to an address without code reverts on the extcodesize check, and a contract that does not implement balanceOf reverts on decode. setKeelToken only rejects address(0), can never be called again, and no other path writes keelToken, so an owner typo, an EOA, or a pre-computed token address whose deployment never happens leaves every mint reverting forever; no note of the 1111 can ever be minted and the collection must be redeployed. The brief asks for 'once (then permanent)', which is preserved by validating at set time: require token.code.length > 0 and that IERC20(token).balanceOf(address(this)) returns, before committing.","line":103,"path":"src/KeelNotes.sol","reproduction":"Fresh KeelNotes(owner, art). owner: setKeelToken(0x0000000000000000000000000000000000001234) succeeds; setPassSigner(signer); setMintOpen(true) succeeds. alice with a valid MintPass: mint(1, deadline, sig) reverts (empty revert from the no-code call at line 153); totalMinted stays 0. owner: setKeelToken(realToken) reverts KeelTokenAlreadySet. Expected: the bad value is refused at set time or can be corrected. Confirmed with test/scratch/Review.t.sol::test_keelTokenWithoutCodeBricksMint.","severity":"low","snippet":"        if (keelToken != address(0)) revert KeelTokenAlreadySet();\n        if (token == address(0)) revert ZeroAddress();\n        keelToken = token;","title":"setKeelToken is one-shot but accepts any non-zero address, so a single wrong value permanently bricks mint() for the whole collection"},{"citation":"resolved","description":"upgradeMany(uint256 seed, uint8 count) loops upgrade(seed + i) with checked arithmetic. The fuzzer's dictionary routinely supplies seed = type(uint256).max, so the second iteration panics with 0x11, and foundry.toml sets fail_on_revert = true, so both invariants (invariant_capsAndWeights, invariant_conservationAndBoundedDust) are reported broken. The brief lists the liability invariant among the required tests; as written that test only passes when run with FOUNDRY_FUZZ_SEED=0x485f8d7d... as the README instructs, and fails under a plain `forge test`. The contract is not at fault: the panic is in the handler before any KeelNotes call. Fix: unchecked { upgrade(seed + i); } or bound the seed, or derive each iteration's seed by hashing.","line":139,"path":"test/AdversarialInvariant.t.sol","reproduction":"Run `forge test --match-contract AdversarialInvariantTest` without the pinned seed (observed with seeds 0xe0ba3372... and a second random seed): both invariants fail with 'panic: arithmetic underflow or overflow (0x11)', shrunk sequence upgradeMany(115792089237316195423570985008687907853269984665640564039457584007913129639935, 28). With FOUNDRY_FUZZ_SEED=0x485f8d7d32111fc48577652b8f1994903cbeb0b387deb8f8dfdab3ee24cab200 and a cleared cache/invariant/failures the same suite passes. Expected: the invariant suite passes for every seed.","severity":"low","snippet":"            upgrade(seed + i);","title":"AdversarialInvariant handler overflows on seed + i, so the invariant suite fails under any fuzz seed that picks a max-uint256 seed and only passes with the README's pinned seed"},{"citation":"resolved","description":"Queried on Ethereum mainnet (merging the audit_math and audit_economics notes): cast call 0xe2fb1c0e5ea5342cd8607efa8e7c44f06b47013c 'project(address)' 0x3E6805e68c9F0DB1e6928f04a10144357B1E5FEE returns 480 bytes: word 0 = creator 0x3ca38ab1e4feb2df1675c2b148544d3b8881dc30, then 0, 1, 0x1f40, 0x6acdd1eb, 0, seven zero uint256 words, then two identical bytes32. The interface (and the brief) list six uint256. Solidity's ABI decoder only requires returndatasize >= 448 bytes, so creator is read correctly from word 0 and upgradeToGold behaves as specified against the live vault; isLaunched(address) on the live factory returns bool as expected. No failing input exists on this code. Recorded so the interface and README are corrected and so nobody later tightens decoding (a returndatasize equality check or a hand-rolled decoder) and breaks Gold upgrades.","line":10,"path":"src/interfaces/IKeel.sol","reproduction":"cast call 0xe2fb1c0e5ea5342cd8607efa8e7c44f06b47013c 'project(address)' 0x3E6805e68c9F0DB1e6928f04a10144357B1E5FEE --rpc-url https://ethereum-rpc.publicnode.com returns 0x followed by 960 hex characters (15 words); the declared struct is 14 words (896). A mock vault returning the 15-word layout decodes without revert in Solidity and upgradeToGold succeeds. Expected per brief: 14 words. Actual: 15.","severity":"info","snippet":"    struct Project {","title":"Live KeelVault.project(address) returns 15 ABI words (seven uint256) while IKeelVault.Project and the brief declare 14; decoding works only because Solidity ignores trailing return data"}],"hash":"f019ee21f31ab1add2c6218ee5c4d41aeda762563fff5ad67592dfb08543967d","nodeId":"674002bb-7d72-4410-a5cb-68b7ce5f8c5c","outcome":"completed","summary":"All snippets match their lines, and git shows nothing changed outside the ignored findings file and scratch directory. The review is complete.\n\n**Outcome:** the findings file at `.imd-findings.json` holds five findings and a 23-row coverage record covering all 19 entry points plus four invariants. The existing test suite passed 70 of 71 tests under a random seed, and the one failure is a test-harness defect reported below, not a contract defect.\n\n**Findings kept, after reproduction:**\n\n- **Medium, surplus timing.** Direct IMD transfers are credited to whichever weights are live when someone calls creditSurplus, not when the IMD arrived. A note minted later takes a share, a Gold upgrade collects at weight 5 retroactively, and a holder who redeems before the credit forfeits their share. This merges the audit_flow finding and the audit_economics pre-mint sweep, which share one root cause. I attached a self-contained proof with inline mocks that fails on the current code in three cases.\n- **Low, dead weight.** A note moved to the KeelNotes contract with transferFrom can never be redeemed, so its weight and all future share of funding are locked forever. Reproduced in a scratch test.\n- **Low, one-shot token setting.** setKeelToken takes any non-zero address and can never be corrected, so a typo or an address without code bricks every mint permanently. Reproduced in a scratch test.\n- **Low, test suite flakiness.** The adversarial invariant handler adds the loop index to a fuzzed seed with checked arithmetic, so a max-uint256 seed panics and both invariants fail under any seed except the README's pinned one. Confirmed by running with the pinned seed after clearing the failure cache, which passes, and with two random seeds, which fail.\n- **Info, live vault layout.** The mainnet KeelVault returns 15 words where the interface and brief declare 14. I confirmed this with a live call. The creator sits in word 0 and Solidity ignores trailing data, so upgradeToGold works as specified. The two specialist notes on this were merged.\n\n**Dropped:** the accumulator overflow ceiling note, which needs roughly 2.8e16 times the IMD supply, and the minKeel snapshot note, which is exactly the behaviour the brief specifies.\n\n**Independent pass:** I traced mint, Gold upgrade, funding, redemption, rescue, ownership and the art renderer against the Pashov and entry-point checklists. Signature binding, replay protection, checks-effects-interactions in redeem, the balance-versus-liabilities invariant, the contract sizes and the manifest all hold. No new contract-level defect appeared beyond those listed.","treeHash":null,"usage":{"cachedInputTokens":883480,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":27407,"runtime":"claude","turns":28,"wallClockMs":488109}}],"verification":[{"checks":[{"durationMs":32158,"exitCode":0,"name":"build","output":"Compiling 61 files with Solc 0.8.26\nSolc 0.8.26 finished in 32.03s\nCompiler run successful!\n","passed":true},{"durationMs":5601,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 8 tests for test/ArtAndDeployment.t.sol:ArtAndDeploymentTest\n[PASS] test_artEscapesAllXMLMetacharacters() (gas: 7718974)\n[PASS] test_artHonorsTokenDecimalsAndTruncatesDisplayOnly() (gas: 32850473)\n[PASS] test_factoryDeploymentOnEmptyDependencyAddresses() (gas: 12405248)\n[PASS] test_goldTokenURIEngravingAndMetadata() (gas: 22746238)\n[PASS] test_ownerCanReplaceRendererWithoutAffectingClaims() (gas: 3822162)\n[PASS] test_runtimeSizesAndForbiddenOpcodes() (gas: 12113204)\n[PASS] test_svgSerialDeterminismAndUniqueGuilloche() (gas: 8780878)\n[PASS] test_tokenURIContainsValidJSONEmbeddedSVGAndLiveRewards() (gas: 46656989)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 160.37ms (626.22ms CPU time)\n\nRan 25 tests for test/Rewards.t.sol:RewardsTest\n[PASS] testFuzz_mintUpgradeTransferThenRedeem(uint128,uint128,bool) (runs: 512, μ: 2992237, ~: 2990516)\nLogs:\n  Bound result 2088134907990204\n  Bound result 2300235515694636732285450312\n\n[PASS] testFuzz_weightedPayoutConservation(uint128) (runs: 512, μ: 2668377, ~: 2668673)\nLogs:\n  Bound result 920938463463374607433469623285\n\n[PASS] test_ETHAndUnknownSelectorsRevertOnBothContracts() (gas: 145755)\n[PASS] test_directTransfersAreCreditedExactlyOnceAndNotByFund() (gas: 1109231)\n[PASS] test_externalBalanceLossIsDetected() (gas: 615407)\n[PASS] test_failedIncomingTransferRollsBackCredit() (gas: 520248)\n[PASS] test_failedOutgoingTransferRollsBackWholeRedemption() (gas: 2805596)\n[PASS] test_fullPrecisionProductsAndAccumulatorOverflowRollback() (gas: 2520742)\n[PASS] test_fundAndRedeemReentrancyBlocked() (gas: 964504)\n[PASS] test_fundNeedsWeightAndNonzeroReceipts() (gas: 662846)\n[PASS] test_incomingTransferFeeCreditsOnlyReceived() (gas: 655158)\n[PASS] test_laterNotesReceiveNoEarlierRewardsOrFractions() (gas: 1572032)\n[PASS] test_noOtherBurnPath() (gas: 390100)\n[PASS] test_noReturnERC20CanFundAndRedeem() (gas: 716659)\n[PASS] test_oneToFiveSplit() (gas: 2447423)\n[PASS] test_outgoingRecipientFeeRollsBackBurnUntilFullPaymentPossible() (gas: 985642)\n[PASS] test_outgoingSurchargeCannotSpendAnotherNotesReserve() (gas: 1034402)\n[PASS] test_redeemZeroRevertsWithoutBurn() (gas: 332691)\n[PASS] test_redemptionBurnsAndRemovesWeightOnlyOwner() (gas: 2902451)\n[PASS] test_rescueOnlyOwnerAndNeverIMD() (gas: 254136)\n[PASS] test_roundingDustIsReservedAndNeverAwardedToLaterNotes() (gas: 1879073)\n[PASS] test_safeTransferReceiverCanRedeemItsNewlyOwnedNote() (gas: 857968)\n[PASS] test_transferKeepsPastAndFutureRewardsWithNote() (gas: 1016580)\n[PASS] test_upgradePreservesFractionalStandardEarnings() (gas: 2657864)\n[PASS] test_upgradeSettlesStandardBeforeGoldRewards() (gas: 2658390)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 174.60ms (357.93ms CPU time)\n\nRan 9 tests for test/Gold.t.sol:GoldTest\n[PASS] testFuzz_symbolValidation(bytes) (runs: 512, μ: 1914426, ~: 1915927)\n[PASS] test_goldEngravesTokenAndSymbolOnce() (gas: 1935554)\n[PASS] test_goldLifetimeCapAndBurnDoesNotFreeSlot() (gas: 199396205)\n[PASS] test_mustBeLaunchedAndStandard() (gas: 1990525)\n[PASS] test_onlyCurrentOwnerAndCreatorCanUpgrade() (gas: 2107890)\n[PASS] test_revertingMalformedExpensiveAndHugeMetadataFallBack() (gas: 3993850)\n[PASS] test_symbolDecoderOnlySelf() (gas: 13206)\n[PASS] test_symbolsMustBeNonemptyAlphanumericAndAtMost16() (gas: 14574697)\n[PASS] test_tokenCannotBackAnotherNoteEvenAfterBurn() (gas: 2549317)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 207.03ms (285.97ms CPU time)\n\nRan 17 tests for test/MintAndConfig.t.sol:MintAndConfigTest\n[PASS] test_ERC1271PassAndRevocation() (gas: 904976)\n[PASS] test_allSettingsRequireOwner() (gas: 140878)\n[PASS] test_contractWithoutReceiverCanMint() (gas: 434071)\n[PASS] test_deadlineInclusiveThenExpired() (gas: 318455)\n[PASS] test_deploymentStartsClosedWithExplicitOwnerAndNoExternalCalls() (gas: 103811)\n[PASS] test_domainBindsChainAndContract() (gas: 323272)\n[PASS] test_lifetimeSupplyCapDoesNotReopenOnBurn() (gas: 232582935)\n[PASS] test_oneMintEverEvenAfterTransferOrRedemption() (gas: 964387)\n[PASS] test_passCannotBeUsedByDifferentRecipient() (gas: 134919)\n[PASS] test_positiveMinimumAndLiveBalanceRequired() (gas: 485174)\n[PASS] test_signerRotationRevocationAndMintClosure() (gas: 348512)\n[PASS] test_twoStepOwnershipAndArtUpdate() (gas: 223093)\n[PASS] test_unconfiguredOpeningAndOneTimeKeel() (gas: 288238)\n[PASS] test_unknownNoteViewsRevert() (gas: 39051)\n[PASS] test_validPassMintsFreeAndReportsInfo() (gas: 378663)\n[PASS] test_wrongSignerTamperedMinimumDeadlineAndMalformedSignature() (gas: 279988)\n[PASS] test_zeroConstructorArgumentsRejected() (gas: 6775)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 797.36ms (802.67ms CPU time)\n\nRan 1 test for test/RewardsInvariant.t.sol:RewardsInvariantTest\n[PASS]\nRewardsInvariantTest invariants:\n[PASS] invariant_balanceCoversReserveAndEveryClaim\n[PASS] invariant_lifetimeLimitsWeightsAndOwners\n RewardsInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| RewardHandler | credit   | 1096  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | donate   | 1248  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | fund     | 1192  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | mint     | 1149  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | redeem   | 1222  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | transfer | 1114  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | upgrade  | 1171  | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 67787158434014\n  Bound result 6455\n  Bound result 162514264337593543950333\n  Bound result 236697285851591308074603\n  Bound result 12338\n  Bound result 610\n  Bound result 4670\n  Bound result 8458\n  Bound result 7678\n  Bound result 2857\n  Bound result 13480\n  Bound result 2\n  Bound result 10240\n  Bound result 8171\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.45s (5.44s CPU time)\n\nRan 5 test suites in 5.45s (6.79s CPU time): 60 tests passed, 0 failed, 0 skipped (60 total tests)\n","passed":true},{"durationMs":99,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KeelNotes.acceptOwnership()\",\"KeelNotes.approve(address,uint256)\",\"KeelNotes.creditSurplus()\",\"KeelNotes.fund(uint256)\",\"KeelNotes.mint(uint256,uint256,bytes)\",\"KeelNotes.redeem(uint256)\",\"KeelNotes.renounceOwnership()\",\"KeelNotes.rescueToken(address,address,uint256)\",\"KeelNotes.revokePassSigner()\",\"KeelNotes.safeTransferFrom(address,address,uint256)\",\"KeelNotes.safeTransferFrom(address,address,uint256,bytes)\",\"KeelNotes.setApprovalForAll(address,bool)\",\"KeelNotes.setArt(address)\",\"KeelNotes.setKeelToken(address)\",\"KeelNotes.setMintOpen(bool)\",\"KeelNotes.setPassSigner(address)\",\"KeelNotes.transferFrom(address,address,uint256)\",\"KeelNotes.transferOwnership(address)\",\"KeelNotes.upgradeToGold(uint256,address)\"],\"files\":{\".gitignore\":4,\"README.md\":111,\"docs/DEPENDENCIES.md\":11,\"docs/SECURITY.md\":38,\"docs/VALIDATION.md\":34,\"docs/gold.svg\":1,\"docs/standard.svg\":1,\"docs/vendor.sha256\":59,\"foundry.toml\":23,\"launch.json\":14,\"remappings.txt\":2,\"src/KeelNoteArt.sol\":219,\"src/KeelNotes.sol\":320,\"src/interfaces/IKeel.sol\":28,\"src/interfaces/IKeelNoteArt.sol\":14,\"test/ArtAndDeployment.t.sol\":195,\"test/Gold.t.sol\":150,\"test/KeelNotesTestBase.sol\":115,\"test/MintAndConfig.t.sol\":293,\"test/Rewards.t.sol\":442,\"test/RewardsInvariant.t.sol\":188,\"test/mocks/Mocks.sol\":181},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"074b50dc3049a40e86d4bde0b7971ade311bc9f71912432ca7e4300d51d4540e","verifiedTreeHash":"0797f7e3eb2a38f68a44be4b9ac5a343e455caa8","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":36891,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 36.76s\nCompiler run successful!\n","passed":true},{"durationMs":5963,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/Gold.t.sol:GoldTest\n[PASS] testFuzz_symbolValidation(bytes) (runs: 512, μ: 1930147, ~: 1931670)\n[PASS] test_goldEngravesTokenAndSymbolOnce() (gas: 1951237)\n[PASS] test_goldLifetimeCapAndBurnDoesNotFreeSlot() (gas: 201150373)\n[PASS] test_mustBeLaunchedAndStandard() (gas: 2006234)\n[PASS] test_onlyCurrentOwnerAndCreatorCanUpgrade() (gas: 2123648)\n[PASS] test_revertingMalformedExpensiveAndHugeMetadataFallBack() (gas: 4072455)\n[PASS] test_symbolDecoderOnlySelf() (gas: 13206)\n[PASS] test_symbolsMustBeNonemptyAlphanumericAndAtMost16() (gas: 14700465)\n[PASS] test_tokenCannotBackAnotherNoteEvenAfterBurn() (gas: 2574196)\n[PASS] test_vaultReturningExtraTrailingWordStillQualifiesCreator() (gas: 1937438)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 193.96ms (384.79ms CPU time)\n\nRan 8 tests for test/ArtAndDeployment.t.sol:ArtAndDeploymentTest\n[PASS] test_artEscapesAllXMLMetacharacters() (gas: 7718974)\n[PASS] test_artHonorsTokenDecimalsAndTruncatesDisplayOnly() (gas: 32855795)\n[PASS] test_factoryDeploymentOnEmptyDependencyAddresses() (gas: 12632788)\n[PASS] test_goldTokenURIEngravingAndMetadata() (gas: 22761959)\n[PASS] test_ownerCanReplaceRendererWithoutAffectingClaims() (gas: 3827484)\n[PASS] test_runtimeSizesAndForbiddenOpcodes() (gas: 12340734)\n[PASS] test_svgSerialDeterminismAndUniqueGuilloche() (gas: 8780878)\n[PASS] test_tokenURIContainsValidJSONEmbeddedSVGAndLiveRewards() (gas: 46662311)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 194.91ms (784.10ms CPU time)\n\nRan 9 tests for test/SurplusSync.t.sol:SurplusSyncTest\n[PASS] testFuzz_directIMDFollowsLiveWeightsAcrossWeightChanges(uint96,uint96,bool) (runs: 512, μ: 1930796, ~: 2566722)\nLogs:\n  Bound result 1057\n  Bound result 1662\n\n[PASS] test_depositWhileAllNotesRedeemedIsReservedForNobody() (gas: 1085380)\n[PASS] test_goldUpgradeCollectsEarlierDirectIMDAtStandardWeight() (gas: 2579499)\n[PASS] test_laterMintGetsNoneOfEarlierDirectIMD() (gas: 852046)\n[PASS] test_mintThenCreditThenRedeemInOneTransactionTakesNothingEarlier() (gas: 992819)\n[PASS] test_preMintDepositIsReservedForNobody() (gas: 1158482)\n[PASS] test_redeemBeforeAnyCreditPaysTheNoteShare() (gas: 1175679)\n[PASS] test_redeemWithOnlyUncreditedRewardsSucceeds() (gas: 874298)\n[PASS] test_shortfallDoesNotBlockWeightChanges() (gas: 2802524)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 196.78ms (241.77ms CPU time)\n\nRan 26 tests for test/Rewards.t.sol:RewardsTest\n[PASS] testFuzz_mintUpgradeTransferThenRedeem(uint128,uint128,bool) (runs: 512, μ: 3015949, ~: 3014362)\nLogs:\n  Bound result 517273807\n  Bound result 5855199738424491298202818046\n\n[PASS] testFuzz_weightedPayoutConservation(uint128) (runs: 512, μ: 2694659, ~: 2695016)\nLogs:\n  Bound result 186177\n\n[PASS] test_ETHAndUnknownSelectorsRevertOnBothContracts() (gas: 145777)\n[PASS] test_directTransfersAreCreditedExactlyOnceAndNotByFund() (gas: 1153268)\n[PASS] test_externalBalanceLossIsDetected() (gas: 620787)\n[PASS] test_failedIncomingTransferRollsBackCredit() (gas: 525634)\n[PASS] test_failedOutgoingTransferRollsBackWholeRedemption() (gas: 2829096)\n[PASS] test_fullPrecisionProductsAndAccumulatorOverflowRollback() (gas: 2537589)\n[PASS] test_fundAndRedeemReentrancyBlocked() (gas: 970951)\n[PASS] test_fundNeedsWeightAndNonzeroReceipts() (gas: 793458)\n[PASS] test_incomingTransferFeeCreditsOnlyReceived() (gas: 660544)\n[PASS] test_laterNotesReceiveNoEarlierRewardsOrFractions() (gas: 1601076)\n[PASS] test_noOtherBurnPath() (gas: 398008)\n[PASS] test_noReturnERC20CanFundAndRedeem() (gas: 723107)\n[PASS] test_noteCannotBeTransferredToTheCollection() (gas: 711619)\n[PASS] test_oneToFiveSplit() (gas: 2471094)\n[PASS] test_outgoingRecipientFeeRollsBackBurnUntilFullPaymentPossible() (gas: 993417)\n[PASS] test_outgoingSurchargeCannotSpendAnotherNotesReserve() (gas: 1049001)\n[PASS] test_redeemZeroRevertsWithoutBurn() (gas: 348388)\n[PASS] test_redemptionBurnsAndRemovesWeightOnlyOwner() (gas: 2927435)\n[PASS] test_rescueOnlyOwnerAndNeverIMD() (gas: 254136)\n[PASS] test_roundingDustIsReservedAndNeverAwardedToLaterNotes() (gas: 1941447)\n[PASS] test_safeTransferReceiverCanRedeemItsNewlyOwnedNote() (gas: 864433)\n[PASS] test_transferKeepsPastAndFutureRewardsWithNote() (gas: 1023049)\n[PASS] test_upgradePreservesFractionalStandardEarnings() (gas: 2679057)\n[PASS] test_upgradeSettlesStandardBeforeGoldRewards() (gas: 2679583)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 234.69ms (427.39ms CPU time)\n\nRan 17 tests for test/MintAndConfig.t.sol:MintAndConfigTest\n[PASS] test_ERC1271PassAndRevocation() (gas: 912801)\n[PASS] test_allSettingsRequireOwner() (gas: 140878)\n[PASS] test_contractWithoutReceiverCanMint() (gas: 441893)\n[PASS] test_deadlineInclusiveThenExpired() (gas: 326277)\n[PASS] test_deploymentStartsClosedWithExplicitOwnerAndNoExternalCalls() (gas: 103811)\n[PASS] test_domainBindsChainAndContract() (gas: 329266)\n[PASS] test_lifetimeSupplyCapDoesNotReopenOnBurn() (gas: 241274504)\n[PASS] test_oneMintEverEvenAfterTransferOrRedemption() (gas: 978879)\n[PASS] test_passCannotBeUsedByDifferentRecipient() (gas: 134919)\n[PASS] test_positiveMinimumAndLiveBalanceRequired() (gas: 492996)\n[PASS] test_signerRotationRevocationAndMintClosure() (gas: 348512)\n[PASS] test_twoStepOwnershipAndArtUpdate() (gas: 223093)\n[PASS] test_unconfiguredOpeningAndOneTimeKeel() (gas: 375587)\n[PASS] test_unknownNoteViewsRevert() (gas: 39051)\n[PASS] test_validPassMintsFreeAndReportsInfo() (gas: 386485)\n[PASS] test_wrongSignerTamperedMinimumDeadlineAndMalformedSignature() (gas: 279988)\n[PASS] test_zeroConstructorArgumentsRejected() (gas: 6775)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 770.81ms (819.01ms CPU time)\n\nRan 1 test for test/RewardsInvariant.t.sol:RewardsInvariantTest\n[PASS]\nRewardsInvariantTest invariants:\n[PASS] invariant_balanceCoversReserveAndEveryClaim\n[PASS] invariant_lifetimeLimitsWeightsAndOwners\n RewardsInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| RewardHandler | credit   | 1096  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | donate   | 1248  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | fund     | 1192  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | mint     | 1149  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | redeem   | 1222  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | transfer | 1114  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | upgrade  | 1171  | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 67787158434014\n  Bound result 6486\n  Bound result 162514264337593543950333\n  Bound result 6415\n  Bound result 236697285851591308074603\n  Bound result 7959\n  Bound result 91\n  Bound result 1546\n  Bound result 7396\n  Bound result 8611\n  Bound result 2834\n  Bound result 13687\n  Bound result 2\n  Bound result 10173\n  Bound result 4363\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.83s (5.82s CPU time)\n\nRan 6 test suites in 5.83s (7.42s CPU time): 71 tests passed, 0 failed, 0 skipped (71 total tests)\n","passed":true},{"durationMs":74,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KeelNotes.acceptOwnership()\",\"KeelNotes.approve(address,uint256)\",\"KeelNotes.creditSurplus()\",\"KeelNotes.fund(uint256)\",\"KeelNotes.mint(uint256,uint256,bytes)\",\"KeelNotes.redeem(uint256)\",\"KeelNotes.renounceOwnership()\",\"KeelNotes.rescueToken(address,address,uint256)\",\"KeelNotes.revokePassSigner()\",\"KeelNotes.safeTransferFrom(address,address,uint256)\",\"KeelNotes.safeTransferFrom(address,address,uint256,bytes)\",\"KeelNotes.setApprovalForAll(address,bool)\",\"KeelNotes.setArt(address)\",\"KeelNotes.setKeelToken(address)\",\"KeelNotes.setMintOpen(bool)\",\"KeelNotes.setPassSigner(address)\",\"KeelNotes.transferFrom(address,address,uint256)\",\"KeelNotes.transferOwnership(address)\",\"KeelNotes.upgradeToGold(uint256,address)\"],\"files\":{\".gitignore\":4,\"README.md\":113,\"docs/DEPENDENCIES.md\":11,\"docs/SECURITY.md\":40,\"docs/VALIDATION.md\":35,\"docs/gold.svg\":1,\"docs/standard.svg\":1,\"docs/vendor.sha256\":59,\"foundry.toml\":23,\"remappings.txt\":2,\"src/KeelNoteArt.sol\":219,\"src/KeelNotes.sol\":354,\"src/interfaces/IKeel.sol\":32,\"src/interfaces/IKeelNoteArt.sol\":14,\"test/ArtAndDeployment.t.sol\":195,\"test/Gold.t.sol\":174,\"test/KeelNotesTestBase.sol\":120,\"test/MintAndConfig.t.sol\":301,\"test/Rewards.t.sol\":474,\"test/RewardsInvariant.t.sol\":208,\"test/SurplusSync.t.sol\":179,\"test/mocks/Mocks.sol\":201},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":5961,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/KeelNotes.sol:242: KeelNotes._credit(uint256,bool) (src/KeelNotes.sol#242-247) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/KeelNotes.sol:266: KeelNotes.redeem(uint256) (src/KeelNotes.sol#266-285) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/KeelNotes.sol:178: KeelNotes.upgradeToGold(uint256,address) (src/KeelNotes.sol#178-198) uses a dangerous strict equality:\n[low/medium] timestamp at src/KeelNotes.sol:145: KeelNotes.mint(uint256,uint256,bytes) (src/KeelNotes.sol#145-172) uses timestamp for comparisons","passed":true},{"durationMs":858,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/KeelNotes.sol:160: Reentrancy: State change after external call (4 places)\n[low] centralization-risk at src/KeelNotes.sol:18: Centralization Risk (7 places)\n[low] large-numeric-literal at src/KeelNotes.sol:27: Large Numeric Literal\n[low] literal-instead-of-constant at src/KeelNoteArt.sol:115: Literal Instead of Constant (14 places)\n[low] non-reentrant-not-first at src/KeelNotes.sol:287: `nonReentrant` is Not the First Modifier\n[low] unchecked-return at src/KeelNotes.sol:299: Unchecked Return (3 places)\n[low] uninitialized-local-variable at src/KeelNoteArt.sol:106: Uninitialized Local Variable (4 places)\n[low] unsafe-oz-erc721-mint at src/KeelNotes.sol:170: Unsafe `ERC721::_mint()`\n[low] unused-public-function at src/KeelNotes.sol:298: Public Function Not Used Internally","passed":true},{"durationMs":11497,"exitCode":0,"name":"proof ad8e44fd7c07","output":"Compiling 57 files with Solc 0.8.26\nSolc 0.8.26 finished in 10.61s\nCompiler run successful!\n\nRan 3 tests for test/imd-proof-10354677/Proof_ad8e44fd7c07.t.sol:SurplusTimingProof\n[PASS] test_goldUpgradeTakesEarlierDirectIMDAtWeightFive() (gas: 1406440)\n[PASS] test_laterMintSharesEarlierDirectIMD() (gas: 666266)\n[PASS] test_redeemBeforeCreditForfeitsShare() (gas: 891473)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 3.17ms (3.36ms CPU time)\n\nRan 1 test suite in 5.07ms (3.17ms CPU time): 3 tests passed, 0 failed, 0 skipped (3 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"87c8374dde0f78d059147b08f6017cf8f024915b1f29f1c5a5b64f30def53fdd","verifiedTreeHash":"4a562f0d3e7ab29280a59ce2890c80b32bba8c46","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":28313,"exitCode":0,"name":"build","output":"Compiling 63 files with Solc 0.8.26\nSolc 0.8.26 finished in 28.18s\nCompiler run successful!\n","passed":true},{"durationMs":9992,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 8 tests for test/ArtAndDeployment.t.sol:ArtAndDeploymentTest\n[PASS] test_artEscapesAllXMLMetacharacters() (gas: 7718974)\n[PASS] test_artHonorsTokenDecimalsAndTruncatesDisplayOnly() (gas: 32850473)\n[PASS] test_factoryDeploymentOnEmptyDependencyAddresses() (gas: 12405248)\n[PASS] test_goldTokenURIEngravingAndMetadata() (gas: 22746238)\n[PASS] test_ownerCanReplaceRendererWithoutAffectingClaims() (gas: 3822162)\n[PASS] test_runtimeSizesAndForbiddenOpcodes() (gas: 12113204)\n[PASS] test_svgSerialDeterminismAndUniqueGuilloche() (gas: 8780878)\n[PASS] test_tokenURIContainsValidJSONEmbeddedSVGAndLiveRewards() (gas: 46656989)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 278.87ms (428.41ms CPU time)\n\nRan 25 tests for test/Rewards.t.sol:RewardsTest\n[PASS] testFuzz_mintUpgradeTransferThenRedeem(uint128,uint128,bool) (runs: 512, μ: 2992223, ~: 2990516)\nLogs:\n  Bound result 2088134907990204\n  Bound result 2300235515694636732285450312\n\n[PASS] testFuzz_weightedPayoutConservation(uint128) (runs: 512, μ: 2668359, ~: 2668673)\nLogs:\n  Bound result 186177\n\n[PASS] test_ETHAndUnknownSelectorsRevertOnBothContracts() (gas: 145755)\n[PASS] test_directTransfersAreCreditedExactlyOnceAndNotByFund() (gas: 1109231)\n[PASS] test_externalBalanceLossIsDetected() (gas: 615407)\n[PASS] test_failedIncomingTransferRollsBackCredit() (gas: 520248)\n[PASS] test_failedOutgoingTransferRollsBackWholeRedemption() (gas: 2805596)\n[PASS] test_fullPrecisionProductsAndAccumulatorOverflowRollback() (gas: 2520742)\n[PASS] test_fundAndRedeemReentrancyBlocked() (gas: 964504)\n[PASS] test_fundNeedsWeightAndNonzeroReceipts() (gas: 662846)\n[PASS] test_incomingTransferFeeCreditsOnlyReceived() (gas: 655158)\n[PASS] test_laterNotesReceiveNoEarlierRewardsOrFractions() (gas: 1572032)\n[PASS] test_noOtherBurnPath() (gas: 390100)\n[PASS] test_noReturnERC20CanFundAndRedeem() (gas: 716659)\n[PASS] test_oneToFiveSplit() (gas: 2447423)\n[PASS] test_outgoingRecipientFeeRollsBackBurnUntilFullPaymentPossible() (gas: 985642)\n[PASS] test_outgoingSurchargeCannotSpendAnotherNotesReserve() (gas: 1034402)\n[PASS] test_redeemZeroRevertsWithoutBurn() (gas: 332691)\n[PASS] test_redemptionBurnsAndRemovesWeightOnlyOwner() (gas: 2902451)\n[PASS] test_rescueOnlyOwnerAndNeverIMD() (gas: 254136)\n[PASS] test_roundingDustIsReservedAndNeverAwardedToLaterNotes() (gas: 1879073)\n[PASS] test_safeTransferReceiverCanRedeemItsNewlyOwnedNote() (gas: 857968)\n[PASS] test_transferKeepsPastAndFutureRewardsWithNote() (gas: 1016580)\n[PASS] test_upgradePreservesFractionalStandardEarnings() (gas: 2657864)\n[PASS] test_upgradeSettlesStandardBeforeGoldRewards() (gas: 2658390)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 283.18ms (490.36ms CPU time)\n\nRan 9 tests for test/Gold.t.sol:GoldTest\n[PASS] testFuzz_symbolValidation(bytes) (runs: 512, μ: 1914518, ~: 1915927)\n[PASS] test_goldEngravesTokenAndSymbolOnce() (gas: 1935554)\n[PASS] test_goldLifetimeCapAndBurnDoesNotFreeSlot() (gas: 199396205)\n[PASS] test_mustBeLaunchedAndStandard() (gas: 1990525)\n[PASS] test_onlyCurrentOwnerAndCreatorCanUpgrade() (gas: 2107890)\n[PASS] test_revertingMalformedExpensiveAndHugeMetadataFallBack() (gas: 3993850)\n[PASS] test_symbolDecoderOnlySelf() (gas: 13206)\n[PASS] test_symbolsMustBeNonemptyAlphanumericAndAtMost16() (gas: 14574697)\n[PASS] test_tokenCannotBackAnotherNoteEvenAfterBurn() (gas: 2549317)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 293.96ms (416.03ms CPU time)\n\nRan 10 tests for test/EdgeFuzz.t.sol:EdgeFuzzTest\n[PASS] testFuzz_expiredPassRejected(uint256) (runs: 512, μ: 130281, ~: 130433)\nLogs:\n  Bound result 315360000\n\n[PASS] testFuzz_laterNoteGetsNothingFromEarlierFunding(uint128,uint128) (runs: 512, μ: 1013866, ~: 1013722)\nLogs:\n  Bound result 60141901245509008147727954738\n  Bound result 2429304729201447\n\n[PASS] testFuzz_liveKeelBalanceMustMeetMinimum(uint256,uint256) (runs: 1000, μ: 222260, ~: 152116)\nLogs:\n  Bound result 4999999999999999999\n  Bound result 20\n\n[PASS] testFuzz_passFieldsAreBound(uint256,uint256,uint256,uint256) (runs: 1000, μ: 153374, ~: 153407)\nLogs:\n  Bound result 675687595501390158121540329849\n  Bound result 895574876903194639057357089512\n  Bound result 30945850\n  Bound result 1\n\n[PASS] testFuzz_passFromAnyOtherKeyIsRejected(uint256) (runs: 1000, μ: 151047, ~: 151038)\nLogs:\n  Bound result 5924993975105051572004361\n\n[PASS] testFuzz_splitIsOneToFiveForAnyGoldSet(uint8,uint128) (runs: 1000, μ: 7640518, ~: 7261474)\nLogs:\n  Bound result 448545090738280346078584097008\n\n[PASS] test_closingMintBlocksValidPassAndReopenRestoresIt() (gas: 375759)\n[PASS] test_goldRedeemRemovesFiveWeightButKeepsCapSlot() (gas: 2648118)\n[PASS] test_reopeningAfterSignerRevokedIsRefused() (gas: 98075)\n[PASS] test_zeroAndSelfReferencingBackingTokensRejected() (gas: 497263)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 294.14ms (1.24s CPU time)\n\nRan 17 tests for test/MintAndConfig.t.sol:MintAndConfigTest\n[PASS] test_ERC1271PassAndRevocation() (gas: 904976)\n[PASS] test_allSettingsRequireOwner() (gas: 140878)\n[PASS] test_contractWithoutReceiverCanMint() (gas: 434071)\n[PASS] test_deadlineInclusiveThenExpired() (gas: 318455)\n[PASS] test_deploymentStartsClosedWithExplicitOwnerAndNoExternalCalls() (gas: 103811)\n[PASS] test_domainBindsChainAndContract() (gas: 323272)\n[PASS] test_lifetimeSupplyCapDoesNotReopenOnBurn() (gas: 232582935)\n[PASS] test_oneMintEverEvenAfterTransferOrRedemption() (gas: 964387)\n[PASS] test_passCannotBeUsedByDifferentRecipient() (gas: 134919)\n[PASS] test_positiveMinimumAndLiveBalanceRequired() (gas: 485174)\n[PASS] test_signerRotationRevocationAndMintClosure() (gas: 348512)\n[PASS] test_twoStepOwnershipAndArtUpdate() (gas: 223093)\n[PASS] test_unconfiguredOpeningAndOneTimeKeel() (gas: 288238)\n[PASS] test_unknownNoteViewsRevert() (gas: 39051)\n[PASS] test_validPassMintsFreeAndReportsInfo() (gas: 378663)\n[PASS] test_wrongSignerTamperedMinimumDeadlineAndMalformedSignature() (gas: 279988)\n[PASS] test_zeroConstructorArgumentsRejected() (gas: 6775)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 473.40ms (471.82ms CPU time)\n\nRan 1 test for test/RewardsInvariant.t.sol:RewardsInvariantTest\n[PASS]\nRewardsInvariantTest invariants:\n[PASS] invariant_balanceCoversReserveAndEveryClaim\n[PASS] invariant_lifetimeLimitsWeightsAndOwners\n RewardsInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| RewardHandler | credit   | 1096  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | donate   | 1248  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | fund     | 1192  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | mint     | 1149  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | redeem   | 1222  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | transfer | 1114  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | upgrade  | 1171  | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 67787158434014\n  Bound result 6481\n  Bound result 162514264337593543950333\n  Bound result 6735\n  Bound result 236697285851591308074603\n  Bound result 8075\n  Bound result 3540462979\n  Bound result 1504\n  Bound result 7862\n  Bound result 695589791138637427926118\n  Bound result 3373\n  Bound result 549\n  Bound result 2\n  Bound result 10597\n  Bound result 8034\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.53s (3.53s CPU time)\n\nRan 1 test for test/AdversarialInvariant.t.sol:AdversarialInvariantTest\n[PASS]\nAdversarialInvariantTest invariants:\n[PASS] invariant_capsAndWeights\n[PASS] invariant_conservationAndBoundedDust\n AdversarialInvariantTest invariants (runs: 48, calls: 2304, reverts: 0)\n\n╭--------------------+------------------------+-------+---------+----------╮\n| Contract           | Selector               | Calls | Reverts | Discards |\n+==========================================================================+\n| AdversarialHandler | approvedOperatorRedeem | 93    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | burnedRedeem           | 98    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | creditSurplus          | 104   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | donate                 | 95    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | doubleMint             | 85    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | fund                   | 105   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | mint                   | 101   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | nonCreatorUpgrade      | 89    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | overfund               | 105   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | redeem                 | 106   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | rescueIMD              | 99    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | rescueStray            | 97    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | reuseBacking           | 124   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | sendETH                | 100   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | stolenPass             | 99    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | strangerRedeem         | 110   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | strangerRescue         | 120   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | strayDeposit           | 106   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | transfer               | 80    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | unlaunchedUpgrade      | 86    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | upgrade                | 108   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | upgradeMany            | 94    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | warp                   | 100   | 0       | 0        |\n╰--------------------+------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 14264337593543950334\n  Bound result 469465620619355227756167424\n  Bound result 15\n  Bound result 10\n  Bound result 112\n  Bound result 655385\n  Bound result 1\n  Bound result 914857\n  Bound result 1\n  Bound result 9216\n  Bound result 5\n  Bound result 257\n  Bound result 90285034291191165462\n  Bound result 469437286420458009884885250\n  Bound result 5299042768554404484\n  Bound result 8170\n  Bound result 95\n  Bound result 854\n  Bound result 10993\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.83s (9.77s CPU time)\n\nRan 7 test suites in 9.83s (14.99s CPU time): 71 tests passed, 0 failed, 0 skipped (71 total tests)\n","passed":true},{"durationMs":58,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KeelNotes.acceptOwnership()\",\"KeelNotes.approve(address,uint256)\",\"KeelNotes.creditSurplus()\",\"KeelNotes.fund(uint256)\",\"KeelNotes.mint(uint256,uint256,bytes)\",\"KeelNotes.redeem(uint256)\",\"KeelNotes.renounceOwnership()\",\"KeelNotes.rescueToken(address,address,uint256)\",\"KeelNotes.revokePassSigner()\",\"KeelNotes.safeTransferFrom(address,address,uint256)\",\"KeelNotes.safeTransferFrom(address,address,uint256,bytes)\",\"KeelNotes.setApprovalForAll(address,bool)\",\"KeelNotes.setArt(address)\",\"KeelNotes.setKeelToken(address)\",\"KeelNotes.setMintOpen(bool)\",\"KeelNotes.setPassSigner(address)\",\"KeelNotes.transferFrom(address,address,uint256)\",\"KeelNotes.transferOwnership(address)\",\"KeelNotes.upgradeToGold(uint256,address)\"],\"files\":{\".gitignore\":4,\"README.md\":111,\"docs/DEPENDENCIES.md\":11,\"docs/SECURITY.md\":38,\"docs/VALIDATION.md\":34,\"docs/gold.svg\":1,\"docs/standard.svg\":1,\"docs/vendor.sha256\":59,\"foundry.toml\":23,\"remappings.txt\":2,\"src/KeelNoteArt.sol\":219,\"src/KeelNotes.sol\":320,\"src/interfaces/IKeel.sol\":28,\"src/interfaces/IKeelNoteArt.sol\":14,\"test/AdversarialInvariant.t.sol\":399,\"test/ArtAndDeployment.t.sol\":195,\"test/EdgeFuzz.t.sol\":190,\"test/Gold.t.sol\":150,\"test/KeelNotesTestBase.sol\":115,\"test/MintAndConfig.t.sol\":293,\"test/Rewards.t.sol\":442,\"test/RewardsInvariant.t.sol\":188,\"test/mocks/Mocks.sol\":181},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"9f5912c994b6a7f62c29bf67f25a39b9d6a1b1ea05218444360864b56404ebf8","verifiedTreeHash":"613ee6db5a0f13b9ea411b6f055ef7ccd5aed395","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":34255,"exitCode":0,"name":"build","output":"Compiling 64 files with Solc 0.8.26\nSolc 0.8.26 finished in 34.17s\nCompiler run successful!\n","passed":true},{"durationMs":9774,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 26 tests for test/Rewards.t.sol:RewardsTest\n[PASS] testFuzz_mintUpgradeTransferThenRedeem(uint128,uint128,bool) (runs: 512, μ: 3015926, ~: 3014599)\nLogs:\n  Bound result 96\n  Bound result 524288\n\n[PASS] testFuzz_weightedPayoutConservation(uint128) (runs: 512, μ: 2694657, ~: 2695034)\nLogs:\n  Bound result 920938463463374607433469623285\n\n[PASS] test_ETHAndUnknownSelectorsRevertOnBothContracts() (gas: 145777)\n[PASS] test_directTransfersAreCreditedExactlyOnceAndNotByFund() (gas: 1153268)\n[PASS] test_externalBalanceLossIsDetected() (gas: 620787)\n[PASS] test_failedIncomingTransferRollsBackCredit() (gas: 525634)\n[PASS] test_failedOutgoingTransferRollsBackWholeRedemption() (gas: 2829096)\n[PASS] test_fullPrecisionProductsAndAccumulatorOverflowRollback() (gas: 2537589)\n[PASS] test_fundAndRedeemReentrancyBlocked() (gas: 970951)\n[PASS] test_fundNeedsWeightAndNonzeroReceipts() (gas: 793458)\n[PASS] test_incomingTransferFeeCreditsOnlyReceived() (gas: 660544)\n[PASS] test_laterNotesReceiveNoEarlierRewardsOrFractions() (gas: 1601076)\n[PASS] test_noOtherBurnPath() (gas: 398008)\n[PASS] test_noReturnERC20CanFundAndRedeem() (gas: 723107)\n[PASS] test_noteCannotBeTransferredToTheCollection() (gas: 711619)\n[PASS] test_oneToFiveSplit() (gas: 2471094)\n[PASS] test_outgoingRecipientFeeRollsBackBurnUntilFullPaymentPossible() (gas: 993417)\n[PASS] test_outgoingSurchargeCannotSpendAnotherNotesReserve() (gas: 1049001)\n[PASS] test_redeemZeroRevertsWithoutBurn() (gas: 348388)\n[PASS] test_redemptionBurnsAndRemovesWeightOnlyOwner() (gas: 2927435)\n[PASS] test_rescueOnlyOwnerAndNeverIMD() (gas: 254136)\n[PASS] test_roundingDustIsReservedAndNeverAwardedToLaterNotes() (gas: 1941447)\n[PASS] test_safeTransferReceiverCanRedeemItsNewlyOwnedNote() (gas: 864433)\n[PASS] test_transferKeepsPastAndFutureRewardsWithNote() (gas: 1023049)\n[PASS] test_upgradePreservesFractionalStandardEarnings() (gas: 2679057)\n[PASS] test_upgradeSettlesStandardBeforeGoldRewards() (gas: 2679583)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 314.90ms (526.00ms CPU time)\n\nRan 9 tests for test/SurplusSync.t.sol:SurplusSyncTest\n[PASS] testFuzz_directIMDFollowsLiveWeightsAcrossWeightChanges(uint96,uint96,bool) (runs: 512, μ: 1930796, ~: 2566722)\nLogs:\n  Bound result 162326843580\n  Bound result 4640613810459283136947\n\n[PASS] test_depositWhileAllNotesRedeemedIsReservedForNobody() (gas: 1085380)\n[PASS] test_goldUpgradeCollectsEarlierDirectIMDAtStandardWeight() (gas: 2579499)\n[PASS] test_laterMintGetsNoneOfEarlierDirectIMD() (gas: 852046)\n[PASS] test_mintThenCreditThenRedeemInOneTransactionTakesNothingEarlier() (gas: 992819)\n[PASS] test_preMintDepositIsReservedForNobody() (gas: 1158482)\n[PASS] test_redeemBeforeAnyCreditPaysTheNoteShare() (gas: 1175679)\n[PASS] test_redeemWithOnlyUncreditedRewardsSucceeds() (gas: 874298)\n[PASS] test_shortfallDoesNotBlockWeightChanges() (gas: 2802524)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 314.93ms (312.05ms CPU time)\n\nRan 8 tests for test/ArtAndDeployment.t.sol:ArtAndDeploymentTest\n[PASS] test_artEscapesAllXMLMetacharacters() (gas: 7718974)\n[PASS] test_artHonorsTokenDecimalsAndTruncatesDisplayOnly() (gas: 32855795)\n[PASS] test_factoryDeploymentOnEmptyDependencyAddresses() (gas: 12632788)\n[PASS] test_goldTokenURIEngravingAndMetadata() (gas: 22761959)\n[PASS] test_ownerCanReplaceRendererWithoutAffectingClaims() (gas: 3827484)\n[PASS] test_runtimeSizesAndForbiddenOpcodes() (gas: 12340734)\n[PASS] test_svgSerialDeterminismAndUniqueGuilloche() (gas: 8780878)\n[PASS] test_tokenURIContainsValidJSONEmbeddedSVGAndLiveRewards() (gas: 46662311)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 315.07ms (399.13ms CPU time)\n\nRan 10 tests for test/Gold.t.sol:GoldTest\n[PASS] testFuzz_symbolValidation(bytes) (runs: 512, μ: 1930124, ~: 1931670)\n[PASS] test_goldEngravesTokenAndSymbolOnce() (gas: 1951237)\n[PASS] test_goldLifetimeCapAndBurnDoesNotFreeSlot() (gas: 201150373)\n[PASS] test_mustBeLaunchedAndStandard() (gas: 2006234)\n[PASS] test_onlyCurrentOwnerAndCreatorCanUpgrade() (gas: 2123648)\n[PASS] test_revertingMalformedExpensiveAndHugeMetadataFallBack() (gas: 4072455)\n[PASS] test_symbolDecoderOnlySelf() (gas: 13206)\n[PASS] test_symbolsMustBeNonemptyAlphanumericAndAtMost16() (gas: 14700465)\n[PASS] test_tokenCannotBackAnotherNoteEvenAfterBurn() (gas: 2574196)\n[PASS] test_vaultReturningExtraTrailingWordStillQualifiesCreator() (gas: 1937438)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 314.94ms (434.88ms CPU time)\n\nRan 10 tests for test/EdgeFuzz.t.sol:EdgeFuzzTest\n[PASS] testFuzz_expiredPassRejected(uint256) (runs: 512, μ: 130277, ~: 130433)\nLogs:\n  Bound result 264392750\n\n[PASS] testFuzz_laterNoteGetsNothingFromEarlierFunding(uint128,uint128) (runs: 512, μ: 1034820, ~: 1034677)\nLogs:\n  Bound result 60141901245509008147727954738\n  Bound result 2429304729201447\n\n[PASS] testFuzz_liveKeelBalanceMustMeetMinimum(uint256,uint256) (runs: 1000, μ: 227367, ~: 152116)\nLogs:\n  Bound result 3784350984130\n  Bound result 449983624748485865669281949060\n\n[PASS] testFuzz_passFieldsAreBound(uint256,uint256,uint256,uint256) (runs: 1000, μ: 153392, ~: 153419)\nLogs:\n  Bound result 1699999999\n  Bound result 23000000000000000000\n  Bound result 112\n  Bound result 6951\n\n[PASS] testFuzz_passFromAnyOtherKeyIsRejected(uint256) (runs: 1000, μ: 151047, ~: 151038)\nLogs:\n  Bound result 5924993975105051572004361\n\n[PASS] testFuzz_splitIsOneToFiveForAnyGoldSet(uint8,uint128) (runs: 1000, μ: 7644353, ~: 7519567)\nLogs:\n  Bound result 23607999084602961\n\n[PASS] test_closingMintBlocksValidPassAndReopenRestoresIt() (gas: 383581)\n[PASS] test_goldRedeemRemovesFiveWeightButKeepsCapSlot() (gas: 2672988)\n[PASS] test_reopeningAfterSignerRevokedIsRefused() (gas: 98075)\n[PASS] test_zeroAndSelfReferencingBackingTokensRejected() (gas: 505073)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 315.61ms (1.72s CPU time)\n\nRan 17 tests for test/MintAndConfig.t.sol:MintAndConfigTest\n[PASS] test_ERC1271PassAndRevocation() (gas: 912801)\n[PASS] test_allSettingsRequireOwner() (gas: 140878)\n[PASS] test_contractWithoutReceiverCanMint() (gas: 441893)\n[PASS] test_deadlineInclusiveThenExpired() (gas: 326277)\n[PASS] test_deploymentStartsClosedWithExplicitOwnerAndNoExternalCalls() (gas: 103811)\n[PASS] test_domainBindsChainAndContract() (gas: 329266)\n[PASS] test_lifetimeSupplyCapDoesNotReopenOnBurn() (gas: 241274504)\n[PASS] test_oneMintEverEvenAfterTransferOrRedemption() (gas: 978879)\n[PASS] test_passCannotBeUsedByDifferentRecipient() (gas: 134919)\n[PASS] test_positiveMinimumAndLiveBalanceRequired() (gas: 492996)\n[PASS] test_signerRotationRevocationAndMintClosure() (gas: 348512)\n[PASS] test_twoStepOwnershipAndArtUpdate() (gas: 223093)\n[PASS] test_unconfiguredOpeningAndOneTimeKeel() (gas: 375587)\n[PASS] test_unknownNoteViewsRevert() (gas: 39051)\n[PASS] test_validPassMintsFreeAndReportsInfo() (gas: 386485)\n[PASS] test_wrongSignerTamperedMinimumDeadlineAndMalformedSignature() (gas: 279988)\n[PASS] test_zeroConstructorArgumentsRejected() (gas: 6775)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 452.74ms (458.01ms CPU time)\n\nRan 1 test for test/RewardsInvariant.t.sol:RewardsInvariantTest\n[PASS]\nRewardsInvariantTest invariants:\n[PASS] invariant_balanceCoversReserveAndEveryClaim\n[PASS] invariant_lifetimeLimitsWeightsAndOwners\n RewardsInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| RewardHandler | credit   | 1096  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | donate   | 1248  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | fund     | 1192  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | mint     | 1149  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | redeem   | 1222  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | transfer | 1114  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | upgrade  | 1171  | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 67787158434014\n  Bound result 6379\n  Bound result 162514264337593543950333\n  Bound result 10163\n  Bound result 236697285851591308074603\n  Bound result 33087249134494\n  Bound result 3051876531\n  Bound result 1544\n  Bound result 7396\n  Bound result 8557\n  Bound result 2156\n  Bound result 13651\n  Bound result 2\n  Bound result 10596\n  Bound result 56363638593045336825957\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.62s (3.62s CPU time)\n\nRan 1 test for test/AdversarialInvariant.t.sol:AdversarialInvariantTest\n[PASS]\nAdversarialInvariantTest invariants:\n[PASS] invariant_capsAndWeights\n[PASS] invariant_conservationAndBoundedDust\n AdversarialInvariantTest invariants (runs: 48, calls: 2304, reverts: 0)\n\n╭--------------------+------------------------+-------+---------+----------╮\n| Contract           | Selector               | Calls | Reverts | Discards |\n+==========================================================================+\n| AdversarialHandler | approvedOperatorRedeem | 93    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | burnedRedeem           | 98    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | creditSurplus          | 104   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | donate                 | 95    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | doubleMint             | 85    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | fund                   | 105   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | mint                   | 101   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | nonCreatorUpgrade      | 89    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | overfund               | 105   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | redeem                 | 106   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | rescueIMD              | 99    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | rescueStray            | 97    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | reuseBacking           | 124   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | sendETH                | 100   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | stolenPass             | 99    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | strangerRedeem         | 110   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | strangerRescue         | 120   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | strayDeposit           | 106   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | transfer               | 80    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | unlaunchedUpgrade      | 86    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | upgrade                | 108   | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | upgradeMany            | 94    | 0       | 0        |\n|--------------------+------------------------+-------+---------+----------|\n| AdversarialHandler | warp                   | 100   | 0       | 0        |\n╰--------------------+------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 14264337593543950334\n  Bound result 187640240495242556762501436\n  Bound result 3\n  Bound result 5\n  Bound result 131072\n  Bound result 655385\n  Bound result 1\n  Bound result 914857\n  Bound result 16\n  Bound result 394166319\n  Bound result 5\n  Bound result 509902\n  Bound result 98702194617749030457\n  Bound result 168154423805149209872128197\n  Bound result 13605221146284988762\n  Bound result 561659697082281770989806918\n  Bound result 244\n  Bound result 249\n  Bound result 2270\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.68s (9.62s CPU time)\n\nRan 8 test suites in 9.68s (15.33s CPU time): 82 tests passed, 0 failed, 0 skipped (82 total tests)\n","passed":true},{"durationMs":62,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KeelNotes.acceptOwnership()\",\"KeelNotes.approve(address,uint256)\",\"KeelNotes.creditSurplus()\",\"KeelNotes.fund(uint256)\",\"KeelNotes.mint(uint256,uint256,bytes)\",\"KeelNotes.redeem(uint256)\",\"KeelNotes.renounceOwnership()\",\"KeelNotes.rescueToken(address,address,uint256)\",\"KeelNotes.revokePassSigner()\",\"KeelNotes.safeTransferFrom(address,address,uint256)\",\"KeelNotes.safeTransferFrom(address,address,uint256,bytes)\",\"KeelNotes.setApprovalForAll(address,bool)\",\"KeelNotes.setArt(address)\",\"KeelNotes.setKeelToken(address)\",\"KeelNotes.setMintOpen(bool)\",\"KeelNotes.setPassSigner(address)\",\"KeelNotes.transferFrom(address,address,uint256)\",\"KeelNotes.transferOwnership(address)\",\"KeelNotes.upgradeToGold(uint256,address)\"],\"files\":{\".gitignore\":4,\"README.md\":113,\"docs/DEPENDENCIES.md\":11,\"docs/SECURITY.md\":40,\"docs/VALIDATION.md\":35,\"docs/gold.svg\":1,\"docs/standard.svg\":1,\"docs/vendor.sha256\":59,\"foundry.toml\":23,\"remappings.txt\":2,\"src/KeelNoteArt.sol\":219,\"src/KeelNotes.sol\":354,\"src/interfaces/IKeel.sol\":32,\"src/interfaces/IKeelNoteArt.sol\":14,\"test/AdversarialInvariant.t.sol\":455,\"test/ArtAndDeployment.t.sol\":195,\"test/EdgeFuzz.t.sol\":190,\"test/Gold.t.sol\":174,\"test/KeelNotesTestBase.sol\":120,\"test/MintAndConfig.t.sol\":301,\"test/Rewards.t.sol\":474,\"test/RewardsInvariant.t.sol\":208,\"test/SurplusSync.t.sol\":179,\"test/mocks/Mocks.sol\":201},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"c0fb4ac9562c9141cd676dfd53eb07f16c465f14ec3e4959a237c84c0db2a252","verifiedTreeHash":"2cb676801edde4096d84c5abd9f8d23110bd5464","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":31771,"exitCode":0,"name":"build","output":"Compiling 61 files with Solc 0.8.26\nSolc 0.8.26 finished in 31.61s\nCompiler run successful!\n","passed":true},{"durationMs":5484,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/Gold.t.sol:GoldTest\n[PASS] testFuzz_symbolValidation(bytes) (runs: 512, μ: 1914426, ~: 1915927)\n[PASS] test_goldEngravesTokenAndSymbolOnce() (gas: 1935554)\n[PASS] test_goldLifetimeCapAndBurnDoesNotFreeSlot() (gas: 199396205)\n[PASS] test_mustBeLaunchedAndStandard() (gas: 1990525)\n[PASS] test_onlyCurrentOwnerAndCreatorCanUpgrade() (gas: 2107890)\n[PASS] test_revertingMalformedExpensiveAndHugeMetadataFallBack() (gas: 3993850)\n[PASS] test_symbolDecoderOnlySelf() (gas: 13206)\n[PASS] test_symbolsMustBeNonemptyAlphanumericAndAtMost16() (gas: 14574697)\n[PASS] test_tokenCannotBackAnotherNoteEvenAfterBurn() (gas: 2549317)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 186.89ms (324.25ms CPU time)\n\nRan 8 tests for test/ArtAndDeployment.t.sol:ArtAndDeploymentTest\n[PASS] test_artEscapesAllXMLMetacharacters() (gas: 7718974)\n[PASS] test_artHonorsTokenDecimalsAndTruncatesDisplayOnly() (gas: 32850473)\n[PASS] test_factoryDeploymentOnEmptyDependencyAddresses() (gas: 12405248)\n[PASS] test_goldTokenURIEngravingAndMetadata() (gas: 22746238)\n[PASS] test_ownerCanReplaceRendererWithoutAffectingClaims() (gas: 3822162)\n[PASS] test_runtimeSizesAndForbiddenOpcodes() (gas: 12113204)\n[PASS] test_svgSerialDeterminismAndUniqueGuilloche() (gas: 8780878)\n[PASS] test_tokenURIContainsValidJSONEmbeddedSVGAndLiveRewards() (gas: 46656989)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 220.76ms (686.19ms CPU time)\n\nRan 25 tests for test/Rewards.t.sol:RewardsTest\n[PASS] testFuzz_mintUpgradeTransferThenRedeem(uint128,uint128,bool) (runs: 512, μ: 2992237, ~: 2990516)\nLogs:\n  Bound result 62\n  Bound result 2797\n\n[PASS] testFuzz_weightedPayoutConservation(uint128) (runs: 512, μ: 2668377, ~: 2668673)\nLogs:\n  Bound result 4126\n\n[PASS] test_ETHAndUnknownSelectorsRevertOnBothContracts() (gas: 145755)\n[PASS] test_directTransfersAreCreditedExactlyOnceAndNotByFund() (gas: 1109231)\n[PASS] test_externalBalanceLossIsDetected() (gas: 615407)\n[PASS] test_failedIncomingTransferRollsBackCredit() (gas: 520248)\n[PASS] test_failedOutgoingTransferRollsBackWholeRedemption() (gas: 2805596)\n[PASS] test_fullPrecisionProductsAndAccumulatorOverflowRollback() (gas: 2520742)\n[PASS] test_fundAndRedeemReentrancyBlocked() (gas: 964504)\n[PASS] test_fundNeedsWeightAndNonzeroReceipts() (gas: 662846)\n[PASS] test_incomingTransferFeeCreditsOnlyReceived() (gas: 655158)\n[PASS] test_laterNotesReceiveNoEarlierRewardsOrFractions() (gas: 1572032)\n[PASS] test_noOtherBurnPath() (gas: 390100)\n[PASS] test_noReturnERC20CanFundAndRedeem() (gas: 716659)\n[PASS] test_oneToFiveSplit() (gas: 2447423)\n[PASS] test_outgoingRecipientFeeRollsBackBurnUntilFullPaymentPossible() (gas: 985642)\n[PASS] test_outgoingSurchargeCannotSpendAnotherNotesReserve() (gas: 1034402)\n[PASS] test_redeemZeroRevertsWithoutBurn() (gas: 332691)\n[PASS] test_redemptionBurnsAndRemovesWeightOnlyOwner() (gas: 2902451)\n[PASS] test_rescueOnlyOwnerAndNeverIMD() (gas: 254136)\n[PASS] test_roundingDustIsReservedAndNeverAwardedToLaterNotes() (gas: 1879073)\n[PASS] test_safeTransferReceiverCanRedeemItsNewlyOwnedNote() (gas: 857968)\n[PASS] test_transferKeepsPastAndFutureRewardsWithNote() (gas: 1016580)\n[PASS] test_upgradePreservesFractionalStandardEarnings() (gas: 2657864)\n[PASS] test_upgradeSettlesStandardBeforeGoldRewards() (gas: 2658390)\nSuite result: ok. 25 passed; 0 failed; 0 skipped; finished in 228.75ms (505.17ms CPU time)\n\nRan 17 tests for test/MintAndConfig.t.sol:MintAndConfigTest\n[PASS] test_ERC1271PassAndRevocation() (gas: 904976)\n[PASS] test_allSettingsRequireOwner() (gas: 140878)\n[PASS] test_contractWithoutReceiverCanMint() (gas: 434071)\n[PASS] test_deadlineInclusiveThenExpired() (gas: 318455)\n[PASS] test_deploymentStartsClosedWithExplicitOwnerAndNoExternalCalls() (gas: 103811)\n[PASS] test_domainBindsChainAndContract() (gas: 323272)\n[PASS] test_lifetimeSupplyCapDoesNotReopenOnBurn() (gas: 232582935)\n[PASS] test_oneMintEverEvenAfterTransferOrRedemption() (gas: 964387)\n[PASS] test_passCannotBeUsedByDifferentRecipient() (gas: 134919)\n[PASS] test_positiveMinimumAndLiveBalanceRequired() (gas: 485174)\n[PASS] test_signerRotationRevocationAndMintClosure() (gas: 348512)\n[PASS] test_twoStepOwnershipAndArtUpdate() (gas: 223093)\n[PASS] test_unconfiguredOpeningAndOneTimeKeel() (gas: 288238)\n[PASS] test_unknownNoteViewsRevert() (gas: 39051)\n[PASS] test_validPassMintsFreeAndReportsInfo() (gas: 378663)\n[PASS] test_wrongSignerTamperedMinimumDeadlineAndMalformedSignature() (gas: 279988)\n[PASS] test_zeroConstructorArgumentsRejected() (gas: 6775)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 831.80ms (883.48ms CPU time)\n\nRan 1 test for test/RewardsInvariant.t.sol:RewardsInvariantTest\n[PASS]\nRewardsInvariantTest invariants:\n[PASS] invariant_balanceCoversReserveAndEveryClaim\n[PASS] invariant_lifetimeLimitsWeightsAndOwners\n RewardsInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| RewardHandler | credit   | 1096  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | donate   | 1248  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | fund     | 1192  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | mint     | 1149  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | redeem   | 1222  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | transfer | 1114  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | upgrade  | 1171  | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 67787158434014\n  Bound result 6455\n  Bound result 162514264337593543950333\n  Bound result 236697285851591308074603\n  Bound result 12338\n  Bound result 610\n  Bound result 4670\n  Bound result 8458\n  Bound result 7678\n  Bound result 2857\n  Bound result 13480\n  Bound result 2\n  Bound result 10240\n  Bound result 8171\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.32s (5.28s CPU time)\n\nRan 5 test suites in 5.33s (6.78s CPU time): 60 tests passed, 0 failed, 0 skipped (60 total tests)\n","passed":true},{"durationMs":77,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KeelNotes.acceptOwnership()\",\"KeelNotes.approve(address,uint256)\",\"KeelNotes.creditSurplus()\",\"KeelNotes.fund(uint256)\",\"KeelNotes.mint(uint256,uint256,bytes)\",\"KeelNotes.redeem(uint256)\",\"KeelNotes.renounceOwnership()\",\"KeelNotes.rescueToken(address,address,uint256)\",\"KeelNotes.revokePassSigner()\",\"KeelNotes.safeTransferFrom(address,address,uint256)\",\"KeelNotes.safeTransferFrom(address,address,uint256,bytes)\",\"KeelNotes.setApprovalForAll(address,bool)\",\"KeelNotes.setArt(address)\",\"KeelNotes.setKeelToken(address)\",\"KeelNotes.setMintOpen(bool)\",\"KeelNotes.setPassSigner(address)\",\"KeelNotes.transferFrom(address,address,uint256)\",\"KeelNotes.transferOwnership(address)\",\"KeelNotes.upgradeToGold(uint256,address)\"],\"files\":{\".gitignore\":4,\"README.md\":111,\"docs/DEPENDENCIES.md\":11,\"docs/SECURITY.md\":38,\"docs/VALIDATION.md\":34,\"docs/gold.svg\":1,\"docs/standard.svg\":1,\"docs/vendor.sha256\":59,\"foundry.toml\":23,\"remappings.txt\":2,\"src/KeelNoteArt.sol\":219,\"src/KeelNotes.sol\":320,\"src/interfaces/IKeel.sol\":28,\"src/interfaces/IKeelNoteArt.sol\":14,\"test/ArtAndDeployment.t.sol\":195,\"test/Gold.t.sol\":150,\"test/KeelNotesTestBase.sol\":115,\"test/MintAndConfig.t.sol\":293,\"test/Rewards.t.sol\":442,\"test/RewardsInvariant.t.sol\":188,\"test/mocks/Mocks.sol\":181},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":6718,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/KeelNotes.sol:239: KeelNotes.redeem(uint256) (src/KeelNotes.sol#239-257) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/KeelNotes.sol:170: KeelNotes.upgradeToGold(uint256,address) (src/KeelNotes.sol#170-189) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/KeelNotes.sol:232: KeelNotes._credit(uint256,bool) (src/KeelNotes.sol#232-237) uses a dangerous strict equality:\n[low/medium] timestamp at src/KeelNotes.sol:138: KeelNotes.mint(uint256,uint256,bytes) (src/KeelNotes.sol#138-164) uses timestamp for comparisons","passed":true},{"durationMs":1185,"exitCode":0,"name":"aderyn","output":"[high] reentrancy-state-change at src/KeelNotes.sol:153: Reentrancy: State change after external call (4 places)\n[low] centralization-risk at src/KeelNotes.sol:18: Centralization Risk (7 places)\n[low] large-numeric-literal at src/KeelNotes.sol:27: Large Numeric Literal\n[low] literal-instead-of-constant at src/KeelNoteArt.sol:115: Literal Instead of Constant (14 places)\n[low] non-reentrant-not-first at src/KeelNotes.sol:259: `nonReentrant` is Not the First Modifier\n[low] unchecked-return at src/KeelNotes.sol:271: Unchecked Return (3 places)\n[low] uninitialized-local-variable at src/KeelNoteArt.sol:106: Uninitialized Local Variable (4 places)\n[low] unsafe-oz-erc721-mint at src/KeelNotes.sol:162: Unsafe `ERC721::_mint()`\n[low] unused-public-function at src/KeelNotes.sol:270: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d3c21801af53327a192341da4b6cccb1269d5bb387729d03a80b0a6b741fad11","verifiedTreeHash":"0140c8a29cd29f029e67ad2b2bbf3f34acf180b3","verifierVersion":"0.1.0+67c588fa"},{"checks":[{"durationMs":36371,"exitCode":0,"name":"build","output":"Compiling 62 files with Solc 0.8.26\nSolc 0.8.26 finished in 36.16s\nCompiler run successful!\n","passed":true},{"durationMs":5945,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/SurplusSync.t.sol:SurplusSyncTest\n[PASS] testFuzz_directIMDFollowsLiveWeightsAcrossWeightChanges(uint96,uint96,bool) (runs: 512, μ: 1930796, ~: 2566722)\nLogs:\n  Bound result 237926714430979428953148\n  Bound result 20414\n\n[PASS] test_depositWhileAllNotesRedeemedIsReservedForNobody() (gas: 1085380)\n[PASS] test_goldUpgradeCollectsEarlierDirectIMDAtStandardWeight() (gas: 2579499)\n[PASS] test_laterMintGetsNoneOfEarlierDirectIMD() (gas: 852046)\n[PASS] test_mintThenCreditThenRedeemInOneTransactionTakesNothingEarlier() (gas: 992819)\n[PASS] test_preMintDepositIsReservedForNobody() (gas: 1158482)\n[PASS] test_redeemBeforeAnyCreditPaysTheNoteShare() (gas: 1175679)\n[PASS] test_redeemWithOnlyUncreditedRewardsSucceeds() (gas: 874298)\n[PASS] test_shortfallDoesNotBlockWeightChanges() (gas: 2802524)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 181.00ms (191.18ms CPU time)\n\nRan 26 tests for test/Rewards.t.sol:RewardsTest\n[PASS] testFuzz_mintUpgradeTransferThenRedeem(uint128,uint128,bool) (runs: 512, μ: 3015949, ~: 3014362)\nLogs:\n  Bound result 2088134907990204\n  Bound result 2300235515694636732285450312\n\n[PASS] testFuzz_weightedPayoutConservation(uint128) (runs: 512, μ: 2694659, ~: 2695016)\nLogs:\n  Bound result 3402\n\n[PASS] test_ETHAndUnknownSelectorsRevertOnBothContracts() (gas: 145777)\n[PASS] test_directTransfersAreCreditedExactlyOnceAndNotByFund() (gas: 1153268)\n[PASS] test_externalBalanceLossIsDetected() (gas: 620787)\n[PASS] test_failedIncomingTransferRollsBackCredit() (gas: 525634)\n[PASS] test_failedOutgoingTransferRollsBackWholeRedemption() (gas: 2829096)\n[PASS] test_fullPrecisionProductsAndAccumulatorOverflowRollback() (gas: 2537589)\n[PASS] test_fundAndRedeemReentrancyBlocked() (gas: 970951)\n[PASS] test_fundNeedsWeightAndNonzeroReceipts() (gas: 793458)\n[PASS] test_incomingTransferFeeCreditsOnlyReceived() (gas: 660544)\n[PASS] test_laterNotesReceiveNoEarlierRewardsOrFractions() (gas: 1601076)\n[PASS] test_noOtherBurnPath() (gas: 398008)\n[PASS] test_noReturnERC20CanFundAndRedeem() (gas: 723107)\n[PASS] test_noteCannotBeTransferredToTheCollection() (gas: 711619)\n[PASS] test_oneToFiveSplit() (gas: 2471094)\n[PASS] test_outgoingRecipientFeeRollsBackBurnUntilFullPaymentPossible() (gas: 993417)\n[PASS] test_outgoingSurchargeCannotSpendAnotherNotesReserve() (gas: 1049001)\n[PASS] test_redeemZeroRevertsWithoutBurn() (gas: 348388)\n[PASS] test_redemptionBurnsAndRemovesWeightOnlyOwner() (gas: 2927435)\n[PASS] test_rescueOnlyOwnerAndNeverIMD() (gas: 254136)\n[PASS] test_roundingDustIsReservedAndNeverAwardedToLaterNotes() (gas: 1941447)\n[PASS] test_safeTransferReceiverCanRedeemItsNewlyOwnedNote() (gas: 864433)\n[PASS] test_transferKeepsPastAndFutureRewardsWithNote() (gas: 1023049)\n[PASS] test_upgradePreservesFractionalStandardEarnings() (gas: 2679057)\n[PASS] test_upgradeSettlesStandardBeforeGoldRewards() (gas: 2679583)\nSuite result: ok. 26 passed; 0 failed; 0 skipped; finished in 194.46ms (424.67ms CPU time)\n\nRan 8 tests for test/ArtAndDeployment.t.sol:ArtAndDeploymentTest\n[PASS] test_artEscapesAllXMLMetacharacters() (gas: 7718974)\n[PASS] test_artHonorsTokenDecimalsAndTruncatesDisplayOnly() (gas: 32855795)\n[PASS] test_factoryDeploymentOnEmptyDependencyAddresses() (gas: 12632788)\n[PASS] test_goldTokenURIEngravingAndMetadata() (gas: 22761959)\n[PASS] test_ownerCanReplaceRendererWithoutAffectingClaims() (gas: 3827484)\n[PASS] test_runtimeSizesAndForbiddenOpcodes() (gas: 12340734)\n[PASS] test_svgSerialDeterminismAndUniqueGuilloche() (gas: 8780878)\n[PASS] test_tokenURIContainsValidJSONEmbeddedSVGAndLiveRewards() (gas: 46662311)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 231.30ms (638.14ms CPU time)\n\nRan 10 tests for test/Gold.t.sol:GoldTest\n[PASS] testFuzz_symbolValidation(bytes) (runs: 512, μ: 1930147, ~: 1931670)\n[PASS] test_goldEngravesTokenAndSymbolOnce() (gas: 1951237)\n[PASS] test_goldLifetimeCapAndBurnDoesNotFreeSlot() (gas: 201150373)\n[PASS] test_mustBeLaunchedAndStandard() (gas: 2006234)\n[PASS] test_onlyCurrentOwnerAndCreatorCanUpgrade() (gas: 2123648)\n[PASS] test_revertingMalformedExpensiveAndHugeMetadataFallBack() (gas: 4072455)\n[PASS] test_symbolDecoderOnlySelf() (gas: 13206)\n[PASS] test_symbolsMustBeNonemptyAlphanumericAndAtMost16() (gas: 14700465)\n[PASS] test_tokenCannotBackAnotherNoteEvenAfterBurn() (gas: 2574196)\n[PASS] test_vaultReturningExtraTrailingWordStillQualifiesCreator() (gas: 1937438)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 231.26ms (432.66ms CPU time)\n\nRan 17 tests for test/MintAndConfig.t.sol:MintAndConfigTest\n[PASS] test_ERC1271PassAndRevocation() (gas: 912801)\n[PASS] test_allSettingsRequireOwner() (gas: 140878)\n[PASS] test_contractWithoutReceiverCanMint() (gas: 441893)\n[PASS] test_deadlineInclusiveThenExpired() (gas: 326277)\n[PASS] test_deploymentStartsClosedWithExplicitOwnerAndNoExternalCalls() (gas: 103811)\n[PASS] test_domainBindsChainAndContract() (gas: 329266)\n[PASS] test_lifetimeSupplyCapDoesNotReopenOnBurn() (gas: 241274504)\n[PASS] test_oneMintEverEvenAfterTransferOrRedemption() (gas: 978879)\n[PASS] test_passCannotBeUsedByDifferentRecipient() (gas: 134919)\n[PASS] test_positiveMinimumAndLiveBalanceRequired() (gas: 492996)\n[PASS] test_signerRotationRevocationAndMintClosure() (gas: 348512)\n[PASS] test_twoStepOwnershipAndArtUpdate() (gas: 223093)\n[PASS] test_unconfiguredOpeningAndOneTimeKeel() (gas: 375587)\n[PASS] test_unknownNoteViewsRevert() (gas: 39051)\n[PASS] test_validPassMintsFreeAndReportsInfo() (gas: 386485)\n[PASS] test_wrongSignerTamperedMinimumDeadlineAndMalformedSignature() (gas: 279988)\n[PASS] test_zeroConstructorArgumentsRejected() (gas: 6775)\nSuite result: ok. 17 passed; 0 failed; 0 skipped; finished in 736.35ms (738.74ms CPU time)\n\nRan 1 test for test/RewardsInvariant.t.sol:RewardsInvariantTest\n[PASS]\nRewardsInvariantTest invariants:\n[PASS] invariant_balanceCoversReserveAndEveryClaim\n[PASS] invariant_lifetimeLimitsWeightsAndOwners\n RewardsInvariantTest invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭---------------+----------+-------+---------+----------╮\n| Contract      | Selector | Calls | Reverts | Discards |\n+=======================================================+\n| RewardHandler | credit   | 1096  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | donate   | 1248  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | fund     | 1192  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | mint     | 1149  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | redeem   | 1222  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | transfer | 1114  | 0       | 0        |\n|---------------+----------+-------+---------+----------|\n| RewardHandler | upgrade  | 1171  | 0       | 0        |\n╰---------------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 67787158434014\n  Bound result 6486\n  Bound result 162514264337593543950333\n  Bound result 6416\n  Bound result 236697285851591308074603\n  Bound result 7943\n  Bound result 3540462977\n  Bound result 1546\n  Bound result 7390\n  Bound result 8613\n  Bound result 2851\n  Bound result 13637\n  Bound result 2\n  Bound result 10692\n  Bound result 306708861855027756692849\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.81s (5.77s CPU time)\n\nRan 6 test suites in 5.82s (7.38s CPU time): 71 tests passed, 0 failed, 0 skipped (71 total tests)\n","passed":true},{"durationMs":74,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"KeelNotes.acceptOwnership()\",\"KeelNotes.approve(address,uint256)\",\"KeelNotes.creditSurplus()\",\"KeelNotes.fund(uint256)\",\"KeelNotes.mint(uint256,uint256,bytes)\",\"KeelNotes.redeem(uint256)\",\"KeelNotes.renounceOwnership()\",\"KeelNotes.rescueToken(address,address,uint256)\",\"KeelNotes.revokePassSigner()\",\"KeelNotes.safeTransferFrom(address,address,uint256)\",\"KeelNotes.safeTransferFrom(address,address,uint256,bytes)\",\"KeelNotes.setApprovalForAll(address,bool)\",\"KeelNotes.setArt(address)\",\"KeelNotes.setKeelToken(address)\",\"KeelNotes.setMintOpen(bool)\",\"KeelNotes.setPassSigner(address)\",\"KeelNotes.transferFrom(address,address,uint256)\",\"KeelNotes.transferOwnership(address)\",\"KeelNotes.upgradeToGold(uint256,address)\"],\"files\":{\".gitignore\":4,\"README.md\":113,\"docs/DEPENDENCIES.md\":11,\"docs/SECURITY.md\":40,\"docs/VALIDATION.md\":35,\"docs/gold.svg\":1,\"docs/standard.svg\":1,\"docs/vendor.sha256\":59,\"foundry.toml\":23,\"launch.json\":14,\"remappings.txt\":2,\"src/KeelNoteArt.sol\":219,\"src/KeelNotes.sol\":354,\"src/interfaces/IKeel.sol\":32,\"src/interfaces/IKeelNoteArt.sol\":14,\"test/ArtAndDeployment.t.sol\":195,\"test/Gold.t.sol\":174,\"test/KeelNotesTestBase.sol\":120,\"test/MintAndConfig.t.sol\":301,\"test/Rewards.t.sol\":474,\"test/RewardsInvariant.t.sol\":208,\"test/SurplusSync.t.sol\":179,\"test/mocks/Mocks.sol\":201},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d46c044a386968a9d629afd3684f4ede4376b75b1d24639fe28435f5c6c52215","verifiedTreeHash":"871d4fd2455a4f428e5018b990e3adcdf8259d7b","verifierVersion":"0.1.0+67c588fa"}]}