{"assessments":[],"deployments":[],"fuzz":[],"identity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"interpretation":"Records acceptance and evidence. Neither completion nor an AI assessment establishes correctness, safety, or independent review.","jobId":"373c761a-a182-4421-aa61-7162d3108159","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"848f6472ed36d8acb118e43f4a326466f9fb69c69cd4bee2338f7cff479e06de","dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"2d7d84a1a9972f1497e5bc68a704cbf42294d08c62d2868f102ffe3b909b9b51","skillId":"adapt-contract-project","tools":[]},"key":"adapt_contract_project","kind":"code","role":"implement","skillHash":"2d7d84a1a9972f1497e5bc68a704cbf42294d08c62d2868f102ffe3b909b9b51","skillId":"adapt-contract-project","state":"accepted"},{"acceptedSubmissionHash":"105edf0a4c6fa0b23f9fbfb0473b83e66cf5082cd2102b08f6d8ee7056708486","dependsOn":["adapt_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":"ad53d7bd02dcf66c84f3189512e1f7c6c332c2dcf8572d7847879c7158062832","dependsOn":["adapt_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":"7092f4519357fe014ebaff92b2e873abebf351d7485958aca8906a310fb40d2c","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","state":"accepted"},{"acceptedSubmissionHash":"0962652ea867dbecbe8891f7ed53c8b2cc53e9406371e9656eac02434012708d","dependsOn":["adapt_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":"0d604b404fbcc2d85fe787a84708b19c371af2c78ac15257abee59b0825e0d60","dependsOn":["adapt_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":"e872822a277315280a29c598237077bfda671694f1097955ccc2bf55b4154b51","dependsOn":["adapt_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":"e0f4e8391563786ee7de4b7151772e2e57d16bb14ade8374248e31ce8208707d","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"435ddcf3b9976a7e7150a8c122e5982b8d5326e5b4dd7a32153958c97c4a7fb6","dependsOn":["adapt_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":"Deploy the NFT, vault, liquidity contract and renderer from this repository on Ethereum mainnet, in this order, with no changes to their logic: (1) InkPortraits with constructor ($owner); (2) InkVault with constructor ($contract:InkPortraits, 0x0000ec93127baa929e58e97dd0095a2bfb38ec1d, $owner); (3) InkLiquidity with constructor (the official Uniswap v4 PoolManager on this chain, the official Uniswap v4 PositionManager on this chain, $owner, 0xbc10b3662231c830d2ee10bc6359db17bea4b3d4); (4) InkRenderer with constructor ($contract:InkPortraits, $owner). Do not deploy src/InkFeeRouter.sol: the owner deploys it separately. Run the existing Foundry tests before deploying. The treasury argument (0xbc10b3662231c830d2ee10bc6359db17bea4b3d4) only receives IMD fees and is not an owner. Owner roles and royalties must go to $owner, the address in the owner field, never to the deploying wallet or any swarm address. Do not add any other owner, admin or upgrade role. Do not deploy src/testnet either.","parentJobId":null,"planHash":"59634f3b1eecf7d4adf225da588731d735d3359015d2e30bf6cdbbde5987acdf","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"373c761a-a182-4421-aa61-7162d3108159","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-2305-deploy-nft-vault-liquidity"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52706","feedbackHash":"afcd3e1110aca408650a4c0b0ccc419b0295c2113ddc851523ea0444229c7b9c","nodeKey":"adapt_contract_project","submissionHash":"848f6472ed36d8acb118e43f4a326466f9fb69c69cd4bee2338f7cff479e06de","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52643","feedbackHash":"da956689d08c5342b190b1b6076d962c213e22d60b934d89a76d3dcdc75c295d","nodeKey":"adapt_contract_project","submissionHash":"a77883f78d2f7c9392607aacf8a4a5626ec65ba2e4580c3cffffabeab3944199","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"50962","feedbackHash":"77b03548991c22cf29d01a8e736ee6621bec2af4bdd566cd98a91e53c5d7a19f","nodeKey":"audit_economics","submissionHash":"105edf0a4c6fa0b23f9fbfb0473b83e66cf5082cd2102b08f6d8ee7056708486","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52681","feedbackHash":"14333c1ad4395f490061da9b112a9be331a5e24c243023ac54a7f3284f1a3ab0","nodeKey":"audit_flow","submissionHash":"ad53d7bd02dcf66c84f3189512e1f7c6c332c2dcf8572d7847879c7158062832","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52264","feedbackHash":"95e5c6b293002ae91eb6af4e833b326546d34780b237dd4bfc5d4287b84c6d34","nodeKey":"audit_imported_code","submissionHash":"7092f4519357fe014ebaff92b2e873abebf351d7485958aca8906a310fb40d2c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51225","feedbackHash":"c54647ce85937aba80fe06feb6d6d8d30b97d2c85ca7510fd9382ac7bdab2610","nodeKey":"audit_judge","submissionHash":"aa4e27e10100bee873c40fe61c0917805cc19356cc8398d6a04fbc58a3260323","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51168","feedbackHash":"36eac27a691ad8b5cba99d4ba96b058e962d9cb1fdb06d0b7e71caff0c53dc00","nodeKey":"audit_judge","submissionHash":"0962652ea867dbecbe8891f7ed53c8b2cc53e9406371e9656eac02434012708d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52419","feedbackHash":"26804ed4525fb177158e46a33e34ec8fc60890c2b7195a782a882a20eb552724","nodeKey":"audit_math","submissionHash":"0d604b404fbcc2d85fe787a84708b19c371af2c78ac15257abee59b0825e0d60","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52210","feedbackHash":"442ce39a98fb831aa9e074d4870e58e4f7ce85d23b48a3c6e4fff8172f8b9ec0","nodeKey":"audit_permissions","submissionHash":"e872822a277315280a29c598237077bfda671694f1097955ccc2bf55b4154b51","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52342","feedbackHash":"b83d63c0576b230b7ea63f4a53b71b8a7dd46579c541358da26b8b93c7b58010","nodeKey":"manifest","submissionHash":"797b711efc6f4e1d0e353405e97695482b8350ebedf6e5f685e1f963bd4a481a","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51445","feedbackHash":"61c0788502a6501a6e6da00fea41eb461e57e4e5f73822cac425f6f3520e9a0e","nodeKey":"manifest","submissionHash":"e0f4e8391563786ee7de4b7151772e2e57d16bb14ade8374248e31ce8208707d","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52157","feedbackHash":"565104fd5abeb05eae51ca52ad006851ebe23ffe558b59c3e9722f7d40cd6b37","nodeKey":"write_foundry_tests","submissionHash":"435ddcf3b9976a7e7150a8c122e5982b8d5326e5b4dd7a32153958c97c4a7fb6","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52591","feedbackHash":"e4acc78c8eba3d5209c1eba1bb2f4b8a6862d9f1994bb200ca5403826934646b","nodeKey":"write_foundry_tests","submissionHash":"4842df221cd4ac3416270bcb12752449f5e886aa5f1244df5a25f7e2edca9566","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"94f8ec46eaa4455bac919f595f91a575f5cf4005678adc8d24d7661a9e07f281","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"6208734cdf5317a1","findings":[{"citation":"resolved","description":"Settlement of the earlier medium: the fix holds for the case reported (a proxy taking a portrait out of a user's wallet now pays rerollFeeBps; the attached proof passes on this tree and test/LaunchSafety.t.sol covers deposit and rerollTraits through the proxy). What remains is the delegation the author's note in ADAPTATION.md ('The residual delegation left is a custodial wrapper that pulls on users' behalf') acknowledges: heldFromVault is true for any swarm portrait the vault minted or revealed to its current holder, and the vault's hand-out goes to the puller (src/InkVault.sol:356 portraits.mintSwarm(p.to, ...)). A contract that parks one agent portrait and calls pull() with a user's INK receives the swarm portrait with heldFromVault == true, and its later deposit() (src/InkVault.sol:402 'bool discounted = agentHolder && portraits.heldFromVault(tokenId)') is priced at agentRerollFeeBps although the economic owner is a non-holder. Diversion per portrait is the same 2,000 INK at default fees (10,000 at the 500/250 bps maximum), but it needs the user to hold a wrapped claim instead of the real portrait for the whole lifecycle, which is why this is low and not the medium it was. The author recorded it as accepted; this entry keeps the record honest rather than reopening the work. If the owner wants to close it: require the puller to be an EOA-like holder is not enforceable, so the remaining lever is to price pull-time discounts per pull (record at pull() whether the puller was an agent holder, as pullExpiryFee already does) and require the depositor to be the original puller recorded for that portrait.","line":268,"path":"src/InkPortraits.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {MockERC20} from \"solmate/src/test/utils/mocks/MockERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {InkPortraits} from \"src/InkPortraits.sol\";\nimport {InkVault} from \"src/InkVault.sol\";\nimport {TestSeats} from \"src/testnet/TestSeats.sol\";\n\ncontract Sink {\n    IERC20 public ink;\n    bool public poolSet = true;\n    constructor(IERC20 i) { ink = i; }\n    function addLiquidity() external returns (uint256) { return 0; }\n}\n\n/// A custodial wrapper: it parks one agent portrait, pulls on behalf of users (the swarm portrait is minted\n/// straight to it, so heldFromVault stays true) and hands it in later at the agent-holder rate.\ncontract CustodialWrapper {\n    InkVault immutable vault;\n    InkPortraits immutable portraits;\n    mapping(uint256 => address) public ownerOfWrapped;\n\n    constructor(InkVault v) {\n        vault = v;\n        portraits = v.portraits();\n        portraits.setApprovalForAll(address(v), true);\n        v.ink().approve(address(v), type(uint256).max);\n    }\n\n    function pullFor() external returns (uint256 id) {\n        vault.ink().transferFrom(msg.sender, address(this), 400_000e18);\n        id = vault.pull(1);\n    }\n\n    function record(uint256 tokenId, address user) external { ownerOfWrapped[tokenId] = user; }\n\n    function depositFor(uint256 tokenId) external returns (uint256 out) {\n        require(ownerOfWrapped[tokenId] == msg.sender);\n        uint256[] memory ids = new uint256[](1);\n        ids[0] = tokenId;\n        out = vault.deposit(ids, 0);\n        vault.ink().transfer(msg.sender, out);\n    }\n}\n\ncontract ResidualCustodialTest is Test {\n    uint256 constant P = 400_000e18;\n    address owner = makeAddr(\"owner\");\n    address agent = makeAddr(\"agent\");\n    address bob = makeAddr(\"bob\");\n    InkPortraits portraits;\n    InkVault vault;\n    MockERC20 ink;\n    TestSeats seats;\n    Sink sink;\n\n    function setUp() public {\n        vm.roll(100);\n        seats = new TestSeats();\n        portraits = new InkPortraits(owner);\n        vault = new InkVault(portraits, seats, owner);\n        ink = new MockERC20(\"INK\", \"INK\", 18);\n        sink = new Sink(IERC20(address(ink)));\n        ink.mint(address(vault), 2000 * P);\n        InkPortraits.AgentStats memory stats;\n        bytes32 root = keccak256(\n            bytes.concat(keccak256(abi.encode(uint256(0), uint32(0), uint16(0), uint32(0), uint32(0), false)))\n        );\n        vm.startPrank(owner);\n        portraits.setVault(address(vault));\n        vault.setInk(IERC20(address(ink)));\n        vault.setup(root, uint64(block.timestamp), address(sink));\n        vm.stopPrank();\n        vm.startPrank(agent);\n        seats.mint(0);\n        vault.claim(0, stats, new bytes32[](0));\n        vm.stopPrank();\n        vm.warp(vault.claimEnd());\n        vault.release();\n        vm.roll(101);\n    }\n\n    function test_custodialWrapperStillResellsDiscount() public {\n        CustodialWrapper w = new CustodialWrapper(vault);\n        vm.prank(agent);\n        portraits.transferFrom(agent, address(w), 0);\n        vm.roll(block.number + 1);\n        assertTrue(portraits.isAgentHolder(address(w)));\n        assertFalse(portraits.isAgentHolder(bob));\n\n        ink.mint(bob, P);\n        vm.startPrank(bob);\n        ink.approve(address(w), type(uint256).max);\n        uint256 id = w.pullFor();\n        vm.stopPrank();\n        uint256 tokenId = 10_000 + portraits.swarmMinted();\n        uint256 entropyBlock = block.number + 1;\n        vm.roll(block.number + 2);\n        vm.setBlockhash(entropyBlock, keccak256(abi.encode(\"x\", entropyBlock)));\n        assertTrue(vault.reveal(id));\n        assertEq(portraits.ownerOf(tokenId), address(w));\n        assertTrue(portraits.heldFromVault(tokenId));\n        w.record(tokenId, bob);\n\n        uint256 before = ink.balanceOf(address(sink));\n        vm.prank(bob);\n        uint256 out = w.depositFor(tokenId);\n        uint256 fee = ink.balanceOf(address(sink)) - before;\n        emit log_named_uint(\"fee paid\", fee);\n        emit log_named_uint(\"bob received\", out);\n        assertEq(fee, P * 100 / 10_000, \"non-holder Bob paid the agent-holder fee through a custodial wrapper\");\n    }\n}","reproduction":"Same fixture as test/LaunchSafety.t.sol (vault released, default fees 100/50 bps, seat 0 claimed by `agent`). CustodialWrapper W parks agent portrait #0 (agent transfers it to W; roll one block so isAgentHolder(W) is true). Non-holder Bob approves W for 400,000e18 INK and calls W.pullFor(): W calls vault.pull(1). After the entropy block, reveal(id) mints swarm portrait #10000 to W via mintSwarm; portraits.heldFromVault(10000) == true. Bob calls W.depositFor(10000): W calls vault.deposit([10000], 0) and forwards the INK. Expected (fee keyed to the economic owner Bob, who is not an agent holder): liquidity receives 4,000e18 and Bob 396,000e18. Actual: liquidity receives 2,000e18 and Bob 398,000e18. Ran as test/scratch/Residual_custodial.t.sol: FAIL 'non-holder Bob paid the agent-holder fee through a custodial wrapper: 2000000000000000000000 != 4000000000000000000000'.","severity":"low","snippet":"            heldFromVault[tokenId] = msg.sender == vault;","title":"Residual of fix 16c7464acc9e: a custodial wrapper that pulls on users' behalf still hands their swarm portraits in at the agent-holder fee (documented in ADAPTATION.md)"},{"citation":"resolved","description":"Re-ran the attached proof Proof_6f73716ceb9a.t.sol on this tree: it still fails with 'holder kept the old seed after seeing the offered one', so the behaviour is unchanged. The author's answer is accurate: a request-time commitment is equally computable by the holder, and a forced reveal needs a keeper incentive that changes the fee split; the choice is recorded in ADAPTATION.md as an owner decision. Kept at low as an advisory the owner has consciously accepted; no code change is required for admission.","line":450,"path":"src/InkVault.sol","reproduction":"Released vault, Bob owns swarm portrait #10000 with seed S0. Bob calls rerollTraits(10000, type(uint256).max) at block b (4,000 INK to liquidity). At b+2 Bob computes S1 = keccak256(abi.encode(blockhash(b+1), 10000, \"traits\")) and prefers S0, so does nothing. At b+258 revealTraits(10000) returns false and swarmSeed(10000) == S0. Expected: a paid reroll replaces S0. Actual: S0 retained. Re-ran as test/scratch/Proof_6f73716ceb9a.t.sol on the revised tree: FAIL as before.","severity":"low","snippet":"        if (entropy == bytes32(0)) {\n            emit TraitRerollExpired(tokenId);\n            return false;","title":"Trait reroll remains a free option: the holder sees the offered seed at b+2 and can let the request expire to keep the old traits for the same fee (settled: author keeps it by design)"},{"citation":"resolved","description":"Unchanged this round apart from the new released() gate, which does not affect post-release calls. Once the price is past releaseStartTick() every call lists the whole balance (at least MIN_ADD) one spacing past the spot tick read in the same call, so the caller chooses the listing price, bounded below only by the release floor. ADAPTATION.md records 'permissionless liquidity placement at spot subject to the existing floor' as retained. Advisory for the owner to accept consciously; not blocking.","line":170,"path":"src/InkLiquidity.sol","reproduction":"State after release on the InkSystem fixture with the price above the opening tick. Attacker in one transaction: sell INK to move the tick down one width band (never below floor), ensure ink.balanceOf(liq) >= 400_000e18, call liq.addLiquidity() -> position [floor(tick)+spacing, +width] minted at the depressed tick (lines 170-174, 261-281), then buy that INK back at the depressed price. Expected: fee INK listed at a price not chosen by an arbitrary caller; actual: listed just above whatever tick the caller set.","severity":"low","snippet":"        (, int24 tick,,) = poolManager.getSlot0(poolKey.toId());\n        int24 start = releaseStartTick();\n        if (_inkIs0() ? tick <= start : tick >= start) return _addLadder(start, amount);\n        (int24 lower, int24 upper) = _rangeFrom(tick, width);","title":"Permissionless addLiquidity() lists accumulated fee INK at whatever spot tick the caller sets in the same transaction (settled: documented trust assumption)"},{"citation":"resolved","description":"InkHook is not among the four contracts launch.json deploys; the author added the converse case to the @dev comment at lines 35-39 and changed no logic. Recorded at info for the later hook launch's owner.","line":161,"path":"src/InkHook.sol","reproduction":"Hook with feeBps = 100. EOA B holds agent portrait #7 since block n-1. Non-holder U's swap is submitted inside a transaction sent by B (bundler handleOps, relayer, searcher) in block n. afterSwap reads tx.origin == B and returns (selector, 0): no hook fee, no claim minted. The same swap sent by U directly is charged feeBps of the unspecified side.","severity":"info","snippet":"        address swapper = tx.origin;\n\n        if (feeBps == 0 || portraits.isAgentHolder(swapper)) return (IHooks.afterSwap.selector, 0);","title":"Hook fee exemption keyed to tx.origin exempts every swap in a transaction an agent-holder EOA sends for others (settled: not in this launch, now documented in the hook's @dev note)"},{"citation":"resolved","description":"Unchanged and still reproducible. Bounded by MAX_FEE_BPS, realised only if nobody reveals within 256 blocks (the puller can always reveal its own pull), and setFees is an owner power the brief keeps. Author declined to change pull(uint256)'s signature; recorded in ADAPTATION.md. Informational.","line":271,"path":"src/InkVault.sol","reproduction":"released == true, rerollFeeBps == 100. User signs pull(1) expecting a 4,000 INK expiry fee. Owner's setFees(500, 500) mines first in the same block. The user's pull(1) mines: pullExpiryFee[id] == 20,000e18. Nobody reveals within 256 blocks; reveal(id) refunds 380,000 INK and sends 20,000 INK to liquidity. Expected by symmetry with deposit(minOut)/rerollTraits(maxFee): a caller-set bound; actual: none.","severity":"info","snippet":"        uint256 expiryFee =\n            (PORTRAIT * (portraits.isAgentHolder(msg.sender) ? agentRerollFeeBps : rerollFeeBps)) / 10_000;","title":"pull() has no fee-bound parameter; the expiry fee is snapshotted at whatever rate setFees set when the pull mines (settled: author keeps the interface, documented)"},{"citation":"resolved","description":"Unchanged. setInk/setup accept more than SEATS * PORTRAIT and every payout path moves exact PORTRAIT multiples, so any surplus has no exit. Backing is never under-collateralised. Author documents 'fund the vault with exactly the allocation' instead of adding a payout path. Informational.","line":164,"path":"src/InkVault.sol","reproduction":"Fund the vault with 800,000,000e18 + 1e18 INK, setInk, setup, let seat 7 claim, warp past claimEnd, release(): 1,999 * 400,000e18 goes to liquidity and the vault keeps 400,000e18 + 1e18. Every later path (pull, reveal, expiry, deposit) moves exact PORTRAIT multiples, so the extra 1e18 can never leave. Expected: surplus routed or recoverable; actual: stranded.","severity":"info","snippet":"        if (ink_.balanceOf(address(this)) < SEATS * PORTRAIT) revert NotFunded();","title":"INK sent to the vault beyond outstanding backing is stranded forever (settled: by design, owner told to fund the exact allocation)"},{"citation":"resolved","description":"Unchanged. Fee-free band is below 100 wei of an 18-decimal token per swap; gas dwarfs the saving and no accounting depends on a non-zero fee. Informational only.","line":169,"path":"src/InkHook.sol","reproduction":"Pool released, feeBps = 25, non-holder EOA. Exact-output buy of 399 wei INK: the unspecified input side is 1 wei; fee = 1 * 25 / 10_000 = 0; afterSwap returns (selector, 0) and no claim is minted. A following exact-output buy of 1e18 INK mints a claim.","severity":"info","snippet":"        uint256 fee = (uint256(uint128(amount)) * feeBps) / 10_000;\n        if (fee == 0) return (IHooks.afterSwap.selector, 0);","title":"Hook fee rounds down to zero for unspecified-side amounts below 10_000/feeBps wei (settled: rounding-direction note, hook not in this launch)"}],"hash":"0962652ea867dbecbe8891f7ed53c8b2cc53e9406371e9656eac02434012708d","nodeId":"9c2cd134-75f2-435a-9615-f8020d0b3d8f","outcome":"completed","summary":"The review is settled and written to `.imd-findings.json` with 7 findings and a 58-row coverage record covering all 56 entry points. No tracked file was changed.\n\n**The medium is fixed.** The attached proof for the reroll-discount proxy now passes on the revised tree. The fix keys the discount to the portrait's own history: `InkPortraits._update` sets `heldFromVault` only when the vault itself hands the portrait out, and `deposit()` reads that flag before its own transfer. The full suite passes with 118 tests green and the fork suite excluded.\n\n**Three fixed lows and infos confirmed.** All four launched contracts now inherit Ownable2Step, so a mistaken transfer stays pending until accepted. `addLiquidity()` reverts NotReleased until the bound vault has released, so the premium can no longer be locked before the owner sets it. `_rangeFrom` widens in int256 and clamps, and the RangeMath fuzz test passes.\n\n**One residual kept at low, with a failing test.** The author's own ADAPTATION.md names it: a custodial wrapper that parks one agent portrait, calls `pull()` with a user's INK, and keeps the minted portrait in its own custody still hands it in at the half fee, since the vault mints straight to the puller with the flag set. My scratch test fails with 2,000 instead of 4,000 INK. It needs users to hold a wrapped claim instead of the real portrait, so it is advisory rather than blocking, and the proof source is attached.\n\n**Disputed items re-run and kept as advisory.** The trait-reroll free option still reproduces, and the author's reasoning that neither remedy is a smaller change holds. The spot-tick listing, tx.origin exemption, pull fee bound, stranded surplus, and hook rounding remain documented design decisions, recorded at low or info with their reproductions so the admission record is complete.","treeHash":null,"usage":{"cachedInputTokens":1640689,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":21227,"runtime":"claude","turns":19,"wallClockMs":355329}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"7e50667f872c7f5e","findings":[{"citation":"resolved","description":"Math Precision guide, 'fees round UP' / 'zero-rounding': the hook's extra fee is floor(amount * feeBps / 10_000) on the swap's unspecified side. For any swap whose unspecified amount is below 10_000/feeBps wei (400 wei at the 25 bps used in the tests, 100 wei at the 100 bps cap) the fee truncates to 0 and the swap is fee-free. Both INK and IMD have 18 decimals, so the fee-free band is 10^-16 of a token per swap and each such swap costs far more in gas than the fee it avoids; splitting a real trade into fee-free pieces is uneconomic. No funds are at risk and no accounting invariant depends on the fee being non-zero (a zero fee simply skips the claim mint). Recorded for completeness of the rounding-direction check; the author may prefer ceil division (fee = (amount * feeBps + 9_999) / 10_000) so the fee never rounds to zero, which is the direction the guide prescribes for protocol fees.","line":167,"path":"src/InkHook.sol","reproduction":"State: pool released and trading open, feeBps = 25, swapper is not an agent holder. Call: exact-output buy of 399 wei INK (amountSpecified = 399, zeroForOne = !inkIs0) through PoolSwapTest with an EOA tx.origin. The unspecified (input, IMD) side is 1 wei; fee = 1 * 25 / 10_000 = 0, afterSwap returns (selector, 0) and no claim is minted: poolManager.balanceOf(hook, currency0) + poolManager.balanceOf(hook, currency1) is unchanged. Expected by the 'fees round up' rule: a 1-wei fee. A following exact-output buy of 1e18 INK does mint a claim. Verified with a scratch Foundry test on the InkSystemTest fixture (test passes: delta 0 for the 399-wei swap, > 0 for the 1e18 swap).","severity":"info","snippet":"        uint256 fee = (uint256(uint128(amount)) * feeBps) / 10_000;\n        if (fee == 0) return (IHooks.afterSwap.selector, 0);","title":"Hook fee rounds down to zero for unspecified-side amounts below 10_000/feeBps wei"}],"hash":"0d604b404fbcc2d85fe787a84708b19c371af2c78ac15257abee59b0825e0d60","nodeId":"34d29448-318b-49f3-81ef-ab48139d5fc7","outcome":"completed","summary":"The review is complete and the findings file is in place.\n\n**Outcome.** The math in the assigned area holds. I traced every value-moving formula in the vault, liquidity contract, hook and renderer against the Math Precision, Boundary and Numerical Gap checklists and found no defect that moves funds, breaks an invariant or bricks a path from any reachable state. One informational entry is recorded in `.imd-findings.json`, with a coverage row for all 51 entry points plus five invariants.\n\n**What I verified**\n- **Vault fees are exact.** Every allowed basis-point value divides 400,000 INK evenly, so hand-backs, expiries and trait rerolls never truncate and vault solvency has no drift term.\n- **Pull capacity bound guarantees a draw.** The check in `pull()` against available plus already-waiting portraits, with same-block deposits excluded and admission cut at each pull's own block, keeps at least one candidate at every reveal. The `NoneAvailable` revert in the reveal path is unreachable.\n- **Single-sided mints never need more INK than sent.** Liquidity rounds down and the pool's required amount is a nested ceiling that stays at or below the sent amount, so the sweep only returns dust. The ladder split floors each band and gives the remainder to the last, so nothing leaks.\n- **Tick arithmetic stays inside `int24`.** I tested the one concrete overflow lead, a maximum `setWidth`, on the real v4 stack: it clamps to the usable tick bounds and adds liquidity fine. Overflow would need a tick of the opposite sign to the launch price, so I dropped it.\n- **Renderer thresholds match the script's real-valued thresholds** exactly, including the six agent integer cutoffs.\n\n**The one recorded item** is informational: the hook fee floors to zero for swaps whose unspecified side is under 400 wei at 25 bps. A scratch test confirmed a 399-wei buy mints no fee claim. Impact is dust and uneconomic to exploit, so no proof file is attached.\n\n**Not reached.** Whether the mainnet IMD token reverts on a zero-amount transfer, which would make `InkLiquidity.collectFees` revert when one side has no fees. That needs a fork, which this review could not run.\n\nThe existing suite passed as a baseline (91 tests, 14 skipped fork or invariant suites). My scratch tests were removed and no repository file was changed.","treeHash":null,"usage":{"cachedInputTokens":2281569,"inputTokens":706,"model":"claude-fable-5-1","outputTokens":45734,"runtime":"claude","turns":32,"wallClockMs":708354}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3f6a9bdd601cb99f","findings":[{"citation":"resolved","description":"deposit() (and rerollTraits()/pull() the same way, lines 422 and 269) decides the swarm reroll fee from portraits.isAgentHolder(msg.sender). The holder check only proves that the calling address has held an agent portrait since an earlier block; it does not authenticate the owner of the swarm portrait being handed in. A contract that keeps one agent portrait permanently (its 400,000 INK backing is never at risk, it can always deposit it fee-free) can expose depositFor(tokenId): pull the caller's swarm portrait with transferFrom, call vault.deposit([tokenId], 0) and forward the INK, atomically and trustlessly. Every non-holder in the market then pays agentRerollFeeBps (default 50 bps = 2,000 INK) instead of rerollFeeBps (100 bps = 4,000 INK), and the protocol-owned liquidity that the fee feeds receives half of the intended reroll income. With fees at the 500/250 bps maximum the diversion is 10,000 INK per portrait. This is the 'intermediary wallets / contract callers / exemption bypass' case of the brief: the exemption is shareable, not bound to the actual end user. Fix options (design decision for the owner): price the fee from the portrait's history rather than the caller (e.g. require the depositor to have held the deposited swarm portrait itself since an earlier block, or record the agent holder discount per portrait owner at reveal), or accept and document that the discount is transferable to any caller via a proxy.","line":385,"path":"src/InkVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IERC721} from \"@openzeppelin/contracts/token/ERC721/IERC721.sol\";\nimport {InkPortraits} from \"src/InkPortraits.sol\";\nimport {InkVault} from \"src/InkVault.sol\";\nimport {TestSeats} from \"src/testnet/TestSeats.sol\";\n\ncontract MockInk is ERC20(\"INK\", \"INK\") {\n    function mint(address to, uint256 a) external { _mint(to, a); }\n}\n\ncontract MockLiquidity {\n    IERC20 public immutable ink;\n    bool public poolSet = true;\n    constructor(IERC20 ink_) { ink = ink_; }\n    function addLiquidity() external pure returns (uint256) { return 0; }\n}\n\n/// Holds one agent portrait and lets anyone hand in swarm portraits through it at the holder rate.\ncontract DiscountProxy {\n    InkVault immutable vault;\n    InkPortraits immutable portraits;\n    constructor(InkVault v) { vault = v; portraits = v.portraits(); portraits.setApprovalForAll(address(v), true); }\n    function depositFor(uint256 tokenId) external returns (uint256 out) {\n        portraits.transferFrom(msg.sender, address(this), tokenId);\n        uint256[] memory ids = new uint256[](1);\n        ids[0] = tokenId;\n        out = vault.deposit(ids, 0);\n        vault.ink().transfer(msg.sender, out);\n    }\n}\n\ncontract EconTest is Test {\n    MockInk ink;\n    TestSeats seats;\n    InkPortraits portraits;\n    InkVault vault;\n    MockLiquidity liq;\n    address owner = address(0xA11CE);\n    address user = address(0xB0B);\n    address holderEoa = address(0xCAFE);\n    uint256 constant P = 400_000e18;\n\n    function setUp() public {\n        ink = new MockInk();\n        seats = new TestSeats();\n        portraits = new InkPortraits(owner);\n        vault = new InkVault(portraits, IERC721(address(seats)), owner);\n        vm.prank(owner);\n        portraits.setVault(address(vault));\n        ink.mint(address(vault), 2000 * P);\n        liq = new MockLiquidity(IERC20(address(ink)));\n        InkPortraits.AgentStats memory s;\n        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(uint256(7), s.accepted, s.rateBps, s.turns, s.hoursTenths, s.outdated))));\n        vm.startPrank(owner);\n        vault.setInk(IERC20(address(ink)));\n        vault.setup(leaf, uint64(block.timestamp), address(liq));\n        vm.stopPrank();\n        vm.prank(holderEoa);\n        seats.mint(7);\n        vm.prank(holderEoa);\n        vault.claim(7, s, new bytes32[](0));\n        vm.warp(block.timestamp + 1 days);\n        vault.release();\n        // user pulls one swarm portrait\n        ink.mint(user, P);\n        vm.startPrank(user);\n        ink.approve(address(vault), type(uint256).max);\n        vault.pull(1);\n        vm.stopPrank();\n        uint256 pb = block.number;\n        vm.roll(pb + 2);\n        vm.setBlockhash(pb + 1, keccak256(\"h\"));\n        vault.reveal(0);\n        assertEq(portraits.ownerOf(10_000), user);\n    }\n\n    /// A non-holder hands in a swarm portrait through a holder's contract and pays the holder rate.\n    function test_nonHolderGetsHolderRateThroughProxy() public {\n        DiscountProxy proxy = new DiscountProxy(vault);\n        vm.prank(holderEoa);\n        portraits.transferFrom(holderEoa, address(proxy), 7);\n        vm.roll(block.number + 1);\n        assertFalse(portraits.isAgentHolder(user));\n        uint256 liqBefore = ink.balanceOf(address(liq));\n        vm.startPrank(user);\n        portraits.approve(address(proxy), 10_000);\n        proxy.depositFor(10_000);\n        vm.stopPrank();\n        uint256 feePaid = ink.balanceOf(address(liq)) - liqBefore;\n        // A non-holder should pay rerollFeeBps (1% = 4,000 INK); it paid the 0.5% holder rate.\n        assertEq(feePaid, (P * vault.rerollFeeBps()) / 10_000, \"non-holder paid the agent-holder fee\");\n    }\n}","reproduction":"Default fees (100/50 bps). Seat 7's owner claims agent portrait #7 and transfers it to DiscountProxy (a contract that setApprovalForAll(vault) and exposes depositFor). Roll 1 block. Non-holder Bob (isAgentHolder(Bob) == false) holds swarm portrait #10000 from a revealed pull, approves the proxy and calls proxy.depositFor(10000). Expected: liquidity receives the non-holder fee 400,000e18*100/10,000 = 4,000e18 INK and Bob receives 396,000e18. Actual: liquidity receives 2,000e18 and Bob receives 398,000e18. The proof test asserts the expected 4,000e18 fee and fails with 2000000000000000000000 != 4000000000000000000000.","severity":"medium","snippet":"        bool agentHolder = portraits.isAgentHolder(msg.sender);","title":"Agent-holder fee discount is granted to msg.sender, so any contract holding one agent portrait resells the half-price reroll to every non-holder"},{"citation":"resolved","description":"rerollTraits() takes the fee immediately and the new seed is keccak256(blockhash(b+1), tokenId, \"traits\"), which the holder can compute from block b+2. Only the holder benefits from revealTraits() and only the holder's token is locked, so in practice nobody else calls it. If the offered seed is worse, the holder waits until b+258; revealTraits() then deletes the request and keeps the OLD seed. The holder therefore pays one fee for the maximum of (old traits, new traits) instead of a draw that replaces its traits. The fee for a pull abort is economically neutral (it equals the deposit fee), but for trait rerolls the abort removes all downside: a holder of a rare portrait can reroll risk-free and keep the better result, shifting rarity toward rerollers at a discount and contradicting the documented 'Same token, new art and traits' (line 34) semantics. Cost to the holder of the abort path: one 256-block transfer lock. Possible fixes: on expiry, apply a seed that is fixed but unknown to the holder at the time it decides (e.g. derived from the request block's own hash, blockhash(b), committed into storage at request time together with the fee, so expiry yields a different seed rather than the old one); or document that rerolls are an option and rely on a public keeper calling revealTraits() for every request.","line":439,"path":"src/InkVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IERC721} from \"@openzeppelin/contracts/token/ERC721/IERC721.sol\";\nimport {InkPortraits} from \"src/InkPortraits.sol\";\nimport {InkVault} from \"src/InkVault.sol\";\nimport {TestSeats} from \"src/testnet/TestSeats.sol\";\n\ncontract MockInk is ERC20(\"INK\", \"INK\") {\n    function mint(address to, uint256 a) external { _mint(to, a); }\n}\n\ncontract MockLiquidity {\n    IERC20 public immutable ink;\n    bool public poolSet = true;\n    constructor(IERC20 ink_) { ink = ink_; }\n    function addLiquidity() external pure returns (uint256) { return 0; }\n}\n\ncontract EconTest is Test {\n    MockInk ink;\n    TestSeats seats;\n    InkPortraits portraits;\n    InkVault vault;\n    MockLiquidity liq;\n    address owner = address(0xA11CE);\n    address user = address(0xB0B);\n    address holderEoa = address(0xCAFE);\n    uint256 constant P = 400_000e18;\n\n    function setUp() public {\n        ink = new MockInk();\n        seats = new TestSeats();\n        portraits = new InkPortraits(owner);\n        vault = new InkVault(portraits, IERC721(address(seats)), owner);\n        vm.prank(owner);\n        portraits.setVault(address(vault));\n        ink.mint(address(vault), 2000 * P);\n        liq = new MockLiquidity(IERC20(address(ink)));\n        InkPortraits.AgentStats memory s;\n        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(uint256(7), s.accepted, s.rateBps, s.turns, s.hoursTenths, s.outdated))));\n        vm.startPrank(owner);\n        vault.setInk(IERC20(address(ink)));\n        vault.setup(leaf, uint64(block.timestamp), address(liq));\n        vm.stopPrank();\n        vm.prank(holderEoa);\n        seats.mint(7);\n        vm.prank(holderEoa);\n        vault.claim(7, s, new bytes32[](0));\n        vm.warp(block.timestamp + 1 days);\n        vault.release();\n        // user pulls one swarm portrait\n        ink.mint(user, P);\n        vm.startPrank(user);\n        ink.approve(address(vault), type(uint256).max);\n        vault.pull(1);\n        vm.stopPrank();\n        uint256 pb = block.number;\n        vm.roll(pb + 2);\n        vm.setBlockhash(pb + 1, keccak256(\"h\"));\n        vault.reveal(0);\n        assertEq(portraits.ownerOf(10_000), user);\n    }\n\n    /// The holder sees the new seed before anyone must reveal it, and keeps the old one by waiting.\n    function test_traitRerollBestOfTwo() public {\n        bytes32 oldSeed = portraits.swarmSeed(10_000);\n        ink.mint(user, P);\n        vm.prank(user);\n        vault.rerollTraits(10_000, type(uint256).max);\n        uint256 b = block.number;\n        vm.roll(b + 2);\n        vm.setBlockhash(b + 1, keccak256(\"next\"));\n        bytes32 offered = keccak256(abi.encode(blockhash(b + 1), uint256(10_000), \"traits\"));\n        assertTrue(offered != oldSeed); // known to the holder now; it decides whether anyone reveals\n        vm.roll(b + 258);\n        assertFalse(vault.revealTraits(10_000));\n        // One paid reroll should not let the holder keep its old traits after seeing the new ones.\n        assertTrue(portraits.swarmSeed(10_000) != oldSeed, \"holder kept the old seed after seeing the offered one\");\n    }\n}","reproduction":"Released vault, Bob owns swarm portrait #10000 with seed S0. Bob calls rerollTraits(10000, max) at block b (fee 4,000 INK to liquidity). At b+2 Bob computes S1 = keccak256(abi.encode(blockhash(b+1), 10000, \"traits\")). Bob prefers S0 and does nothing. At b+258 anyone (Bob) calls revealTraits(10000): returns false, swarmSeed(10000) == S0. Expected: the paid reroll replaces S0 (the holder should not be able to retain S0 after observing S1). Actual: S0 retained; the proof asserts swarmSeed != S0 and fails with 'holder kept the old seed after seeing the offered one'.","severity":"low","snippet":"        if (entropy == bytes32(0)) {\n            emit TraitRerollExpired(tokenId);\n            return false;\n        }","title":"Trait reroll gives the holder best-of-two: after seeing the new seed it can let the request expire and keep its old traits for the same single fee"},{"citation":"resolved","description":"setWidth() (line 142) only enforces width_ >= tickSpacing. _rangeFrom() computes upper = lower + w (INK as currency0) or lower = upper - w (INK as currency1) in checked int24 arithmetic before clamping to the usable tick range, so a width near type(int24).max overflows and reverts. Every addLiquidity() call once the price has passed releaseStartTick (and therefore InkFeeRouter.route() whenever it reaches MIN_ADD) reverts, and all reroll fees, hook INK fees and LP INK fees pile up unlisted in InkLiquidity. renounceOwnership() is allowed as soon as poolSet is true, after which no call can repair it. Owner-only misconfiguration, so low; fix by bounding width_ (e.g. <= TickMath.MAX_TICK) or by clamping before the addition (compute in int256).","line":263,"path":"src/InkLiquidity.sol","reproduction":"Pool with tickSpacing 60, INK = currency0, price above releaseStartTick at tick 100. Owner calls setWidth(8_388_607) (accepted: >= 60) and renounceOwnership(). InkLiquidity holds 400,000e18 INK. addLiquidity(): _rangeFrom computes w = 8_388_600, lower = 120, upper = 120 + 8_388_600 = 8_388_720 > type(int24).max -> panic 0x11. Expected: position clamped to maxUsableTick and INK listed; actual: revert on every call, INK permanently unlisted after renounce.","severity":"low","snippet":"            upper = lower + w;","title":"setWidth has no upper bound: a large width makes every post-start addLiquidity() revert on int24 overflow, permanently once ownership is renounced"},{"citation":"resolved","description":"afterSwap() waives feeBps when portraits.isAgentHolder(tx.origin). The comment acknowledges smart-contract wallets lose the discount, but the converse is not covered: any EOA that has held an agent portrait since an earlier block and submits transactions on behalf of others makes all swaps inside them fee-free. An ERC-4337 bundler EOA holding one agent portrait exempts every UserOp swap it bundles; an aggregator/relayer (or an EIP-7702 delegated holder EOA executing batched orders for others) does the same; an MEV/arbitrage EOA pays no hook fee on its sandwiches of ordinary users. The treasury (IMD side) and InkLiquidity (INK side) lose feeBps of that volume. Not deployed in this four-contract launch (InkHook belongs to the later hook launch), but it determines the fee flow InkLiquidity and the treasury receive. Fix options: authenticate the swapper from hookData signed by the holder through supported routers, or document that the exemption follows the transaction sender.","line":159,"path":"src/InkHook.sol","reproduction":"Hook with feeBps = 100. EOA B holds agent portrait #7 since block n-1 (isAgentHolder(B) true). Non-holder user U signs a UserOp that swaps 10,000 IMD exact-input for INK through a router; bundler B submits handleOps in block n. afterSwap sees tx.origin == B, returns delta 0: U pays no hook fee. Expected: U is not an agent holder, so 1% of the INK output (feeBps) should be minted to the hook as a claim. Same swap sent by U directly is charged.","severity":"low","snippet":"        address swapper = tx.origin;","title":"Hook fee exemption keyed to tx.origin exempts every swap in a transaction sent by an agent-holder EOA (bundlers, relayers, searchers), not the actual swapper"},{"citation":"resolved","description":"setInk/setup require at least SEATS*PORTRAIT but accept more, and every payout path (release, deposit, pull expiry) moves exact multiples of PORTRAIT tied to portraits or pulls. No function forwards a surplus to InkLiquidity, so any excess from the hook launch's allocation or any direct transfer to the vault is permanently unusable. Backing is never under-collateralised by this; it is only lost value. Fix: have release() also forward balance - (agentMinted * PORTRAIT) to liquidity, or document.","line":161,"path":"src/InkVault.sol","reproduction":"Fund the vault with 800,000,000e18 + 1e18 INK, setInk, setup, let seat 7 claim, warp past claimEnd, release(): 1,999 * 400,000e18 goes to liquidity and the vault keeps 400,000e18 + 1e18. Every later path (pull, reveal, expiry, deposit) moves exact PORTRAIT multiples, so the extra 1e18 can never leave. Expected: surplus routed to liquidity or recoverable; actual: stranded.","severity":"info","snippet":"        if (ink_.balanceOf(address(this)) < SEATS * PORTRAIT) revert NotFunded();","title":"INK sent to the vault beyond outstanding backing (setInk surplus or direct transfers) is stranded forever"}],"hash":"105edf0a4c6fa0b23f9fbfb0473b83e66cf5082cd2102b08f6d8ee7056708486","nodeId":"31872015-4c90-426e-90a3-4b53d0e7be64","outcome":"completed","summary":"I finished the economics review and wrote it to `.imd-findings.json`: 5 findings and 53 coverage rows, one for each of the 52 listed entry points plus one for the backing invariant. I checked every snippet against the tree and it matches. No source files were changed. The scratch tests and the `out/` and `cache/` folders my test runs created have been deleted.\n\n**Tests:** I ran the existing suite with `forge test --offline`. 96 tests passed, 0 failed, and 28 were skipped, mostly the fork tests, which need an RPC URL.\n\n**Findings, most severe first:**\n1. **Medium – `src/InkVault.sol:385`.** The agent-holder fee discount goes to `msg.sender`, so it can be passed on to anyone. A contract that holds one agent portrait can hand in other people's swarm portraits for them in one call, with no trust needed. Non-holders then pay 50 bps instead of 100, so the liquidity contract gets 2,000 INK instead of 4,000 per hand-in. The attached proof test fails on the current code (`2000e18 != 4000e18`).\n2. **Low – `src/InkVault.sol:439`.** A trait reroll lets the holder keep the better of the old and new traits for one fee. The holder can work out the new seed from block b+2. If they don't like it, they wait out the 256 blocks and `revealTraits` keeps the old seed. The attached proof test fails on the current code.\n3. **Low – `src/InkLiquidity.sol:263`.** `setWidth` has no upper limit. A width near the int24 maximum overflows in `_rangeFrom`, so every `addLiquidity()` reverts once the price is above the release start, and accumulated fee INK stays in the liquidity contract. If the owner has already renounced ownership, nothing can fix it. I worked this out from the arithmetic and did not run it.\n4. **Low – `src/InkHook.sol:159`.** The hook's fee waiver checks `tx.origin`. Any agent-holder account that sends transactions for other people, such as a bundler, relayer or trading bot, makes all the swaps inside them fee-free. The hook isn't part of this four-contract launch, but it decides what fees the liquidity contract and the treasury receive later.\n5. **Info – `src/InkVault.sol:161`.** INK sent to the vault beyond the 800M it needs, whether at `setInk` or sent directly later, can never be moved out.\n\n**Checked and found sound:**\n- **Vault backing:** INK in the vault always equals 400,000 per portrait outside it plus per pending pull. This holds through claim, release, pull, reveal, expiry refund and deposit.\n- **Pull reservations:** The reservation check guarantees every queued pull has a portrait to land on.\n- **Claim window:** The claim and release time boundaries line up.\n- **Price floor:** The floor and premium apply for both token orderings, and the release ladder goes in before any swap is possible.\n- **Hook fees:** The fee is taken from the right side and with the right sign for exact-input and exact-output swaps.\n- **Seat contract:** The seat contract on chain 1 has token IDs 0–1999 (2000 is missing), which matches the vault's seat limit.\n\n**Not covered:**\n- **Fee router:** `InkFeeRouter.claimAndRoute` is marked unreached. I only read it and didn't test it; it's also not part of this deployment.\n- **Sandwiching new liquidity:** Someone could push the price down just before `addLiquidity()` lists INK at the current price. The repo already documents this as an accepted risk. I didn't measure how big it is, so I left it out of the findings.","treeHash":null,"usage":{"cachedInputTokens":2496007,"inputTokens":46,"model":"claude-opus-5-5","outputTokens":51441,"runtime":"claude","turns":23,"wallClockMs":558688}},{"artifacts":[],"attempt":1,"bundleHash":"bf00f18fdb6e3197c5794d2c394be9e10848c5b9da20971dfe7a598d45180543","device":"37eed9f56188ea8b","findings":[],"hash":"435ddcf3b9976a7e7150a8c122e5982b8d5326e5b4dd7a32153958c97c4a7fb6","nodeId":"6dd08bf3-c16e-4302-86fe-4a518d261f4e","outcome":"completed","summary":"I fixed the one test of mine that broke after the contracts changed, and `forge test` now passes with the verifier's seed: 117 passed, 0 failed, 28 skipped. `forge build` is also clean.\n\n**What broke:** since my last round, InkPortraits, InkVault, InkLiquidity and InkRenderer all switched to two-step ownership transfer. Now, when the owner names a new owner, nothing changes until that person accepts the role. My test in `test/RendererBoundaryProperties.t.sol` still expected the old owner to be cut off as soon as they named someone else, so it failed. The two-step behaviour is intended, so I updated the test to match it instead of reporting it as a defect.\n\n**What the updated test now checks:**\n- **Before acceptance:** the old owner is still the owner. The nominee is recorded as pending but can't add art, set the preview link or lock the renderer.\n- **Who can accept:** only the nominee. A stranger and the current owner both fail.\n- **After acceptance:** the old owner can't add art, set the preview link, lock, hand ownership back or renounce it. The new owner can upload and lock.\n- **The NFT is unaffected:** changing the renderer's owner doesn't change the NFT contract's owner or where royalties go.\n\nI also updated the line about this test in `test/TestingCoverage.md`.\n\n**Other changes in this round** were already covered by other contributors' tests, so I left them alone: two-step ownership on all four contracts, the reroll discount now applying only to portraits the vault handed out, and `addLiquidity` being refused before the vault releases. My liquidity invariant suite passes unchanged.\n\nI found no new defects, so there is no `.imd-findings.json`. The 28 skipped tests are the mainnet fork suites, which need an RPC this offline run doesn't have, plus the snapshot tests whose external data file isn't in the repo. A run against live mainnet state is still owed. Nothing is committed; the two changed files are left in the working tree.","treeHash":"ba9ba3fabdddf6fb4cb6e5ca7ca5245e9084cd69","usage":{"cachedInputTokens":276635,"inputTokens":14,"model":"claude-opus-5-5","outputTokens":4200,"runtime":"claude","turns":7,"wallClockMs":145338}},{"artifacts":[],"attempt":1,"bundleHash":"beab7344164c9a4391a3662343d5ded53f975205f20e8c6cedd2095940a8f3c9","device":"75816c4fc9cac7f3","findings":[],"hash":"4842df221cd4ac3416270bcb12752449f5e886aa5f1244df5a25f7e2edca9566","nodeId":"6dd08bf3-c16e-4302-86fe-4a518d261f4e","outcome":"completed","summary":"Added tests under `test/` covering liquidity custody invariants, fee conservation, failure rollback, threshold boundaries, and 1,000 renderer fuzz cases.\n\n`forge build` and `forge test` with the supplied seed pass: **107 passed, 0 failed, 28 skipped**. Skipped checks require RPC access or the full snapshot fixture.\n\nSource and configuration remain unchanged.","treeHash":"bd740df9713fafe768f1c0f2ffa56ec8c7e3843c","usage":{"cachedInputTokens":1854976,"inputTokens":113765,"model":"gpt-6.1-sol","outputTokens":15239,"runtime":"codex","turns":6,"wallClockMs":422247}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"c97fd6a73e3737dc","findings":[{"citation":"resolved","description":"rerollTraits() charges the fee once and fixes the draw to blockhash(request+1). Settling is permissionless but only the holder has any reason to call revealTraits(), and nothing forces it. The drawn seed is computable off-chain from block request+2 on (keccak256(abi.encode(blockhash(b+1), tokenId, \"traits\"))). A holder who dislikes it simply does nothing; after 256 blocks (about 51 minutes) blockhash(b+1) is zero and revealTraits() takes the renewal branch, moving traitRerollBlock to the current block and giving a brand-new draw from a hash nobody knows yet. The holder repeats this until a desirable seed appears (rare scenes such as Line Study are 3%, Clean glitch 20%), then settles. Only one fee (4,000 INK, or 2,000 for agent-portrait holders) was ever paid to the liquidity contract, and the token is merely non-transferable meanwhile. The same renewal path exists for pulls in _revealNext (line 307), where the last puller in a quiet queue can discard an unlucky portrait draw the same way; there, other pullers queued behind are at least motivated to reveal, but with trait rerolls nobody is. This breaks the stated guarantee that the fee buys exactly one random re-seed and leaks fee revenue from the liquidity contract. Fix options: on expiry settle from a fallback (e.g. cancel the request and leave the old seed, or charge the fee again on renewal, or let anyone settle with the hash of the latest available block before expiry).","line":410,"path":"src/InkVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {ERC721} from \"@openzeppelin/contracts/token/ERC721/ERC721.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IERC721} from \"@openzeppelin/contracts/token/ERC721/IERC721.sol\";\nimport {InkPortraits} from \"src/InkPortraits.sol\";\nimport {InkVault} from \"src/InkVault.sol\";\n\ncontract Ink is ERC20(\"INK\", \"INK\") {\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract Seats is ERC721(\"identity.md\", \"IDMD\") {}\n\n/// Stands in for InkLiquidity: setup() only needs code at the address and release() calls addLiquidity().\ncontract LiquiditySink {\n    function addLiquidity() external pure returns (uint256) {\n        return 0;\n    }\n}\n\n/// A trait reroll is paid once, but its draw can be discarded for free: the holder simply does not call\n/// revealTraits() while the drawn hash is live, and after 256 blocks revealTraits() renews the request to a\n/// fresh block instead of settling it. Nobody but the holder is paid to reveal, so the holder repeats this\n/// until a seed they like appears, paying the fee a single time.\ncontract TraitRerollFreeRedrawTest is Test {\n    uint256 constant PORTRAIT = 400_000e18;\n\n    address owner = makeAddr(\"owner\");\n    address holder = makeAddr(\"holder\");\n\n    Ink ink;\n    Seats seats;\n    InkPortraits portraits;\n    InkVault vault;\n    LiquiditySink liq;\n\n    function setUp() public {\n        vm.roll(1000);\n        ink = new Ink();\n        seats = new Seats();\n        liq = new LiquiditySink();\n        portraits = new InkPortraits(owner);\n        vault = new InkVault(portraits, IERC721(address(seats)), owner);\n        vm.prank(owner);\n        portraits.setVault(address(vault));\n\n        ink.mint(address(vault), 2000 * PORTRAIT);\n        vm.startPrank(owner);\n        vault.setInk(IERC20(address(ink)));\n        vault.setup(bytes32(uint256(1)), uint64(block.timestamp), address(liq));\n        vm.stopPrank();\n\n        vm.warp(block.timestamp + 24 hours);\n        vault.release();\n\n        // The holder pulls one swarm portrait.\n        ink.mint(holder, PORTRAIT + 10_000e18);\n        vm.startPrank(holder);\n        ink.approve(address(vault), type(uint256).max);\n        vault.pull(1);\n        vm.stopPrank();\n        vm.roll(block.number + 2);\n        assertTrue(vault.reveal(0));\n        assertEq(portraits.ownerOf(10_000), holder);\n    }\n\n    function test_expiredTraitRerollIsRenewedForFree() public {\n        uint256 tokenId = 10_000;\n        bytes32 seedBefore = portraits.swarmSeed(tokenId);\n        uint256 liqBefore = ink.balanceOf(address(liq));\n\n        vm.prank(holder);\n        uint256 fee = vault.rerollTraits(tokenId, type(uint256).max);\n        assertEq(fee, PORTRAIT / 100, \"1% fee\");\n        assertEq(ink.balanceOf(address(liq)) - liqBefore, fee, \"fee paid once\");\n        uint64 requested = vault.traitRerollBlock(tokenId);\n\n        // The drawn seed is public from block requested+2 on. The holder reads it off-chain, dislikes it,\n        // and calls nothing. Nobody else is paid to settle it.\n        vm.roll(uint256(requested) + 2);\n        bytes32 drawn = keccak256(abi.encode(blockhash(uint256(requested) + 1), tokenId, \"traits\"));\n        assertTrue(drawn != seedBefore);\n\n        // 256 blocks later the hash is gone and the same call renews the request instead of settling it.\n        vm.roll(uint256(requested) + 300);\n        bool done = vault.revealTraits(tokenId);\n        assertFalse(done);\n\n        uint64 pending = vault.traitRerollBlock(tokenId);\n        uint256 feesPaid = ink.balanceOf(address(liq)) - liqBefore;\n        // Any sound fix settles or cancels the request here, or makes the new draw cost another fee.\n        // On the current code the request is still pending, at a new block, and only one fee was paid.\n        assertTrue(\n            pending == 0 || feesPaid >= 2 * fee,\n            \"expired trait reroll renewed for free: one fee buys unlimited draws\"\n        );\n    }\n\n    function test_holderGetsADifferentSeedForTheSameFee() public {\n        uint256 tokenId = 10_000;\n        vm.prank(holder);\n        uint256 fee = vault.rerollTraits(tokenId, type(uint256).max);\n        uint64 requested = vault.traitRerollBlock(tokenId);\n        vm.roll(uint256(requested) + 2);\n        bytes32 firstDraw = keccak256(abi.encode(blockhash(uint256(requested) + 1), tokenId, \"traits\"));\n\n        vm.roll(uint256(requested) + 300);\n        assertFalse(vault.revealTraits(tokenId));\n        vm.roll(block.number + 2);\n        uint256 liqBefore = ink.balanceOf(address(liq));\n        bool settled = vault.revealTraits(tokenId);\n        uint256 extraFees = ink.balanceOf(address(liq)) - liqBefore;\n\n        // A second, different draw was applied without a second fee: that is the defect. A fix either\n        // charges again, or settles/cancels on expiry so no second draw exists.\n        if (settled && portraits.swarmSeed(tokenId) != firstDraw) {\n            assertGe(extraFees, fee, \"a second draw for the price of one\");\n        }\n    }\n}","reproduction":"State: released vault, holder owns swarm portrait 10000. (1) holder calls rerollTraits(10000, maxFee) at block b; fee 4,000 INK goes to liquidity; traitRerollBlock[10000]=b. (2) At block b+2 compute keccak256(abi.encode(blockhash(b+1), 10000, \"traits\")) off-chain; holder does not call revealTraits. (3) At block b+300 anyone calls revealTraits(10000): expected the paid draw is applied or the request is closed; actual it returns false, emits TraitRerollRenewed, traitRerollBlock[10000]=b+300 and no fee is charged. (4) At block b+302 revealTraits(10000) applies a different seed than the one drawn in step 2. Liquidity received 4,000 INK in total for two (or n) draws. Proof test: forge test --match-path test/scratch/TraitRerollFreeRedraw.t.sol fails on the current code with 'expired trait reroll renewed for free'.","severity":"medium","snippet":"        if (entropy == bytes32(0)) {\n            traitRerollBlock[tokenId] = uint64(block.number);\n            emit TraitRerollRenewed(tokenId, uint64(block.number));\n            return false;\n        }","title":"Expired trait reroll is renewed for free: one fee buys unlimited draws until the holder likes the seed"},{"citation":"resolved","description":"None of the four contracts is usable as constructed: InkPortraits needs setVault() and setRenderer(), InkVault needs setInk() and setup(), InkLiquidity needs setPool() (and setPremium() before the first position), InkRenderer needs addChunk()s. The launch recipe forbids post-deployment initialization, but here it cannot be removed entirely because INK and the pool only exist after the second (hook) launch; the adapter should therefore at least make the irreversible setters refuse obviously wrong values, since every one of them is final and nothing can undo it. Concretely: InkPortraits.setVault() (line 83) accepts any address, including one with no code or a different contract, and then reverts VaultAlreadySet forever, so no agent or swarm portrait can ever be minted (mintAgent/mintSwarm are onlyVault) and the whole INK allocation logic is dead; InkVault.setInk() (line 153) accepts any address and setup() then reads balanceOf of that address, so a wrong token (or an EOA, which reverts the call) makes setup() unreachable and the 800,000,000 INK the vault already received from the launch has no exit path (no sweep, release() needs setup); InkLiquidity.setPool() (line 97) only checks that one currency is ink_ and that the pool is initialized, not that key.hooks is the InkHook or the other currency is IMD, and fixes floorTick from that pool forever; InkVault.setup() checks only that liquidity_ has code, not that it is an InkLiquidity bound to the same INK. Ownable also lets the owner renounce before any of this is done, with the same permanent effect. Cheap checks that fit the deployment order: setVault requires vault_.code.length != 0 and InkVault(vault_).portraits() == this; setInk requires ink_.balanceOf(address(this)) >= SEATS * PORTRAIT (the vault holds its 80% before the owner wires it); setup requires InkLiquidity(liquidity_).ink() == ink and poolSet(); setPool requires key.hooks to be the expected hook address.","line":83,"path":"src/InkPortraits.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {ERC721} from \"@openzeppelin/contracts/token/ERC721/ERC721.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IERC721} from \"@openzeppelin/contracts/token/ERC721/IERC721.sol\";\nimport {InkPortraits} from \"src/InkPortraits.sol\";\nimport {InkVault} from \"src/InkVault.sol\";\n\ncontract Ink is ERC20(\"INK\", \"INK\") {\n    function mint(address to, uint256 amount) external {\n        _mint(to, amount);\n    }\n}\n\ncontract Seats is ERC721(\"identity.md\", \"IDMD\") {}\n\ncontract LiquiditySink {\n    function addLiquidity() external pure returns (uint256) {\n        return 0;\n    }\n}\n\n/// The system is wired after deployment by one-shot owner setters that accept any address. One wrong\n/// transaction is final: setVault() to a non-vault means no portrait can ever be minted, and setInk() to a\n/// non-INK address means setup() can never pass and the 800,000,000 INK the vault already holds is stuck.\ncontract OneShotSettersTest is Test {\n    uint256 constant PORTRAIT = 400_000e18;\n    address owner = makeAddr(\"owner\");\n    address typo = 0x000000000000000000000000000000000000bEEF; // an address with no code\n\n    Ink ink;\n    Seats seats;\n    InkPortraits portraits;\n    InkVault vault;\n    LiquiditySink liq;\n\n    function setUp() public {\n        ink = new Ink();\n        seats = new Seats();\n        liq = new LiquiditySink();\n        portraits = new InkPortraits(owner);\n        vault = new InkVault(portraits, IERC721(address(seats)), owner);\n    }\n\n    function test_setVaultRejectsAnAddressThatIsNotAVault() public {\n        vm.prank(owner);\n        try portraits.setVault(typo) {\n            // Accepted on the current code. From here on nothing can mint and the pointer is final.\n            assertEq(portraits.vault(), typo);\n            vm.prank(owner);\n            vm.expectRevert(InkPortraits.VaultAlreadySet.selector);\n            portraits.setVault(address(vault));\n            fail(\"setVault accepted an address with no code; the collection can never mint\");\n        } catch {}\n    }\n\n    function test_setInkRejectsATokenTheVaultDoesNotHold() public {\n        // The hook launch has already sent the vault its 80% of INK when the owner wires it.\n        ink.mint(address(vault), 2000 * PORTRAIT);\n        vm.prank(owner);\n        try vault.setInk(IERC20(typo)) {\n            // Accepted on the current code: setup() now reads the balance of a non-token and can never pass.\n            vm.prank(owner);\n            vm.expectRevert();\n            vault.setup(bytes32(uint256(1)), uint64(block.timestamp), address(liq));\n            vm.prank(owner);\n            vm.expectRevert(InkVault.InkAlreadySet.selector);\n            vault.setInk(IERC20(address(ink)));\n            assertEq(ink.balanceOf(address(vault)), 2000 * PORTRAIT, \"800M INK stranded with no exit\");\n            fail(\"setInk accepted an address the vault holds no balance of; the vault is bricked for good\");\n        } catch {}\n    }\n}","reproduction":"(a) Owner calls InkPortraits.setVault(0x000000000000000000000000000000000000bEEF) (no code). Expected: revert. Actual: vault = 0xbEEF, VaultSet emitted; a later setVault(realVault) reverts VaultAlreadySet; every InkVault.claim() then reverts OnlyVault in mintAgent, so the collection can never mint. (b) Vault holds 800,000,000 INK; owner calls InkVault.setInk(IERC20(0xbEEF)). Expected: revert. Actual: accepted; setup(root, now, liq) reverts (balanceOf call to an address with no code), setInk(realInk) reverts InkAlreadySet, release()/pull() revert, and the 800M INK sits in the vault with no function that can move it. Proof test: forge test --match-path test/scratch/OneShotSetters.t.sol fails on the current code with both messages.","severity":"medium","snippet":"    function setVault(address vault_) external onlyOwner {\n        if (vault != address(0)) revert VaultAlreadySet();\n        vault = vault_;\n        emit VaultSet(vault_);\n    }","title":"System is wired after deployment by irreversible one-shot setters that validate nothing: one wrong owner call strands 800M INK or bricks minting forever"},{"citation":"resolved","description":"The vault rejects safeTransferFrom (onERC721Received reverts UseDeposit), but InkPortraits is a standard ERC-721 whose transferFrom performs no receiver check, so a holder (or a marketplace settlement, or a wallet UI that uses transferFrom) can send a portrait straight to the vault. Only deposit() records custody (pushes to _incoming and pays out INK); _revealNext only hands out tokens that are in _inventory. A directly transferred portrait is owned by the vault, is in neither list, and no code path ever references it again: the sender gets no INK, no pull can land on it, and there is no owner rescue. Its 400,000 INK backing then sits in the vault forever, so the collection permanently has one fewer portrait in circulation. The same gap exists in InkLiquidity, which accepts any ERC-721 via onERC721Received. A cheap mitigation is to override InkPortraits._update to refuse to == vault unless the vault is the caller (msg.sender == vault), so only deposit() can move portraits into the vault.","line":462,"path":"src/InkVault.sol","reproduction":"State: released vault; Alice owns swarm portrait 10000. Alice calls portraits.transferFrom(alice, vault, 10000) (not vault.deposit). Expected: refused, as safeTransferFrom is. Actual: succeeds; portraits.ownerOf(10000) == vault, vault.heldCount() and vault.available() are unchanged, Alice received 0 INK, and no later pull or deposit can ever return token 10000 to circulation.","severity":"low","snippet":"    function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) {\n        // Only deposit() accounts for custody and pays the sender. It uses transferFrom, not safeTransferFrom.\n        revert UseDeposit();\n    }","title":"A portrait sent to the vault with plain transferFrom is lost: it never joins the draw and no function can move it out"},{"citation":"resolved","description":"Read in full: src/InkPortraits.sol, src/InkVault.sol, src/InkLiquidity.sol, src/InkRenderer.sol (the four contracts this launch deploys), plus src/InkHook.sol and src/InkFeeRouter.sol for the flows they drive (hook fees to vault.liquidity(), router to InkLiquidity.addLiquidity); src/testnet/TestSeats.sol was not reviewed (not deployed). Vendored OpenZeppelin 5.1.0 / Uniswap v4-core and v4-periphery sources were read where the contracts call into them (ERC721 _update/transferFrom, PositionManager MINT_POSITION/SETTLE/SWEEP/DECREASE_LIQUIDITY/TAKE, DeltaResolver, LiquidityAmounts rounding). Reached over a public mainnet RPC at block 26234486: the seat contract 0x0000ec93127baa929e58e97dd0095a2bfb38ec1d is an ERC-721 'identity.md' (IDMD) with totalSupply 2000, ownerOf(0) and ownerOf(1999) set and ownerOf(2000) reverting TokenDoesNotExist, no paused(), so InkVault.claim's seat = token id 0..1999 assumption holds; PoolManager 0x000000000004444c5dc75cB358380D2e3dE08A90 and PositionManager 0xbD216513d74C8cf14cf4747E6AaA6420FF64ee9e both have code and POSM.poolManager() is that PoolManager; owner 0x824e12b38cff2d3e7ad9607a0212f7f9eb6a06e3 and treasury 0xbc10b3662231c830d2ee10bc6359db17bea4b3d4 are EOAs; IMD 0xD34a99Bc0f67aE1bbd63C660e6d0b0dd03E263B7 answers name() 'Identity.md'. Not reachable/not in this tree: the INK token and pool (created by the later univ4_hook launch), so INK transfer semantics are assumed plain ERC-20; the real snapshot root. Run: forge test (forge 1.8.3, solc 0.8.26): 75 passed, 0 failed, 14 fork tests skipped (no FORK_RPC_URL); invariants 64 runs x 120 depth clean. Launch-floor rehearsal (test/scratch/OpcodeScan.t.sol): all four contracts deploy with the brief's constructor arguments on an empty chain, runtimes 8,078 / 11,120 / 9,724 / 12,721 bytes (under EIP-170), no DELEGATECALL/CALLCODE/SELFDESTRUCT in runtime, owner() == $owner on every contract, royaltyInfo returns ($owner, 0). Constructor argument order in the brief matches the declarations: InkPortraits(owner), InkVault(portraits, seats, owner), InkLiquidity(poolManager, positionManager, owner, treasury), InkRenderer(portraits, owner). None of the constructors calls another contract. No proxy, initializer, DELEGATECALL, SELFDESTRUCT, pause, blacklist or balance seizure exists; the only post-deployment configuration is the owner wiring covered by the one-shot-setter finding, which the brief itself requires (INK does not exist at deployment). InkRenderer.addChunk uses CREATE (allowed) for SSTORE2-style data contracts; the init code 600B5981380380925939F3 was decoded and is correct, and the 0x00 prefix avoids EIP-3541. Trait rules in InkRenderer were compared against art/original-art.js deriveTraits (seed prefixes 'seat:'/'swarm:' and ':palette' match) and are pinned by test/InkRenderer.t.sol against test/fixtures/traits.json. tokenURI costs about 4.9M gas per call, fine for eth_call. Not flagged as defects (design, documented in code): next-block-hash randomness is builder-influenceable; addLiquidity() of accumulated fee INK is sandwichable in principle (comment acknowledges; the amounts are a few IMD); no time fallback if the owner never calls setup(); owner can change renderer/art until the two locks; INK or other tokens sent directly to the vault or liquidity contract are unrecoverable.","line":22,"path":"src/InkPortraits.sol","reproduction":"Coverage note, not a defect. Commands: forge test; forge test --match-path 'test/scratch/*'; cast call 0x0000ec93127baa929e58e97dd0095a2bfb38ec1d 'ownerOf(uint256)(address)' 2000 --rpc-url https://ethereum-rpc.publicnode.com (reverts TokenDoesNotExist).","severity":"info","snippet":"contract InkPortraits is ERC721, ERC2981, Ownable {","title":"Audit coverage: what was read, what was reached on mainnet, what was run, and what the launch checks require"}],"hash":"7092f4519357fe014ebaff92b2e873abebf351d7485958aca8906a310fb40d2c","nodeId":"d532d5a0-291d-4985-b7bd-ef3c9291c7f5","outcome":"completed","summary":"Review complete. The findings file at `.imd-findings.json` holds four entries, each with a concrete reproduction, and two of them carry Foundry proofs under `test/scratch/` that fail on the current code.\n\n**What I found**\n\n- **Medium, InkVault.revealTraits (line 410):** an expired trait reroll is renewed for free. The holder pays the fee once, reads the drawn seed off-chain, and if they dislike it simply never settles. After 256 blocks the same call renews the request to a fresh block, giving a new draw with no new fee. Nobody else is paid to settle a trait reroll, so one fee buys unlimited draws until a rare seed appears. The pull queue has the same renewal path, mitigated only by later pullers' incentive to reveal. Proof: `test/scratch/TraitRerollFreeRedraw.t.sol`.\n- **Medium, InkPortraits.setVault (line 83) and the other one-shot setters:** the system is wired after deployment by irreversible setters that validate nothing. A single mistyped `setVault` makes the collection unable to mint forever. A wrong `setInk` makes `setup` unreachable and strands the 800M INK the vault already holds, with no exit path. The brief itself requires this post-deploy wiring because INK does not exist yet, so the adapter should add cheap checks rather than remove the setters. Proof: `test/scratch/OneShotSetters.t.sol`.\n- **Low, InkVault.onERC721Received (line 462):** a portrait sent with plain `transferFrom` instead of `deposit` is accepted, never joins the draw, pays nothing, and can never be moved out. Verified with a scratch test.\n- **Info:** a coverage note listing what was read, reached and run.\n\n**What checked out clean**\n\n- All four contracts deploy with the brief's constructor arguments on an empty chain, stay under the EIP-170 size limit, contain no DELEGATECALL, CALLCODE or SELFDESTRUCT, report the owner as `$owner`, and return a zero royalty to `$owner`. Argument order matches every constructor declaration, and no constructor calls another contract.\n- On mainnet the seat contract at 0x0000ec93… is an ERC-721 with exactly ids 0 to 1999 and no pause, both Uniswap v4 addresses have code and are linked, and the owner and treasury are EOAs.\n- The existing suite passes: 75 tests, 0 failures, 14 fork tests skipped for lack of an RPC. The invariant suite covering backing, custody, cap and queue coverage is clean, and I could not break the pull, admission, renewal or ladder accounting by hand.\n- Renderer trait rules match the original art script's seed prefixes and are pinned by fixture tests. A full tokenURI costs about 4.9M gas.\n\n**Not reached:** the INK token and pool, which only exist after the later hook launch, and the real snapshot root. `src/testnet/TestSeats.sol` was not reviewed since it is not deployed.","treeHash":null,"usage":{"cachedInputTokens":1801757,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":64148,"runtime":"claude","turns":32,"wallClockMs":1005390}},{"artifacts":[],"attempt":1,"bundleHash":"e44245c7d0eda8497b3a163a1c6fdcc7ca3a7bbb8af7db707658bf9face8c52a","device":"cd2c3a8d55620566","findings":[],"hash":"797b711efc6f4e1d0e353405e97695482b8350ebedf6e5f685e1f963bd4a481a","nodeId":"58440903-f3bf-4ba1-ba7a-c458da1eccf5","outcome":"completed","summary":"Created [launch.json](/home/imd/.identitymd/work/373c761a-a182-4421-aa61-7162d3108159/58440903-f3bf-4ba1-ba7a-c458da1eccf5/launch.json) with the required contract order, constructor arguments, `$owner` assignments and [official Uniswap mainnet addresses](https://developers.uniswap.org/docs/protocols/v4/deployments).\n\nValidation passed. `forge build` succeeded; `forge test` with the supplied seed reported **96 passed, 0 failed, 28 skipped**. Skips require RPC or snapshot inputs.\n\nOnly `launch.json` is changed for submission. No deployment was broadcast.","treeHash":"b8459b2b90d754e54170c6c2c7c2c9ce9d1b8761","usage":{"cachedInputTokens":631552,"inputTokens":40443,"model":"gpt-6-astra","outputTokens":3665,"runtime":"codex","turns":4,"wallClockMs":161816}},{"artifacts":[],"attempt":1,"bundleHash":"de325de3f467fc8c1b62e399e9111b4aaf8ba19281623dfd3c7e974ab92ce316","device":"f3b611cf8afb513f","findings":[],"hash":"848f6472ed36d8acb118e43f4a326466f9fb69c69cd4bee2338f7cff479e06de","nodeId":"82b0f7d7-805f-4b2a-8783-dc0a954afec3","outcome":"completed","summary":"The responses file is in place at the repository root, excluded from git by the harness's own exclude rule, which is where the reviewer reads it. Everything is verified, so the work is complete.\n\n## Recap\n\nThe reviewer's medium finding reproduced and is fixed, three advisory findings reproduced and were fixed cheaply, and the remaining six are answered as disputed with reasons. No build configuration or dependency changed.\n\n**Fixed, with tests**\n\n- **Discount resold through an agent-holding contract (16c7464acc9e).** The proof failed on the starting tree. The cause was pricing the discount from the caller's holder status. The discount is now keyed to the portrait's own history: `InkPortraits` records whether the current holder received a swarm portrait straight from the vault and has not moved it, and the vault's deposit and trait-reroll paths grant the half fee only on such portraits. The proxy path now pays the full fee, and the proof passes. I chose this over the reviewer's \"held since an earlier block\" example because a standing custodial proxy still passes that rule after one block. The trade-off, recorded in ADAPTATION.md, is that an agent holder pays the full fee on swarm portraits bought or received from another wallet.\n- **One-step ownership transfer (68252bb4caa5).** All four launched contracts now inherit Ownable2Step, so a transfer to a wrong address never takes effect. Constructors still take `$owner`.\n- **Premium lockable before release (b22beff1453b).** `InkLiquidity` records the hook's vault at `setPool` and refuses `addLiquidity` until the vault has released. The release path itself is unchanged.\n- **Range overflow at extreme width (96b69dd541d1).** `_rangeFrom` now widens in 256-bit arithmetic and clamps. A new harness test fuzzes both token orderings.\n\n**Left unchanged, answered as disputed**: the trait-reroll free option (a deliberate residual of the earlier accepted fix, where both remedies change the fee design), spot-tick listing (already a recorded trust assumption), the hook's tx.origin exemption and fee rounding (hook not deployed by this launch, comment extended), the missing pull fee bound, and stranded surplus INK.\n\n**Verification** with the verifier's fuzz seed:\n\n| Check | Result |\n| --- | --- |\n| forge build | passes, pre-existing warnings only |\n| forge test | 109 passed, 0 failed, 28 skipped (fork tests without RPC) |\n| Reviewer proof and remaining audit proof | both pass |\n| Runtime sizes | 9,307 / 12,400 / 12,437 / 12,991 bytes, all under EIP-170 |\n\nOne note: the older audit proof for the trait-reroll renewal fails in its own setUp on both the starting and the finished tree, because its liquidity stand-in predates the accepted setter-validation fix. That was already the case when the previous round was accepted, so I left it alone.","treeHash":"7929e7e0f742ed941a37906bfbe23a8629e98bdd","usage":{"cachedInputTokens":3223595,"inputTokens":834,"model":"claude-fable-5-1","outputTokens":54610,"runtime":"claude","turns":51,"wallClockMs":961773}},{"artifacts":[],"attempt":1,"bundleHash":"1b8344c9c9c7541d098ec250f9345384b12ee03ab5d5b439eb83ba50ae3210fe","device":"f6591e7f19d7b066","findings":[],"hash":"a77883f78d2f7c9392607aacf8a4a5626ec65ba2e4580c3cffffabeab3944199","nodeId":"82b0f7d7-805f-4b2a-8783-dc0a954afec3","outcome":"completed","summary":"Implemented the four-contract launch adaptation and fixed all three reproduced audit defects. Ownership and royalties remain assigned to `$owner`; the fee router stays excluded.\n\n- `forge build`: passed.\n- Seeded tests: 96 passed, 28 optional skips.\n- Mainnet fork tests: 14 passed.\n\nChanges and deployment handoff are documented in [ADAPTATION.md](/home/seat/.identitymd/work/373c761a-a182-4421-aa61-7162d3108159/82b0f7d7-805f-4b2a-8783-dc0a954afec3/ADAPTATION.md). Protected files are unchanged. No transactions were broadcast.","treeHash":"48b003eba5f20e8f41a72f173cc815b67c36f58b","usage":{"cachedInputTokens":8096256,"inputTokens":168579,"model":"gpt-6-astra","outputTokens":50450,"runtime":"codex","turns":12,"wallClockMs":1754558}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4dd74fd7c315da80","findings":[{"citation":"resolved","description":"deposit() (and rerollTraits() at line 422, pull() at line 269 the same way) prices the swarm reroll fee from portraits.isAgentHolder(msg.sender). isAgentHolder only proves the calling address has held an agent portrait since an earlier block; it says nothing about who owns the swarm portrait being handed in. A contract that permanently holds one agent portrait (its 400,000 INK backing is never at risk) can expose depositFor(tokenId): transferFrom the caller's swarm portrait, call vault.deposit([tokenId], 0) and forward the INK, atomically and trustlessly. Every non-holder then pays agentRerollFeeBps (50 bps = 2,000 INK) instead of rerollFeeBps (100 bps = 4,000 INK), halving the reroll income that feeds the protocol-owned liquidity; at the 500/250 bps maximum the diversion is 10,000 INK per portrait. The 'held since an earlier block' guard at InkPortraits.sol:185 only stops a one-transaction loan, not a standing proxy. Reproduced by the attached test (fails on this code with 2000e18 != 4000e18). Merged with the equivalent paths in rerollTraits() and pull(). The audit brief (item 6) explicitly asks whether exemptions authenticate the actual end user through intermediary contracts; this one does not. Fix options for the owner: price the fee from the deposited portrait's own holding history (e.g. require the depositor to have held the swarm portrait since an earlier block and be an agent holder for that same period), or document the discount as transferable by delegation.","line":385,"path":"src/InkVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IERC721} from \"@openzeppelin/contracts/token/ERC721/IERC721.sol\";\nimport {InkPortraits} from \"src/InkPortraits.sol\";\nimport {InkVault} from \"src/InkVault.sol\";\nimport {TestSeats} from \"src/testnet/TestSeats.sol\";\n\ncontract MockInk is ERC20(\"INK\", \"INK\") {\n    function mint(address to, uint256 a) external { _mint(to, a); }\n}\n\ncontract MockLiquidity {\n    IERC20 public immutable ink;\n    bool public poolSet = true;\n    constructor(IERC20 ink_) { ink = ink_; }\n    function addLiquidity() external pure returns (uint256) { return 0; }\n}\n\n/// Holds one agent portrait and lets anyone hand in swarm portraits through it at the holder rate.\ncontract DiscountProxy {\n    InkVault immutable vault;\n    InkPortraits immutable portraits;\n    constructor(InkVault v) { vault = v; portraits = v.portraits(); portraits.setApprovalForAll(address(v), true); }\n    function depositFor(uint256 tokenId) external returns (uint256 out) {\n        portraits.transferFrom(msg.sender, address(this), tokenId);\n        uint256[] memory ids = new uint256[](1);\n        ids[0] = tokenId;\n        out = vault.deposit(ids, 0);\n        vault.ink().transfer(msg.sender, out);\n    }\n}\n\ncontract EconTest is Test {\n    MockInk ink;\n    TestSeats seats;\n    InkPortraits portraits;\n    InkVault vault;\n    MockLiquidity liq;\n    address owner = address(0xA11CE);\n    address user = address(0xB0B);\n    address holderEoa = address(0xCAFE);\n    uint256 constant P = 400_000e18;\n\n    function setUp() public {\n        ink = new MockInk();\n        seats = new TestSeats();\n        portraits = new InkPortraits(owner);\n        vault = new InkVault(portraits, IERC721(address(seats)), owner);\n        vm.prank(owner);\n        portraits.setVault(address(vault));\n        ink.mint(address(vault), 2000 * P);\n        liq = new MockLiquidity(IERC20(address(ink)));\n        InkPortraits.AgentStats memory s;\n        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(uint256(7), s.accepted, s.rateBps, s.turns, s.hoursTenths, s.outdated))));\n        vm.startPrank(owner);\n        vault.setInk(IERC20(address(ink)));\n        vault.setup(leaf, uint64(block.timestamp), address(liq));\n        vm.stopPrank();\n        vm.prank(holderEoa);\n        seats.mint(7);\n        vm.prank(holderEoa);\n        vault.claim(7, s, new bytes32[](0));\n        vm.warp(block.timestamp + 1 days);\n        vault.release();\n        // user pulls one swarm portrait\n        ink.mint(user, P);\n        vm.startPrank(user);\n        ink.approve(address(vault), type(uint256).max);\n        vault.pull(1);\n        vm.stopPrank();\n        uint256 pb = block.number;\n        vm.roll(pb + 2);\n        vm.setBlockhash(pb + 1, keccak256(\"h\"));\n        vault.reveal(0);\n        assertEq(portraits.ownerOf(10_000), user);\n    }\n\n    /// A non-holder hands in a swarm portrait through a holder's contract and pays the holder rate.\n    function test_nonHolderGetsHolderRateThroughProxy() public {\n        DiscountProxy proxy = new DiscountProxy(vault);\n        vm.prank(holderEoa);\n        portraits.transferFrom(holderEoa, address(proxy), 7);\n        vm.roll(block.number + 1);\n        assertFalse(portraits.isAgentHolder(user));\n        uint256 liqBefore = ink.balanceOf(address(liq));\n        vm.startPrank(user);\n        portraits.approve(address(proxy), 10_000);\n        proxy.depositFor(10_000);\n        vm.stopPrank();\n        uint256 feePaid = ink.balanceOf(address(liq)) - liqBefore;\n        // A non-holder should pay rerollFeeBps (1% = 4,000 INK); it paid the 0.5% holder rate.\n        assertEq(feePaid, (P * vault.rerollFeeBps()) / 10_000, \"non-holder paid the agent-holder fee\");\n    }\n}","reproduction":"Default fees (100/50 bps). Seat 7's owner claims agent portrait #7 and transfers it to DiscountProxy (a contract that does portraits.setApprovalForAll(vault, true) and exposes depositFor). Roll one block so isAgentHolder(proxy) is true. Non-holder Bob (isAgentHolder(Bob) == false) owns swarm portrait #10000 from a revealed pull, approves the proxy and calls proxy.depositFor(10000). Expected: liquidity receives the non-holder fee 400,000e18 * 100 / 10,000 = 4,000e18 INK and Bob receives 396,000e18. Actual: liquidity receives 2,000e18 and Bob receives 398,000e18. Ran as test/scratch/Proof_bd9e7ff61807.t.sol: FAIL 'non-holder paid the agent-holder fee: 2000000000000000000000 != 4000000000000000000000'.","severity":"medium","snippet":"        bool agentHolder = portraits.isAgentHolder(msg.sender);","title":"Agent-holder reroll discount is keyed to msg.sender, so one agent portrait parked in a contract resells the half-price deposit to every non-holder"},{"citation":"resolved","description":"rerollTraits() takes the fee immediately; the new seed is keccak256(abi.encode(blockhash(b+1), tokenId, \"traits\")), computable by anyone from block b+2. revealTraits() is permissionless but only the holder benefits and only the holder's token is locked, so nobody else is motivated to call it. If the offered seed is worse the holder waits until b+258; revealTraits() then clears the request and keeps the OLD seed (lines 439-442). The holder therefore pays one fee for max(old traits, new traits) rather than a replacement draw, contradicting the 'Same token, new art and traits' semantics at line 34 and shifting rarity toward rerollers. Cost of the abort path is a 256-block transfer lock. This is the residual of accepted fix 7a54cd1bfe87, which deliberately chose 'keep old seed' over 'renewal'; a seed fixed but unknown at decision time (e.g. derive the expiry seed from a value committed at request time that the holder cannot observe before the 256-block window closes, or a keeper that reveals every request) would close it.","line":439,"path":"src/InkVault.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {IERC20} from \"@openzeppelin/contracts/token/ERC20/IERC20.sol\";\nimport {IERC721} from \"@openzeppelin/contracts/token/ERC721/IERC721.sol\";\nimport {InkPortraits} from \"src/InkPortraits.sol\";\nimport {InkVault} from \"src/InkVault.sol\";\nimport {TestSeats} from \"src/testnet/TestSeats.sol\";\n\ncontract MockInk is ERC20(\"INK\", \"INK\") {\n    function mint(address to, uint256 a) external { _mint(to, a); }\n}\n\ncontract MockLiquidity {\n    IERC20 public immutable ink;\n    bool public poolSet = true;\n    constructor(IERC20 ink_) { ink = ink_; }\n    function addLiquidity() external pure returns (uint256) { return 0; }\n}\n\ncontract EconTest is Test {\n    MockInk ink;\n    TestSeats seats;\n    InkPortraits portraits;\n    InkVault vault;\n    MockLiquidity liq;\n    address owner = address(0xA11CE);\n    address user = address(0xB0B);\n    address holderEoa = address(0xCAFE);\n    uint256 constant P = 400_000e18;\n\n    function setUp() public {\n        ink = new MockInk();\n        seats = new TestSeats();\n        portraits = new InkPortraits(owner);\n        vault = new InkVault(portraits, IERC721(address(seats)), owner);\n        vm.prank(owner);\n        portraits.setVault(address(vault));\n        ink.mint(address(vault), 2000 * P);\n        liq = new MockLiquidity(IERC20(address(ink)));\n        InkPortraits.AgentStats memory s;\n        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(uint256(7), s.accepted, s.rateBps, s.turns, s.hoursTenths, s.outdated))));\n        vm.startPrank(owner);\n        vault.setInk(IERC20(address(ink)));\n        vault.setup(leaf, uint64(block.timestamp), address(liq));\n        vm.stopPrank();\n        vm.prank(holderEoa);\n        seats.mint(7);\n        vm.prank(holderEoa);\n        vault.claim(7, s, new bytes32[](0));\n        vm.warp(block.timestamp + 1 days);\n        vault.release();\n        // user pulls one swarm portrait\n        ink.mint(user, P);\n        vm.startPrank(user);\n        ink.approve(address(vault), type(uint256).max);\n        vault.pull(1);\n        vm.stopPrank();\n        uint256 pb = block.number;\n        vm.roll(pb + 2);\n        vm.setBlockhash(pb + 1, keccak256(\"h\"));\n        vault.reveal(0);\n        assertEq(portraits.ownerOf(10_000), user);\n    }\n\n    /// The holder sees the new seed before anyone must reveal it, and keeps the old one by waiting.\n    function test_traitRerollBestOfTwo() public {\n        bytes32 oldSeed = portraits.swarmSeed(10_000);\n        ink.mint(user, P);\n        vm.prank(user);\n        vault.rerollTraits(10_000, type(uint256).max);\n        uint256 b = block.number;\n        vm.roll(b + 2);\n        vm.setBlockhash(b + 1, keccak256(\"next\"));\n        bytes32 offered = keccak256(abi.encode(blockhash(b + 1), uint256(10_000), \"traits\"));\n        assertTrue(offered != oldSeed); // known to the holder now; it decides whether anyone reveals\n        vm.roll(b + 258);\n        assertFalse(vault.revealTraits(10_000));\n        // One paid reroll should not let the holder keep its old traits after seeing the new ones.\n        assertTrue(portraits.swarmSeed(10_000) != oldSeed, \"holder kept the old seed after seeing the offered one\");\n    }\n}","reproduction":"Released vault, Bob owns swarm portrait #10000 with seed S0. Bob calls rerollTraits(10000, type(uint256).max) at block b (4,000 INK to liquidity). At b+2 Bob computes S1 = keccak256(abi.encode(blockhash(b+1), 10000, \"traits\")) and prefers S0, so does nothing. At b+258 revealTraits(10000) returns false and swarmSeed(10000) == S0. Expected: a paid reroll replaces S0 (holder cannot retain S0 after observing S1). Actual: S0 retained. Ran as test/scratch/Proof_6f73716ceb9a.t.sol: FAIL 'holder kept the old seed after seeing the offered one'.","severity":"low","snippet":"        if (entropy == bytes32(0)) {\n            emit TraitRerollExpired(tokenId);\n            return false;\n        }","title":"Trait reroll is a free option: the holder sees the offered seed at b+2 and can let the request expire to keep the old traits for the same single fee"},{"citation":"resolved","description":"setPremium() is refused once positions.length != 0, while addLiquidity() (lines 158-168) has no owner, released() or time gate: it needs only poolSet and an INK balance >= MIN_ADD. Before release the pool is swap-locked but liquidity can still be added (the hook has no beforeAddLiquidity permission), and 400,000 INK is obtainable pre-release from the swarm's 10% share or by claiming an agent portrait and calling deposit() (which has no released check) for exactly 400,000 INK. If the owner's setPool() lands before setPremium(), an unprivileged caller lists a zero-premium ladder from the floor and every later setPremium() reverts BadPremium forever; release() then lists up to 800,000,000 INK from the opening price instead of the intended premium (script/InkSteps.sol RELEASE_PREMIUM = 16,000). The documented procedure (SETUP.md, InkSteps.stepOpenClaims) calls setPremium() before setPool(), which closes the window, so this is low: the contract relies on off-chain ordering. Contract-level fix that keeps the design: allow setPremium() while !InkVault(vault).released(), or require released() in addLiquidity().","line":149,"path":"src/InkLiquidity.sol","reproduction":"On the InkSystem fixture (setPool called, premium 0, no positions, not released): swarm transfers 400_000e18 INK to InkLiquidity; `other` (no role) calls liq.addLiquidity() -> succeeds, positionCount() == 8 (ladder from floorTick). Owner then calls liq.setPremium(16_000) -> reverts BadPremium(). Expected: owner can still set the premium before release; actual: permanently locked at 0. Ran as test/scratch/JudgeChecks.t.sol::test_premiumLockedByPermissionlessAdd (inherits the InkSystem fixture): PASS with those assertions.","severity":"low","snippet":"        if (premium_ < 0 || premium_ > 100_000 || positions.length != 0) revert BadPremium();","title":"Anyone holding 400,000 INK can lock the release premium at 0 by calling addLiquidity() between setPool() and setPremium()"},{"citation":"resolved","description":"Once the price has passed releaseStartTick(), every addLiquidity() call lists the whole INK balance (hook fees, reroll fees, LP fee INK; at least MIN_ADD) as one width-tick position starting one spacing past the current tick read from slot0 in the same call. The caller chooses the listing price: in one transaction sell INK to depress the tick (bounded below only by the release floor), call addLiquidity(), buy the freshly listed INK back cheaply, and let the price recover. The permanent liquidity (every INK holder) sells fee INK for less IMD than at the undisturbed price; the attacker's gain is 400,000 * (P_true - P_depressed) less the 1.25% pool fee and the hook fee on the round-trip volume, so it pays once the natural or induced dip exceeds roughly the round-trip fees. InkFeeRouter.route() (src/InkFeeRouter.sol:74) triggers the same path. ADAPTATION.md records 'permissionless liquidity placement at spot subject to the existing floor' as a deliberately retained assumption, so this is reported at low as a trust assumption for the owner to accept consciously. Fix without an owner power: list fee INK only on the ladder from releaseStartTick, or require the spot tick to be within N ticks of a short TWAP / the previous listing.","line":163,"path":"src/InkLiquidity.sol","reproduction":"State after release with the InkSystem fixture (INK/IMD pool, price above the opening tick after buys). Attacker with IMD and INK, in one transaction via a swap router: (1) sell INK to move the tick down one width band (about -5,800 ticks, 1.79x cheaper, never below floor), (2) transfer or wait for ink.balanceOf(liq) >= 400_000e18 and call liq.addLiquidity() -> position [floor(tick)+spacing, +width] minted at the depressed tick (traced at lines 163-167 and 254-273), (3) buy INK back through that position at the depressed price. Expected: fee INK listed at a price not chosen by an arbitrary caller; actual: listed just above whatever tick the caller set.","severity":"low","snippet":"        (, int24 tick,,) = poolManager.getSlot0(poolKey.toId());","title":"Permissionless addLiquidity() lists accumulated fee INK at whatever spot tick the caller sets in the same transaction (documented trust assumption)"},{"citation":"resolved","description":"afterSwap() waives feeBps when portraits.isAgentHolder(tx.origin). The comment at lines 35-37 covers smart-contract wallets losing the discount but not the converse: an EOA that has held an agent portrait since an earlier block and submits transactions on behalf of others (an ERC-4337 bundler's handleOps, a relayer, an EIP-7702-delegated holder executing batched orders, an MEV searcher sandwiching ordinary users) makes every swap inside those transactions fee-free. The treasury (IMD side) and InkLiquidity (INK side) lose feeBps of that volume. InkHook is not among the four contracts in this manifest (it belongs to the later hook launch), but it is in the tree and determines the fee flow InkLiquidity and the treasury receive, so it is reported for the owner. Fix options: authenticate the swapper from signed hookData through supported routers, or document that the exemption follows the transaction sender.","line":159,"path":"src/InkHook.sol","reproduction":"Hook with feeBps = 100. EOA B holds agent portrait #7 since block n-1 (isAgentHolder(B) true). Non-holder U signs a UserOp that swaps 10,000 IMD exact-input for INK through a router; bundler B submits handleOps in block n. afterSwap reads tx.origin == B at line 159, line 161 returns (selector, 0): U pays no hook fee and no claim is minted. The same swap sent by U directly is charged 1% of the INK output. Expected: fee keyed to U; actual: keyed to B.","severity":"low","snippet":"        address swapper = tx.origin;","title":"Hook fee exemption keyed to tx.origin exempts every swap in any transaction sent by an agent-holder EOA (bundlers, relayers, searchers), not the actual swapper"},{"citation":"resolved","description":"Accepted fix 8ff058a5ba34 added SetupIncomplete / PoolNotSet / NoScript guards to renounceOwnership() in InkPortraits (139-142), InkVault (193-196), InkLiquidity (136-139) and InkRenderer (80-83). The inherited OpenZeppelin Ownable.transferOwnership(address) (lib/openzeppelin-contracts/contracts/access/Ownable.sol:84, one-step, no override anywhere in src/) is the only other writer of the owner slot and is left unguarded. Transferring to an address the owner does not control before setVault / setInk+setup / setPool / addChunk produces exactly the state the renounce guard forbids: InkPortraits can never bind a vault so the collection never mints; InkVault can never run setup so the 800,000,000 INK the hook launch sends it is stranded; InkLiquidity can never setPool so the vault's setup() (which requires poolSet()) also fails. Owner-error path, not an unprivileged exploit, hence low. Fix: inherit Ownable2Step, or override transferOwnership with the same readiness condition (a hand-off to a multisig after wiring stays possible).","line":139,"path":"src/InkPortraits.sol","reproduction":"Deploy InkPortraits(owner), InkVault(portraits, seats, owner), InkRenderer(portraits, owner). As owner: portraits.renounceOwnership() -> SetupIncomplete; vault.renounceOwnership() -> SetupIncomplete; renderer.renounceOwnership() -> NoScript. Then as owner: portraits.transferOwnership(0x000000000000000000000000000000000000dEaD) -> succeeds (Ownable.sol:84-88 only rejects address(0)); owner() == 0xdEaD; portraits.setVault(vault) from the original owner reverts OwnableUnauthorizedAccount and vault() stays 0 forever. Expected: the transfer path is protected like the renounce path, or is two-step.","severity":"low","snippet":"    function renounceOwnership() public override onlyOwner {\n        if (vault == address(0) || renderer == address(0)) revert SetupIncomplete();","title":"renounceOwnership is guarded until configuration completes, but one-step transferOwnership reaches the same unrecoverable state unguarded (all four contracts)"},{"citation":"resolved","description":"setWidth() (line 142) only enforces width_ >= tickSpacing. _rangeFrom() computes upper = lower + w in checked int24 arithmetic before clamping to maxUsableTick, so with w near 8,388,600 and a non-negative lower tick the addition panics and every post-start addLiquidity() (and InkFeeRouter.route()) reverts; after renounceOwnership() nothing can repair it. Downgraded from the specialist's low: at the pool's realistic ticks it cannot trigger. Verified on the InkSystem fixture (INK = currency1, tick 128,999): setWidth(type(int24).max) then addLiquidity() succeeds and clamps (positions 8 -> 9). For INK = currency0 the opening tick is about -129,000, so lower + 8,388,600 = 8,259,800 fits; overflow needs INK to trade at >= 1 IMD per INK (a 400,000x rise). Owner-only misconfiguration under an unrealistic price. Fix: bound width_ (<= TickMath.MAX_TICK) or compute in int256 before clamping.","line":263,"path":"src/InkLiquidity.sol","reproduction":"Pool with tickSpacing 60, INK = currency0, tick 100 above releaseStartTick. Owner calls setWidth(8_388_607) (accepted: >= 60), InkLiquidity holds 400,000e18 INK. addLiquidity(): _rangeFrom computes w = 8_388_600, lower = 120, upper = 120 + 8_388_600 = 8_388_720 > type(int24).max -> panic 0x11. Expected: clamp to maxUsableTick; actual: revert. At the fixture's actual tick (128,999, INK = currency1) the same call succeeds (test/scratch/JudgeChecks.t.sol::test_widthMaxAtRealTick).","severity":"info","snippet":"            upper = lower + w;","title":"setWidth has no upper bound; a width near type(int24).max overflows in _rangeFrom when INK is currency0 and the tick is at or above about 0"},{"citation":"resolved","description":"deposit() takes minOut and rerollTraits() takes maxFee because fees can change between signing and mining (lines 378, 416). pull() reads rerollFeeBps / agentRerollFeeBps with no caller bound and fixes that as the pull's expiry fee. An owner setFees() mined before a pending pull() raises the expiry fee from the 1% signed against to up to MAX_FEE_BPS = 5% (20,000 INK per pull) with no way to reject it. Bounded, only realised if the hash expires unrecorded (anyone can reveal within 256 blocks), and the owner is trusted, so informational. Minimal fix: pull(uint256 count, uint256 maxExpiryFee) reverting FeeTooHigh.","line":268,"path":"src/InkVault.sol","reproduction":"released == true, rerollFeeBps == 100. User signs pull(1) expecting a 4,000 INK expiry fee. Owner's setFees(500, 500) lands first in the same block. The user's pull(1) mines: pullExpiryFee[id] == 20,000e18. Nobody reveals within 256 blocks; reveal(id) refunds 380,000 INK and sends 20,000 INK to liquidity. Expected (by symmetry with deposit/rerollTraits): a bound the user could set; actual: none.","severity":"info","snippet":"        uint256 expiryFee =\n            (PORTRAIT * (portraits.isAgentHolder(msg.sender) ? agentRerollFeeBps : rerollFeeBps)) / 10_000;","title":"pull() has no fee-bound parameter while deposit(minOut) and rerollTraits(maxFee) do; the expiry fee is snapshotted at whatever rate setFees set when the pull mines"},{"citation":"resolved","description":"setInk/setup require at least SEATS * PORTRAIT but accept more, and every payout path (release lines 234-236, deposit lines 403-408, pull expiry lines 324-325) moves exact multiples of PORTRAIT tied to portraits or pulls. No function forwards a surplus to InkLiquidity, so any excess from the hook launch's allocation or a direct transfer is permanently unusable. Backing is never under-collateralised; it is only lost value. Fix: have release() also forward balance - (agentMinted * PORTRAIT) to liquidity, or document.","line":161,"path":"src/InkVault.sol","reproduction":"Fund the vault with 800,000,000e18 + 1e18 INK, setInk, setup, let seat 7 claim, warp past claimEnd, release(): 1,999 * 400,000e18 goes to liquidity and the vault keeps 400,000e18 + 1e18. Every later path moves exact PORTRAIT multiples, so the extra 1e18 can never leave. Expected: surplus routed or recoverable; actual: stranded.","severity":"info","snippet":"        if (ink_.balanceOf(address(this)) < SEATS * PORTRAIT) revert NotFunded();","title":"INK sent to the vault beyond outstanding backing (setInk surplus or direct transfers) is stranded forever"},{"citation":"resolved","description":"The extra fee is floor(amount * feeBps / 10_000) on the unspecified side; below 10_000/feeBps wei (100 wei at the 100 bps cap) it truncates to 0 and line 168 skips the claim. Both tokens have 18 decimals, so the fee-free band is 1e-16 of a token per swap and gas dwarfs the saving; no accounting depends on a non-zero fee. Recorded for the rounding-direction check only; ceil division would make the fee never round to zero.","line":167,"path":"src/InkHook.sol","reproduction":"Pool released, feeBps = 25, non-holder EOA. Exact-output buy of 399 wei INK: the unspecified input side is 1 wei; fee = 1 * 25 / 10_000 = 0; afterSwap returns (selector, 0), no claim minted; poolManager.balanceOf(hook, currency0) + balanceOf(hook, currency1) unchanged. A following exact-output buy of 1e18 INK mints a claim.","severity":"info","snippet":"        uint256 fee = (uint256(uint128(amount)) * feeBps) / 10_000;","title":"Hook fee rounds down to zero for unspecified-side amounts below 10_000/feeBps wei"}],"hash":"aa4e27e10100bee873c40fe61c0917805cc19356cc8398d6a04fbc58a3260323","nodeId":"9c2cd134-75f2-435a-9615-f8020d0b3d8f","outcome":"completed","summary":"Review complete. The findings file is at `.imd-findings.json` and the working tree has no other new files.\n\n**Outcome: 10 findings kept, 0 dropped outright, 1 downgraded, nothing critical or high.**\n\nWhat I did with the specialist reports:\n\n- **Kept at medium, with proof.** The agent-holder reroll discount keys off the caller, so one agent portrait parked in a contract resells the half-price deposit to every non-holder. The specialist's proof fails on this code exactly as stated. I merged the equivalent paths in `rerollTraits` and `pull` into it.\n- **Kept at low, with proof.** Trait rerolls are a free option: the holder sees the seed at b+2 and can let the request expire to keep the old traits. The proof fails as stated.\n- **Kept at low, reproduced on the real pool fixture.** Between `setPool` and `setPremium`, anyone with 400,000 INK can list a zero-premium ladder and lock the premium forever. My test on the InkSystem fixture confirmed 8 positions listed by a non-owner before release, then `BadPremium` on the owner's call.\n- **Kept at low by trace.** Permissionless `addLiquidity` lists fee INK at the caller-chosen spot tick, the hook's tx.origin exemption covers bundlers and relayers, and one-step `transferOwnership` is unguarded where `renounceOwnership` is guarded. I confirmed no override exists anywhere in `src/`.\n- **Downgraded to info.** The `setWidth` overflow does not fire at the pool's real ticks. With the maximum width and INK as currency1 at tick 128,999, `addLiquidity` succeeds and clamps. For INK as currency0 the opening tick is near -129,000, so the sum still fits. It needs INK at or above 1 IMD per INK.\n- **Kept at info.** Pull has no fee bound, surplus vault INK is stranded, and the hook fee rounds to zero on dust.\n\nMy own pass added no new findings. I chased one lead the specialists did not cover: whether the exact `amount0Max = amount` bound in `_mintSingleSided` could trip on v4's round-up math and make `release()` revert. A scan of 108,036 amount and tick combinations produced zero excesses, so I dropped it. I also verified the manifest's Uniswap addresses live on mainnet: the PositionManager reports the manifest's PoolManager.\n\nCoverage answers all 52 entry points plus three invariants, with no `unreached` rows. The static-analysis leads were checked and none rose to a finding beyond those already listed.","treeHash":null,"usage":{"cachedInputTokens":1841665,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":56719,"runtime":"claude","turns":38,"wallClockMs":725084}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1a7606be97f168d4","findings":[{"citation":"resolved","description":"setPremium() is refused once positions.length != 0, and addLiquidity() (src/InkLiquidity.sol:158-168) is permissionless with no released() or owner gate: it only needs poolSet and an INK balance of at least MIN_ADD (400,000 INK). So the guarantee 'the owner sets the release premium before any INK is listed' is enforced only by transaction ordering outside the contract. If the owner's setPool() transaction lands before setPremium(), an unprivileged holder of 400,000 INK (0.04% of supply; the swarm's 10% share exists before release, and any agent can mint 400,000 INK during the window by depositing its claimed portrait) sends it to InkLiquidity and calls addLiquidity() in between. That mints a zero-premium ladder from the opening price, and every later setPremium() reverts BadPremium forever. The whole released allocation (up to 800,000,000 INK at release()) is then listed from the opening price instead of 5x above it (script/InkSteps.sol RELEASE_PREMIUM = 16,000), and the vault's portraits are backed at the lower price. Mitigation in this tree: script/InkSteps.sol stepOpenClaims and SETUP.md line 41 deliberately call setPremium() before setPool(), and setPremium() does not require poolSet, so the documented procedure closes the window. The defect is that the contract itself does not: an owner who sends the calls in the order named by Rehearse.s.sol's step label ('setInk + setPool + setPremium + setup') or by hand is exposed. A contract-level fix that keeps the design: let setPremium() be called while positions.length == 0 OR InkVault(vault).released() == false (premium is only meaningful for the release ladder), or require released() in addLiquidity() so no position can exist before release().","line":149,"path":"src/InkLiquidity.sol","reproduction":"State: InkLiquidity deployed, pool initialized and locked (hook rejects swaps), attacker holds 400,000e18 INK (from the swarm share or from an agent-portrait deposit during the claim window). Sequence: (1) owner calls liq.setPool(key, ink) -> poolSet = true, floorTick = opening tick. (2) attacker: ink.transfer(address(liq), 400_000e18); liq.addLiquidity() -> succeeds (poolSet, amount == MIN_ADD, tick <= releaseStartTick with premium 0 -> _addLadder from floorTick), positions.length becomes 8. (3) owner calls liq.setPremium(16_000) -> expected: premium = 16,000; actual: revert BadPremium(), permanently. (4) later release() lists the unclaimed INK from floorTick instead of floorTick + 16,000. Same scenario with the owner using the documented order (setPremium first) does not reproduce, which is why this is low.","severity":"low","snippet":"        if (premium_ < 0 || premium_ > 100_000 || positions.length != 0) revert BadPremium();","title":"Anyone holding 400,000 INK can lock the release premium at 0 by calling addLiquidity() between setPool() and setPremium()"}],"hash":"ad53d7bd02dcf66c84f3189512e1f7c6c332c2dcf8572d7847879c7158062832","nodeId":"3610361c-e631-40b7-91b0-469fae2049dd","outcome":"completed","summary":"Review complete. The findings file is written and valid, and nothing in the repository was changed (scratch tests live only under `test/scratch/`, which is discarded).\n\n**Outcome**\n\nThe control flow of the four launch contracts holds in my area. The existing suite passes (96 tests, 0 failures, 28 skipped fork and snapshot tests), and two scratch suites I wrote against the real Uniswap v4 PoolManager and PositionManager also pass. I report one low finding and no critical, high or medium ones.\n\n**Finding 1 (low), `src/InkLiquidity.sol:149`.** `setPremium()` is refused forever once any position exists, and `addLiquidity()` is permissionless with no `released()` gate. If the owner's `setPool()` lands before `setPremium()`, anyone holding 400,000 INK before release can send it to InkLiquidity, call `addLiquidity()`, and lock the premium at zero. The released allocation would then be listed from the opening price rather than 5x above it. I reproduced the exact sequence on the local v4 stack: after the attacker's call, the owner's `setPremium(16000)` reverts `BadPremium`. It is low because `script/InkSteps.sol` and SETUP.md already order the premium before `setPool()`, so the documented procedure closes the window. The contract itself does not.\n\n**Leads checked and dropped**\n\n- The posm rounding lead on the release ladder (mint needing one wei more than sent). A fuzz over both token orderings, 1 to 2000 released seats, premiums 0 to 100,000 and opening ticks 1,000 to 256,000 never reverted, so `release()` cannot be stuck that way.\n- Agent-portrait deposit during the claim window, then release, 2000 pulls and reveals. Backing stayed exactly 400,000 INK per portrait outside the vault, the cap held at 2000, and the deposited agent portrait was drawn by a pull.\n- The `avail == 0` revert in `_revealNext`. By induction on the pull-time check it is unreachable, since candidates only shrink through reveals.\n- Every static-analysis line listed in the task. None reproduced as a defect; the reasons are recorded in the file's scope note.\n\n**Coverage.** All 53 listed entry points have a row, 52 `holds` and 1 `finding`, plus three invariant rows for backing conservation, single-sided mint rounding, and the launch constructor arguments and roles. No row is `unreached`.","treeHash":null,"usage":{"cachedInputTokens":1576896,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":49071,"runtime":"claude","turns":35,"wallClockMs":758714}},{"artifacts":[],"attempt":1,"bundleHash":"3bc96e80e6b2b64e2dde5f3186b7dbc566e46ab6e5e5be4ca0afed5d363caf3e","device":"fa8fc4653a9e883d","findings":[],"hash":"e0f4e8391563786ee7de4b7151772e2e57d16bb14ade8374248e31ce8208707d","nodeId":"58440903-f3bf-4ba1-ba7a-c458da1eccf5","outcome":"completed","summary":"The accepted launch.json already satisfies every acceptance criterion, so this revision changes nothing. No findings were attached to the assignment, and the full verification found no defect to repair.\n\n**What I verified**\n\n- **Constructors match the manifest.** InkPortraits takes one owner, InkVault takes portraits, seats and owner, InkLiquidity takes PoolManager, PositionManager, owner and treasury, and InkRenderer takes portraits and owner. The argument counts and order in launch.json match the source in all four cases.\n- **Addresses are correct.** The PoolManager and PositionManager values match the official Uniswap v4 Ethereum deployments page, and the seats and treasury addresses match the brief. The same addresses appear in the repository's own mainnet launch tests.\n- **Ownership goes to the owner.** Every owner argument is \"$owner\". InkPortraits sets its royalty recipient to the same owner argument, and the treasury is only a fee recipient with no privileged role.\n- **Exclusions are respected.** InkFeeRouter, InkHook and the testnet seats contract are not listed.\n- **Schema check passes.** The file has only kind, contracts and notes, four unique identifiers in dependency order, backward-only contract references, no zero address, and notes well under the limit.\n- **Build and tests pass.** With the verifier's fuzz seed the suite reports 106 tests passed, 0 failed, 28 skipped. The skipped tests are fork tests that need a mainnet RPC. The constructors make no external calls, so the protected empty-chain deployment rehearsal will not revert.\n\nThe working tree is clean. launch.json is the only deliverable and it is unchanged from the accepted version.","treeHash":"5f0a6596182b2c6c8d1311f30a2dad3e9901bf62","usage":{"cachedInputTokens":261756,"inputTokens":226,"model":"claude-fable-5-1","outputTokens":4488,"runtime":"claude","turns":15,"wallClockMs":78081}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"dff6c0d3de4aa913","findings":[{"citation":"resolved","description":"Asymmetry between the paired owner-exit functions. Accepted fix 8ff058a5ba34 added SetupIncomplete / PoolNotSet / NoScript guards to renounceOwnership() in InkPortraits (src/InkPortraits.sol:139-142), InkVault (src/InkVault.sol:193-196), InkLiquidity (src/InkLiquidity.sol:136-139) and InkRenderer (src/InkRenderer.sol:80-83) so an owner cannot abandon a contract before it is wired. The inherited one-step OpenZeppelin Ownable.transferOwnership(address) is left unguarded on all four, and it is the only other writer of the owner slot. Transferring to any address the owner does not control (a typo, a dead address, a contract without an owner path) before setVault / setInk+setup / setPool / addChunk produces exactly the state the renounce guard forbids: in InkPortraits the vault can never be bound, so the collection can never mint; in InkVault the stats root, window and liquidity can never be set and the 800,000,000 INK already transferred in by the hook launch is stranded with no recovery path; in InkLiquidity the pool can never be set, so the vault's setup() (which requires poolSet()) can never pass either. This is an owner-error path, not an unprivileged exploit, and is reported at low as the remaining gap of the accepted finding. Fix options: (a) inherit Ownable2Step so a mistaken transfer is harmless until accepted, or (b) override transferOwnership with the same readiness condition as renounceOwnership (which still allows a hand-off to a multisig after wiring). Option (a) changes nothing economically and keeps the explicit-owner constructor the launch requires.","line":139,"path":"src/InkPortraits.sol","reproduction":"Deploy InkPortraits(owner), InkVault(portraits, seats, owner), InkRenderer(portraits, owner). As owner: portraits.renounceOwnership() -> reverts SetupIncomplete; vault.renounceOwnership() -> reverts SetupIncomplete; renderer.renounceOwnership() -> reverts NoScript. Then as owner: portraits.transferOwnership(0x000000000000000000000000000000000000dEaD) -> succeeds; portraits.owner() == 0xdEaD; portraits.setVault(vault) from the original owner reverts OwnableUnauthorizedAccount and portraits.vault() stays 0 forever (only the vault may mint, so the collection is permanently empty). Same sequence on the vault after the hook launch has funded it strands SEATS*PORTRAIT INK. Expected: the transfer path is protected the same way the renounce path is, or is two-step.","severity":"low","snippet":"    function renounceOwnership() public override onlyOwner {\n        if (vault == address(0) || renderer == address(0)) revert SetupIncomplete();\n        super.renounceOwnership();\n    }","title":"renounceOwnership is guarded until configuration completes, but transferOwnership reaches the same unrecoverable state unguarded (all four contracts)"},{"citation":"resolved","description":"addLiquidity() has no caller restriction and no price guard other than the release floor: once the pool has traded above releaseStartTick(), every call lists the whole INK balance (hook fees, reroll fees, LP fee INK; at least MIN_ADD = 400,000 INK) as one width-tick position starting one spacing past the current tick, read from slot0 in the same transaction. The caller therefore chooses the listing price. An unprivileged actor can, in one transaction, (1) sell INK to push the tick down (never below the floor, which only bounds the worst case at the opening price + premium), (2) call addLiquidity(), which lists 400,000+ INK just above the depressed tick, (3) buy that INK back at the depressed price, and (4) let the price recover. The pool (the permanent liquidity, i.e. every INK holder) sells the fee INK for less IMD than it would at the undisturbed price; the attacker's gain is 400,000 * (P_true - P_depressed) minus the pool fee and hook fee on the round-trip volume. Even without an active sandwich, the attacker can simply wait for a natural dip and call it then, and the FeeRouter's route() calls addLiquidity() in the same way (src/InkFeeRouter.sol:74). ADAPTATION.md records 'permissionless liquidity placement at spot subject to the existing floor' as an existing assumption that was deliberately not redesigned, so this is reported at low as a trust assumption the owner should consciously accept rather than a blocking defect. If the author wants to close it without adding an owner power: list fee INK only on the ladder from releaseStartTick (never at spot), or require the spot tick to be within N ticks of a short TWAP / the last listing, or add a minimum listing tick parameter that the caller must satisfy relative to the previous listing.","line":163,"path":"src/InkLiquidity.sol","reproduction":"State after release with the InkSystem fixture (INK/IMD pool, hook fee 25 bps, ladder listed from the opening tick, price now 3x the opening price after buys). Attacker has IMD and INK. Tx 1 (single transaction via a swap router): sell enough INK to move the tick down one band (about -5,800 ticks, 1.79x cheaper), transfer-in or wait for liquidity.ink.balanceOf(liq) >= 400,000e18, call liq.addLiquidity() -> position [floor(tick)+spacing, +width] is minted at the depressed tick; then buy INK back through that new position at the depressed price. Compare to the undisturbed case where the same addLiquidity() call lists the 400,000 INK at 1.79x higher. Expected: fee INK listed at a price not chosen by an arbitrary caller; actual: listed just above whatever tick the caller set in the same transaction.","severity":"low","snippet":"        (, int24 tick,,) = poolManager.getSlot0(poolKey.toId());\n        int24 start = releaseStartTick();\n        if (_inkIs0() ? tick <= start : tick >= start) return _addLadder(start, amount);\n        (int24 lower, int24 upper) = _rangeFrom(tick, width);\n        positionId = _add(lower, upper, amount);","title":"Permissionless addLiquidity() lists accumulated fee INK at whatever the spot tick is, so any caller can sandwich the listing down to the release floor (documented trust assumption, access x economics)"},{"citation":"resolved","description":"Paired-function asymmetry. deposit() takes minOut and rerollTraits() takes maxFee because 'fees can change between signing and mining' (src/InkVault.sol:378, 416). pull() reads rerollFeeBps / agentRerollFeeBps with no caller-supplied bound and fixes that rate as the pull's expiry fee (src/InkVault.sol:268-271). A setFees() call mined before a pending pull() (owner action; unprivileged amplifier is the ordinary mempool race) raises the puller's expiry fee from the 1% they signed against to up to MAX_FEE_BPS = 5% (20,000 INK per pull), and the user has no way to reject it. The impact is bounded (5%), only realised if the pull's hash expires unrecorded (anyone may reveal within 256 blocks, so the puller can avoid it), and the owner is a trusted role, so this is informational. Minimal fix that preserves behaviour: pull(uint256 count, uint256 maxExpiryFee) reverting FeeTooHigh when expiryFee > maxExpiryFee, mirroring rerollTraits.","line":268,"path":"src/InkVault.sol","reproduction":"released == true, rerollFeeBps == 100. User signs pull(1) expecting a 4,000 INK expiry fee. Owner's setFees(500, 500) lands first in the same block. The user's pull(1) mines: pullExpiryFee[id] == 20,000e18. Nobody calls reveal within 256 blocks; reveal(id) then refunds 380,000 INK and sends 20,000 INK to liquidity. Expected (by symmetry with deposit/rerollTraits): the user could have bounded the fee and the pull would have reverted FeeTooHigh instead.","severity":"info","snippet":"        uint256 expiryFee =\n            (PORTRAIT * (portraits.isAgentHolder(msg.sender) ? agentRerollFeeBps : rerollFeeBps)) / 10_000;","title":"pull() has no fee bound parameter while its paired user paths deposit(minOut) and rerollTraits(maxFee) do; the expiry fee is snapshotted at whatever rate the owner's setFees sets when the pull mines"}],"hash":"e872822a277315280a29c598237077bfda671694f1097955ccc2bf55b4154b51","nodeId":"3e734055-649f-437f-a418-1042d9cb3567","outcome":"completed","summary":"The review is complete and the findings file is written and validated.\n\n**Outcome.** The permission model of the four launched contracts holds up: every privileged setter is owner-only and one-shot with binding checks, minting is vault-only, and no outsider can act between deployment and owner wiring. Constructor roles match the brief (explicit owner for all four, treasury only as the immutable IMD-fee recipient in InkLiquidity, royalties to the owner at 0 bps, official Uniswap v4 manager addresses). The existing suite passes (90 passed, 5 skipped fork tests).\n\n**Findings written to `.imd-findings.json`** (3 findings, 55 coverage rows, all snippets verified against the tree):\n\n1. **Low, all four contracts.** The accepted fix guarded `renounceOwnership` before configuration, but one-step `transferOwnership` reaches the same unrecoverable state unguarded. Confirmed with a scratch test: transferring to a dead address before `setVault` leaves the collection permanently unmintable, and the same on the vault strands the 800M INK. Suggested fix is Ownable2Step or mirroring the readiness guard.\n2. **Low, InkLiquidity.addLiquidity.** Permissionless listing of fee INK at the spot tick read in the same transaction, bounded only by the release floor, so any caller can sandwich the listing. This is recorded in ADAPTATION.md as a deliberately kept assumption, so it is reported as a trust assumption for the owner to accept, not a blocker.\n3. **Info, InkVault.pull.** Unlike `deposit(minOut)` and `rerollTraits(maxFee)`, `pull` has no fee bound, so an owner `setFees` landing first fixes a higher expiry fee (max 5%) on a pull the user already signed.\n\n**Coverage.** All 50 listed entry points have a row, plus three invariant rows. Hook, fee router and TestSeats entries are outside this launch and were traced for my area only. No critical or high finding was found, so no proof file is attached. The scratch test was removed.","treeHash":null,"usage":{"cachedInputTokens":1151605,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":41284,"runtime":"claude","turns":29,"wallClockMs":686027}}],"verification":[{"checks":[{"durationMs":13156,"exitCode":0,"name":"build","output":"Compiling 186 files with Solc 0.8.26\nSolc 0.8.26 finished in 13.00s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:26:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                          ^^^^^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:38:5:\n   |\n38 |     InkPortraits portraits;\n   |     ^^^^^^^^^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:50:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                                                  ^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:39:5:\n   |\n39 |     InkVault vault;\n   |     ^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:20:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                    ^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:44:5:\n   |\n44 |     PoolKey key;\n   |     ^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:65:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                                                                 ^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:43:5:\n   |\n43 |     ForkInk ink;\n   |     ^^^^^^^^^^^\n\nWarning (3149): The result type of the shift operation is equal to the type of the first operand (uint128) ignoring the (larger) type of the second operand (uint256) which might be unexpected. Silence this warning by either converting the first or the second operand to the type of the other.\n   --> src/InkLiquidity.sol:196:31:\n    |\n196 |             uint128 unitLiq = uint128(1e24) << k;\n    |                               ^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":8313,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/LaunchFeeHandoff.t.sol:LaunchFeeHandoffTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 269.13µs (0.00ns CPU time)\n\nRan 9 tests for test/SnapshotAllSeats.t.sol:SnapshotAllSeatsTest\n[SKIP] test_claims_0000() (gas: 0)\n[SKIP] test_claims_0250() (gas: 0)\n[SKIP] test_claims_0500() (gas: 0)\n[SKIP] test_claims_0750() (gas: 0)\n[SKIP] test_claims_1000() (gas: 0)\n[SKIP] test_claims_1250() (gas: 0)\n[SKIP] test_claims_1500() (gas: 0)\n[SKIP] test_claims_1750() (gas: 0)\n[SKIP] test_treeHasEverySeatOnce() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 9 skipped; finished in 590.63µs (1.48ms CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium2xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 641.53µs (17.86µs CPU time)\n\nRan 1 test for test/LaunchDay.t.sol:LaunchDayTest\n[SKIP] test_fork_launchDay() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 752.55µs (34.52µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium1xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 795.86µs (24.29µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium5xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 785.90µs (22.80µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs0Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 813.32µs (276.65µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs1Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 891.12µs (196.39µs CPU time)\n\nRan 4 tests for test/RangeMath.t.sol:RangeMathTest\n[PASS] testFuzz_rangeNeverRevertsAndStaysUsable(int24,int24,bool) (runs: 256, μ: 118253, ~: 118357)\n[PASS] test_maxWidthClampsWhenInkIsCurrency0() (gas: 131887)\n[PASS] test_maxWidthClampsWhenInkIsCurrency1() (gas: 115332)\n[PASS] test_ordinaryWidthUnchanged() (gas: 118547)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 6.20ms (6.14ms CPU time)\n\nRan 2 tests for test/SnapshotProofs.t.sol:SnapshotProofsTest\n[PASS] test_realSnapshotProofsClaim() (gas: 3222192)\n[PASS] test_tamperedStatsAreRejected() (gas: 605318)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.60ms (8.25ms CPU time)\n\nRan 3 tests for test/RendererBoundaryProperties.t.sol:RendererBoundaryPropertiesTest\n[PASS] testFuzz_binaryChunksRoundTripAndLock(bytes,bytes) (runs: 1000, μ: 463920, ~: 460533)\n[PASS] test_chunkLimitAndRejectedWritesDoNotDamageUploadedArt() (gas: 5657977)\n[PASS] test_ownershipTransferRevokesOldOwnerAcrossEveryRendererMutation() (gas: 615341)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 39.63ms (40.17ms CPU time)\n\nRan 1 test for test/TokenURIPreview.t.sol:TokenURIPreviewTest\n[PASS] test_writeTokenURIs() (gas: 25423286)\nLogs:\n  read-only profile: not written out-preview/agent-280.txt (use FOUNDRY_PROFILE=preview)\n  read-only profile: not written out-preview/pool.txt (use FOUNDRY_PROFILE=preview)\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 44.72ms (44.49ms CPU time)\n\nRan 3 tests for test/MainnetContractsLaunch.t.sol:MainnetContractsLaunchTest\n[PASS] test_exactFourContractOrderReferencesAndOwners() (gas: 221680)\n[PASS] test_factoryTreasuryAndOriginHaveNoAdministrativeRights() (gas: 1697915)\n[PASS] test_runtimeSizeAndOpcodesMeetProtectedFloor() (gas: 16755780)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 102.16ms (41.23ms CPU time)\n\nRan 54 tests for test/InkSystem.t.sol:InkSystemTest\n[PASS] test_agentClaimsOwnPortraitFree() (gas: 311177)\n[PASS] test_agentDepositReturnsExactlyItsBacking() (gas: 3516171)\n[PASS] test_agentHoldersPayNoHookFee() (gas: 2734016)\n[PASS] test_agentHoldersRerollForHalf() (gas: 4215016)\n[PASS] test_borrowedAgentPortraitGetsNoWaiver() (gas: 2555057)\n[PASS] test_cantLockAnEmptyRenderer() (gas: 3138116)\n[PASS] test_claimRules() (gas: 465550)\n[PASS] test_collectFeesPaysImdToTreasuryAndKeepsInk() (gas: 3367122)\n[PASS] test_contractURIOwnerOnly() (gas: 94718)\n[PASS] test_contractWalletSeatCanClaim() (gas: 387248)\n[PASS] test_depositMinOut() (gas: 3038421)\n[PASS] test_depositedAgentPortraitIsPullable() (gas: 3344958)\n[PASS] test_feeRouterClaimAndRoute() (gas: 4381579)\n[PASS] test_feeRouterNeedsPoolAndAddresses() (gas: 3872711)\n[PASS] test_feeRouterSplitsAndCompounds() (gas: 4127736)\n[PASS] test_gasAtScale() (gas: 78817128)\nLogs:\n  pull(1) gas: 161004\n  revealNext(1) gas: 221322\n    per reveal: 221322\n  pull(10) gas: 548232\n  revealNext(10) gas: 1140496\n    per reveal: 114049\n  pull(100) gas: 4762670\n  revealNext(100) gas: 10847735\n    per reveal: 108477\n  pull(400) gas: 18812864\n  revealNext(400) gas: 43238119\n    per reveal: 108095\n  deposit(100 portraits) gas: 6950387\n\n[PASS] test_guards() (gas: 12434326)\n[PASS] test_hookFeeImdToTreasury() (gas: 2210776)\n[PASS] test_hookFeeInkGoesToLiquidity() (gas: 2701835)\n[PASS] test_idleSeatClaimsToo() (gas: 227103)\n[PASS] test_inkFeesGoBackIntoThePool() (gas: 6092386)\n[PASS] test_launchSplit() (gas: 67244)\n[PASS] test_liquidityCannotRenounceBeforePoolIsConfigured() (gas: 3071191)\n[PASS] test_liquidityFloorHoldsIfPriceIsDragged() (gas: 6606864)\n[PASS] test_liquidityOwnershipTransferRequiresAcceptance() (gas: 222592)\n[PASS] test_liquidityRefusesUnrelatedNFTsButKeepsPositionCustody() (gas: 2106517)\n[PASS] test_mainnetPoolMustPairWithOfficialIMD() (gas: 3079926)\n[PASS] test_noPerWalletLimitAfterRelease() (gas: 2124632)\n[PASS] test_noPullsUntilRelease() (gas: 2230624)\n[PASS] test_onlyOnePool() (gas: 46322)\n[PASS] test_ownerIsOwnerWalletNotDeployer() (gas: 7990860)\n[PASS] test_permissionlessListingTracksSpotAboveReleaseFloor() (gas: 3372258)\n[PASS] test_poolLockedUntilRelease() (gas: 2618410)\n[PASS] test_poolNeverOpensWithoutRelease() (gas: 2477214)\n[PASS] test_premiumBeforePoolCannotBeFrontRun() (gas: 6049546)\n[PASS] test_premiumCannotBeLockedByListingBeforeRelease() (gas: 6665678)\n[PASS] test_pullRerollPullAgain() (gas: 3918693)\n[PASS] test_releaseLadderShape() (gas: 2319548)\n[PASS] test_releasePremiumListsAboveTheOpeningPrice() (gas: 1925754)\n[PASS] test_releaseTurnsUnclaimedSeatsIntoSlotsAndPoolInk() (gas: 2074111)\n[PASS] test_revealIsNotEarlyAndNotTwice() (gas: 2625294)\n[PASS] test_seatSoldDuringWindowNewOwnerClaims() (gas: 301442)\n[PASS] test_setPoolRejectsHooklessPoolAndRemainsCorrectable() (gas: 3123415)\n[PASS] test_setPoolRejectsInconsistentHookBindings() (gas: 6644544)\n[PASS] test_setPoolRequiresPositionManagerBoundToSameManager() (gas: 3102635)\n[PASS] test_setupRules() (gas: 3137736)\n[PASS] test_setupStartMustBeSoon() (gas: 3127148)\n[PASS] test_traitRerollBeforeReleaseReverts() (gas: 39919)\n[PASS] test_traitRerollExpiredHashClosesWithoutFreeDraw() (gas: 2972065)\n[PASS] test_traitRerollGuards() (gas: 3463153)\n[PASS] test_traitRerollHalfPriceForAgentHolders() (gas: 3130903)\n[PASS] test_traitRerollRepaintsSameToken() (gas: 3032596)\n[PASS] test_vaultStaysFullyBacked() (gas: 3972012)\n[PASS] test_zeroTradingFee() (gas: 29321)\nSuite result: ok. 54 passed; 0 failed; 0 skipped; finished in 102.39ms (130.77ms CPU time)\n\nRan 6 tests for test/InkRenderer.t.sol:InkRendererTest\n[PASS] test_agentTraitsMatchTheScript() (gas: 61685138)\n[PASS] test_noClaimsAfterTheFirstSwarmPortrait() (gas: 1397672)\n[PASS] test_poolTraitsMatchTheScript() (gas: 60520738)\n[PASS] test_scriptStoredInChunksAndLocked() (gas: 12171384)\n[PASS] test_swarmNumbersFillUnclaimedSeats() (gas: 163375864)\n[PASS] test_tokenURIForAnAgentPortrait() (gas: 3733117)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 145.50ms (310.21ms CPU time)\n\nRan 16 tests for test/LaunchSafety.t.sol:LaunchSafetyTest\n[PASS] testFuzz_directPortraitTransfersCannotBypassDeposit(bool,uint8) (runs: 256, μ: 635502, ~: 860773)\n[PASS] testFuzz_expiredPullChargesPurchaseFeeAndPreservesBacking(uint16,uint16,bool) (runs: 256, μ: 563273, ~: 567248)\n[PASS] test_agentHolderDiscountOnlyOnOwnPullsStillHeld() (gas: 3268385)\n[PASS] test_cannotRenounceBeforeRequiredConfiguration() (gas: 478947)\n[PASS] test_expiredBatchRespectsProcessingLimit() (gas: 691194)\n[PASS] test_expiredHeadCannotStallLaterLivePull() (gas: 800011)\n[PASS] test_expiredTraitRerollConsumesFeeAndRequiresNewPayment() (gas: 977128)\n[PASS] test_nonHolderPaysFullFeeThroughAgentHoldingContract() (gas: 1368938)\n[PASS] test_ownershipTransferRequiresAcceptance() (gas: 640058)\n[PASS] test_portraitsCannotBeSentToLiquidity() (gas: 586110)\n[PASS] test_pullLastAvailableBlockRevealsWithoutExpiryFee() (gas: 484320)\n[PASS] test_setInkRequiresCodeAndFullAllocationBeforeItCommits() (gas: 319403)\n[PASS] test_setupRejectsWrongTokenLiquidityAndRemainsCorrectable() (gas: 1228493)\n[PASS] test_traitRerollLastAvailableBlockUsesOriginalHash() (gas: 688736)\n[PASS] test_traitRerollThroughAgentHoldingContractPaysFullFee() (gas: 1216106)\n[PASS] test_vaultAndRendererBindingFailuresRemainCorrectable() (gas: 433271)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 372.57ms (189.83ms CPU time)\n\nRan 4 tests for test/SepoliaLaunch.t.sol:SepoliaLaunchTest\n[PASS] test_allFiveRuntimesMeetProtectedFloor() (gas: 18566852)\n[PASS] test_existingOwnerSetupRemainsRequiredAndAvailable() (gas: 441282)\n[PASS] test_factoryDeploymentPreservesOwnersRoyaltiesAndReferences() (gas: 183938)\n[PASS] test_factoryTreasuryAndOutsiderHaveNoAdminRole() (gas: 1701232)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 414.92ms (38.37ms CPU time)\n\nRan 15 tests for test/AuditRegression.t.sol:AuditRegressionTest\n[PASS] testFuzz_admissionCompletesWithBothRevealEntryPoints(uint16,bool) (runs: 256, μ: 27233585, ~: 23002648)\n[PASS] test_directSafeTransfersRejectedForBothOverloadsAndOtherNFTs() (gas: 900066)\n[PASS] test_expiredHeadPullNeverWidensALaterPullsDraw() (gas: 2219191)\n[PASS] test_expiredPullsCloseWithoutWideningDrawsOrRenewing() (gas: 1439598)\n[PASS] test_hashExpiryDuringAdmissionKeepsOriginalDraw() (gas: 47716799)\n[PASS] test_initializedPoolAtZeroTickIsValid() (gas: 4799473)\n[PASS] test_largeAdmissionQueueMakesProgressWithinGasBudget() (gas: 468123702)\nLogs:\n  largest admission/reveal step gas (including call overhead): 3466561\n\n[PASS] test_partialAdmissionPreservesDrawAndExcludesSameBlockAndLaterDeposits() (gas: 25071279)\n[PASS] test_pendingRerollBlocksApprovedOperatorsUntilExpiry() (gas: 1668950)\n[PASS] test_pendingRerollCannotBeSoldWithOldTraits() (gas: 802202)\n[PASS] test_safeTransferToVaultIsRejected() (gas: 531345)\n[PASS] test_sameBlockHandInsDoNotCountForPullsMadeNow() (gas: 275287704)\n[PASS] test_setupRefusesUnconfiguredLiquidityBeforeCommitting() (gas: 4689583)\n[PASS] test_setupRejectsLiquidityWithoutCode() (gas: 291131)\n[PASS] test_uninitializedPoolCannotSetPermanentFloor() (gas: 4893464)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 453.96ms (739.71ms CPU time)\n\nRan 4 tests for test/LiquidityCustodyInvariants.t.sol:LiquidityCustodyInk1Test\n[PASS]\nLiquidityCustodyInk1Test invariants:\n[PASS] invariant_configurationAndBackingStayIntact\n[PASS] invariant_conservationAndFeeDestinations\n[PASS] invariant_positionsStayLockedAndFullyLiquid\n LiquidityCustodyInk1Test invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------------------+-------------+-------+---------+----------╮\n| Contract                | Selector    | Calls | Reverts | Discards |\n+====================================================================+\n| LiquidityCustodyHandler | add         | 1164  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | changeWidth | 1179  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | collect     | 1232  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | collectHook | 1120  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | donate      | 1116  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | rejectTheft | 1150  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | trade       | 1231  | 0       | 0        |\n╰-------------------------+-------------+-------+---------+----------╯\n\n[PASS] test_minimumAddBoundaryAndEmptyCollectionRanges() (gas: 4623282)\n[PASS] test_positionManagerFailureRollsBackPrefundingAndCanBeRetried() (gas: 4041920)\n[PASS] test_roundTripCollectAndCompound() (gas: 3039572)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 5.47s (5.47s CPU time)\n\nRan 4 tests for test/LiquidityCustodyInvariants.t.sol:LiquidityCustodyInk0Test\n[PASS]\nLiquidityCustodyInk0Test invariants:\n[PASS] invariant_configurationAndBackingStayIntact\n[PASS] invariant_conservationAndFeeDestinations\n[PASS] invariant_positionsStayLockedAndFullyLiquid\n LiquidityCustodyInk0Test invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------------------+-------------+-------+---------+----------╮\n| Contract                | Selector    | Calls | Reverts | Discards |\n+====================================================================+\n| LiquidityCustodyHandler | add         | 1164  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | changeWidth | 1179  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | collect     | 1232  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | collectHook | 1120  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | donate      | 1116  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | rejectTheft | 1150  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | trade       | 1231  | 0       | 0        |\n╰-------------------------+-------------+-------+---------+----------╯\n\n[PASS] test_minimumAddBoundaryAndEmptyCollectionRanges() (gas: 4633516)\n[PASS] test_positionManagerFailureRollsBackPrefundingAndCanBeRetried() (gas: 4057387)\n[PASS] test_roundTripCollectAndCompound() (gas: 2956123)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 5.55s (5.55s CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantsTest\n[PASS]\nVaultInvariantsTest invariants:\n[PASS] invariant_agentBalancesMatch\n[PASS] invariant_capAndSlots\n[PASS] invariant_custody\n[PASS] invariant_feesToLiquidity\n[PASS] invariant_fullyBacked\n[PASS] invariant_inkConserved\n[PASS] invariant_queueCovered\n VaultInvariantsTest invariants (runs: 64, calls: 7680, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | claim            | 576   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | deposit          | 547   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | moveSeat         | 553   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pull             | 541   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullAll          | 549   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullTooMany      | 554   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | release          | 511   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | rerollTraits     | 580   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | reveal           | 569   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealNext       | 517   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealTraits     | 525   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | roll             | 577   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | setFees          | 552   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | transferPortrait | 529   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.16s (8.16s CPU time)\n\nRan 21 test suites in 8.17s (20.87s CPU time): 117 tests passed, 0 failed, 28 skipped (145 total tests)\n","passed":true},{"durationMs":112,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"InkFeeRouter.claimAndRoute(address,uint64)\",\"InkFeeRouter.route()\",\"InkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"InkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"InkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"InkHook.collectFees()\",\"InkHook.unlockCallback(bytes)\",\"InkLiquidity.acceptOwnership()\",\"InkLiquidity.addLiquidity()\",\"InkLiquidity.collectFees(uint256,uint256)\",\"InkLiquidity.renounceOwnership()\",\"InkLiquidity.setPool((address,address,uint24,int24,address),address)\",\"InkLiquidity.setPremium(int24)\",\"InkLiquidity.setWidth(int24)\",\"InkLiquidity.transferOwnership(address)\",\"InkPortraits.acceptOwnership()\",\"InkPortraits.approve(address,uint256)\",\"InkPortraits.lockRenderer()\",\"InkPortraits.mintAgent(uint256,address,(uint32,uint16,uint32,uint32,bool))\",\"InkPortraits.mintSwarm(address,bytes32)\",\"InkPortraits.renounceOwnership()\",\"InkPortraits.safeTransferFrom(address,address,uint256)\",\"InkPortraits.safeTransferFrom(address,address,uint256,bytes)\",\"InkPortraits.setApprovalForAll(address,bool)\",\"InkPortraits.setContractURI(string)\",\"InkPortraits.setRenderer(address)\",\"InkPortraits.setSwarmSeed(uint256,bytes32)\",\"InkPortraits.setVault(address)\",\"InkPortraits.transferFrom(address,address,uint256)\",\"InkPortraits.transferOwnership(address)\",\"InkRenderer.acceptOwnership()\",\"InkRenderer.addChunk(bytes)\",\"InkRenderer.lock()\",\"InkRenderer.renounceOwnership()\",\"InkRenderer.setPreviewBase(string)\",\"InkRenderer.transferOwnership(address)\",\"InkVault.acceptOwnership()\",\"InkVault.claim(uint256,(uint32,uint16,uint32,uint32,bool),bytes32[])\",\"InkVault.deposit(uint256[],uint256)\",\"InkVault.pull(uint256)\",\"InkVault.release()\",\"InkVault.renounceOwnership()\",\"InkVault.rerollTraits(uint256,uint256)\",\"InkVault.reveal(uint256)\",\"InkVault.revealNext(uint256)\",\"InkVault.revealTraits(uint256)\",\"InkVault.setFees(uint16,uint16)\",\"InkVault.setInk(address)\",\"InkVault.setup(bytes32,uint64,address)\",\"InkVault.transferOwnership(address)\",\"TestSeats.approve(address,uint256)\",\"TestSeats.mint(uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256,bytes)\",\"TestSeats.setApprovalForAll(address,bool)\",\"TestSeats.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"ADAPTATION.md\":303,\"AUDIT_BRIEF.md\":71,\"BYTECODE_HASHES.json\":52,\"DEPENDENCIES.md\":10,\"PACKAGING.md\":16,\"README.md\":55,\"SETUP.md\":101,\"SHA256SUMS\":1640,\"SOURCE_PROVENANCE.json\":11377,\"VALIDATION.txt\":554,\"art/original-art.js\":418,\"foundry.toml\":33,\"launch/README.md\":35,\"launch/mainnet-1-contracts.json\":24,\"launch/mainnet-2-hook.json\":31,\"launch/sepolia-1-contracts.json\":24,\"launch/sepolia-2-hook.json\":31,\"remappings.txt\":8,\"script/Configure.s.sol\":63,\"script/InkSteps.sol\":135,\"script/LocalStack.s.sol\":135,\"script/Rehearse.s.sol\":236,\"script/UploadRenderer.s.sol\":33,\"src/InkFeeRouter.sol\":77,\"src/InkHook.sol\":257,\"src/InkLiquidity.sol\":335,\"src/InkPortraits.sol\":271,\"src/InkRenderer.sol\":311,\"src/InkVault.sol\":503,\"src/testnet/TestSeats.sol\":13,\"test/AuditRegression.t.sol\":476,\"test/InkRenderer.t.sol\":210,\"test/InkSystem.t.sol\":1397,\"test/LaunchDay.t.sol\":270,\"test/LaunchFeeHandoff.t.sol\":82,\"test/LaunchSafety.t.sol\":478,\"test/LiquidityCustodyInvariants.t.sol\":428,\"test/MainnetContractsLaunch.t.sol\":159,\"test/MainnetFork.t.sol\":519,\"test/RangeMath.t.sol\":80,\"test/RendererBoundaryProperties.t.sol\":137,\"test/SepoliaLaunch.t.sol\":186,\"test/SnapshotAllSeats.t.sol\":159,\"test/SnapshotProofs.t.sol\":91,\"test/SybilOptions.t.sol\":104,\"test/TestingCoverage.md\":42,\"test/TokenURIPreview.t.sol\":79,\"test/VaultInvariants.t.sol\":403,\"test/fixtures/snapshot-sample.json\":98,\"test/fixtures/traits.json\":1,\"test/helpers/ConfiguredLiquidity.sol\":18},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"435ddcf3b9976a7e7150a8c122e5982b8d5326e5b4dd7a32153958c97c4a7fb6","verifiedTreeHash":"ba9ba3fabdddf6fb4cb6e5ca7ca5245e9084cd69","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":12467,"exitCode":0,"name":"build","output":"Compiling 184 files with Solc 0.8.26\nSolc 0.8.26 finished in 12.32s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:26:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                          ^^^^^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:38:5:\n   |\n38 |     InkPortraits portraits;\n   |     ^^^^^^^^^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:50:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                                                  ^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:39:5:\n   |\n39 |     InkVault vault;\n   |     ^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:20:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                    ^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:44:5:\n   |\n44 |     PoolKey key;\n   |     ^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:65:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                                                                 ^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:43:5:\n   |\n43 |     ForkInk ink;\n   |     ^^^^^^^^^^^\n\nWarning (3149): The result type of the shift operation is equal to the type of the first operand (uint128) ignoring the (larger) type of the second operand (uint256) which might be unexpected. Silence this warning by either converting the first or the second operand to the type of the other.\n   --> src/InkLiquidity.sol:189:31:\n    |\n189 |             uint128 unitLiq = uint128(1e24) << k;\n    |                               ^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":8219,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/LaunchFeeHandoff.t.sol:LaunchFeeHandoffTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 264.36µs (0.00ns CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium1xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 548.83µs (16.06µs CPU time)\n\nRan 9 tests for test/SnapshotAllSeats.t.sol:SnapshotAllSeatsTest\n[SKIP] test_claims_0000() (gas: 0)\n[SKIP] test_claims_0250() (gas: 0)\n[SKIP] test_claims_0500() (gas: 0)\n[SKIP] test_claims_0750() (gas: 0)\n[SKIP] test_claims_1000() (gas: 0)\n[SKIP] test_claims_1250() (gas: 0)\n[SKIP] test_claims_1500() (gas: 0)\n[SKIP] test_claims_1750() (gas: 0)\n[SKIP] test_treeHasEverySeatOnce() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 9 skipped; finished in 537.64µs (1.44ms CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium5xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 577.34µs (25.04µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium2xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 760.59µs (19.41µs CPU time)\n\nRan 1 test for test/LaunchDay.t.sol:LaunchDayTest\n[SKIP] test_fork_launchDay() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 776.78µs (23.40µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs0Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 840.32µs (181.92µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs1Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 927.31µs (151.94µs CPU time)\n\nRan 2 tests for test/SnapshotProofs.t.sol:SnapshotProofsTest\n[PASS] test_realSnapshotProofsClaim() (gas: 3221612)\n[PASS] test_tamperedStatsAreRejected() (gas: 605207)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.23ms (3.49ms CPU time)\n\nRan 4 tests for test/SepoliaLaunch.t.sol:SepoliaLaunchTest\n[PASS] test_allFiveRuntimesMeetProtectedFloor() (gas: 17925603)\n[PASS] test_existingOwnerSetupRemainsRequiredAndAvailable() (gas: 441184)\n[PASS] test_factoryDeploymentPreservesOwnersRoyaltiesAndReferences() (gas: 183956)\n[PASS] test_factoryTreasuryAndOutsiderHaveNoAdminRole() (gas: 1700968)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 23.54ms (24.21ms CPU time)\n\nRan 1 test for test/TokenURIPreview.t.sol:TokenURIPreviewTest\n[PASS] test_writeTokenURIs() (gas: 25400631)\nLogs:\n  read-only profile: not written out-preview/agent-280.txt (use FOUNDRY_PROFILE=preview)\n  read-only profile: not written out-preview/pool.txt (use FOUNDRY_PROFILE=preview)\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 45.99ms (45.82ms CPU time)\n\nRan 3 tests for test/RendererBoundaryProperties.t.sol:RendererBoundaryPropertiesTest\n[PASS] testFuzz_binaryChunksRoundTripAndLock(bytes,bytes) (runs: 1000, μ: 463923, ~: 460572)\n[PASS] test_chunkLimitAndRejectedWritesDoNotDamageUploadedArt() (gas: 5657976)\n[PASS] test_ownershipTransferRevokesOldOwnerAcrossEveryRendererMutation() (gas: 334335)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 108.13ms (108.17ms CPU time)\n\nRan 52 tests for test/InkSystem.t.sol:InkSystemTest\n[PASS] test_agentClaimsOwnPortraitFree() (gas: 311054)\n[PASS] test_agentDepositReturnsExactlyItsBacking() (gas: 3486647)\n[PASS] test_agentHoldersPayNoHookFee() (gas: 2732536)\n[PASS] test_agentHoldersRerollForHalf() (gas: 4162562)\n[PASS] test_borrowedAgentPortraitGetsNoWaiver() (gas: 2553565)\n[PASS] test_cantLockAnEmptyRenderer() (gas: 3095036)\n[PASS] test_claimRules() (gas: 464993)\n[PASS] test_collectFeesPaysImdToTreasuryAndKeepsInk() (gas: 3365783)\n[PASS] test_contractURIOwnerOnly() (gas: 94674)\n[PASS] test_contractWalletSeatCanClaim() (gas: 387125)\n[PASS] test_depositMinOut() (gas: 3012131)\n[PASS] test_depositedAgentPortraitIsPullable() (gas: 3343364)\n[PASS] test_feeRouterClaimAndRoute() (gas: 4374498)\n[PASS] test_feeRouterNeedsPoolAndAddresses() (gas: 3741402)\n[PASS] test_feeRouterSplitsAndCompounds() (gas: 4120806)\n[PASS] test_gasAtScale() (gas: 68506639)\nLogs:\n  pull(1) gas: 160960\n  revealNext(1) gas: 198923\n    per reveal: 198923\n  pull(10) gas: 548188\n  revealNext(10) gas: 916308\n    per reveal: 91630\n  pull(100) gas: 4762626\n  revealNext(100) gas: 8605657\n    per reveal: 86056\n  pull(400) gas: 18812820\n  revealNext(400) gas: 34269741\n    per reveal: 85674\n  deposit(100 portraits) gas: 6699209\n\n[PASS] test_guards() (gas: 12196889)\n[PASS] test_hookFeeImdToTreasury() (gas: 2209441)\n[PASS] test_hookFeeInkGoesToLiquidity() (gas: 2700478)\n[PASS] test_idleSeatClaimsToo() (gas: 226980)\n[PASS] test_inkFeesGoBackIntoThePool() (gas: 6054706)\n[PASS] test_launchSplit() (gas: 67288)\n[PASS] test_liquidityCannotRenounceBeforePoolIsConfigured() (gas: 2949296)\n[PASS] test_liquidityFloorHoldsIfPriceIsDragged() (gas: 6479873)\n[PASS] test_liquidityRefusesUnrelatedNFTsButKeepsPositionCustody() (gas: 2105371)\n[PASS] test_mainnetPoolMustPairWithOfficialIMD() (gas: 2960165)\n[PASS] test_noPerWalletLimitAfterRelease() (gas: 2123275)\n[PASS] test_noPullsUntilRelease() (gas: 2206824)\n[PASS] test_onlyOnePool() (gas: 46344)\n[PASS] test_ownerIsOwnerWalletNotDeployer() (gas: 7709584)\n[PASS] test_permissionlessListingTracksSpotAboveReleaseFloor() (gas: 3359704)\n[PASS] test_poolLockedUntilRelease() (gas: 2617053)\n[PASS] test_poolNeverOpensWithoutRelease() (gas: 2475857)\n[PASS] test_premiumBeforePoolCannotBeFrontRun() (gas: 5810042)\n[PASS] test_pullRerollPullAgain() (gas: 3869727)\n[PASS] test_releaseLadderShape() (gas: 2318478)\n[PASS] test_releasePremiumListsAboveTheOpeningPrice() (gas: 1923943)\n[PASS] test_releaseTurnsUnclaimedSeatsIntoSlotsAndPoolInk() (gas: 2072629)\n[PASS] test_revealIsNotEarlyAndNotTwice() (gas: 2601471)\n[PASS] test_seatSoldDuringWindowNewOwnerClaims() (gas: 301319)\n[PASS] test_setPoolRejectsHooklessPoolAndRemainsCorrectable() (gas: 3003675)\n[PASS] test_setPoolRejectsInconsistentHookBindings() (gas: 6352403)\n[PASS] test_setPoolRequiresPositionManagerBoundToSameManager() (gas: 2982911)\n[PASS] test_setupRules() (gas: 3062176)\n[PASS] test_setupStartMustBeSoon() (gas: 3051521)\n[PASS] test_traitRerollBeforeReleaseReverts() (gas: 39963)\n[PASS] test_traitRerollExpiredHashClosesWithoutFreeDraw() (gas: 2945657)\n[PASS] test_traitRerollGuards() (gas: 3434055)\n[PASS] test_traitRerollHalfPriceForAgentHolders() (gas: 3104372)\n[PASS] test_traitRerollRepaintsSameToken() (gas: 3006188)\n[PASS] test_vaultStaysFullyBacked() (gas: 3919869)\n[PASS] test_zeroTradingFee() (gas: 29277)\nSuite result: ok. 52 passed; 0 failed; 0 skipped; finished in 108.24ms (110.14ms CPU time)\n\nRan 6 tests for test/InkRenderer.t.sol:InkRendererTest\n[PASS] test_agentTraitsMatchTheScript() (gas: 61685138)\n[PASS] test_noClaimsAfterTheFirstSwarmPortrait() (gas: 1375327)\n[PASS] test_poolTraitsMatchTheScript() (gas: 60520738)\n[PASS] test_scriptStoredInChunksAndLocked() (gas: 12171384)\n[PASS] test_swarmNumbersFillUnclaimedSeats() (gas: 163195398)\n[PASS] test_tokenURIForAnAgentPortrait() (gas: 3688020)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 129.02ms (302.77ms CPU time)\n\nRan 3 tests for test/MainnetContractsLaunch.t.sol:MainnetContractsLaunchTest\n[PASS] test_exactFourContractOrderReferencesAndOwners() (gas: 221632)\n[PASS] test_factoryTreasuryAndOriginHaveNoAdministrativeRights() (gas: 1697579)\n[PASS] test_runtimeSizeAndOpcodesMeetProtectedFloor() (gas: 16134665)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 440.70ms (23.56ms CPU time)\n\nRan 12 tests for test/LaunchSafety.t.sol:LaunchSafetyTest\n[PASS] testFuzz_directPortraitTransfersCannotBypassDeposit(bool,uint8) (runs: 256, μ: 621220, ~: 835617)\n[PASS] testFuzz_expiredPullChargesPurchaseFeeAndPreservesBacking(uint16,uint16,bool) (runs: 256, μ: 562741, ~: 567193)\n[PASS] test_cannotRenounceBeforeRequiredConfiguration() (gas: 472291)\n[PASS] test_expiredBatchRespectsProcessingLimit() (gas: 691260)\n[PASS] test_expiredHeadCannotStallLaterLivePull() (gas: 777326)\n[PASS] test_expiredTraitRerollConsumesFeeAndRequiresNewPayment() (gas: 949557)\n[PASS] test_portraitsCannotBeSentToLiquidity() (gas: 563556)\n[PASS] test_pullLastAvailableBlockRevealsWithoutExpiryFee() (gas: 461789)\n[PASS] test_setInkRequiresCodeAndFullAllocationBeforeItCommits() (gas: 319425)\n[PASS] test_setupRejectsWrongTokenLiquidityAndRemainsCorrectable() (gas: 1228159)\n[PASS] test_traitRerollLastAvailableBlockUsesOriginalHash() (gas: 663662)\n[PASS] test_vaultAndRendererBindingFailuresRemainCorrectable() (gas: 433271)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 440.79ms (142.91ms CPU time)\n\nRan 15 tests for test/AuditRegression.t.sol:AuditRegressionTest\n[PASS] testFuzz_admissionCompletesWithBothRevealEntryPoints(uint16,bool) (runs: 256, μ: 25635071, ~: 21556246)\n[PASS] test_directSafeTransfersRejectedForBothOverloadsAndOtherNFTs() (gas: 875044)\n[PASS] test_expiredHeadPullNeverWidensALaterPullsDraw() (gas: 2124372)\n[PASS] test_expiredPullsCloseWithoutWideningDrawsOrRenewing() (gas: 1414489)\n[PASS] test_hashExpiryDuringAdmissionKeepsOriginalDraw() (gas: 43216044)\n[PASS] test_initializedPoolAtZeroTickIsValid() (gas: 4799316)\n[PASS] test_largeAdmissionQueueMakesProgressWithinGasBudget() (gas: 423679832)\nLogs:\n  largest admission/reveal step gas (including call overhead): 3464062\n\n[PASS] test_partialAdmissionPreservesDrawAndExcludesSameBlockAndLaterDeposits() (gas: 22742217)\n[PASS] test_pendingRerollBlocksApprovedOperatorsUntilExpiry() (gas: 1643180)\n[PASS] test_pendingRerollCannotBeSoldWithOldTraits() (gas: 776696)\n[PASS] test_safeTransferToVaultIsRejected() (gas: 508769)\n[PASS] test_sameBlockHandInsDoNotCountForPullsMadeNow() (gas: 235863254)\n[PASS] test_setupRefusesUnconfiguredLiquidityBeforeCommitting() (gas: 4689582)\n[PASS] test_setupRejectsLiquidityWithoutCode() (gas: 290820)\n[PASS] test_uninitializedPoolCannotSetPermanentFloor() (gas: 4893283)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 491.76ms (739.10ms CPU time)\n\nRan 4 tests for test/LiquidityCustodyInvariants.t.sol:LiquidityCustodyInk0Test\n[PASS]\nLiquidityCustodyInk0Test invariants:\n[PASS] invariant_configurationAndBackingStayIntact\n[PASS] invariant_conservationAndFeeDestinations\n[PASS] invariant_positionsStayLockedAndFullyLiquid\n LiquidityCustodyInk0Test invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------------------+-------------+-------+---------+----------╮\n| Contract                | Selector    | Calls | Reverts | Discards |\n+====================================================================+\n| LiquidityCustodyHandler | add         | 1164  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | changeWidth | 1179  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | collect     | 1232  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | collectHook | 1120  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | donate      | 1116  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | rejectTheft | 1150  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | trade       | 1231  | 0       | 0        |\n╰-------------------------+-------------+-------+---------+----------╯\n\n[PASS] test_minimumAddBoundaryAndEmptyCollectionRanges() (gas: 4623073)\n[PASS] test_positionManagerFailureRollsBackPrefundingAndCanBeRetried() (gas: 4046631)\n[PASS] test_roundTripCollectAndCompound() (gas: 2951062)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 5.60s (5.61s CPU time)\n\nRan 4 tests for test/LiquidityCustodyInvariants.t.sol:LiquidityCustodyInk1Test\n[PASS]\nLiquidityCustodyInk1Test invariants:\n[PASS] invariant_configurationAndBackingStayIntact\n[PASS] invariant_conservationAndFeeDestinations\n[PASS] invariant_positionsStayLockedAndFullyLiquid\n LiquidityCustodyInk1Test invariants (runs: 128, calls: 8192, reverts: 0)\n\n╭-------------------------+-------------+-------+---------+----------╮\n| Contract                | Selector    | Calls | Reverts | Discards |\n+====================================================================+\n| LiquidityCustodyHandler | add         | 1164  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | changeWidth | 1179  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | collect     | 1232  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | collectHook | 1120  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | donate      | 1116  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | rejectTheft | 1150  | 0       | 0        |\n|-------------------------+-------------+-------+---------+----------|\n| LiquidityCustodyHandler | trade       | 1231  | 0       | 0        |\n╰-------------------------+-------------+-------+---------+----------╯\n\n[PASS] test_minimumAddBoundaryAndEmptyCollectionRanges() (gas: 4612855)\n[PASS] test_positionManagerFailureRollsBackPrefundingAndCanBeRetried() (gas: 4031196)\n[PASS] test_roundTripCollectAndCompound() (gas: 3034513)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 5.64s (5.65s CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantsTest\n[PASS]\nVaultInvariantsTest invariants:\n[PASS] invariant_agentBalancesMatch\n[PASS] invariant_capAndSlots\n[PASS] invariant_custody\n[PASS] invariant_feesToLiquidity\n[PASS] invariant_fullyBacked\n[PASS] invariant_inkConserved\n[PASS] invariant_queueCovered\n VaultInvariantsTest invariants (runs: 64, calls: 7680, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | claim            | 576   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | deposit          | 547   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | moveSeat         | 553   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pull             | 541   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullAll          | 549   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullTooMany      | 554   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | release          | 511   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | rerollTraits     | 580   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | reveal           | 569   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealNext       | 517   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealTraits     | 525   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | roll             | 577   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | setFees          | 552   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | transferPortrait | 529   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.06s (8.06s CPU time)\n\nRan 20 test suites in 8.06s (21.10s CPU time): 107 tests passed, 0 failed, 28 skipped (135 total tests)\n","passed":true},{"durationMs":106,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"InkFeeRouter.claimAndRoute(address,uint64)\",\"InkFeeRouter.route()\",\"InkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"InkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"InkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"InkHook.collectFees()\",\"InkHook.unlockCallback(bytes)\",\"InkLiquidity.addLiquidity()\",\"InkLiquidity.collectFees(uint256,uint256)\",\"InkLiquidity.renounceOwnership()\",\"InkLiquidity.setPool((address,address,uint24,int24,address),address)\",\"InkLiquidity.setPremium(int24)\",\"InkLiquidity.setWidth(int24)\",\"InkLiquidity.transferOwnership(address)\",\"InkPortraits.approve(address,uint256)\",\"InkPortraits.lockRenderer()\",\"InkPortraits.mintAgent(uint256,address,(uint32,uint16,uint32,uint32,bool))\",\"InkPortraits.mintSwarm(address,bytes32)\",\"InkPortraits.renounceOwnership()\",\"InkPortraits.safeTransferFrom(address,address,uint256)\",\"InkPortraits.safeTransferFrom(address,address,uint256,bytes)\",\"InkPortraits.setApprovalForAll(address,bool)\",\"InkPortraits.setContractURI(string)\",\"InkPortraits.setRenderer(address)\",\"InkPortraits.setSwarmSeed(uint256,bytes32)\",\"InkPortraits.setVault(address)\",\"InkPortraits.transferFrom(address,address,uint256)\",\"InkPortraits.transferOwnership(address)\",\"InkRenderer.addChunk(bytes)\",\"InkRenderer.lock()\",\"InkRenderer.renounceOwnership()\",\"InkRenderer.setPreviewBase(string)\",\"InkRenderer.transferOwnership(address)\",\"InkVault.claim(uint256,(uint32,uint16,uint32,uint32,bool),bytes32[])\",\"InkVault.deposit(uint256[],uint256)\",\"InkVault.pull(uint256)\",\"InkVault.release()\",\"InkVault.renounceOwnership()\",\"InkVault.rerollTraits(uint256,uint256)\",\"InkVault.reveal(uint256)\",\"InkVault.revealNext(uint256)\",\"InkVault.revealTraits(uint256)\",\"InkVault.setFees(uint16,uint16)\",\"InkVault.setInk(address)\",\"InkVault.setup(bytes32,uint64,address)\",\"InkVault.transferOwnership(address)\",\"TestSeats.approve(address,uint256)\",\"TestSeats.mint(uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256,bytes)\",\"TestSeats.setApprovalForAll(address,bool)\",\"TestSeats.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"ADAPTATION.md\":204,\"AUDIT_BRIEF.md\":71,\"BYTECODE_HASHES.json\":52,\"DEPENDENCIES.md\":10,\"PACKAGING.md\":16,\"README.md\":55,\"SETUP.md\":101,\"SHA256SUMS\":1640,\"SOURCE_PROVENANCE.json\":11377,\"VALIDATION.txt\":554,\"art/original-art.js\":418,\"foundry.toml\":33,\"launch/README.md\":35,\"launch/mainnet-1-contracts.json\":24,\"launch/mainnet-2-hook.json\":31,\"launch/sepolia-1-contracts.json\":24,\"launch/sepolia-2-hook.json\":31,\"remappings.txt\":8,\"script/Configure.s.sol\":63,\"script/InkSteps.sol\":135,\"script/LocalStack.s.sol\":135,\"script/Rehearse.s.sol\":236,\"script/UploadRenderer.s.sol\":33,\"src/InkFeeRouter.sol\":77,\"src/InkHook.sol\":255,\"src/InkLiquidity.sol\":327,\"src/InkPortraits.sol\":256,\"src/InkRenderer.sol\":310,\"src/InkVault.sol\":492,\"src/testnet/TestSeats.sol\":13,\"test/AuditRegression.t.sol\":476,\"test/InkRenderer.t.sol\":210,\"test/InkSystem.t.sol\":1342,\"test/LaunchDay.t.sol\":270,\"test/LaunchFeeHandoff.t.sol\":82,\"test/LaunchSafety.t.sol\":330,\"test/LiquidityCustodyInvariants.t.sol\":428,\"test/MainnetContractsLaunch.t.sol\":159,\"test/MainnetFork.t.sol\":519,\"test/RendererBoundaryProperties.t.sol\":104,\"test/SepoliaLaunch.t.sol\":186,\"test/SnapshotAllSeats.t.sol\":159,\"test/SnapshotProofs.t.sol\":91,\"test/SybilOptions.t.sol\":104,\"test/TestingCoverage.md\":41,\"test/TokenURIPreview.t.sol\":79,\"test/VaultInvariants.t.sol\":403,\"test/fixtures/snapshot-sample.json\":98,\"test/fixtures/traits.json\":1,\"test/helpers/ConfiguredLiquidity.sol\":18},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"4842df221cd4ac3416270bcb12752449f5e886aa5f1244df5a25f7e2edca9566","verifiedTreeHash":"bd740df9713fafe768f1c0f2ffa56ec8c7e3843c","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":13581,"exitCode":0,"name":"build","output":"Compiling 182 files with Solc 0.8.26\nSolc 0.8.26 finished in 13.38s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:26:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                          ^^^^^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:38:5:\n   |\n38 |     InkPortraits portraits;\n   |     ^^^^^^^^^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:50:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                                                  ^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:39:5:\n   |\n39 |     InkVault vault;\n   |     ^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:20:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                    ^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:44:5:\n   |\n44 |     PoolKey key;\n   |     ^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:65:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                                                                 ^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:43:5:\n   |\n43 |     ForkInk ink;\n   |     ^^^^^^^^^^^\n\nWarning (3149): The result type of the shift operation is equal to the type of the first operand (uint128) ignoring the (larger) type of the second operand (uint256) which might be unexpected. Silence this warning by either converting the first or the second operand to the type of the other.\n   --> src/InkLiquidity.sol:189:31:\n    |\n189 |             uint128 unitLiq = uint128(1e24) << k;\n    |                               ^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":9162,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/LaunchFeeHandoff.t.sol:LaunchFeeHandoffTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 257.65µs (0.00ns CPU time)\n\nRan 9 tests for test/SnapshotAllSeats.t.sol:SnapshotAllSeatsTest\n[SKIP] test_claims_0000() (gas: 0)\n[SKIP] test_claims_0250() (gas: 0)\n[SKIP] test_claims_0500() (gas: 0)\n[SKIP] test_claims_0750() (gas: 0)\n[SKIP] test_claims_1000() (gas: 0)\n[SKIP] test_claims_1250() (gas: 0)\n[SKIP] test_claims_1500() (gas: 0)\n[SKIP] test_claims_1750() (gas: 0)\n[SKIP] test_treeHasEverySeatOnce() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 9 skipped; finished in 438.26µs (1.11ms CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium5xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 607.98µs (19.54µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium2xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 611.76µs (11.63µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs0Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 817.59µs (153.79µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs1Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 1.04ms (216.57µs CPU time)\n\nRan 1 test for test/LaunchDay.t.sol:LaunchDayTest\n[SKIP] test_fork_launchDay() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 971.95µs (24.84µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium1xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 1.19ms (26.12µs CPU time)\n\nRan 2 tests for test/SnapshotProofs.t.sol:SnapshotProofsTest\n[PASS] test_realSnapshotProofsClaim() (gas: 3221612)\n[PASS] test_tamperedStatsAreRejected() (gas: 605207)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.42ms (3.16ms CPU time)\n\nRan 12 tests for test/LaunchSafety.t.sol:LaunchSafetyTest\n[PASS] testFuzz_directPortraitTransfersCannotBypassDeposit(bool,uint8) (runs: 256, μ: 621207, ~: 835617)\n[PASS] testFuzz_expiredPullChargesPurchaseFeeAndPreservesBacking(uint16,uint16,bool) (runs: 256, μ: 563096, ~: 567193)\n[PASS] test_cannotRenounceBeforeRequiredConfiguration() (gas: 472291)\n[PASS] test_expiredBatchRespectsProcessingLimit() (gas: 691260)\n[PASS] test_expiredHeadCannotStallLaterLivePull() (gas: 777326)\n[PASS] test_expiredTraitRerollConsumesFeeAndRequiresNewPayment() (gas: 949557)\n[PASS] test_portraitsCannotBeSentToLiquidity() (gas: 563556)\n[PASS] test_pullLastAvailableBlockRevealsWithoutExpiryFee() (gas: 461789)\n[PASS] test_setInkRequiresCodeAndFullAllocationBeforeItCommits() (gas: 319425)\n[PASS] test_setupRejectsWrongTokenLiquidityAndRemainsCorrectable() (gas: 1228159)\n[PASS] test_traitRerollLastAvailableBlockUsesOriginalHash() (gas: 663662)\n[PASS] test_vaultAndRendererBindingFailuresRemainCorrectable() (gas: 433271)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 28.75ms (46.60ms CPU time)\n\nRan 4 tests for test/SepoliaLaunch.t.sol:SepoliaLaunchTest\n[PASS] test_allFiveRuntimesMeetProtectedFloor() (gas: 17925603)\n[PASS] test_existingOwnerSetupRemainsRequiredAndAvailable() (gas: 441184)\n[PASS] test_factoryDeploymentPreservesOwnersRoyaltiesAndReferences() (gas: 183956)\n[PASS] test_factoryTreasuryAndOutsiderHaveNoAdminRole() (gas: 1700968)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 37.96ms (38.08ms CPU time)\n\nRan 1 test for test/TokenURIPreview.t.sol:TokenURIPreviewTest\n[PASS] test_writeTokenURIs() (gas: 25400631)\nLogs:\n  read-only profile: not written out-preview/agent-280.txt (use FOUNDRY_PROFILE=preview)\n  read-only profile: not written out-preview/pool.txt (use FOUNDRY_PROFILE=preview)\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 54.21ms (53.99ms CPU time)\n\nRan 6 tests for test/InkRenderer.t.sol:InkRendererTest\n[PASS] test_agentTraitsMatchTheScript() (gas: 61685138)\n[PASS] test_noClaimsAfterTheFirstSwarmPortrait() (gas: 1375327)\n[PASS] test_poolTraitsMatchTheScript() (gas: 60520738)\n[PASS] test_scriptStoredInChunksAndLocked() (gas: 12171384)\n[PASS] test_swarmNumbersFillUnclaimedSeats() (gas: 163195398)\n[PASS] test_tokenURIForAnAgentPortrait() (gas: 3688020)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 154.87ms (314.58ms CPU time)\n\nRan 52 tests for test/InkSystem.t.sol:InkSystemTest\n[PASS] test_agentClaimsOwnPortraitFree() (gas: 311054)\n[PASS] test_agentDepositReturnsExactlyItsBacking() (gas: 3486647)\n[PASS] test_agentHoldersPayNoHookFee() (gas: 2732536)\n[PASS] test_agentHoldersRerollForHalf() (gas: 4162562)\n[PASS] test_borrowedAgentPortraitGetsNoWaiver() (gas: 2553565)\n[PASS] test_cantLockAnEmptyRenderer() (gas: 3095036)\n[PASS] test_claimRules() (gas: 464993)\n[PASS] test_collectFeesPaysImdToTreasuryAndKeepsInk() (gas: 3365783)\n[PASS] test_contractURIOwnerOnly() (gas: 94674)\n[PASS] test_contractWalletSeatCanClaim() (gas: 387125)\n[PASS] test_depositMinOut() (gas: 3012131)\n[PASS] test_depositedAgentPortraitIsPullable() (gas: 3343364)\n[PASS] test_feeRouterClaimAndRoute() (gas: 4374498)\n[PASS] test_feeRouterNeedsPoolAndAddresses() (gas: 3741402)\n[PASS] test_feeRouterSplitsAndCompounds() (gas: 4120806)\n[PASS] test_gasAtScale() (gas: 68506639)\nLogs:\n  pull(1) gas: 160960\n  revealNext(1) gas: 198923\n    per reveal: 198923\n  pull(10) gas: 548188\n  revealNext(10) gas: 916308\n    per reveal: 91630\n  pull(100) gas: 4762626\n  revealNext(100) gas: 8605657\n    per reveal: 86056\n  pull(400) gas: 18812820\n  revealNext(400) gas: 34269741\n    per reveal: 85674\n  deposit(100 portraits) gas: 6699209\n\n[PASS] test_guards() (gas: 12196889)\n[PASS] test_hookFeeImdToTreasury() (gas: 2209441)\n[PASS] test_hookFeeInkGoesToLiquidity() (gas: 2700478)\n[PASS] test_idleSeatClaimsToo() (gas: 226980)\n[PASS] test_inkFeesGoBackIntoThePool() (gas: 6054706)\n[PASS] test_launchSplit() (gas: 67288)\n[PASS] test_liquidityCannotRenounceBeforePoolIsConfigured() (gas: 2949296)\n[PASS] test_liquidityFloorHoldsIfPriceIsDragged() (gas: 6479873)\n[PASS] test_liquidityRefusesUnrelatedNFTsButKeepsPositionCustody() (gas: 2105371)\n[PASS] test_mainnetPoolMustPairWithOfficialIMD() (gas: 2960165)\n[PASS] test_noPerWalletLimitAfterRelease() (gas: 2123275)\n[PASS] test_noPullsUntilRelease() (gas: 2206824)\n[PASS] test_onlyOnePool() (gas: 46344)\n[PASS] test_ownerIsOwnerWalletNotDeployer() (gas: 7709584)\n[PASS] test_permissionlessListingTracksSpotAboveReleaseFloor() (gas: 3359704)\n[PASS] test_poolLockedUntilRelease() (gas: 2617053)\n[PASS] test_poolNeverOpensWithoutRelease() (gas: 2475857)\n[PASS] test_premiumBeforePoolCannotBeFrontRun() (gas: 5810042)\n[PASS] test_pullRerollPullAgain() (gas: 3869727)\n[PASS] test_releaseLadderShape() (gas: 2318478)\n[PASS] test_releasePremiumListsAboveTheOpeningPrice() (gas: 1923943)\n[PASS] test_releaseTurnsUnclaimedSeatsIntoSlotsAndPoolInk() (gas: 2072629)\n[PASS] test_revealIsNotEarlyAndNotTwice() (gas: 2601471)\n[PASS] test_seatSoldDuringWindowNewOwnerClaims() (gas: 301319)\n[PASS] test_setPoolRejectsHooklessPoolAndRemainsCorrectable() (gas: 3003675)\n[PASS] test_setPoolRejectsInconsistentHookBindings() (gas: 6352403)\n[PASS] test_setPoolRequiresPositionManagerBoundToSameManager() (gas: 2982911)\n[PASS] test_setupRules() (gas: 3062176)\n[PASS] test_setupStartMustBeSoon() (gas: 3051521)\n[PASS] test_traitRerollBeforeReleaseReverts() (gas: 39963)\n[PASS] test_traitRerollExpiredHashClosesWithoutFreeDraw() (gas: 2945657)\n[PASS] test_traitRerollGuards() (gas: 3434055)\n[PASS] test_traitRerollHalfPriceForAgentHolders() (gas: 3104372)\n[PASS] test_traitRerollRepaintsSameToken() (gas: 3006188)\n[PASS] test_vaultStaysFullyBacked() (gas: 3919869)\n[PASS] test_zeroTradingFee() (gas: 29277)\nSuite result: ok. 52 passed; 0 failed; 0 skipped; finished in 154.86ms (160.04ms CPU time)\n\nRan 3 tests for test/MainnetContractsLaunch.t.sol:MainnetContractsLaunchTest\n[PASS] test_exactFourContractOrderReferencesAndOwners() (gas: 221632)\n[PASS] test_factoryTreasuryAndOriginHaveNoAdministrativeRights() (gas: 1697579)\n[PASS] test_runtimeSizeAndOpcodesMeetProtectedFloor() (gas: 16134665)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 452.74ms (27.98ms CPU time)\n\nRan 15 tests for test/AuditRegression.t.sol:AuditRegressionTest\n[PASS] testFuzz_admissionCompletesWithBothRevealEntryPoints(uint16,bool) (runs: 256, μ: 25194521, ~: 21556246)\n[PASS] test_directSafeTransfersRejectedForBothOverloadsAndOtherNFTs() (gas: 875044)\n[PASS] test_expiredHeadPullNeverWidensALaterPullsDraw() (gas: 2124372)\n[PASS] test_expiredPullsCloseWithoutWideningDrawsOrRenewing() (gas: 1414489)\n[PASS] test_hashExpiryDuringAdmissionKeepsOriginalDraw() (gas: 43216044)\n[PASS] test_initializedPoolAtZeroTickIsValid() (gas: 4799316)\n[PASS] test_largeAdmissionQueueMakesProgressWithinGasBudget() (gas: 423679832)\nLogs:\n  largest admission/reveal step gas (including call overhead): 3464062\n\n[PASS] test_partialAdmissionPreservesDrawAndExcludesSameBlockAndLaterDeposits() (gas: 22742217)\n[PASS] test_pendingRerollBlocksApprovedOperatorsUntilExpiry() (gas: 1643180)\n[PASS] test_pendingRerollCannotBeSoldWithOldTraits() (gas: 776696)\n[PASS] test_safeTransferToVaultIsRejected() (gas: 508769)\n[PASS] test_sameBlockHandInsDoNotCountForPullsMadeNow() (gas: 235863254)\n[PASS] test_setupRefusesUnconfiguredLiquidityBeforeCommitting() (gas: 4689582)\n[PASS] test_setupRejectsLiquidityWithoutCode() (gas: 290820)\n[PASS] test_uninitializedPoolCannotSetPermanentFloor() (gas: 4893283)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 511.16ms (776.12ms CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantsTest\n[PASS]\nVaultInvariantsTest invariants:\n[PASS] invariant_agentBalancesMatch\n[PASS] invariant_capAndSlots\n[PASS] invariant_custody\n[PASS] invariant_feesToLiquidity\n[PASS] invariant_fullyBacked\n[PASS] invariant_inkConserved\n[PASS] invariant_queueCovered\n VaultInvariantsTest invariants (runs: 64, calls: 7680, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | claim            | 576   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | deposit          | 547   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | moveSeat         | 553   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pull             | 541   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullAll          | 549   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullTooMany      | 554   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | release          | 511   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | rerollTraits     | 580   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | reveal           | 569   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealNext       | 517   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealTraits     | 525   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | roll             | 577   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | setFees          | 552   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | transferPortrait | 529   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.99s (8.99s CPU time)\n\nRan 17 test suites in 9.00s (10.40s CPU time): 96 tests passed, 0 failed, 28 skipped (124 total tests)\n","passed":true},{"durationMs":152,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"InkFeeRouter.claimAndRoute(address,uint64)\",\"InkFeeRouter.route()\",\"InkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"InkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"InkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"InkHook.collectFees()\",\"InkHook.unlockCallback(bytes)\",\"InkLiquidity.addLiquidity()\",\"InkLiquidity.collectFees(uint256,uint256)\",\"InkLiquidity.renounceOwnership()\",\"InkLiquidity.setPool((address,address,uint24,int24,address),address)\",\"InkLiquidity.setPremium(int24)\",\"InkLiquidity.setWidth(int24)\",\"InkLiquidity.transferOwnership(address)\",\"InkPortraits.approve(address,uint256)\",\"InkPortraits.lockRenderer()\",\"InkPortraits.mintAgent(uint256,address,(uint32,uint16,uint32,uint32,bool))\",\"InkPortraits.mintSwarm(address,bytes32)\",\"InkPortraits.renounceOwnership()\",\"InkPortraits.safeTransferFrom(address,address,uint256)\",\"InkPortraits.safeTransferFrom(address,address,uint256,bytes)\",\"InkPortraits.setApprovalForAll(address,bool)\",\"InkPortraits.setContractURI(string)\",\"InkPortraits.setRenderer(address)\",\"InkPortraits.setSwarmSeed(uint256,bytes32)\",\"InkPortraits.setVault(address)\",\"InkPortraits.transferFrom(address,address,uint256)\",\"InkPortraits.transferOwnership(address)\",\"InkRenderer.addChunk(bytes)\",\"InkRenderer.lock()\",\"InkRenderer.renounceOwnership()\",\"InkRenderer.setPreviewBase(string)\",\"InkRenderer.transferOwnership(address)\",\"InkVault.claim(uint256,(uint32,uint16,uint32,uint32,bool),bytes32[])\",\"InkVault.deposit(uint256[],uint256)\",\"InkVault.pull(uint256)\",\"InkVault.release()\",\"InkVault.renounceOwnership()\",\"InkVault.rerollTraits(uint256,uint256)\",\"InkVault.reveal(uint256)\",\"InkVault.revealNext(uint256)\",\"InkVault.revealTraits(uint256)\",\"InkVault.setFees(uint16,uint16)\",\"InkVault.setInk(address)\",\"InkVault.setup(bytes32,uint64,address)\",\"InkVault.transferOwnership(address)\",\"TestSeats.approve(address,uint256)\",\"TestSeats.mint(uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256,bytes)\",\"TestSeats.setApprovalForAll(address,bool)\",\"TestSeats.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"ADAPTATION.md\":204,\"AUDIT_BRIEF.md\":71,\"BYTECODE_HASHES.json\":52,\"DEPENDENCIES.md\":10,\"PACKAGING.md\":16,\"README.md\":55,\"SETUP.md\":101,\"SHA256SUMS\":1640,\"SOURCE_PROVENANCE.json\":11377,\"VALIDATION.txt\":554,\"art/original-art.js\":418,\"foundry.toml\":33,\"launch.json\":36,\"launch/README.md\":35,\"launch/mainnet-1-contracts.json\":24,\"launch/mainnet-2-hook.json\":31,\"launch/sepolia-1-contracts.json\":24,\"launch/sepolia-2-hook.json\":31,\"remappings.txt\":8,\"script/Configure.s.sol\":63,\"script/InkSteps.sol\":135,\"script/LocalStack.s.sol\":135,\"script/Rehearse.s.sol\":236,\"script/UploadRenderer.s.sol\":33,\"src/InkFeeRouter.sol\":77,\"src/InkHook.sol\":255,\"src/InkLiquidity.sol\":327,\"src/InkPortraits.sol\":256,\"src/InkRenderer.sol\":310,\"src/InkVault.sol\":492,\"src/testnet/TestSeats.sol\":13,\"test/AuditRegression.t.sol\":476,\"test/InkRenderer.t.sol\":210,\"test/InkSystem.t.sol\":1342,\"test/LaunchDay.t.sol\":270,\"test/LaunchFeeHandoff.t.sol\":82,\"test/LaunchSafety.t.sol\":330,\"test/MainnetContractsLaunch.t.sol\":159,\"test/MainnetFork.t.sol\":519,\"test/SepoliaLaunch.t.sol\":186,\"test/SnapshotAllSeats.t.sol\":159,\"test/SnapshotProofs.t.sol\":91,\"test/SybilOptions.t.sol\":104,\"test/TokenURIPreview.t.sol\":79,\"test/VaultInvariants.t.sol\":403,\"test/fixtures/snapshot-sample.json\":98,\"test/fixtures/traits.json\":1,\"test/helpers/ConfiguredLiquidity.sol\":18},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"797b711efc6f4e1d0e353405e97695482b8350ebedf6e5f685e1f963bd4a481a","verifiedTreeHash":"b8459b2b90d754e54170c6c2c7c2c9ce9d1b8761","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":14566,"exitCode":0,"name":"build","output":"Compiling 184 files with Solc 0.8.26\nSolc 0.8.26 finished in 14.37s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:26:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                          ^^^^^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:38:5:\n   |\n38 |     InkPortraits portraits;\n   |     ^^^^^^^^^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:50:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                                                  ^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:39:5:\n   |\n39 |     InkVault vault;\n   |     ^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:20:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                    ^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:44:5:\n   |\n44 |     PoolKey key;\n   |     ^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:65:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                                                                 ^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:43:5:\n   |\n43 |     ForkInk ink;\n   |     ^^^^^^^^^^^\n\nWarning (3149): The result type of the shift operation is equal to the type of the first operand (uint128) ignoring the (larger) type of the second operand (uint256) which might be unexpected. Silence this warning by either converting the first or the second operand to the type of the other.\n   --> src/InkLiquidity.sol:196:31:\n    |\n196 |             uint128 unitLiq = uint128(1e24) << k;\n    |                               ^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":9986,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/LaunchFeeHandoff.t.sol:LaunchFeeHandoffTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 358.34µs (0.00ns CPU time)\n\nRan 9 tests for test/SnapshotAllSeats.t.sol:SnapshotAllSeatsTest\n[SKIP] test_claims_0000() (gas: 0)\n[SKIP] test_claims_0250() (gas: 0)\n[SKIP] test_claims_0500() (gas: 0)\n[SKIP] test_claims_0750() (gas: 0)\n[SKIP] test_claims_1000() (gas: 0)\n[SKIP] test_claims_1250() (gas: 0)\n[SKIP] test_claims_1500() (gas: 0)\n[SKIP] test_claims_1750() (gas: 0)\n[SKIP] test_treeHasEverySeatOnce() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 9 skipped; finished in 472.56µs (1.26ms CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium5xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 680.81µs (19.73µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium2xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 721.29µs (28.12µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs1Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 882.26µs (155.29µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs0Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 950.36µs (181.25µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium1xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 1.06ms (23.28µs CPU time)\n\nRan 1 test for test/LaunchDay.t.sol:LaunchDayTest\n[SKIP] test_fork_launchDay() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 2.66ms (25.32µs CPU time)\n\nRan 2 tests for test/SnapshotProofs.t.sol:SnapshotProofsTest\n[PASS] test_realSnapshotProofsClaim() (gas: 3222192)\n[PASS] test_tamperedStatsAreRejected() (gas: 605318)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.75ms (3.84ms CPU time)\n\nRan 4 tests for test/RangeMath.t.sol:RangeMathTest\n[PASS] testFuzz_rangeNeverRevertsAndStaysUsable(int24,int24,bool) (runs: 256, μ: 118301, ~: 118435)\n[PASS] test_maxWidthClampsWhenInkIsCurrency0() (gas: 131887)\n[PASS] test_maxWidthClampsWhenInkIsCurrency1() (gas: 115332)\n[PASS] test_ordinaryWidthUnchanged() (gas: 118547)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 8.38ms (8.65ms CPU time)\n\nRan 1 test for test/TokenURIPreview.t.sol:TokenURIPreviewTest\n[PASS] test_writeTokenURIs() (gas: 25423286)\nLogs:\n  read-only profile: not written out-preview/agent-280.txt (use FOUNDRY_PROFILE=preview)\n  read-only profile: not written out-preview/pool.txt (use FOUNDRY_PROFILE=preview)\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 43.36ms (43.16ms CPU time)\n\nRan 54 tests for test/InkSystem.t.sol:InkSystemTest\n[PASS] test_agentClaimsOwnPortraitFree() (gas: 311177)\n[PASS] test_agentDepositReturnsExactlyItsBacking() (gas: 3516171)\n[PASS] test_agentHoldersPayNoHookFee() (gas: 2734016)\n[PASS] test_agentHoldersRerollForHalf() (gas: 4215016)\n[PASS] test_borrowedAgentPortraitGetsNoWaiver() (gas: 2555057)\n[PASS] test_cantLockAnEmptyRenderer() (gas: 3138116)\n[PASS] test_claimRules() (gas: 465550)\n[PASS] test_collectFeesPaysImdToTreasuryAndKeepsInk() (gas: 3367122)\n[PASS] test_contractURIOwnerOnly() (gas: 94718)\n[PASS] test_contractWalletSeatCanClaim() (gas: 387248)\n[PASS] test_depositMinOut() (gas: 3038421)\n[PASS] test_depositedAgentPortraitIsPullable() (gas: 3344958)\n[PASS] test_feeRouterClaimAndRoute() (gas: 4381579)\n[PASS] test_feeRouterNeedsPoolAndAddresses() (gas: 3872711)\n[PASS] test_feeRouterSplitsAndCompounds() (gas: 4127736)\n[PASS] test_gasAtScale() (gas: 78817128)\nLogs:\n  pull(1) gas: 161004\n  revealNext(1) gas: 221322\n    per reveal: 221322\n  pull(10) gas: 548232\n  revealNext(10) gas: 1140496\n    per reveal: 114049\n  pull(100) gas: 4762670\n  revealNext(100) gas: 10847735\n    per reveal: 108477\n  pull(400) gas: 18812864\n  revealNext(400) gas: 43238119\n    per reveal: 108095\n  deposit(100 portraits) gas: 6950387\n\n[PASS] test_guards() (gas: 12434326)\n[PASS] test_hookFeeImdToTreasury() (gas: 2210776)\n[PASS] test_hookFeeInkGoesToLiquidity() (gas: 2701835)\n[PASS] test_idleSeatClaimsToo() (gas: 227103)\n[PASS] test_inkFeesGoBackIntoThePool() (gas: 6092386)\n[PASS] test_launchSplit() (gas: 67244)\n[PASS] test_liquidityCannotRenounceBeforePoolIsConfigured() (gas: 3071191)\n[PASS] test_liquidityFloorHoldsIfPriceIsDragged() (gas: 6606864)\n[PASS] test_liquidityOwnershipTransferRequiresAcceptance() (gas: 222592)\n[PASS] test_liquidityRefusesUnrelatedNFTsButKeepsPositionCustody() (gas: 2106517)\n[PASS] test_mainnetPoolMustPairWithOfficialIMD() (gas: 3079926)\n[PASS] test_noPerWalletLimitAfterRelease() (gas: 2124632)\n[PASS] test_noPullsUntilRelease() (gas: 2230624)\n[PASS] test_onlyOnePool() (gas: 46322)\n[PASS] test_ownerIsOwnerWalletNotDeployer() (gas: 7990860)\n[PASS] test_permissionlessListingTracksSpotAboveReleaseFloor() (gas: 3372258)\n[PASS] test_poolLockedUntilRelease() (gas: 2618410)\n[PASS] test_poolNeverOpensWithoutRelease() (gas: 2477214)\n[PASS] test_premiumBeforePoolCannotBeFrontRun() (gas: 6049546)\n[PASS] test_premiumCannotBeLockedByListingBeforeRelease() (gas: 6665678)\n[PASS] test_pullRerollPullAgain() (gas: 3918693)\n[PASS] test_releaseLadderShape() (gas: 2319548)\n[PASS] test_releasePremiumListsAboveTheOpeningPrice() (gas: 1925754)\n[PASS] test_releaseTurnsUnclaimedSeatsIntoSlotsAndPoolInk() (gas: 2074111)\n[PASS] test_revealIsNotEarlyAndNotTwice() (gas: 2625294)\n[PASS] test_seatSoldDuringWindowNewOwnerClaims() (gas: 301442)\n[PASS] test_setPoolRejectsHooklessPoolAndRemainsCorrectable() (gas: 3123415)\n[PASS] test_setPoolRejectsInconsistentHookBindings() (gas: 6644544)\n[PASS] test_setPoolRequiresPositionManagerBoundToSameManager() (gas: 3102635)\n[PASS] test_setupRules() (gas: 3137736)\n[PASS] test_setupStartMustBeSoon() (gas: 3127148)\n[PASS] test_traitRerollBeforeReleaseReverts() (gas: 39919)\n[PASS] test_traitRerollExpiredHashClosesWithoutFreeDraw() (gas: 2972065)\n[PASS] test_traitRerollGuards() (gas: 3463153)\n[PASS] test_traitRerollHalfPriceForAgentHolders() (gas: 3130903)\n[PASS] test_traitRerollRepaintsSameToken() (gas: 3032596)\n[PASS] test_vaultStaysFullyBacked() (gas: 3972012)\n[PASS] test_zeroTradingFee() (gas: 29321)\nSuite result: ok. 54 passed; 0 failed; 0 skipped; finished in 45.57ms (147.96ms CPU time)\n\nRan 4 tests for test/SepoliaLaunch.t.sol:SepoliaLaunchTest\n[PASS] test_allFiveRuntimesMeetProtectedFloor() (gas: 18566852)\n[PASS] test_existingOwnerSetupRemainsRequiredAndAvailable() (gas: 441282)\n[PASS] test_factoryDeploymentPreservesOwnersRoyaltiesAndReferences() (gas: 183938)\n[PASS] test_factoryTreasuryAndOutsiderHaveNoAdminRole() (gas: 1701232)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 47.28ms (45.56ms CPU time)\n\nRan 3 tests for test/MainnetContractsLaunch.t.sol:MainnetContractsLaunchTest\n[PASS] test_exactFourContractOrderReferencesAndOwners() (gas: 221680)\n[PASS] test_factoryTreasuryAndOriginHaveNoAdministrativeRights() (gas: 1697915)\n[PASS] test_runtimeSizeAndOpcodesMeetProtectedFloor() (gas: 16755780)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 99.43ms (32.58ms CPU time)\n\nRan 6 tests for test/InkRenderer.t.sol:InkRendererTest\n[PASS] test_agentTraitsMatchTheScript() (gas: 61685138)\n[PASS] test_noClaimsAfterTheFirstSwarmPortrait() (gas: 1397672)\n[PASS] test_poolTraitsMatchTheScript() (gas: 60520738)\n[PASS] test_scriptStoredInChunksAndLocked() (gas: 12171384)\n[PASS] test_swarmNumbersFillUnclaimedSeats() (gas: 163375864)\n[PASS] test_tokenURIForAnAgentPortrait() (gas: 3733117)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 125.87ms (328.34ms CPU time)\n\nRan 16 tests for test/LaunchSafety.t.sol:LaunchSafetyTest\n[PASS] testFuzz_directPortraitTransfersCannotBypassDeposit(bool,uint8) (runs: 256, μ: 635495, ~: 860773)\n[PASS] testFuzz_expiredPullChargesPurchaseFeeAndPreservesBacking(uint16,uint16,bool) (runs: 256, μ: 563162, ~: 567514)\n[PASS] test_agentHolderDiscountOnlyOnOwnPullsStillHeld() (gas: 3268385)\n[PASS] test_cannotRenounceBeforeRequiredConfiguration() (gas: 478947)\n[PASS] test_expiredBatchRespectsProcessingLimit() (gas: 691194)\n[PASS] test_expiredHeadCannotStallLaterLivePull() (gas: 800011)\n[PASS] test_expiredTraitRerollConsumesFeeAndRequiresNewPayment() (gas: 977128)\n[PASS] test_nonHolderPaysFullFeeThroughAgentHoldingContract() (gas: 1368938)\n[PASS] test_ownershipTransferRequiresAcceptance() (gas: 640058)\n[PASS] test_portraitsCannotBeSentToLiquidity() (gas: 586110)\n[PASS] test_pullLastAvailableBlockRevealsWithoutExpiryFee() (gas: 484320)\n[PASS] test_setInkRequiresCodeAndFullAllocationBeforeItCommits() (gas: 319403)\n[PASS] test_setupRejectsWrongTokenLiquidityAndRemainsCorrectable() (gas: 1228493)\n[PASS] test_traitRerollLastAvailableBlockUsesOriginalHash() (gas: 688736)\n[PASS] test_traitRerollThroughAgentHoldingContractPaysFullFee() (gas: 1216106)\n[PASS] test_vaultAndRendererBindingFailuresRemainCorrectable() (gas: 433271)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 143.93ms (69.14ms CPU time)\n\nRan 15 tests for test/AuditRegression.t.sol:AuditRegressionTest\n[PASS] testFuzz_admissionCompletesWithBothRevealEntryPoints(uint16,bool) (runs: 256, μ: 27820664, ~: 24304999)\n[PASS] test_directSafeTransfersRejectedForBothOverloadsAndOtherNFTs() (gas: 900066)\n[PASS] test_expiredHeadPullNeverWidensALaterPullsDraw() (gas: 2219191)\n[PASS] test_expiredPullsCloseWithoutWideningDrawsOrRenewing() (gas: 1439598)\n[PASS] test_hashExpiryDuringAdmissionKeepsOriginalDraw() (gas: 47716799)\n[PASS] test_initializedPoolAtZeroTickIsValid() (gas: 4799473)\n[PASS] test_largeAdmissionQueueMakesProgressWithinGasBudget() (gas: 468123702)\nLogs:\n  largest admission/reveal step gas (including call overhead): 3466561\n\n[PASS] test_partialAdmissionPreservesDrawAndExcludesSameBlockAndLaterDeposits() (gas: 25071279)\n[PASS] test_pendingRerollBlocksApprovedOperatorsUntilExpiry() (gas: 1668950)\n[PASS] test_pendingRerollCannotBeSoldWithOldTraits() (gas: 802202)\n[PASS] test_safeTransferToVaultIsRejected() (gas: 531345)\n[PASS] test_sameBlockHandInsDoNotCountForPullsMadeNow() (gas: 275287704)\n[PASS] test_setupRefusesUnconfiguredLiquidityBeforeCommitting() (gas: 4689583)\n[PASS] test_setupRejectsLiquidityWithoutCode() (gas: 291131)\n[PASS] test_uninitializedPoolCannotSetPermanentFloor() (gas: 4893464)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 566.23ms (829.64ms CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantsTest\n[PASS]\nVaultInvariantsTest invariants:\n[PASS] invariant_agentBalancesMatch\n[PASS] invariant_capAndSlots\n[PASS] invariant_custody\n[PASS] invariant_feesToLiquidity\n[PASS] invariant_fullyBacked\n[PASS] invariant_inkConserved\n[PASS] invariant_queueCovered\n VaultInvariantsTest invariants (runs: 64, calls: 7680, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | claim            | 576   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | deposit          | 547   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | moveSeat         | 553   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pull             | 541   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullAll          | 549   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullTooMany      | 554   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | release          | 511   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | rerollTraits     | 580   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | reveal           | 569   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealNext       | 517   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealTraits     | 525   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | roll             | 577   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | setFees          | 552   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | transferPortrait | 529   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.80s (9.79s CPU time)\n\nRan 18 test suites in 9.80s (10.89s CPU time): 106 tests passed, 0 failed, 28 skipped (134 total tests)\n","passed":true},{"durationMs":187,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"InkFeeRouter.claimAndRoute(address,uint64)\",\"InkFeeRouter.route()\",\"InkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"InkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"InkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"InkHook.collectFees()\",\"InkHook.unlockCallback(bytes)\",\"InkLiquidity.acceptOwnership()\",\"InkLiquidity.addLiquidity()\",\"InkLiquidity.collectFees(uint256,uint256)\",\"InkLiquidity.renounceOwnership()\",\"InkLiquidity.setPool((address,address,uint24,int24,address),address)\",\"InkLiquidity.setPremium(int24)\",\"InkLiquidity.setWidth(int24)\",\"InkLiquidity.transferOwnership(address)\",\"InkPortraits.acceptOwnership()\",\"InkPortraits.approve(address,uint256)\",\"InkPortraits.lockRenderer()\",\"InkPortraits.mintAgent(uint256,address,(uint32,uint16,uint32,uint32,bool))\",\"InkPortraits.mintSwarm(address,bytes32)\",\"InkPortraits.renounceOwnership()\",\"InkPortraits.safeTransferFrom(address,address,uint256)\",\"InkPortraits.safeTransferFrom(address,address,uint256,bytes)\",\"InkPortraits.setApprovalForAll(address,bool)\",\"InkPortraits.setContractURI(string)\",\"InkPortraits.setRenderer(address)\",\"InkPortraits.setSwarmSeed(uint256,bytes32)\",\"InkPortraits.setVault(address)\",\"InkPortraits.transferFrom(address,address,uint256)\",\"InkPortraits.transferOwnership(address)\",\"InkRenderer.acceptOwnership()\",\"InkRenderer.addChunk(bytes)\",\"InkRenderer.lock()\",\"InkRenderer.renounceOwnership()\",\"InkRenderer.setPreviewBase(string)\",\"InkRenderer.transferOwnership(address)\",\"InkVault.acceptOwnership()\",\"InkVault.claim(uint256,(uint32,uint16,uint32,uint32,bool),bytes32[])\",\"InkVault.deposit(uint256[],uint256)\",\"InkVault.pull(uint256)\",\"InkVault.release()\",\"InkVault.renounceOwnership()\",\"InkVault.rerollTraits(uint256,uint256)\",\"InkVault.reveal(uint256)\",\"InkVault.revealNext(uint256)\",\"InkVault.revealTraits(uint256)\",\"InkVault.setFees(uint16,uint16)\",\"InkVault.setInk(address)\",\"InkVault.setup(bytes32,uint64,address)\",\"InkVault.transferOwnership(address)\",\"TestSeats.approve(address,uint256)\",\"TestSeats.mint(uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256,bytes)\",\"TestSeats.setApprovalForAll(address,bool)\",\"TestSeats.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"ADAPTATION.md\":303,\"AUDIT_BRIEF.md\":71,\"BYTECODE_HASHES.json\":52,\"DEPENDENCIES.md\":10,\"PACKAGING.md\":16,\"README.md\":55,\"SETUP.md\":101,\"SHA256SUMS\":1640,\"SOURCE_PROVENANCE.json\":11377,\"VALIDATION.txt\":554,\"art/original-art.js\":418,\"foundry.toml\":33,\"launch/README.md\":35,\"launch/mainnet-1-contracts.json\":24,\"launch/mainnet-2-hook.json\":31,\"launch/sepolia-1-contracts.json\":24,\"launch/sepolia-2-hook.json\":31,\"remappings.txt\":8,\"script/Configure.s.sol\":63,\"script/InkSteps.sol\":135,\"script/LocalStack.s.sol\":135,\"script/Rehearse.s.sol\":236,\"script/UploadRenderer.s.sol\":33,\"src/InkFeeRouter.sol\":77,\"src/InkHook.sol\":257,\"src/InkLiquidity.sol\":335,\"src/InkPortraits.sol\":271,\"src/InkRenderer.sol\":311,\"src/InkVault.sol\":503,\"src/testnet/TestSeats.sol\":13,\"test/AuditRegression.t.sol\":476,\"test/InkRenderer.t.sol\":210,\"test/InkSystem.t.sol\":1397,\"test/LaunchDay.t.sol\":270,\"test/LaunchFeeHandoff.t.sol\":82,\"test/LaunchSafety.t.sol\":478,\"test/MainnetContractsLaunch.t.sol\":159,\"test/MainnetFork.t.sol\":519,\"test/RangeMath.t.sol\":80,\"test/SepoliaLaunch.t.sol\":186,\"test/SnapshotAllSeats.t.sol\":159,\"test/SnapshotProofs.t.sol\":91,\"test/SybilOptions.t.sol\":104,\"test/TokenURIPreview.t.sol\":79,\"test/VaultInvariants.t.sol\":403,\"test/fixtures/snapshot-sample.json\":98,\"test/fixtures/traits.json\":1,\"test/helpers/ConfiguredLiquidity.sol\":18},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":5852,"exitCode":0,"name":"slither","output":"[high/medium] weak-prng at src/InkVault.sol:312: InkVault._revealNext() (src/InkVault.sol#312-360) uses a weak PRNG: \"pick = r % avail (src/InkVault.sol#347)\"\n[medium/medium] divide-before-multiply at src/InkLiquidity.sol:330: InkLiquidity._floor(int24,int24) (src/InkLiquidity.sol#330-334) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/InkLiquidity.sol:261: InkLiquidity._rangeFrom(int24,int24) (src/InkLiquidity.sol#261-281) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/InkLiquidity.sol:164: InkLiquidity.addLiquidity() (src/InkLiquidity.sol#164-175) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/InkVault.sol:479: InkVault.waitingBefore() (src/InkVault.sol#479-482) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/InkVault.sol:384: InkVault.deposit(uint256[],uint256) (src/InkVault.sol#384-416) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/InkVault.sol:384: Reentrancy in InkVault.deposit(uint256[],uint256) (src/InkVault.sol#384-416):\n[medium/medium] uninitialized-local at src/InkLiquidity.sol:188: InkLiquidity._addLadder(int24,uint256).total (src/InkLiquidity.sol#188) is a local variable never initialized\n[medium/medium] uninitialized-local at src/InkRenderer.sol:117: InkRenderer.tokenJSON(uint256).extra (src/InkRenderer.sol#117) is a local variable never initialized\n[medium/medium] uninitialized-local at src/InkVault.sol:397: InkVault.deposit(uint256[],uint256).fees (src/InkVault.sol#397) is a local variable never initialized\n[medium/medium] unused-return at src/InkFeeRouter.sol:54: InkFeeRouter.claimAndRoute(ILaunchFeeFactory,uint64) (src/InkFeeRouter.sol#54-61) ignores return value by factory.claimFees(launchNumber) (src/InkFeeRouter.sol#58)\n[medium/medium] unused-return at src/InkLiquidity.sol:164: InkLiquidity.addLiquidity() (src/InkLiquidity.sol#164-175) ignores return value by (None,tick,None,None) = poolManager.getSlot0(poolKey.toId()) (src/InkLiquidity.sol#170)\n[medium/medium] unused-return at src/InkVault.sol:231: InkVault.release() (src/InkVault.sol#231-243) ignores return value by IInkLiquidity(liquidity).addLiquidity() (src/InkVault.sol#240)\n[medium/medium] unused-return at src/InkPortraits.sol:114: InkPortraits.setRenderer(address) (src/InkPortraits.sol#114-122) ignores return value by IInkRendererBinding(renderer_).chunkCount() (src/InkPortraits.sol#118)\n[medium/medium] unused-return at src/InkLiquidity.sol:107: InkLiquidity.setPool(PoolKey,IERC20) (src/InkLiquidity.sol#107-121) ignores return value by (sqrtPriceX96,tick,None,None) = poolManager.getSlot0(key.toId()) (src/InkLiquidity.sol#111)\n[medium/high] write-after-write at src/InkRenderer.sol:243: InkRenderer.swarmLabels(string).u (src/InkRenderer.sol#243) is written in both\n[medium/high] write-after-write at src/InkRenderer.sol:243: InkRenderer.swarmLabels(string).u (src/InkRenderer.sol#243) is written in both\n[low/medium] missing-zero-check at src/InkPortraits.sol:114: InkPortraits.setRenderer(address).renderer_ (src/InkPortraits.sol#114) lacks a zero-check on :\n[low/medium] calls-loop at src/InkVault.sol:384: InkVault.deposit(uint256[],uint256) (src/InkVault.sol#384-416) has external calls inside a loop: portraits.isAgentPortrait(tokenId) (src/InkVault.sol#406-408)\n[low/medium] calls-loop at src/InkVault.sol:312: InkVault._revealNext() (src/InkVault.sol#312-360) has external calls inside a loop: portraits.transferFrom(address(this),p.to,tokenId) (src/InkVault.sol#353)\n[low/medium] calls-loop at src/InkVault.sol:254: InkVault.available() (src/InkVault.sol#254-256) has external calls inside a loop: _inventory.length + swarmSlots() - portraits.swarmMinted() (src/InkVault.sol#255)\n[low/medium] calls-loop at src/InkVault.sol:384: InkVault.deposit(uint256[],uint256) (src/InkVault.sol#384-416) has external calls inside a loop: portraits.transferFrom(msg.sender,address(this),tokenId) (src/InkVault.sol#403)\n[low/medium] calls-loop at src/InkLiquidity.sol:283: InkLiquidity._mintSingleSided(int24,int24,uint256) (src/InkLiquidity.sol#283-319) has external calls inside a loop: positionManager.modifyLiquidities(abi.encode(abi.encodePacked(uint8(Actions.MINT_POSITION),uint8(Actions.SETTLE),uint8(Actions.SWEEP)),params),block.timestamp) (src/InkLiquidity.sol#312-318)\n[low/medium] calls-loop at src/InkVault.sol:312: InkVault._revealNext() (src/InkVault.sol#312-360) has external calls inside a loop: tokenId_scope_0 = portraits.mintSwarm(p.to,keccak256(bytes)(abi.encode(entropy,pullId,seed))) (src/InkVault.sol#356)\n[low/medium] calls-loop at src/InkVault.sol:254: InkVault.available() (src/InkVault.sol#254-256) has external calls inside a loop: _inventory.length + swarmSlots() - portraits.swarmMinted() (src/InkVault.sol#255)\n[low/medium] calls-loop at src/InkVault.sol:312: InkVault._revealNext() (src/InkVault.sol#312-360) has external calls inside a loop: portraits.transferFrom(address(this),p.to,tokenId) (src/InkVault.sol#353)\n[low/medium] calls-loop at src/InkVault.sol:384: InkVault.deposit(uint256[],uint256) (src/InkVault.sol#384-416) has external calls inside a loop: discounted = agentHolder && portraits.heldFromVault(tokenId) (src/InkVault.sol#402)\n[low/medium] calls-loop at src/InkVault.sol:312: InkVault._revealNext() (src/InkVault.sol#312-360) has external calls inside a loop: tokenId_scope_0 = portraits.mintSwarm(p.to,keccak256(bytes)(abi.encode(entropy,pullId,seed))) (src/InkVault.sol#356)\n[low/medium] calls-loop at src/InkLiquidity.sol:283: InkLiquidity._mintSingleSided(int24,int24,uint256) (src/InkLiquidity.sol#283-319) has external calls inside a loop: positionId = positionManager.nextTokenId() (src/InkLiquidity.sol#296)\n[low/medium] calls-loop at src/InkVault.sol:248: InkVault.swarmSlots() (src/InkVault.sol#248-251) has external calls inside a loop: SEATS - portraits.agentMinted() (src/InkVault.sol#250)\n[low/medium] calls-loop at src/InkVault.sol:248: InkVault.swarmSlots() (src/InkVault.sol#248-251) has external calls inside a loop: SEATS - portraits.agentMinted() (src/InkVault.sol#250)\n[low/medium] reentrancy-benign at src/InkLiquidity.sol:216: Reentrancy in InkLiquidity._add(int24,int24,uint256) (src/InkLiquidity.sol#216-220):\n[low/medium] reentrancy-events at src/InkLiquidity.sol:216: Reentrancy in InkLiquidity._add(int24,int24,uint256) (src/InkLiquidity.sol#216-220):\n[low/medium] reentrancy-events at src/InkFeeRouter.sol:65: Reentrancy in InkFeeRouter.route() (src/InkFeeRouter.sol#65-76):\n[low/medium] reentrancy-events at src/InkHook.sol:190: Reentrancy in InkHook._payOut(Currency,address) (src/InkHook.sol#190-196):\n[low/medium] reentrancy-events at src/InkFeeRouter.sol:54: Reentrancy in InkFeeRouter.claimAndRoute(ILaunchFeeFactory,uint64) (src/InkFeeRouter.sol#54-61):\n[low/medium] reentrancy-events at src/InkFeeRouter.sol:54: Reentrancy in InkFeeRouter.claimAndRoute(ILaunchFeeFactory,uint64) (src/InkFeeRouter.sol#54-61):\n[low/medium] reentrancy-events at src/InkLiquidity.sol:225: Reentrancy in InkLiquidity.collectFees(uint256,uint256) (src/InkLiquidity.sol#225-252):\n[low/medium] reentrancy-events at src/InkHook.sol:154: Reentrancy in InkHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/InkHook.sol#154-176):\n[low/medium] timestamp at src/InkVault.sol:231: InkVault.release() (src/InkVault.sol#231-243) uses timestamp for comparisons\n[low/medium] timestamp at src/InkVault.sol:171: InkVault.setup(bytes32,uint64,address) (src/InkVault.sol#171-187) uses timestamp for comparisons\n[low/medium] timestamp at src/InkVault.sol:248: InkVault.swarmSlots() (src/InkVault.sol#248-251) uses timestamp for comparisons\n[low/medium] timestamp at src/InkHook.sol:137: InkHook.tradingOpen() (src/InkHook.sol#137-139) uses timestamp for comparisons\n[low/medium] timestamp at src/InkVault.sol:203: InkVault.claimOpen() (src/InkVault.sol#203-205) uses timestamp for comparisons\n[low/medium] timestamp at src/InkHook.sol:141: InkHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/InkHook.sol#141-152) uses timestamp for comparisons","passed":true},{"durationMs":919,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/InkRenderer.sol:58: `abi.encodePacked()` Hash Collision (4 places)\n[high] reentrancy-state-change at src/InkHook.sol:147: Reentrancy: State change after external call (15 places)\n[high] unsafe-casting at src/InkLiquidity.sol:304: Unsafe Casting of integers (2 places)\n[low] centralization-risk at src/InkLiquidity.sol:35: Centralization Risk (21 places)\n[low] costly-loop at src/InkLiquidity.sol:207: Costly operations inside loop (6 places)\n[low] large-numeric-literal at src/InkHook.sol:169: Large Numeric Literal (7 places)\n[low] literal-instead-of-constant at src/InkPortraits.sol:157: Literal Instead of Constant (36 places)\n[low] require-revert-in-loop at src/InkLiquidity.sol:190: Loop Contains `require`/`revert` (5 places)\n[low] state-no-address-check at src/InkLiquidity.sol:116: Address State Variable Set Without Checks (2 places)\n[low] state-variable-could-be-constant at src/InkRenderer.sol:21: State Variable Could Be Constant\n[low] storage-array-length-not-cached at src/InkRenderer.sol:94: Storage Array Length not Cached\n[low] unchecked-return at src/InkFeeRouter.sol:58: Unchecked Return (6 places)\n[low] uninitialized-local-variable at src/InkLiquidity.sol:207: Uninitialized Local Variable (7 places)\n[low] unsafe-oz-erc721-mint at src/InkPortraits.sol:162: Unsafe `ERC721::_mint()` (3 places)\n[low] unused-public-function at src/InkHook.sol:137: Public Function Not Used Internally","passed":true},{"durationMs":3208,"exitCode":0,"name":"proof 16c7464acc9e","output":"Compiling 104 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.32s\nCompiler run successful with warnings:\nWarning (3149): The result type of the shift operation is equal to the type of the first operand (uint128) ignoring the (larger) type of the second operand (uint256) which might be unexpected. Silence this warning by either converting the first or the second operand to the type of the other.\n   --> src/InkLiquidity.sol:196:31:\n    |\n196 |             uint128 unitLiq = uint128(1e24) << k;\n    |                               ^^^^^^^^^^^^^^^^^^\n\n\nRan 1 test for test/imd-proof-81f6f0a6/Proof_16c7464acc9e.t.sol:EconTest\n[PASS] test_nonHolderGetsHolderRateThroughProxy() (gas: 772791)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.21ms (198.36µs CPU time)\n\nRan 1 test suite in 4.08ms (1.21ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"848f6472ed36d8acb118e43f4a326466f9fb69c69cd4bee2338f7cff479e06de","verifiedTreeHash":"7929e7e0f742ed941a37906bfbe23a8629e98bdd","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":13823,"exitCode":0,"name":"build","output":"Compiling 182 files with Solc 0.8.26\nSolc 0.8.26 finished in 13.62s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:26:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                          ^^^^^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:38:5:\n   |\n38 |     InkPortraits portraits;\n   |     ^^^^^^^^^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:50:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                                                  ^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:39:5:\n   |\n39 |     InkVault vault;\n   |     ^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:20:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                    ^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:44:5:\n   |\n44 |     PoolKey key;\n   |     ^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:65:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                                                                 ^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:43:5:\n   |\n43 |     ForkInk ink;\n   |     ^^^^^^^^^^^\n\nWarning (3149): The result type of the shift operation is equal to the type of the first operand (uint128) ignoring the (larger) type of the second operand (uint256) which might be unexpected. Silence this warning by either converting the first or the second operand to the type of the other.\n   --> src/InkLiquidity.sol:189:31:\n    |\n189 |             uint128 unitLiq = uint128(1e24) << k;\n    |                               ^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":9050,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/LaunchFeeHandoff.t.sol:LaunchFeeHandoffTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 287.50µs (0.00ns CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium5xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 661.01µs (36.47µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium1xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 821.28µs (24.32µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium2xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 655.86µs (34.95µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs0Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 934.69µs (183.62µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs1Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 935.88µs (211.94µs CPU time)\n\nRan 1 test for test/LaunchDay.t.sol:LaunchDayTest\n[SKIP] test_fork_launchDay() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 1.12ms (22.85µs CPU time)\n\nRan 2 tests for test/SnapshotProofs.t.sol:SnapshotProofsTest\n[PASS] test_realSnapshotProofsClaim() (gas: 3221612)\n[PASS] test_tamperedStatsAreRejected() (gas: 605207)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.20ms (2.84ms CPU time)\n\nRan 1 test for test/TokenURIPreview.t.sol:TokenURIPreviewTest\n[PASS] test_writeTokenURIs() (gas: 25400631)\nLogs:\n  read-only profile: not written out-preview/agent-280.txt (use FOUNDRY_PROFILE=preview)\n  read-only profile: not written out-preview/pool.txt (use FOUNDRY_PROFILE=preview)\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 33.35ms (33.18ms CPU time)\n\nRan 52 tests for test/InkSystem.t.sol:InkSystemTest\n[PASS] test_agentClaimsOwnPortraitFree() (gas: 311054)\n[PASS] test_agentDepositReturnsExactlyItsBacking() (gas: 3486647)\n[PASS] test_agentHoldersPayNoHookFee() (gas: 2732536)\n[PASS] test_agentHoldersRerollForHalf() (gas: 4162562)\n[PASS] test_borrowedAgentPortraitGetsNoWaiver() (gas: 2553565)\n[PASS] test_cantLockAnEmptyRenderer() (gas: 3095036)\n[PASS] test_claimRules() (gas: 464993)\n[PASS] test_collectFeesPaysImdToTreasuryAndKeepsInk() (gas: 3365783)\n[PASS] test_contractURIOwnerOnly() (gas: 94674)\n[PASS] test_contractWalletSeatCanClaim() (gas: 387125)\n[PASS] test_depositMinOut() (gas: 3012131)\n[PASS] test_depositedAgentPortraitIsPullable() (gas: 3343364)\n[PASS] test_feeRouterClaimAndRoute() (gas: 4374498)\n[PASS] test_feeRouterNeedsPoolAndAddresses() (gas: 3741402)\n[PASS] test_feeRouterSplitsAndCompounds() (gas: 4120806)\n[PASS] test_gasAtScale() (gas: 68506639)\nLogs:\n  pull(1) gas: 160960\n  revealNext(1) gas: 198923\n    per reveal: 198923\n  pull(10) gas: 548188\n  revealNext(10) gas: 916308\n    per reveal: 91630\n  pull(100) gas: 4762626\n  revealNext(100) gas: 8605657\n    per reveal: 86056\n  pull(400) gas: 18812820\n  revealNext(400) gas: 34269741\n    per reveal: 85674\n  deposit(100 portraits) gas: 6699209\n\n[PASS] test_guards() (gas: 12196889)\n[PASS] test_hookFeeImdToTreasury() (gas: 2209441)\n[PASS] test_hookFeeInkGoesToLiquidity() (gas: 2700478)\n[PASS] test_idleSeatClaimsToo() (gas: 226980)\n[PASS] test_inkFeesGoBackIntoThePool() (gas: 6054706)\n[PASS] test_launchSplit() (gas: 67288)\n[PASS] test_liquidityCannotRenounceBeforePoolIsConfigured() (gas: 2949296)\n[PASS] test_liquidityFloorHoldsIfPriceIsDragged() (gas: 6479873)\n[PASS] test_liquidityRefusesUnrelatedNFTsButKeepsPositionCustody() (gas: 2105371)\n[PASS] test_mainnetPoolMustPairWithOfficialIMD() (gas: 2960165)\n[PASS] test_noPerWalletLimitAfterRelease() (gas: 2123275)\n[PASS] test_noPullsUntilRelease() (gas: 2206824)\n[PASS] test_onlyOnePool() (gas: 46344)\n[PASS] test_ownerIsOwnerWalletNotDeployer() (gas: 7709584)\n[PASS] test_permissionlessListingTracksSpotAboveReleaseFloor() (gas: 3359704)\n[PASS] test_poolLockedUntilRelease() (gas: 2617053)\n[PASS] test_poolNeverOpensWithoutRelease() (gas: 2475857)\n[PASS] test_premiumBeforePoolCannotBeFrontRun() (gas: 5810042)\n[PASS] test_pullRerollPullAgain() (gas: 3869727)\n[PASS] test_releaseLadderShape() (gas: 2318478)\n[PASS] test_releasePremiumListsAboveTheOpeningPrice() (gas: 1923943)\n[PASS] test_releaseTurnsUnclaimedSeatsIntoSlotsAndPoolInk() (gas: 2072629)\n[PASS] test_revealIsNotEarlyAndNotTwice() (gas: 2601471)\n[PASS] test_seatSoldDuringWindowNewOwnerClaims() (gas: 301319)\n[PASS] test_setPoolRejectsHooklessPoolAndRemainsCorrectable() (gas: 3003675)\n[PASS] test_setPoolRejectsInconsistentHookBindings() (gas: 6352403)\n[PASS] test_setPoolRequiresPositionManagerBoundToSameManager() (gas: 2982911)\n[PASS] test_setupRules() (gas: 3062176)\n[PASS] test_setupStartMustBeSoon() (gas: 3051521)\n[PASS] test_traitRerollBeforeReleaseReverts() (gas: 39963)\n[PASS] test_traitRerollExpiredHashClosesWithoutFreeDraw() (gas: 2945657)\n[PASS] test_traitRerollGuards() (gas: 3434055)\n[PASS] test_traitRerollHalfPriceForAgentHolders() (gas: 3104372)\n[PASS] test_traitRerollRepaintsSameToken() (gas: 3006188)\n[PASS] test_vaultStaysFullyBacked() (gas: 3919869)\n[PASS] test_zeroTradingFee() (gas: 29277)\nSuite result: ok. 52 passed; 0 failed; 0 skipped; finished in 113.83ms (144.13ms CPU time)\n\nRan 6 tests for test/InkRenderer.t.sol:InkRendererTest\n[PASS] test_agentTraitsMatchTheScript() (gas: 61685138)\n[PASS] test_noClaimsAfterTheFirstSwarmPortrait() (gas: 1375327)\n[PASS] test_poolTraitsMatchTheScript() (gas: 60520738)\n[PASS] test_scriptStoredInChunksAndLocked() (gas: 12171384)\n[PASS] test_swarmNumbersFillUnclaimedSeats() (gas: 163195398)\n[PASS] test_tokenURIForAnAgentPortrait() (gas: 3688020)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 153.45ms (368.40ms CPU time)\n\nRan 4 tests for test/SepoliaLaunch.t.sol:SepoliaLaunchTest\n[PASS] test_allFiveRuntimesMeetProtectedFloor() (gas: 17925603)\n[PASS] test_existingOwnerSetupRemainsRequiredAndAvailable() (gas: 441184)\n[PASS] test_factoryDeploymentPreservesOwnersRoyaltiesAndReferences() (gas: 183956)\n[PASS] test_factoryTreasuryAndOutsiderHaveNoAdminRole() (gas: 1700968)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 166.06ms (29.96ms CPU time)\n\nRan 12 tests for test/LaunchSafety.t.sol:LaunchSafetyTest\n[PASS] testFuzz_directPortraitTransfersCannotBypassDeposit(bool,uint8) (runs: 256, μ: 621207, ~: 835617)\n[PASS] testFuzz_expiredPullChargesPurchaseFeeAndPreservesBacking(uint16,uint16,bool) (runs: 256, μ: 563096, ~: 567193)\n[PASS] test_cannotRenounceBeforeRequiredConfiguration() (gas: 472291)\n[PASS] test_expiredBatchRespectsProcessingLimit() (gas: 691260)\n[PASS] test_expiredHeadCannotStallLaterLivePull() (gas: 777326)\n[PASS] test_expiredTraitRerollConsumesFeeAndRequiresNewPayment() (gas: 949557)\n[PASS] test_portraitsCannotBeSentToLiquidity() (gas: 563556)\n[PASS] test_pullLastAvailableBlockRevealsWithoutExpiryFee() (gas: 461789)\n[PASS] test_setInkRequiresCodeAndFullAllocationBeforeItCommits() (gas: 319425)\n[PASS] test_setupRejectsWrongTokenLiquidityAndRemainsCorrectable() (gas: 1228159)\n[PASS] test_traitRerollLastAvailableBlockUsesOriginalHash() (gas: 663662)\n[PASS] test_vaultAndRendererBindingFailuresRemainCorrectable() (gas: 433271)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 446.79ms (48.26ms CPU time)\n\nRan 9 tests for test/SnapshotAllSeats.t.sol:SnapshotAllSeatsTest\n[SKIP] test_claims_0000() (gas: 0)\n[SKIP] test_claims_0250() (gas: 0)\n[SKIP] test_claims_0500() (gas: 0)\n[SKIP] test_claims_0750() (gas: 0)\n[SKIP] test_claims_1000() (gas: 0)\n[SKIP] test_claims_1250() (gas: 0)\n[SKIP] test_claims_1500() (gas: 0)\n[SKIP] test_claims_1750() (gas: 0)\n[SKIP] test_treeHasEverySeatOnce() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 9 skipped; finished in 446.78ms (1.91ms CPU time)\n\nRan 3 tests for test/MainnetContractsLaunch.t.sol:MainnetContractsLaunchTest\n[PASS] test_exactFourContractOrderReferencesAndOwners() (gas: 221632)\n[PASS] test_factoryTreasuryAndOriginHaveNoAdministrativeRights() (gas: 1697579)\n[PASS] test_runtimeSizeAndOpcodesMeetProtectedFloor() (gas: 16134665)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 446.93ms (23.91ms CPU time)\n\nRan 15 tests for test/AuditRegression.t.sol:AuditRegressionTest\n[PASS] testFuzz_admissionCompletesWithBothRevealEntryPoints(uint16,bool) (runs: 256, μ: 25194521, ~: 21556246)\n[PASS] test_directSafeTransfersRejectedForBothOverloadsAndOtherNFTs() (gas: 875044)\n[PASS] test_expiredHeadPullNeverWidensALaterPullsDraw() (gas: 2124372)\n[PASS] test_expiredPullsCloseWithoutWideningDrawsOrRenewing() (gas: 1414489)\n[PASS] test_hashExpiryDuringAdmissionKeepsOriginalDraw() (gas: 43216044)\n[PASS] test_initializedPoolAtZeroTickIsValid() (gas: 4799316)\n[PASS] test_largeAdmissionQueueMakesProgressWithinGasBudget() (gas: 423679832)\nLogs:\n  largest admission/reveal step gas (including call overhead): 3464062\n\n[PASS] test_partialAdmissionPreservesDrawAndExcludesSameBlockAndLaterDeposits() (gas: 22742217)\n[PASS] test_pendingRerollBlocksApprovedOperatorsUntilExpiry() (gas: 1643180)\n[PASS] test_pendingRerollCannotBeSoldWithOldTraits() (gas: 776696)\n[PASS] test_safeTransferToVaultIsRejected() (gas: 508769)\n[PASS] test_sameBlockHandInsDoNotCountForPullsMadeNow() (gas: 235863254)\n[PASS] test_setupRefusesUnconfiguredLiquidityBeforeCommitting() (gas: 4689582)\n[PASS] test_setupRejectsLiquidityWithoutCode() (gas: 290820)\n[PASS] test_uninitializedPoolCannotSetPermanentFloor() (gas: 4893283)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 457.44ms (736.59ms CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantsTest\n[PASS]\nVaultInvariantsTest invariants:\n[PASS] invariant_agentBalancesMatch\n[PASS] invariant_capAndSlots\n[PASS] invariant_custody\n[PASS] invariant_feesToLiquidity\n[PASS] invariant_fullyBacked\n[PASS] invariant_inkConserved\n[PASS] invariant_queueCovered\n VaultInvariantsTest invariants (runs: 64, calls: 7680, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | claim            | 576   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | deposit          | 547   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | moveSeat         | 553   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pull             | 541   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullAll          | 549   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullTooMany      | 554   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | release          | 511   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | rerollTraits     | 580   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | reveal           | 569   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealNext       | 517   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealTraits     | 525   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | roll             | 577   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | setFees          | 552   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | transferPortrait | 529   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 8.83s (8.83s CPU time)\n\nRan 17 test suites in 8.84s (11.11s CPU time): 96 tests passed, 0 failed, 28 skipped (124 total tests)\n","passed":true},{"durationMs":173,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"InkFeeRouter.claimAndRoute(address,uint64)\",\"InkFeeRouter.route()\",\"InkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"InkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"InkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"InkHook.collectFees()\",\"InkHook.unlockCallback(bytes)\",\"InkLiquidity.addLiquidity()\",\"InkLiquidity.collectFees(uint256,uint256)\",\"InkLiquidity.renounceOwnership()\",\"InkLiquidity.setPool((address,address,uint24,int24,address),address)\",\"InkLiquidity.setPremium(int24)\",\"InkLiquidity.setWidth(int24)\",\"InkLiquidity.transferOwnership(address)\",\"InkPortraits.approve(address,uint256)\",\"InkPortraits.lockRenderer()\",\"InkPortraits.mintAgent(uint256,address,(uint32,uint16,uint32,uint32,bool))\",\"InkPortraits.mintSwarm(address,bytes32)\",\"InkPortraits.renounceOwnership()\",\"InkPortraits.safeTransferFrom(address,address,uint256)\",\"InkPortraits.safeTransferFrom(address,address,uint256,bytes)\",\"InkPortraits.setApprovalForAll(address,bool)\",\"InkPortraits.setContractURI(string)\",\"InkPortraits.setRenderer(address)\",\"InkPortraits.setSwarmSeed(uint256,bytes32)\",\"InkPortraits.setVault(address)\",\"InkPortraits.transferFrom(address,address,uint256)\",\"InkPortraits.transferOwnership(address)\",\"InkRenderer.addChunk(bytes)\",\"InkRenderer.lock()\",\"InkRenderer.renounceOwnership()\",\"InkRenderer.setPreviewBase(string)\",\"InkRenderer.transferOwnership(address)\",\"InkVault.claim(uint256,(uint32,uint16,uint32,uint32,bool),bytes32[])\",\"InkVault.deposit(uint256[],uint256)\",\"InkVault.pull(uint256)\",\"InkVault.release()\",\"InkVault.renounceOwnership()\",\"InkVault.rerollTraits(uint256,uint256)\",\"InkVault.reveal(uint256)\",\"InkVault.revealNext(uint256)\",\"InkVault.revealTraits(uint256)\",\"InkVault.setFees(uint16,uint16)\",\"InkVault.setInk(address)\",\"InkVault.setup(bytes32,uint64,address)\",\"InkVault.transferOwnership(address)\",\"TestSeats.approve(address,uint256)\",\"TestSeats.mint(uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256,bytes)\",\"TestSeats.setApprovalForAll(address,bool)\",\"TestSeats.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"ADAPTATION.md\":204,\"AUDIT_BRIEF.md\":71,\"BYTECODE_HASHES.json\":52,\"DEPENDENCIES.md\":10,\"PACKAGING.md\":16,\"README.md\":55,\"SETUP.md\":101,\"SHA256SUMS\":1640,\"SOURCE_PROVENANCE.json\":11377,\"VALIDATION.txt\":554,\"art/original-art.js\":418,\"foundry.toml\":33,\"launch/README.md\":35,\"launch/mainnet-1-contracts.json\":24,\"launch/mainnet-2-hook.json\":31,\"launch/sepolia-1-contracts.json\":24,\"launch/sepolia-2-hook.json\":31,\"remappings.txt\":8,\"script/Configure.s.sol\":63,\"script/InkSteps.sol\":135,\"script/LocalStack.s.sol\":135,\"script/Rehearse.s.sol\":236,\"script/UploadRenderer.s.sol\":33,\"src/InkFeeRouter.sol\":77,\"src/InkHook.sol\":255,\"src/InkLiquidity.sol\":327,\"src/InkPortraits.sol\":256,\"src/InkRenderer.sol\":310,\"src/InkVault.sol\":492,\"src/testnet/TestSeats.sol\":13,\"test/AuditRegression.t.sol\":476,\"test/InkRenderer.t.sol\":210,\"test/InkSystem.t.sol\":1342,\"test/LaunchDay.t.sol\":270,\"test/LaunchFeeHandoff.t.sol\":82,\"test/LaunchSafety.t.sol\":330,\"test/MainnetContractsLaunch.t.sol\":159,\"test/MainnetFork.t.sol\":519,\"test/SepoliaLaunch.t.sol\":186,\"test/SnapshotAllSeats.t.sol\":159,\"test/SnapshotProofs.t.sol\":91,\"test/SybilOptions.t.sol\":104,\"test/TokenURIPreview.t.sol\":79,\"test/VaultInvariants.t.sol\":403,\"test/fixtures/snapshot-sample.json\":98,\"test/fixtures/traits.json\":1,\"test/helpers/ConfiguredLiquidity.sol\":18},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":5865,"exitCode":0,"name":"slither","output":"[high/medium] weak-prng at src/InkVault.sol:309: InkVault._revealNext() (src/InkVault.sol#309-357) uses a weak PRNG: \"pick = r % avail (src/InkVault.sol#344)\"\n[medium/medium] divide-before-multiply at src/InkLiquidity.sol:254: InkLiquidity._rangeFrom(int24,int24) (src/InkLiquidity.sol#254-273) performs a multiplication on the result of a division:\n[medium/medium] divide-before-multiply at src/InkLiquidity.sol:322: InkLiquidity._floor(int24,int24) (src/InkLiquidity.sol#322-326) performs a multiplication on the result of a division:\n[medium/high] incorrect-equality at src/InkLiquidity.sol:158: InkLiquidity.addLiquidity() (src/InkLiquidity.sol#158-168) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/InkVault.sol:468: InkVault.waitingBefore() (src/InkVault.sol#468-471) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/InkVault.sol:379: InkVault.deposit(uint256[],uint256) (src/InkVault.sol#379-409) uses a dangerous strict equality:\n[medium/medium] reentrancy-no-eth at src/InkVault.sol:379: Reentrancy in InkVault.deposit(uint256[],uint256) (src/InkVault.sol#379-409):\n[medium/medium] uninitialized-local at src/InkLiquidity.sol:181: InkLiquidity._addLadder(int24,uint256).total (src/InkLiquidity.sol#181) is a local variable never initialized\n[medium/medium] uninitialized-local at src/InkRenderer.sol:116: InkRenderer.tokenJSON(uint256).extra (src/InkRenderer.sol#116) is a local variable never initialized\n[medium/medium] uninitialized-local at src/InkVault.sol:392: InkVault.deposit(uint256[],uint256).fees (src/InkVault.sol#392) is a local variable never initialized\n[medium/medium] unused-return at src/InkPortraits.sol:109: InkPortraits.setRenderer(address) (src/InkPortraits.sol#109-117) ignores return value by IInkRendererBinding(renderer_).chunkCount() (src/InkPortraits.sol#113)\n[medium/medium] unused-return at src/InkFeeRouter.sol:54: InkFeeRouter.claimAndRoute(ILaunchFeeFactory,uint64) (src/InkFeeRouter.sol#54-61) ignores return value by factory.claimFees(launchNumber) (src/InkFeeRouter.sol#58)\n[medium/medium] unused-return at src/InkLiquidity.sol:158: InkLiquidity.addLiquidity() (src/InkLiquidity.sol#158-168) ignores return value by (None,tick,None,None) = poolManager.getSlot0(poolKey.toId()) (src/InkLiquidity.sol#163)\n[medium/medium] unused-return at src/InkLiquidity.sol:103: InkLiquidity.setPool(PoolKey,IERC20) (src/InkLiquidity.sol#103-116) ignores return value by (sqrtPriceX96,tick,None,None) = poolManager.getSlot0(key.toId()) (src/InkLiquidity.sol#107)\n[medium/medium] unused-return at src/InkVault.sol:228: InkVault.release() (src/InkVault.sol#228-240) ignores return value by IInkLiquidity(liquidity).addLiquidity() (src/InkVault.sol#237)\n[medium/high] write-after-write at src/InkRenderer.sol:242: InkRenderer.swarmLabels(string).u (src/InkRenderer.sol#242) is written in both\n[medium/high] write-after-write at src/InkRenderer.sol:242: InkRenderer.swarmLabels(string).u (src/InkRenderer.sol#242) is written in both\n[low/medium] missing-zero-check at src/InkPortraits.sol:109: InkPortraits.setRenderer(address).renderer_ (src/InkPortraits.sol#109) lacks a zero-check on :\n[low/medium] calls-loop at src/InkVault.sol:379: InkVault.deposit(uint256[],uint256) (src/InkVault.sol#379-409) has external calls inside a loop: portraits.isAgentPortrait(tokenId) (src/InkVault.sol#399-401)\n[low/medium] calls-loop at src/InkVault.sol:309: InkVault._revealNext() (src/InkVault.sol#309-357) has external calls inside a loop: portraits.transferFrom(address(this),p.to,tokenId) (src/InkVault.sol#350)\n[low/medium] calls-loop at src/InkVault.sol:309: InkVault._revealNext() (src/InkVault.sol#309-357) has external calls inside a loop: tokenId_scope_0 = portraits.mintSwarm(p.to,keccak256(bytes)(abi.encode(entropy,pullId,seed))) (src/InkVault.sol#353)\n[low/medium] calls-loop at src/InkVault.sol:379: InkVault.deposit(uint256[],uint256) (src/InkVault.sol#379-409) has external calls inside a loop: portraits.transferFrom(msg.sender,address(this),tokenId) (src/InkVault.sol#396)\n[low/medium] calls-loop at src/InkVault.sol:251: InkVault.available() (src/InkVault.sol#251-253) has external calls inside a loop: _inventory.length + swarmSlots() - portraits.swarmMinted() (src/InkVault.sol#252)\n[low/medium] calls-loop at src/InkVault.sol:245: InkVault.swarmSlots() (src/InkVault.sol#245-248) has external calls inside a loop: SEATS - portraits.agentMinted() (src/InkVault.sol#247)\n[low/medium] calls-loop at src/InkVault.sol:251: InkVault.available() (src/InkVault.sol#251-253) has external calls inside a loop: _inventory.length + swarmSlots() - portraits.swarmMinted() (src/InkVault.sol#252)\n[low/medium] calls-loop at src/InkVault.sol:245: InkVault.swarmSlots() (src/InkVault.sol#245-248) has external calls inside a loop: SEATS - portraits.agentMinted() (src/InkVault.sol#247)\n[low/medium] calls-loop at src/InkLiquidity.sol:275: InkLiquidity._mintSingleSided(int24,int24,uint256) (src/InkLiquidity.sol#275-311) has external calls inside a loop: positionManager.modifyLiquidities(abi.encode(abi.encodePacked(uint8(Actions.MINT_POSITION),uint8(Actions.SETTLE),uint8(Actions.SWEEP)),params),block.timestamp) (src/InkLiquidity.sol#304-310)\n[low/medium] calls-loop at src/InkVault.sol:309: InkVault._revealNext() (src/InkVault.sol#309-357) has external calls inside a loop: tokenId_scope_0 = portraits.mintSwarm(p.to,keccak256(bytes)(abi.encode(entropy,pullId,seed))) (src/InkVault.sol#353)\n[low/medium] calls-loop at src/InkVault.sol:309: InkVault._revealNext() (src/InkVault.sol#309-357) has external calls inside a loop: portraits.transferFrom(address(this),p.to,tokenId) (src/InkVault.sol#350)\n[low/medium] calls-loop at src/InkLiquidity.sol:275: InkLiquidity._mintSingleSided(int24,int24,uint256) (src/InkLiquidity.sol#275-311) has external calls inside a loop: positionId = positionManager.nextTokenId() (src/InkLiquidity.sol#288)\n[low/medium] reentrancy-benign at src/InkLiquidity.sol:209: Reentrancy in InkLiquidity._add(int24,int24,uint256) (src/InkLiquidity.sol#209-213):\n[low/medium] reentrancy-events at src/InkHook.sol:152: Reentrancy in InkHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/InkHook.sol#152-174):\n[low/medium] reentrancy-events at src/InkFeeRouter.sol:65: Reentrancy in InkFeeRouter.route() (src/InkFeeRouter.sol#65-76):\n[low/medium] reentrancy-events at src/InkLiquidity.sol:209: Reentrancy in InkLiquidity._add(int24,int24,uint256) (src/InkLiquidity.sol#209-213):\n[low/medium] reentrancy-events at src/InkFeeRouter.sol:54: Reentrancy in InkFeeRouter.claimAndRoute(ILaunchFeeFactory,uint64) (src/InkFeeRouter.sol#54-61):\n[low/medium] reentrancy-events at src/InkFeeRouter.sol:54: Reentrancy in InkFeeRouter.claimAndRoute(ILaunchFeeFactory,uint64) (src/InkFeeRouter.sol#54-61):\n[low/medium] reentrancy-events at src/InkHook.sol:188: Reentrancy in InkHook._payOut(Currency,address) (src/InkHook.sol#188-194):\n[low/medium] reentrancy-events at src/InkLiquidity.sol:218: Reentrancy in InkLiquidity.collectFees(uint256,uint256) (src/InkLiquidity.sol#218-245):\n[low/medium] timestamp at src/InkVault.sol:200: InkVault.claimOpen() (src/InkVault.sol#200-202) uses timestamp for comparisons\n[low/medium] timestamp at src/InkHook.sol:135: InkHook.tradingOpen() (src/InkHook.sol#135-137) uses timestamp for comparisons\n[low/medium] timestamp at src/InkHook.sol:139: InkHook.beforeSwap(address,PoolKey,SwapParams,bytes) (src/InkHook.sol#139-150) uses timestamp for comparisons\n[low/medium] timestamp at src/InkVault.sol:168: InkVault.setup(bytes32,uint64,address) (src/InkVault.sol#168-184) uses timestamp for comparisons\n[low/medium] timestamp at src/InkVault.sol:228: InkVault.release() (src/InkVault.sol#228-240) uses timestamp for comparisons\n[low/medium] timestamp at src/InkVault.sol:245: InkVault.swarmSlots() (src/InkVault.sol#245-248) uses timestamp for comparisons","passed":true},{"durationMs":1070,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/InkRenderer.sol:57: `abi.encodePacked()` Hash Collision (4 places)\n[high] reentrancy-state-change at src/InkHook.sol:145: Reentrancy: State change after external call (14 places)\n[high] unsafe-casting at src/InkLiquidity.sol:296: Unsafe Casting of integers (2 places)\n[low] centralization-risk at src/InkLiquidity.sol:34: Centralization Risk (21 places)\n[low] costly-loop at src/InkLiquidity.sol:200: Costly operations inside loop (6 places)\n[low] large-numeric-literal at src/InkHook.sol:167: Large Numeric Literal (7 places)\n[low] literal-instead-of-constant at src/InkPortraits.sol:152: Literal Instead of Constant (36 places)\n[low] require-revert-in-loop at src/InkLiquidity.sol:183: Loop Contains `require`/`revert` (5 places)\n[low] state-no-address-check at src/InkLiquidity.sol:112: Address State Variable Set Without Checks (2 places)\n[low] state-variable-could-be-constant at src/InkRenderer.sol:20: State Variable Could Be Constant\n[low] storage-array-length-not-cached at src/InkRenderer.sol:93: Storage Array Length not Cached\n[low] unchecked-return at src/InkFeeRouter.sol:58: Unchecked Return (6 places)\n[low] uninitialized-local-variable at src/InkLiquidity.sol:200: Uninitialized Local Variable (7 places)\n[low] unsafe-oz-erc721-mint at src/InkPortraits.sol:157: Unsafe `ERC721::_mint()` (3 places)\n[low] unused-public-function at src/InkHook.sol:135: Public Function Not Used Internally","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a77883f78d2f7c9392607aacf8a4a5626ec65ba2e4580c3cffffabeab3944199","verifiedTreeHash":"48b003eba5f20e8f41a72f173cc815b67c36f58b","verifierVersion":"0.1.0+6a374fcd"},{"checks":[{"durationMs":13302,"exitCode":0,"name":"build","output":"Compiling 184 files with Solc 0.8.26\nSolc 0.8.26 finished in 13.14s\nCompiler run successful with warnings:\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:26:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                          ^^^^^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:38:5:\n   |\n38 |     InkPortraits portraits;\n   |     ^^^^^^^^^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n  --> script/LocalStack.s.sol:95:50:\n   |\n95 |     function _deployHook(InkPortraits portraits, InkVault vault, address treasury) internal returns (InkHook) {\n   |                                                  ^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:39:5:\n   |\n39 |     InkVault vault;\n   |     ^^^^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:20:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                    ^^^^^^^^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:44:5:\n   |\n44 |     PoolKey key;\n   |     ^^^^^^^^^^^\n\nWarning (2519): This declaration shadows an existing declaration.\n   --> script/LocalStack.s.sol:119:65:\n    |\n119 |     function _seed(PoolKey memory key, bool inkIs0, address to, ForkInk ink) internal {\n    |                                                                 ^^^^^^^^^^^\nNote: The shadowed declaration is here:\n  --> script/LocalStack.s.sol:43:5:\n   |\n43 |     ForkInk ink;\n   |     ^^^^^^^^^^^\n\nWarning (3149): The result type of the shift operation is equal to the type of the first operand (uint128) ignoring the (larger) type of the second operand (uint256) which might be unexpected. Silence this warning by either converting the first or the second operand to the type of the other.\n   --> src/InkLiquidity.sol:196:31:\n    |\n196 |             uint128 unitLiq = uint128(1e24) << k;\n    |                               ^^^^^^^^^^^^^^^^^^\n\n","passed":true},{"durationMs":9159,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/LaunchFeeHandoff.t.sol:LaunchFeeHandoffTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 257.70µs (0.00ns CPU time)\n\nRan 9 tests for test/SnapshotAllSeats.t.sol:SnapshotAllSeatsTest\n[SKIP] test_claims_0000() (gas: 0)\n[SKIP] test_claims_0250() (gas: 0)\n[SKIP] test_claims_0500() (gas: 0)\n[SKIP] test_claims_0750() (gas: 0)\n[SKIP] test_claims_1000() (gas: 0)\n[SKIP] test_claims_1250() (gas: 0)\n[SKIP] test_claims_1500() (gas: 0)\n[SKIP] test_claims_1750() (gas: 0)\n[SKIP] test_treeHasEverySeatOnce() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 9 skipped; finished in 535.13µs (1.13ms CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium5xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 673.32µs (33.01µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium1xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 745.32µs (22.24µs CPU time)\n\nRan 1 test for test/LaunchDay.t.sol:LaunchDayTest\n[SKIP] test_fork_launchDay() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 738.73µs (22.30µs CPU time)\n\nRan 1 test for test/SybilOptions.t.sol:SybilPremium2xTest\n[SKIP] test_fork_sybilScenario() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 808.02µs (22.19µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs0Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 914.18µs (180.62µs CPU time)\n\nRan 7 tests for test/MainnetFork.t.sol:MainnetForkInkIs1Test\n[SKIP] test_fork_agentHolderPaysNoHookFee() (gas: 0)\n[SKIP] test_fork_bigBuyAfterRelease() (gas: 0)\n[SKIP] test_fork_fullLifecycle() (gas: 0)\n[SKIP] test_fork_hookFeeExactInBuyIsInkToLiquidity() (gas: 0)\n[SKIP] test_fork_lockedUntilReleaseHoweverLong() (gas: 0)\n[SKIP] test_fork_sellsPayFeesBothWays() (gas: 0)\n[SKIP] test_fork_sybilFirstHour() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 7 skipped; finished in 1.02ms (387.01µs CPU time)\n\nRan 2 tests for test/SnapshotProofs.t.sol:SnapshotProofsTest\n[PASS] test_realSnapshotProofsClaim() (gas: 3222192)\n[PASS] test_tamperedStatsAreRejected() (gas: 605318)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.39ms (3.75ms CPU time)\n\nRan 4 tests for test/RangeMath.t.sol:RangeMathTest\n[PASS] testFuzz_rangeNeverRevertsAndStaysUsable(int24,int24,bool) (runs: 256, μ: 118301, ~: 118435)\n[PASS] test_maxWidthClampsWhenInkIsCurrency0() (gas: 131887)\n[PASS] test_maxWidthClampsWhenInkIsCurrency1() (gas: 115332)\n[PASS] test_ordinaryWidthUnchanged() (gas: 118547)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 24.74ms (24.61ms CPU time)\n\nRan 1 test for test/TokenURIPreview.t.sol:TokenURIPreviewTest\n[PASS] test_writeTokenURIs() (gas: 25423286)\nLogs:\n  read-only profile: not written out-preview/agent-280.txt (use FOUNDRY_PROFILE=preview)\n  read-only profile: not written out-preview/pool.txt (use FOUNDRY_PROFILE=preview)\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 37.30ms (37.06ms CPU time)\n\nRan 4 tests for test/SepoliaLaunch.t.sol:SepoliaLaunchTest\n[PASS] test_allFiveRuntimesMeetProtectedFloor() (gas: 18566852)\n[PASS] test_existingOwnerSetupRemainsRequiredAndAvailable() (gas: 441282)\n[PASS] test_factoryDeploymentPreservesOwnersRoyaltiesAndReferences() (gas: 183938)\n[PASS] test_factoryTreasuryAndOutsiderHaveNoAdminRole() (gas: 1701232)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 37.33ms (37.62ms CPU time)\n\nRan 6 tests for test/InkRenderer.t.sol:InkRendererTest\n[PASS] test_agentTraitsMatchTheScript() (gas: 61685138)\n[PASS] test_noClaimsAfterTheFirstSwarmPortrait() (gas: 1397672)\n[PASS] test_poolTraitsMatchTheScript() (gas: 60520738)\n[PASS] test_scriptStoredInChunksAndLocked() (gas: 12171384)\n[PASS] test_swarmNumbersFillUnclaimedSeats() (gas: 163375864)\n[PASS] test_tokenURIForAnAgentPortrait() (gas: 3733117)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 124.55ms (307.47ms CPU time)\n\nRan 3 tests for test/MainnetContractsLaunch.t.sol:MainnetContractsLaunchTest\n[PASS] test_exactFourContractOrderReferencesAndOwners() (gas: 221680)\n[PASS] test_factoryTreasuryAndOriginHaveNoAdministrativeRights() (gas: 1697915)\n[PASS] test_runtimeSizeAndOpcodesMeetProtectedFloor() (gas: 16755780)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 124.56ms (27.18ms CPU time)\n\nRan 54 tests for test/InkSystem.t.sol:InkSystemTest\n[PASS] test_agentClaimsOwnPortraitFree() (gas: 311177)\n[PASS] test_agentDepositReturnsExactlyItsBacking() (gas: 3516171)\n[PASS] test_agentHoldersPayNoHookFee() (gas: 2734016)\n[PASS] test_agentHoldersRerollForHalf() (gas: 4215016)\n[PASS] test_borrowedAgentPortraitGetsNoWaiver() (gas: 2555057)\n[PASS] test_cantLockAnEmptyRenderer() (gas: 3138116)\n[PASS] test_claimRules() (gas: 465550)\n[PASS] test_collectFeesPaysImdToTreasuryAndKeepsInk() (gas: 3367122)\n[PASS] test_contractURIOwnerOnly() (gas: 94718)\n[PASS] test_contractWalletSeatCanClaim() (gas: 387248)\n[PASS] test_depositMinOut() (gas: 3038421)\n[PASS] test_depositedAgentPortraitIsPullable() (gas: 3344958)\n[PASS] test_feeRouterClaimAndRoute() (gas: 4381579)\n[PASS] test_feeRouterNeedsPoolAndAddresses() (gas: 3872711)\n[PASS] test_feeRouterSplitsAndCompounds() (gas: 4127736)\n[PASS] test_gasAtScale() (gas: 78817128)\nLogs:\n  pull(1) gas: 161004\n  revealNext(1) gas: 221322\n    per reveal: 221322\n  pull(10) gas: 548232\n  revealNext(10) gas: 1140496\n    per reveal: 114049\n  pull(100) gas: 4762670\n  revealNext(100) gas: 10847735\n    per reveal: 108477\n  pull(400) gas: 18812864\n  revealNext(400) gas: 43238119\n    per reveal: 108095\n  deposit(100 portraits) gas: 6950387\n\n[PASS] test_guards() (gas: 12434326)\n[PASS] test_hookFeeImdToTreasury() (gas: 2210776)\n[PASS] test_hookFeeInkGoesToLiquidity() (gas: 2701835)\n[PASS] test_idleSeatClaimsToo() (gas: 227103)\n[PASS] test_inkFeesGoBackIntoThePool() (gas: 6092386)\n[PASS] test_launchSplit() (gas: 67244)\n[PASS] test_liquidityCannotRenounceBeforePoolIsConfigured() (gas: 3071191)\n[PASS] test_liquidityFloorHoldsIfPriceIsDragged() (gas: 6606864)\n[PASS] test_liquidityOwnershipTransferRequiresAcceptance() (gas: 222592)\n[PASS] test_liquidityRefusesUnrelatedNFTsButKeepsPositionCustody() (gas: 2106517)\n[PASS] test_mainnetPoolMustPairWithOfficialIMD() (gas: 3079926)\n[PASS] test_noPerWalletLimitAfterRelease() (gas: 2124632)\n[PASS] test_noPullsUntilRelease() (gas: 2230624)\n[PASS] test_onlyOnePool() (gas: 46322)\n[PASS] test_ownerIsOwnerWalletNotDeployer() (gas: 7990860)\n[PASS] test_permissionlessListingTracksSpotAboveReleaseFloor() (gas: 3372258)\n[PASS] test_poolLockedUntilRelease() (gas: 2618410)\n[PASS] test_poolNeverOpensWithoutRelease() (gas: 2477214)\n[PASS] test_premiumBeforePoolCannotBeFrontRun() (gas: 6049546)\n[PASS] test_premiumCannotBeLockedByListingBeforeRelease() (gas: 6665678)\n[PASS] test_pullRerollPullAgain() (gas: 3918693)\n[PASS] test_releaseLadderShape() (gas: 2319548)\n[PASS] test_releasePremiumListsAboveTheOpeningPrice() (gas: 1925754)\n[PASS] test_releaseTurnsUnclaimedSeatsIntoSlotsAndPoolInk() (gas: 2074111)\n[PASS] test_revealIsNotEarlyAndNotTwice() (gas: 2625294)\n[PASS] test_seatSoldDuringWindowNewOwnerClaims() (gas: 301442)\n[PASS] test_setPoolRejectsHooklessPoolAndRemainsCorrectable() (gas: 3123415)\n[PASS] test_setPoolRejectsInconsistentHookBindings() (gas: 6644544)\n[PASS] test_setPoolRequiresPositionManagerBoundToSameManager() (gas: 3102635)\n[PASS] test_setupRules() (gas: 3137736)\n[PASS] test_setupStartMustBeSoon() (gas: 3127148)\n[PASS] test_traitRerollBeforeReleaseReverts() (gas: 39919)\n[PASS] test_traitRerollExpiredHashClosesWithoutFreeDraw() (gas: 2972065)\n[PASS] test_traitRerollGuards() (gas: 3463153)\n[PASS] test_traitRerollHalfPriceForAgentHolders() (gas: 3130903)\n[PASS] test_traitRerollRepaintsSameToken() (gas: 3032596)\n[PASS] test_vaultStaysFullyBacked() (gas: 3972012)\n[PASS] test_zeroTradingFee() (gas: 29321)\nSuite result: ok. 54 passed; 0 failed; 0 skipped; finished in 429.52ms (136.14ms CPU time)\n\nRan 16 tests for test/LaunchSafety.t.sol:LaunchSafetyTest\n[PASS] testFuzz_directPortraitTransfersCannotBypassDeposit(bool,uint8) (runs: 256, μ: 635495, ~: 860773)\n[PASS] testFuzz_expiredPullChargesPurchaseFeeAndPreservesBacking(uint16,uint16,bool) (runs: 256, μ: 563162, ~: 567514)\n[PASS] test_agentHolderDiscountOnlyOnOwnPullsStillHeld() (gas: 3268385)\n[PASS] test_cannotRenounceBeforeRequiredConfiguration() (gas: 478947)\n[PASS] test_expiredBatchRespectsProcessingLimit() (gas: 691194)\n[PASS] test_expiredHeadCannotStallLaterLivePull() (gas: 800011)\n[PASS] test_expiredTraitRerollConsumesFeeAndRequiresNewPayment() (gas: 977128)\n[PASS] test_nonHolderPaysFullFeeThroughAgentHoldingContract() (gas: 1368938)\n[PASS] test_ownershipTransferRequiresAcceptance() (gas: 640058)\n[PASS] test_portraitsCannotBeSentToLiquidity() (gas: 586110)\n[PASS] test_pullLastAvailableBlockRevealsWithoutExpiryFee() (gas: 484320)\n[PASS] test_setInkRequiresCodeAndFullAllocationBeforeItCommits() (gas: 319403)\n[PASS] test_setupRejectsWrongTokenLiquidityAndRemainsCorrectable() (gas: 1228493)\n[PASS] test_traitRerollLastAvailableBlockUsesOriginalHash() (gas: 688736)\n[PASS] test_traitRerollThroughAgentHoldingContractPaysFullFee() (gas: 1216106)\n[PASS] test_vaultAndRendererBindingFailuresRemainCorrectable() (gas: 433271)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 429.59ms (58.48ms CPU time)\n\nRan 15 tests for test/AuditRegression.t.sol:AuditRegressionTest\n[PASS] testFuzz_admissionCompletesWithBothRevealEntryPoints(uint16,bool) (runs: 256, μ: 27820664, ~: 24304999)\n[PASS] test_directSafeTransfersRejectedForBothOverloadsAndOtherNFTs() (gas: 900066)\n[PASS] test_expiredHeadPullNeverWidensALaterPullsDraw() (gas: 2219191)\n[PASS] test_expiredPullsCloseWithoutWideningDrawsOrRenewing() (gas: 1439598)\n[PASS] test_hashExpiryDuringAdmissionKeepsOriginalDraw() (gas: 47716799)\n[PASS] test_initializedPoolAtZeroTickIsValid() (gas: 4799473)\n[PASS] test_largeAdmissionQueueMakesProgressWithinGasBudget() (gas: 468123702)\nLogs:\n  largest admission/reveal step gas (including call overhead): 3466561\n\n[PASS] test_partialAdmissionPreservesDrawAndExcludesSameBlockAndLaterDeposits() (gas: 25071279)\n[PASS] test_pendingRerollBlocksApprovedOperatorsUntilExpiry() (gas: 1668950)\n[PASS] test_pendingRerollCannotBeSoldWithOldTraits() (gas: 802202)\n[PASS] test_safeTransferToVaultIsRejected() (gas: 531345)\n[PASS] test_sameBlockHandInsDoNotCountForPullsMadeNow() (gas: 275287704)\n[PASS] test_setupRefusesUnconfiguredLiquidityBeforeCommitting() (gas: 4689583)\n[PASS] test_setupRejectsLiquidityWithoutCode() (gas: 291131)\n[PASS] test_uninitializedPoolCannotSetPermanentFloor() (gas: 4893464)\nSuite result: ok. 15 passed; 0 failed; 0 skipped; finished in 493.58ms (754.48ms CPU time)\n\nRan 1 test for test/VaultInvariants.t.sol:VaultInvariantsTest\n[PASS]\nVaultInvariantsTest invariants:\n[PASS] invariant_agentBalancesMatch\n[PASS] invariant_capAndSlots\n[PASS] invariant_custody\n[PASS] invariant_feesToLiquidity\n[PASS] invariant_fullyBacked\n[PASS] invariant_inkConserved\n[PASS] invariant_queueCovered\n VaultInvariantsTest invariants (runs: 64, calls: 7680, reverts: 0)\n\n╭--------------+------------------+-------+---------+----------╮\n| Contract     | Selector         | Calls | Reverts | Discards |\n+==============================================================+\n| VaultHandler | claim            | 576   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | deposit          | 547   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | moveSeat         | 553   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pull             | 541   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullAll          | 549   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | pullTooMany      | 554   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | release          | 511   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | rerollTraits     | 580   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | reveal           | 569   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealNext       | 517   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | revealTraits     | 525   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | roll             | 577   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | setFees          | 552   | 0       | 0        |\n|--------------+------------------+-------+---------+----------|\n| VaultHandler | transferPortrait | 529   | 0       | 0        |\n╰--------------+------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 9.00s (9.00s CPU time)\n\nRan 18 test suites in 9.01s (10.71s CPU time): 106 tests passed, 0 failed, 28 skipped (134 total tests)\n","passed":true},{"durationMs":170,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"InkFeeRouter.claimAndRoute(address,uint64)\",\"InkFeeRouter.route()\",\"InkHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"InkHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"InkHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"InkHook.collectFees()\",\"InkHook.unlockCallback(bytes)\",\"InkLiquidity.acceptOwnership()\",\"InkLiquidity.addLiquidity()\",\"InkLiquidity.collectFees(uint256,uint256)\",\"InkLiquidity.renounceOwnership()\",\"InkLiquidity.setPool((address,address,uint24,int24,address),address)\",\"InkLiquidity.setPremium(int24)\",\"InkLiquidity.setWidth(int24)\",\"InkLiquidity.transferOwnership(address)\",\"InkPortraits.acceptOwnership()\",\"InkPortraits.approve(address,uint256)\",\"InkPortraits.lockRenderer()\",\"InkPortraits.mintAgent(uint256,address,(uint32,uint16,uint32,uint32,bool))\",\"InkPortraits.mintSwarm(address,bytes32)\",\"InkPortraits.renounceOwnership()\",\"InkPortraits.safeTransferFrom(address,address,uint256)\",\"InkPortraits.safeTransferFrom(address,address,uint256,bytes)\",\"InkPortraits.setApprovalForAll(address,bool)\",\"InkPortraits.setContractURI(string)\",\"InkPortraits.setRenderer(address)\",\"InkPortraits.setSwarmSeed(uint256,bytes32)\",\"InkPortraits.setVault(address)\",\"InkPortraits.transferFrom(address,address,uint256)\",\"InkPortraits.transferOwnership(address)\",\"InkRenderer.acceptOwnership()\",\"InkRenderer.addChunk(bytes)\",\"InkRenderer.lock()\",\"InkRenderer.renounceOwnership()\",\"InkRenderer.setPreviewBase(string)\",\"InkRenderer.transferOwnership(address)\",\"InkVault.acceptOwnership()\",\"InkVault.claim(uint256,(uint32,uint16,uint32,uint32,bool),bytes32[])\",\"InkVault.deposit(uint256[],uint256)\",\"InkVault.pull(uint256)\",\"InkVault.release()\",\"InkVault.renounceOwnership()\",\"InkVault.rerollTraits(uint256,uint256)\",\"InkVault.reveal(uint256)\",\"InkVault.revealNext(uint256)\",\"InkVault.revealTraits(uint256)\",\"InkVault.setFees(uint16,uint16)\",\"InkVault.setInk(address)\",\"InkVault.setup(bytes32,uint64,address)\",\"InkVault.transferOwnership(address)\",\"TestSeats.approve(address,uint256)\",\"TestSeats.mint(uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256)\",\"TestSeats.safeTransferFrom(address,address,uint256,bytes)\",\"TestSeats.setApprovalForAll(address,bool)\",\"TestSeats.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":7,\"ADAPTATION.md\":303,\"AUDIT_BRIEF.md\":71,\"BYTECODE_HASHES.json\":52,\"DEPENDENCIES.md\":10,\"PACKAGING.md\":16,\"README.md\":55,\"SETUP.md\":101,\"SHA256SUMS\":1640,\"SOURCE_PROVENANCE.json\":11377,\"VALIDATION.txt\":554,\"art/original-art.js\":418,\"foundry.toml\":33,\"launch.json\":36,\"launch/README.md\":35,\"launch/mainnet-1-contracts.json\":24,\"launch/mainnet-2-hook.json\":31,\"launch/sepolia-1-contracts.json\":24,\"launch/sepolia-2-hook.json\":31,\"remappings.txt\":8,\"script/Configure.s.sol\":63,\"script/InkSteps.sol\":135,\"script/LocalStack.s.sol\":135,\"script/Rehearse.s.sol\":236,\"script/UploadRenderer.s.sol\":33,\"src/InkFeeRouter.sol\":77,\"src/InkHook.sol\":257,\"src/InkLiquidity.sol\":335,\"src/InkPortraits.sol\":271,\"src/InkRenderer.sol\":311,\"src/InkVault.sol\":503,\"src/testnet/TestSeats.sol\":13,\"test/AuditRegression.t.sol\":476,\"test/InkRenderer.t.sol\":210,\"test/InkSystem.t.sol\":1397,\"test/LaunchDay.t.sol\":270,\"test/LaunchFeeHandoff.t.sol\":82,\"test/LaunchSafety.t.sol\":478,\"test/MainnetContractsLaunch.t.sol\":159,\"test/MainnetFork.t.sol\":519,\"test/RangeMath.t.sol\":80,\"test/SepoliaLaunch.t.sol\":186,\"test/SnapshotAllSeats.t.sol\":159,\"test/SnapshotProofs.t.sol\":91,\"test/SybilOptions.t.sol\":104,\"test/TokenURIPreview.t.sol\":79,\"test/VaultInvariants.t.sol\":403,\"test/fixtures/snapshot-sample.json\":98,\"test/fixtures/traits.json\":1,\"test/helpers/ConfiguredLiquidity.sol\":18},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"e0f4e8391563786ee7de4b7151772e2e57d16bb14ade8374248e31ce8208707d","verifiedTreeHash":"5f0a6596182b2c6c8d1311f30a2dad3e9901bf62","verifierVersion":"0.1.0+6a374fcd"}]}