{"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":"f3e8de87-f658-42cc-bdd2-6ddc9ba0288a","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"fae8b0aa55be2d3355108c25b4f4c7c0f4a729fb2f80c3881330c3e4d5fd3b07","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"608ad668de3907850d8eb107dc9e9e3d1d982629237f6c5b436e8c10f24ffb6e","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"d3031c63ce48d016e4cd1fcf90dbd61528415daef8dc5fe4e6c68c0ac1c49606","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"f42c69e27e35db8e10919d4be00b5c1310b527bdd1881cb9e1364a10da402c03","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"b57f7f3b56640f0363304743eaf6b3a54b8a648573c857343de762b48474160d","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"3454e6c2a1e77df2e35073ed2475d6a48c1bb18d366c46f67a5c7ebdc7c92a5b","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"6b8d2c2ebd3301cff2be8edb1e1cc6ba65c858c02fe7247be81722b969842265","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"20da37c72b13b4d2d5666115a9d70434c5c27ffa1b1a3e61e078b0b9555dcaf7","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"12901a7bfd6df887f6b6935f2997374a3ea706af0a3091f7652741cfce8ef360","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"4c76766b00591dfe982fe85d3de589fdb2f810e5137c301155d1c5e36a1d3b23","skillId":"write-foundry-tests","state":"accepted"}],"objective":"OVERVIEW:\nToken name: Perkshire IMDway\nToken symbol: PERK\nThis launch is the hook and the token only; logo and website are a later job.\nPerkshire IMDway ($PERK) is a value-investing parody token on Robinhood Chain, paired with IMD. Its Uniswap v4 hook is also its Retained Earnings Vault: every trade pays 1% in IMD to the hook. Half is burned as IMD (the buyback) and half stays as the \"insurance float\"; any holder can tender PERK to the float at book value at any time. Once a week the float buys one answer from the IMD oracle (the Chairman's weekly report): the vault is a paying customer of the swarm. Tendered PERK is burned. Book value per PERK, in IMD terms, rises only from the float, never from newly issued tokens; a tender leaves it unchanged. Book value starts at zero and, at realistic volume, stays far below market price; it only matters if price falls to it. No emissions, rebases, staking APY, admin keys or promised yield. Parody only: not affiliated with Berkshire Hathaway, Warren Buffett or Charlie Munger; PERK is not a share or equity in any company.\n\nCONTRACT: one hook that is also the vault, plus the standard PERK token. No owner, admin or upgrade. Pool: 89% of supply; 1% to the requester.\n1. Binding: constructor takes $poolManager, $token (immutable perk), IMD 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127 (immutable imd) and $factory (immutable factory). beforeInitialize requires sender == factory, currencies exactly perk and imd, fee 12500, not dynamic, no pool bound yet; then binds that one pool. All other initializations revert. Do not hard-code tick spacing. Do not copy #1330's open binding. If $token cannot be passed, take perk from the pool the factory initializes: the currency that is not imd.\n2. Float: reuse launch #1330's fee logic unchanged, RnnnFeeHook in https://github.com/identity-md-launches/launch-1330-launch-rnnn-bid-community-token at commit 7001624 (simulateSwap, return deltas, deferral, redeemPendingFees, same five permission bits) with the hook itself as fee recipient; only binding, burn, ask and tender are new. Every swap in the bound pool, both directions, exact-in and exact-out, takes 1% of the IMD the pool moves in that swap, as #1330 measures it, into the hook and emits FloatCollected. Half of each fee, rounded down, is added to burnOwed; the rest is added to totalFloat. This is on top of the pool's standard fee. Fees the PoolManager cannot pay out yet stay ERC-6909 claims held by the hook until anyone calls redeemPendingFees(). Both halves are counted when the fee is taken, paid or deferred; pendingFloat is the deferred part. Never count a fee twice. Burn: whenever the hook holds IMD (after a fee, a redeem, before a tender or an ask, or when anyone calls burn()), send min(burnOwed, balance) to 0x000000000000000000000000000000000000dEaD, lower burnOwed, raise totalBurned, emit Burned; if IMD refuses the transfer, leave it owed and do not revert.\n3. tender(amount): take PERK from the caller, send it to 0x000000000000000000000000000000000000dEaD, pay amount * (hook's IMD balance less burnOwed) / (totalSupply - PERK held by dEaD before this tender), rounded down. Pending claims are not paid until redeemed. Revert on zero payout. Add to totalTendered, emit Tendered. Reentrancy guard, safe transfers.\n4. ask(): anyone, at most once every 7 days (the first at any time). Requires the hook's IMD less burnOwed to be at least 0.6 IMD. Approve exactly 0.5 IMD to the Intake 0xa43e6F75ee006411F79Ac1C84120606C2330DE82 and call request(bytes32 \"oracle.request@oracle-1\", BODY, callback (address(0), bytes4(0)), imd, 0.5 IMD); pay the caller 0.1 IMD; record lastAsk and totalAsked; emit Asked(requestId). No callback, no signature check. BODY is this constant, byte for byte:\n{\"v\":1,\"question\":\"What is the sum of the value field of the Transfer events of the token 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127 (IMD, 18 decimals) whose to is 0x000000000000000000000000000000000000dEaD on Robinhood Chain (chain id 4663) in the window, in base units?\",\"chainId\":4663,\"window\":{\"hours\":168},\"answerType\":\"uint256\",\"evidence\":\"chain\",\"panelSize\":5,\"quorum\":4,\"validForSeconds\":86400}\n5. IMD leaves the hook only by tender, the burn and ask. Views: perk, imd, factory, totalFloat, pendingFloat, burnOwed, totalBurned, lastAsk, totalAsked, totalTendered, bookValuePerPerk (IMD per PERK, 18 decimals; balance less burnOwed).\nTests: fuzz that a tender never pays above pro-rata and never lowers book value; IMD leaves only by tender, burn and ask; half of every fee reaches the dead address once and only once; ask pays the Intake 0.5 and the caller 0.1, reverts under 0.6 or inside 7 days, leaves no allowance; donations only raise book value; first, last and dust tenders; exactly 1% of the IMD side taken in all four swap modes; deferred fees recorded and redeemed once; binding attacks (non-factory sender, fake token with IMD, wrong or dynamic fee, second pool, re-initialization).\n\nToken name: Perkshire IMDway\nToken symbol: PERK","parentJobId":null,"planHash":"773203ec74a7946b6c4b683e2b249b69178e37ce443ba1d3c54ba0c07da997f5","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"f3e8de87-f658-42cc-bdd2-6ddc9ba0288a","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1660-perkshire-imdway"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52082","feedbackHash":"17038cf79bd4babe17933c20294ba63152e5d893a66167ce55b7e4b8445236d4","nodeKey":"audit_economics","submissionHash":"fae8b0aa55be2d3355108c25b4f4c7c0f4a729fb2f80c3881330c3e4d5fd3b07","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51243","feedbackHash":"d641b4811ce6863e7455bdb3b047507e602c3fa6ea786cd1977fa759bc3eb4df","nodeKey":"audit_flow","submissionHash":"608ad668de3907850d8eb107dc9e9e3d1d982629237f6c5b436e8c10f24ffb6e","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51352","feedbackHash":"018640ff468a44c6e1dcf4d4657511e0cd20ba66a4ad11ba2236e5526e04577f","nodeKey":"audit_judge","submissionHash":"f5347250146019e62b9e84273bd0f804a169349ba9e8a9104bea9670e378e3ab","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52114","feedbackHash":"52c87df92cca894101e27794558f17a606b4de5adc6fb6c3453778d4beaa8f73","nodeKey":"audit_judge","submissionHash":"d3031c63ce48d016e4cd1fcf90dbd61528415daef8dc5fe4e6c68c0ac1c49606","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51023","feedbackHash":"3465cb1b59a9ebdc62ce145771b9f99be4c442be88d7458c81c5b1eb7cc9fb45","nodeKey":"audit_math","submissionHash":"f42c69e27e35db8e10919d4be00b5c1310b527bdd1881cb9e1364a10da402c03","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52397","feedbackHash":"b67f10cdbd82653047e983a82dc7e323caf6334ddc0828103f476537e60fc96d","nodeKey":"audit_permissions","submissionHash":"b57f7f3b56640f0363304743eaf6b3a54b8a648573c857343de762b48474160d","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51486","feedbackHash":"0b575e5635e97e796c1275e8bd8e58802e0870139e0dfb20d45dc6fb85cd254a","nodeKey":"build_contract_project","submissionHash":"3454e6c2a1e77df2e35073ed2475d6a48c1bb18d366c46f67a5c7ebdc7c92a5b","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52167","feedbackHash":"edabab18d804888a01366a57f208ce197320cab5d3a21e6faa40515ac3a3fd31","nodeKey":"build_contract_project","submissionHash":"a2a606dccdbb63d69a2f620d5d239b31f2be40109ccb0fe003f83d84abe2d0cc","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51316","feedbackHash":"4fba0f52d3ea8e189f1a953b89d0e7fa69482cac74f6c66513235dc09cda64bc","nodeKey":"manifest","submissionHash":"20da37c72b13b4d2d5666115a9d70434c5c27ffa1b1a3e61e078b0b9555dcaf7","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52549","feedbackHash":"4d8afb9fb278f7bef411b56604a906cc8917631e0e6be267e605e82c9d86f755","nodeKey":"manifest","submissionHash":"cd2e40092addf0a1d0e40ebfa6b14db79259e9052b4d2e44a446129e179a05f3","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52160","feedbackHash":"65d34f643b8da74e213ed5b58dc0c088ff299328716e2abf179e44a3ebc57bf3","nodeKey":"write_foundry_tests","submissionHash":"0f2b945b16868344c4be5681aa9bd48de5c95f1c34a001c584869bada9fead46","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51476","feedbackHash":"fc37963f2484b09f391235cb0b8551c34742b80b187f9171e31efa113d3ec2cb","nodeKey":"write_foundry_tests","submissionHash":"12901a7bfd6df887f6b6935f2997374a3ea706af0a3091f7652741cfce8ef360","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"f1980b8785cb17e92c1c664d029234ba358cd7683df74d4ac2369ce6ab3a6846","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"7a71cbe00b26a6c56c4a3c488369896ded3f4ba86895c7addc243eba1439f008","device":"cd8076510aad8b8e","findings":[{"description":"When the PoolManager cannot pay the fee within the swap (the launch-shaped pool, whose manager holds no IMD before the first buy; or an IMD token that refuses the hook), `_collect` mints the whole fee as an ERC-6909 claim and `_account` adds fee/2 to `burnOwed` at once. `afterSwap` then runs `_burn()`, which sends `min(burnOwed, balance)` to 0x…dEaD from whatever IMD the hook already holds, i.e. from the float that backs tenders. The fee's own IMD has not arrived (it is pending), so `bookValuePerPerk` (balance less burnOwed over circulating supply) drops by fee/2 / circulating until anyone calls `redeemPendingFees()`, after which it ends above where it started. The brief says book value 'rises only from the float' and that donations only raise it; here a swap lowers it for a while, and a holder who tenders in that window is paid below the vault's real book value (the remaining holders gain the difference). No value leaves the system and the vault is solvent throughout, so this is a timing/fairness defect rather than a loss: it needs a float before the manager has IMD (a donation, or IMD-side fees paid earlier followed by a period where IMD refuses the hook). The brief's own wording (count both halves when deferred; burn whenever the hook holds IMD) prescribes this ordering, so it may be a design decision; if the intent is that book value never falls outside `ask()`, the fix is to burn only out of IMD that has actually arrived, for example by deferring the burn half's `burnOwed` increment to redemption time, or by excluding pending claims' burn halves from `_burn` (burn at most `balance - pendingFloat()/2`-style, or track `burnOwedPending` separately and move it to `burnOwed` in `_payPending`). The suite's invariant `invariant_bookValueFallsOnlyByAsk` therefore checks the claim-inclusive book value (hook IMD + pending claims − burnOwed, over circulating supply), which never falls outside an ask, and asserts the view never exceeds it and equals it whenever nothing is pending. The strict 'view never falls on a swap' assertion is kept only for swaps paid at once.","line":614,"path":"src/PerkVaultHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\n\nimport {PerkVaultHook} from \"src/PerkVaultHook.sol\";\nimport {PerkToken} from \"src/PerkToken.sol\";\n\ncontract Imd {\n    string public constant name = \"IMD\";\n    string public constant symbol = \"IMD\";\n    uint8 public constant decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint256 supply) {\n        totalSupply = supply;\n        balanceOf[msg.sender] = supply;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        return _move(msg.sender, to, a);\n    }\n\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        return _move(f, to, a);\n    }\n\n    function _move(address f, address to, uint256 a) internal returns (bool) {\n        require(balanceOf[f] >= a, \"bal\");\n        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\n/// @notice A deferred fee lowers `bookValuePerPerk` until someone redeems the pending claim.\ncontract DeferredFeeLowersBookValueTest is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint160 constant FLAGS = (1 << 13) | (1 << 7) | (1 << 6) | (1 << 3) | (1 << 2);\n    address constant DEAD = 0x000000000000000000000000000000000000dEaD;\n\n    PoolManager manager;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    Imd imd;\n    PerkToken perk;\n    PerkVaultHook hook;\n    PoolKey key;\n    address factory = makeAddr(\"factory\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        swapRouter = new PoolSwapTest(manager);\n        lpRouter = new PoolModifyLiquidityTest(manager);\n        imd = new Imd(1e30);\n        perk = new PerkToken();\n\n        bytes memory code = abi.encodePacked(\n            type(PerkVaultHook).creationCode, abi.encode(address(manager), address(perk), address(imd), factory)\n        );\n        bytes32 initHash = keccak256(code);\n        for (uint256 i = 0;; i++) {\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash)))));\n            if (uint160(predicted) & ((1 << 14) - 1) == FLAGS) {\n                bytes32 salt = bytes32(i);\n                address at;\n                assembly {\n                    at := create2(0, add(code, 0x20), mload(code), salt)\n                }\n                hook = PerkVaultHook(at);\n                break;\n            }\n        }\n        bool perkIsZero = address(perk) < address(imd);\n        key = PoolKey({\n            currency0: Currency.wrap(perkIsZero ? address(perk) : address(imd)),\n            currency1: Currency.wrap(perkIsZero ? address(imd) : address(perk)),\n            fee: 12_500,\n            tickSpacing: 60,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        imd.approve(address(swapRouter), type(uint256).max);\n        perk.approve(address(lpRouter), type(uint256).max);\n        imd.approve(address(lpRouter), type(uint256).max);\n        // Launch shape: PERK-only liquidity, no IMD in the manager.\n        lpRouter.modifyLiquidity(\n            key,\n            ModifyLiquidityParams(\n                perkIsZero ? int24(60) : TickMath.minUsableTick(60),\n                perkIsZero ? TickMath.maxUsableTick(60) : int24(-60),\n                1e24,\n                bytes32(0)\n            ),\n            \"\"\n        );\n        assertEq(imd.balanceOf(address(manager)), 0);\n    }\n\n    function test_deferredFeeDoesNotLowerBookValue() public {\n        // The float holds 10 IMD (a donation; the same happens with float from earlier paid fees).\n        imd.transfer(address(hook), 10 ether);\n        uint256 bvBefore = hook.bookValuePerPerk();\n        assertEq(bvBefore, 10 ether * 1e18 / perk.totalSupply());\n\n        // First buy on the launch pool: the manager holds no IMD yet, so the 1 IMD fee is deferred.\n        bool zeroForOne = address(perk) > address(imd);\n        swapRouter.swap(\n            key,\n            SwapParams(zeroForOne, -100 ether, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        assertEq(hook.pendingFloat(), 1 ether, \"fee deferred\");\n        assertEq(imd.balanceOf(DEAD), 0.5 ether, \"burn half paid out of the pre-existing float\");\n        assertEq(hook.burnOwed(), 0);\n        assertEq(imd.balanceOf(address(hook)), 9.5 ether);\n\n        // Expected: book value never falls from a fee. Actual: it fell by 0.5 IMD / supply.\n        emit log_named_uint(\"book value before the swap\", bvBefore);\n        emit log_named_uint(\"book value after the swap \", hook.bookValuePerPerk());\n        assertGe(hook.bookValuePerPerk(), bvBefore, \"a deferred fee lowered book value\");\n    }\n\n    function test_afterRedeemBookValueIsAboveTheStart() public {\n        imd.transfer(address(hook), 10 ether);\n        uint256 bvBefore = hook.bookValuePerPerk();\n        bool zeroForOne = address(perk) > address(imd);\n        swapRouter.swap(\n            key,\n            SwapParams(zeroForOne, -100 ether, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        hook.redeemPendingFees();\n        assertEq(imd.balanceOf(address(hook)), 10.5 ether);\n        assertGt(hook.bookValuePerPerk(), bvBefore, \"once redeemed the float is whole again\");\n    }\n}","reproduction":"Fresh PoolManager, PERK/IMD pool bound by the factory, PERK-only liquidity (manager IMD balance 0). Send 10 IMD to the hook (bookValuePerPerk = 10e18*1e18/1e27 = 10000000000). Buy exact-in 100 IMD: the 1 IMD fee is deferred (pendingFloat = 1e18), burnOwed becomes 0.5e18 and _burn immediately sends 0.5 IMD of the donated float to 0x…dEaD. Expected: bookValuePerPerk >= 10000000000 (a fee never lowers book value). Actual: bookValuePerPerk = 9500000000 (hook holds 9.5 IMD, burnOwed 0). After redeemPendingFees() the hook holds 10.5 IMD and book value is 10500000000, above the start. The scratch test below fails on the current code with 'a deferred fee lowered book value: 9500000000 < 10000000000'.","severity":"low","title":"A deferred fee's burn half is paid out of the existing float, lowering bookValuePerPerk until the pending claim is redeemed"},{"description":"`pendingFloat()` is the hook's ERC-6909 IMD balance in the PoolManager. Anyone can mint or transfer such claims to the hook (PoolManager.mint / transfer after paying IMD in). They appear in `pendingFloat`, are redeemed by `redeemPendingFees()` like a fee, and then sit in the hook as float: `totalFloat` and `burnOwed` do not move, so they behave exactly like a direct IMD donation and only raise book value. The brief's 'pendingFloat is the deferred part' is therefore only true when nobody pushes claims. No value is at risk; `test_claimsPushedOntoTheHookBehaveAsADonation` in test/PerkVaultHookAdversarial.t.sol pins the behaviour. If the counter is meant to be exact, track deferred fees in storage instead of reading the ERC-6909 balance.","line":247,"path":"src/PerkVaultHook.sol","reproduction":"Pay 3 IMD into the PoolManager inside an unlock and call manager.mint(hook, imd.toId(), 3e18). pendingFloat() reads 3e18 with no fee taken. redeemPendingFees() returns 3e18; the hook's IMD balance rises by 3e18; totalFloat, burnOwed and totalBurned are unchanged; bookValuePerPerk rises. Expected per the brief's wording: pendingFloat is only the deferred fees. Actual: it includes the pushed claim.","severity":"info","title":"pendingFloat() counts any IMD ERC-6909 claim anyone pushes onto the hook, not only deferred fees"}],"hash":"0f2b945b16868344c4be5681aa9bd48de5c95f1c34a001c584869bada9fead46","nodeId":"d3e00cc5-e511-496e-90c5-3b93a526fb30","outcome":"completed","summary":"Done. The suite now has 68 tests passing under the verifier's fuzz seed, and one defect is reported rather than tested around.\n\n**What I found**\n\nThe existing 63 tests already covered the brief's unit and fuzz list but had no invariant tests, which the acceptance criteria require for a value-holding contract. While building them, the handler surfaced a real behaviour: a deferred fee lowers the book value view. On the launch-shaped pool the manager holds no IMD, so the first fee becomes a pending claim, yet its burn half is owed at once and the burn pays it out of whatever float the hook already holds. Book value drops by half the fee until anyone calls the redeem function, after which it ends above where it started. The brief's own wording prescribes this ordering, so I filed it as low severity with a failing proof test rather than asserting it is correct. A second, info-level note records that anyone can push ERC-6909 IMD claims onto the hook, which then read as pending float and behave as a donation once redeemed.\n\n**What I added** (all under test/)\n\n- `test/invariants/PerkVaultHandler.sol` drives three actors through all four swap modes, liquidity adds, tenders, burns, redeems, asks with time warps, donations, and an IMD stand-in that blocklists the dead address or the hook at random. Every path asserts its own postconditions, including the exact revert reason when a tender or ask must fail.\n- `test/invariants/PerkVaultInvariants.t.sol` checks nine invariants after every call with revert-on-failure on: IMD conservation, IMD leaving the hook only by tender, burn and ask, dead holding exactly totalBurned and totalTendered, the floor/ceiling fee split, debt always covered by cash plus pending claims, the manager always covering pending claims, book value falling only through ask (claim-inclusive, with the view never above it), ask accounting, and fixed PERK supply.\n- `test/PerkVaultHookAdversarial.t.sol` covers pushed IMD claims, pushed PERK claims, whole redeems after a draining sell, and doctored pool keys.\n- `.imd-findings.json` at the repository root holds the two findings. The low one carries a self-contained proof that fails on the current code with book value 9500000000 against an expected 10000000000.\n\n**Verification**\n\n```\nforge build                      ok\nforge test (seed pinned)         68 passed, 0 failed, 9 invariants passed\nforge fmt --check on new files   ok\n```\n\nNothing outside test/ was modified, nothing was installed, and the scratch reproduction lives under test/scratch, which is discarded. The findings file is excluded from git by the harness's own exclude list, so it is read from disk as intended.","treeHash":"d406c274456357ff03e5bb7673493d1756065f44","usage":{"cachedInputTokens":2703455,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":71783,"runtime":"claude","turns":59,"wallClockMs":1166017}},{"artifacts":[],"attempt":1,"bundleHash":"01e097164dccea58fe4a9bd1dcef79025c480e46a6a72bd1bd24f5c2484ec942","device":"e9049e5b12ba53ba","findings":[{"description":"The brief defines the tender payout as amount * (hook's IMD balance less burnOwed) / circulating, and the ask threshold and bookValuePerPerk on balance less burnOwed. The revised hook subtracts burnDue() = burnOwed - deferredBurn instead, so while a fee is pending as an ERC-6909 claim its burn half is not deducted from the IMD on hand. The invariant suite shows this is solvent (deferredBurn <= pendingFloat, burnOwed <= balance + pendingFloat, 0xdEaD ends up receiving every burn half once), and it fixes the earlier finding that a deferred fee lowered the book value view. It is still a departure from the literal formula the requester wrote, recorded here so the requester confirms it.","line":314,"path":"src/PerkVaultHook.sol","reproduction":"Launch-shaped pool (PERK-only liquidity, manager holds no IMD). Donate 10 IMD to the hook, then buy with 100 IMD exact-in: the 1 IMD fee is deferred, burnOwed = 0.5e18, deferredBurn = 0.5e18. Brief formula: tender(circulating) pays 10e18 - 0.5e18 = 9.5e18. Actual: pays 10e18 (balance - burnDue). The 0.5 IMD burn half is later paid from the redeemed claim.","severity":"info","title":"tender/ask/bookValuePerPerk subtract burnDue (burnOwed - deferredBurn), not burnOwed as the brief states"}],"hash":"12901a7bfd6df887f6b6935f2997374a3ea706af0a3091f7652741cfce8ef360","nodeId":"d3e00cc5-e511-496e-90c5-3b93a526fb30","outcome":"completed","summary":"Your existing tests now pass against the revised hook: `forge test` with the verifier's seed runs 74 tests in 6 suites, all passing. Only the 9 invariants, which run as a single suite, had broken. Each invariant ran 128 random sequences of 50 calls, and every handler action was exercised.\n\n**What broke.** Since my last round, the hook's author added `deferredBurn`. When a fee can't be paid out yet, its burn half now waits for that fee to be redeemed instead of being paid from IMD the hook already holds. So the burn can only take `burnDue()` (`burnOwed - deferredBurn`) from the hook's balance. My invariant suite still assumed the old rule, where the burn could use all of `burnOwed`, and it failed on the first short sequences.\n\n**What I changed** (only in `test/invariants/PerkVaultHandler.sol` and `test/invariants/PerkVaultInvariants.t.sol`; the other test files are untouched):\n- After `burn()`, the check is now that either `burnDue()` is zero or the hook holds no IMD.\n- New invariants:\n  - `deferredBurn` is never more than `burnOwed` or the pending fees.\n  - `burnDue()` equals `burnOwed - deferredBurn`.\n  - The float equals the hook's IMD balance less `burnDue`.\n  - The hook's IMD balance always covers `burnDue`.\n- Every swap, paid at once or deferred, must now leave `bookValuePerPerk` unchanged or higher. Before the fix, a deferred fee could lower it; my old handler allowed that and pointed to the earlier finding.\n- A deferred swap must add exactly half its fee to `deferredBurn`.\n- A donation must raise the float by exactly the amount donated.\n\n**One finding, info level, in `.imd-findings.json`.** The brief says a tender pays `amount * (hook's IMD balance less burnOwed) / circulating`. The revised hook subtracts `burnDue` instead, and the ask threshold and `bookValuePerPerk` do the same. I confirmed this with a scratch test:\n- Setup: a pool with PERK-only liquidity, 10 IMD donated to the hook, then a 100 IMD buy whose 1 IMD fee is deferred.\n- Result: tendering the whole circulating supply would pay 10 IMD. The brief's formula gives 9.5 IMD.\n\nThe invariants show this is solvent: the pending fees cover `deferredBurn`, and every burn half still reaches `0x…dEaD` exactly once. It is still a departure from the formula you wrote, so you should confirm it is what you want.\n\nNothing has been committed.","treeHash":"2d2b8e7953a986bef53ff17e0cda0f93cac7ce3a","usage":{"cachedInputTokens":660864,"inputTokens":22,"model":"claude-opus-5-5","outputTokens":7570,"runtime":"claude","turns":12,"wallClockMs":168970}},{"artifacts":[],"attempt":1,"bundleHash":"fbc1c6434dadd8f9714c8735e4122696864338dbd91dd6b51c2098aa846fbd1c","device":"6b0a28df3d585600","findings":[],"hash":"20da37c72b13b4d2d5666115a9d70434c5c27ffa1b1a3e61e078b0b9555dcaf7","nodeId":"3cf1beb8-9d83-496e-a610-ed07321368e9","outcome":"completed","summary":"Build and all 69 tests pass. The manifest in the tree already describes the accepted implementation exactly, so no revision was needed.\n\n**What I verified in launch.json against the source**\n\n- **Discriminator**: the first field is `\"kind\": \"univ4_hook\"`.\n- **Hook contract and constructor**: `PerkVaultHook` takes four arguments in declaration order. The manifest lists them as `$poolManager`, `$token`, the IMD address the brief fixes, and `$factory`. The IMD address is a value the brief gives, not a stand-in, and it is written in lowercase.\n- **Permissions**: the hook's `getHookPermissions` enables beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta and afterSwapReturnDelta. The manifest lists exactly those five, which is what the protected permissions test compares the mined address against.\n- **Token**: `PerkToken` has no constructor arguments, name \"Perkshire IMDway\", symbol \"PERK\", 18 decimals. All three match the manifest.\n- **Pool**: paired currency is IMD, fee 12500, tick spacing 60, initial price \"79228162514264337593543950336\", as the launch reference gives them.\n- **Notes**: one string of 1935 characters, under the 4000 cap. It names the binding rule, the fee split, deferred claims, burn, tender, ask and the absence of any owner or upgrade path.\n\n**Checks run**\n\n```\nforge build   -> ok\nforge test    -> 4 suites, 69 passed, 0 failed\n```\n\nNo file was changed. The working tree is identical to the accepted commit, so the submission is the existing launch.json.","treeHash":"0e3fb8ff8255e9edec91b78c44ea5ac12fbfa95a","usage":{"cachedInputTokens":312776,"inputTokens":194,"model":"claude-fable-5-1","outputTokens":3020,"runtime":"claude","turns":7,"wallClockMs":105369}},{"artifacts":[],"attempt":1,"bundleHash":"5a006dabdd4c23273ac5e7b84cac95b7ab17a8b07b751dda6a7c97a788300399","device":"0e78515c4d95885a","findings":[],"hash":"3454e6c2a1e77df2e35073ed2475d6a48c1bb18d366c46f67a5c7ebdc7c92a5b","nodeId":"3d0ea6df-bd86-4bdb-a25f-5b196dce55c8","outcome":"completed","summary":"The file is excluded by the harness's own `.git/info/exclude` entry alongside `.imd/`, so it is picked up from the working tree rather than from a commit. That is the expected layout. The work is complete.\n\n**Outcome.** The reopened medium finding reproduced and is fixed. The two advisory findings reproduced but are by design, and both are answered as disputed with the reasoning in `.imd-responses.json`.\n\n**What was wrong.** When the PoolManager could not pay a fee inside the swap, the hook minted the fee as a claim but still added half of it to the burn debt at once. The next burn then paid that debt out of IMD the hook already held, so a deferred fee lowered book value, underpaid tenders, and could block asks until someone redeemed the claim. Any swapper could trigger this at will by emptying the manager's IMD inside their own unlock before swapping.\n\n**The fix in `src/PerkVaultHook.sol`**, the minimal one the reviewer proposed:\n- New storage counter for the burn halves of fees still pending as claims, plus a view for the burn that is due now.\n- The burn only sends what is owed beyond the deferred part. The float, book value, tender quote, and the ask minimum subtract only that part.\n- Redemption, whether by the permissionless redeem or by the next swap's flush, releases the deferred part against the redeemed IMD first, so it never exceeds what is owed or pending.\n\n**Tests.** Six regression tests were added in the deferred-fee section of the hook suite, including the reviewer's scenario with the correct intermediate state, a forced-deferral attacker contract in `test/mocks/ForcedDeferral.sol`, a large buy exceeding the manager's IMD, a partial redemption, and a fuzz that no swap, redeem, burn or donation ever lowers book value. Build, format check, and all 69 tests pass with the verifier's fuzz seed.\n\n**Why the proof is disputed rather than marked fixed.** The reviewer's proof asserts, before its final check, that the dead address already received 0.5 IMD, that burn debt is zero, and that the hook holds 9.5 IMD. Those three assertions are the defect itself, and no implementation that keeps the float whole can satisfy them. On the fixed tree the proof fails only on that line, and the same test with those three assertions removed passes. The response file states this with the exact numbers so the reviewer can confirm.\n\n**Documentation.** The README now describes the deferred burn accounting, the updated tender and ask formulas, the new views, the keeper note, and records the fixed Intake price and the pushed-claim behaviour as deliberate choices.","treeHash":"ca659a32c291be2a308457cfc562fbab89de12ea","usage":{"cachedInputTokens":1532065,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":37646,"runtime":"claude","turns":43,"wallClockMs":511330}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"76e5f9ed417094cc","findings":[{"citation":"resolved","description":"When the PoolManager cannot pay a fee inside the swap, _collect mints an ERC-6909 claim and still calls _account(key, fee, false) (src/PerkVaultHook.sol:614-615), which adds fee/2 to burnOwed (line 524). The IMD itself has not reached the hook, yet floatBalance() (line 255) subtracts the whole burnOwed from the hook's actual balance. So a deferred fee immediately lowers bookValuePerPerk() by pendingFloat/2 over the circulating supply, contradicting the brief ('book value rises only from the float', fees and donations never lower it) and the suite's own testFuzz_bookValueNeverFallsFromDonationsOrFees, which only exercises paid fees. Consequences until someone calls redeemPendingFees(): (a) tender() pays amount*(balance-burnOwed)/circulating, below the real book value, the shortfall staying with the remaining holders; (b) if the deferred burn half exceeds the balance, floatBalance() clamps to 0 and both tender() (ZeroPayout) and ask() (FloatTooSmall) revert although IMD sits in the hook and as redeemable claims. The README itself expects the first buys of the PERK-only launch pool to be deferred. Minimal fix: track the burn half of deferred fees separately (e.g. deferredBurn += fee/2 on the mint path, moved into burnOwed when the claim is redeemed in _payPending/_collect's flush), so floatBalance subtracts only burn owed on IMD the hook actually holds.","line":255,"path":"src/PerkVaultHook.sol","reproduction":"Fixture test/utils/PerkVaultBase.sol (PERK-only liquidity, manager holds no IMD). 1) imd.transfer(hook, 10 ether): bookValuePerPerk()=10000000000, floatBalance()=10 ether. 2) buy exact-in 100 ether IMD: fee 1 IMD cannot be taken (manager IMD balance 0 at afterSwap) so pendingFloat()=1e18, burnOwed=0.5e18, hook balance still 10e18. Actual: floatBalance()=9.5e18, bookValuePerPerk()=9500000000 (fell 5%); quoteTender(1e24)=0.0095e18 vs 0.01e18 at the pre-fee book value. Expected: book value unchanged or higher after a fee. 3) Variant: imd.transfer(hook, 1 ether) then buy exact-in 1000 ether (fee 10 IMD deferred, burnOwed 5e18 > balance 1e18): floatBalance()=0, hook.ask() reverts FloatTooSmall(0) and hook.tender(1e24) reverts ZeroPayout, although 1 IMD is in the hook and 10 IMD of claims are redeemable. After redeemPendingFees() everything recovers. Scratch test: test/scratch/Deferral.t.sol test_deferredFeeLowersBookValue and test_deferralCanBlockTenderAndAsk (both pass on this code, i.e. the fall is reproduced).","severity":"medium","snippet":"        return balance > burnOwed ? balance - burnOwed : 0;","title":"Deferred fee lowers book value and can block tender/ask: burn half is charged against the balance while the pending IMD is not counted"},{"citation":"resolved","description":"The brief fixes 0.5 IMD, so this is a design consequence rather than a coding slip, but it is a concrete dead-end: the oracle-consumer reference states the Intake's action id, asset and price are owner-settable and 'a moved price must not need a redeploy', while this hook has no owner and approves/passes exactly ASK_PRICE. If Intake.priceOf(ACTION, imd) ever differs from 0.5e18 (or the action moves to oracle-2), every ask() either reverts at request() or pays a stale amount, permanently, and the 'vault is a paying customer of the swarm' feature stops; IMD then leaves only by tender and burn. The reference does not say how the live Intake treats amount != price, so the exact failure (revert vs. refused request with the price spent) is unverified; what is verified is that the hook cannot follow a price change. If the author keeps the no-admin design, reading priceOf(ACTION, imd) at ask time (and requiring it <= a cap, e.g. floatBalance - ASK_REWARD) keeps ask() alive without an owner.","line":74,"path":"src/PerkVaultHook.sol","reproduction":"State: Intake.priceOf(bytes32('oracle.request@oracle-1'), 0x5f7b...7127) returns 0.6e18 (owner moved the price). Call hook.ask() with floatBalance() >= 0.6e18 and lastAsk + 7 days <= now. Actual: _approve(imd, INTAKE, 0.5e18) then INTAKE.request(..., imd, 0.5e18); with test/mocks/MockIntake.sol's price check this reverts 'MockIntake: wrong price', and the hook has no way to pass any other amount. Expected: the vault still buys an answer at the current price. Verified: test/scratch/AskPrice.t.sol etches an Intake stand-in whose price is 0.6 IMD at the live Intake address, funds the hook with 5 IMD, and hook.ask() reverts 'Intake: wrong price' (test passes on this code, reproducing the dead end).","severity":"low","snippet":"    uint256 public constant ASK_PRICE = 0.5 ether;","title":"ask() hard-codes the Intake price at 0.5 IMD; a moved Intake price leaves the vault unable to buy answers"}],"hash":"608ad668de3907850d8eb107dc9e9e3d1d982629237f6c5b436e8c10f24ffb6e","nodeId":"d7f9d230-c715-4855-b67b-26680a59e962","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds two substantiated findings plus a coverage row for every listed entry point.\n\n**What I covered.** My area was the Execution Trace, Periphery and First Principles guides. I traced every entry point of the hook and token from caller to final state, including the v4-core dispatch that the fee logic depends on. The simulation self-call, the before and after swap delta netting, the paid and deferred fee paths, the burn, tender, ask and redeem flows, and the Intake periphery were all traced with concrete values and fuzzed where the existing suite had a gap. The full suite passes (63 tests). My scratch tests under `test/scratch/` are gitignored and not part of the submission.\n\n**Findings written to the file:**\n\n- **Medium, deferred fees lower book value.** When the PoolManager cannot pay a fee, the hook mints a claim and still adds half the fee to burnOwed. The float view subtracts all of burnOwed from the real balance, so book value drops by half the deferred amount. A tender during that window pays below book value, and if the deferred burn half exceeds the balance, both tender and ask revert while IMD sits in the hook and as redeemable claims. Reproduced with a donation followed by a deferred first buy on the PERK-only pool. Everything recovers after redeemPendingFees, so this is transient but it breaks the stated guarantee and the author's own README expects deferrals at launch.\n- **Low, fixed ask price.** The ask price is a constant with no owner to move it. If the Intake's price or action changes, every ask fails for good. Verified by etching an Intake stand-in priced at 0.6 IMD. The brief mandates the constant, so this is a design consequence to decide on, not a coding slip.\n\n**What holds.** Binding, both swap directions in exact-in and exact-out with and without price limits, delta netting to zero, once-only fee accounting, burn tolerance of refused transfers, tender pro-rata and rounding, reentrancy coverage, the three IMD exit paths, the Intake interface, the right-padded action id, and the request body byte for byte against the brief.\n\n**Not reached.** A third-party ERC-6909 claim sent to the hook is swept in as float by the next redeem. That is donation-equivalent and I did not treat it as a defect. I did not inspect a launch manifest because none exists in the tree.","treeHash":null,"usage":{"cachedInputTokens":2134170,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":45320,"runtime":"claude","turns":32,"wallClockMs":641179}},{"artifacts":[],"attempt":1,"bundleHash":"0b54aedf808f3e6dd1823f45194d6bcfd56499d3059e8c451d15fc94e1043149","device":"1ca477e8d9b58040","findings":[],"hash":"a2a606dccdbb63d69a2f620d5d239b31f2be40109ccb0fe003f83d84abe2d0cc","nodeId":"3d0ea6df-bd86-4bdb-a25f-5b196dce55c8","outcome":"completed","summary":"The deliverable is complete and committed.\n\n**What was built**\n\n- `src/PerkToken.sol`: the standard launch token, name `Perkshire IMDway`, symbol `PERK`, 18 decimals, one billion minted once to the deployer. No mint, owner, pause, fee or upgrade.\n- `src/PerkVaultHook.sol`: the hook and vault in one contract, no owner or admin. Constructor takes `$poolManager`, `$token`, the IMD address and `$factory`. It reuses launch #1330's fee logic unchanged with the hook as recipient, and adds binding, burn, tender and ask.\n- `src/IIntake.sol`: the Intake interface from the oracle-consumer reference.\n- Vendored `lib/` (forge-std, v4-core, solmate's Owned) copied from #1330 as ordinary files, no submodules.\n\n**Key behaviour**\n\n- Binding admits one pool only: factory as initializer, currencies exactly PERK and IMD, static fee 12500, nothing bound yet. Tick spacing is not checked. Swap callbacks also refuse any other key.\n- Every swap takes 1% of the IMD side in all four modes. Each fee is split once when taken, paid or deferred: half to `burnOwed`, the rest to `totalFloat`. Deferred fees stay as ERC-6909 claims until anyone redeems.\n- The burn sends `min(burnOwed, balance)` to the dead address after fees, after redeems, before tenders and asks, and on `burn()`. A refused IMD transfer leaves it owed without reverting.\n- `tender` pays `amount * (balance - burnOwed) / (supply - dead)` rounded down, burns the PERK, reverts on zero payout, and is reentrancy-guarded.\n- `ask` runs at most weekly, needs 0.6 IMD of float, approves exactly 0.5 IMD to the Intake, sends the brief's body byte for byte, pays the caller 0.1 IMD, and leaves no allowance.\n\n**Verification**\n\n| Check | Result |\n| --- | --- |\n| `forge build` (solc 0.8.26, cancun, bytecode_hash none) | passes, lint warnings only |\n| `forge test` with the verifier's fuzz seed | 63 passed, 0 failed |\n| `forge fmt --check` | passes |\n| Protected floor tests (Hook and Token) run from scratch with the verifier's env | 11 passed, including initialization from the factory probe |\n\nThe Owners floor test could not be run locally because its `OwnerChecks` and `Swapper` helpers were not provided. The hook has no owner and no remove-liquidity callbacks, and a zero-liquidity modify is covered by a test of my own.\n\n**Documented choices and open items** are in the README: the Intake, action and price are constants because the brief forbids an owner, so a moved Intake stops `ask()` without affecting the rest. `totalAsked` counts answers bought. The fee stays in IMD rather than ETH on the requester's instruction. An independent adversarial review is recommended before release, with the self-call simulation and blocklisted-IMD paths as the first places to look.","treeHash":"5813eafcc1967e6ed87df1ff0f7f91b079038002","usage":{"cachedInputTokens":5015061,"inputTokens":962,"model":"claude-fable-5-1","outputTokens":79715,"runtime":"claude","turns":42,"wallClockMs":959766}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"827152cce766ca0d","findings":[{"citation":"resolved","description":"Trust gap (access x economics). INTAKE, ACTION and ASK_PRICE are constants and the hook has no owner, as the brief requires. The oracle-consumer reference says the Intake's price per action is set by the Intake's operator ('a moved price must not need a redeploy'). So whether ask() can ever succeed again depends on a party outside this launch. This is not a permission bypass, and the hook behaves as the brief specifies. It is listed as a trust assumption for the README, not as a fix request: making the price settable would add the admin role that the brief rules out.","line":318,"path":"src/PerkVaultHook.sol","reproduction":"State: the float holds >= 0.6 IMD, more than 7 days have passed since lastAsk, and the Intake operator has changed priceOf(ACTION, IMD) to 0.6e18. The Intake requires amount == price, and that requirement is an assumption about the deployed Intake, not verified on chain. Call ask(). Expected: one answer is bought. Actual: INTAKE.request reverts, so ask() reverts for every caller on every later attempt. No IMD is lost, and tender and burn are unaffected.","severity":"info","snippet":"        requestId = INTAKE.request(\n            ACTION, bytes(BODY), IIntake.Callback(address(0), bytes4(0)), Currency.unwrap(imd), ASK_PRICE\n        );","title":"ask() relies on the Intake operator keeping the price of oracle.request@oracle-1 at exactly 0.5 IMD; the hook has no way to adjust (follows the brief)"}],"hash":"b57f7f3b56640f0363304743eaf6b3a54b8a648573c857343de762b48474160d","nodeId":"01600dde-c054-4a04-9123-15f82a2b953f","outcome":"completed","summary":"I found no access-control, trust-gap or asymmetry defect in `PerkVaultHook` or `PerkToken`. `.imd-findings.json` has one informational finding and a coverage row for all 12 entry points, plus 2 invariant rows. The existing suite passes (63 tests). I added no proof tests, since only critical or high findings need one, and I changed no files.\n\n**The informational finding:** `ask()` (`src/PerkVaultHook.sol:318`) only works while the Intake's operator keeps the price of an answer at exactly 0.5 IMD. The Intake address and price are constants with no owner, as the brief requires. If the operator moves the price, every later `ask()` reverts and nothing can fix it. No IMD is lost, and tender and burn keep working. One part is assumed, not checked: that the live Intake rejects any amount other than its current price. This is a dependency to mention in the README, not a fix request, because fixing it would mean adding the admin role the brief rules out.\n\n**What holds, by guide:**\n- **Access control:**\n  - Pool binding accepts only the factory as sender, PERK and IMD as the currencies, and a fixed 12500 fee. It binds once, so a fake token, a wrong or dynamic fee, a second pool or a re-initialization all revert.\n  - Every swap and settlement callback accepts only the PoolManager and only the bound pool.\n  - `simulateSwap` can only be called by the hook itself, and that inner swap doesn't trigger the hook's callbacks, so it can't recurse.\n  - `unlockCallback` can only be reached through `redeemPendingFees`.\n  - There is no owner, setter or upgrade path, and the token can never mint again.\n- **Trust gap:**\n  - The open functions (`burn`, `redeemPendingFees`, `tender`, `ask`) can only send IMD to fixed places: dEaD, the hook itself, the tenderer's own pro-rata share, or the Intake's exact 0.5 IMD, whose approval is reset afterwards.\n  - A tender pays at or below pro-rata, so book value can't fall.\n  - Donating IMD or calling functions during someone else's PoolManager session gives an attacker nothing.\n- **Asymmetry:**\n  - Each fee is counted exactly once, whether it is paid during the swap or held as a claim. Paying out old claims during a later swap uses the same bookkeeping as `redeemPendingFees`.\n  - The fee `beforeSwap` takes is exactly the one `afterSwap` records, carried between them in transient storage.\n\n**Static-analysis leads:** the reentrancy warnings only involve calls to the PoolManager, the IMD token or the fixed Intake, and the user entry points share one reentrancy lock. The ignored `unlock` return value and the variable left at zero by default are harmless.\n\n**Two behaviours I didn't report because they match the brief:**\n- A tender made while fees are still unpaid claims is paid only from the IMD the hook holds. The burn's half of those claims comes out of that IMD first, so tendering before anyone calls `redeemPendingFees()` pays less.\n- When IMD is the amount the trader specifies, the fee is measured the way launch #1330 measures it.\n\nI didn't look deeply at the fee arithmetic or the rounding on very small swaps, which belong to other specialists' areas.","treeHash":null,"usage":{"cachedInputTokens":850412,"inputTokens":26,"model":"claude-opus-5-5","outputTokens":16627,"runtime":"claude","turns":13,"wallClockMs":203754}},{"artifacts":[],"attempt":1,"bundleHash":"b4b9e0e7bb37adfa6b8046b8ef650f8bd6e2e431aba2fee2b8c5152758b0b2f5","device":"da72e21237e01c8a","findings":[],"hash":"cd2e40092addf0a1d0e40ebfa6b14db79259e9052b4d2e44a446129e179a05f3","nodeId":"3cf1beb8-9d83-496e-a610-ed07321368e9","outcome":"completed","summary":"Created [launch.json](/Users/zhangtao/.identitymd-seat3/work/f3e8de87-f658-42cc-bdd2-6ddc9ba0288a/3cf1beb8-9d83-496e-a610-ed07321368e9/launch.json) with matching contract names, constructor arguments, five permissions and required pool settings.\n\nManifest checks passed. `forge build` succeeded with existing warnings; all 63 tests passed. Only `launch.json` is changed for submission.","treeHash":"f5750fa390176c3e2b7cc3acb642dc62a593999f","usage":{"cachedInputTokens":378112,"inputTokens":64280,"model":"gpt-6-astra","outputTokens":4023,"runtime":"codex","turns":3,"wallClockMs":121235}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"918f8261a6fd589c","findings":[],"hash":"d3031c63ce48d016e4cd1fcf90dbd61528415daef8dc5fe4e6c68c0ac1c49606","nodeId":"c9e5ce03-c602-4670-af9b-f2c4fd903213","outcome":"completed","summary":"Review complete. The findings file holds an empty findings list with a 15-row coverage record, and no tracked file was changed.\n\n**The medium finding is fixed.** The author implemented exactly the fix I proposed: a `deferredBurn` counter records the burn half of fees still pending as claims, the burn sends only `burnOwed - deferredBurn`, book value and tender payouts subtract only that part, and redemption releases the deferred half first. I re-ran my attached proof on the revised code. It fails only at my own assertion that the dead address holds 0.5 IMD right after the deferred buy, which was the defective state the finding complained about, so the author's dispute of that assertion is correct. With those three lines removed both tests pass and book value is unchanged across the deferred buy. Independent scratch checks also pass:\n\n- A third-party claim redeemed ahead of a deferred fee pays the deferred burn half first. Book value does not move, and the dead address ends with exactly one burn half once all claims are redeemed.\n- A full tender of all PERK inside the deferral window pays the whole float on hand, and the burn half still reaches the dead address once after the redeem.\n\n**The two advisory items are dropped.** The fixed 0.5 IMD Intake price follows the brief's wording and the no-admin rule, and the README now states plainly that a repriced or moved Intake ends asks with nothing spent. The pushed-claim behaviour of pendingFloat is documented in the natspec and README and only raises book value.\n\n**Nothing new.** The full suite passes with the author's fuzz seed, 74 tests including the invariant suite. The manifest's constructor arguments, permissions, fee, tick spacing, price and notes length all check out against the code.","treeHash":null,"usage":{"cachedInputTokens":983061,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":14517,"runtime":"claude","turns":20,"wallClockMs":261190}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ca080fd306399669","findings":[{"citation":"resolved","description":"When `_collect` cannot take the fee from the PoolManager (manager IMD balance below the fee, or the transfer refused) it mints an ERC-6909 claim and still calls `_account`, which adds `fee / 2` to `burnOwed` (line 524). `afterSwap` then calls `_burn()`, which sends `min(burnOwed, balance)` where `balance` is everything the hook holds, i.e. the float that backs tenders. The IMD behind the deferred fee is still inside the PoolManager, so the burn is funded by the float: `floatBalance()` and `bookValuePerPerk()` fall by `min(fee/2, float)` the moment the fee is taken and only recover when someone calls `redeemPendingFees()` (or a later swap flushes the claim). Seam: boundary (deferral path) x invariant (README line 128 and testFuzz_bookValueNeverFallsFromDonationsOrFees claim book value rises only from the float; brief: tender pays book value). In the window a tender pays below book value or reverts with ZeroPayout, and `ask()` reverts FloatTooSmall. Deferral is reachable at will: any swapper can `take` all IMD out of the manager inside its own unlock before swapping (settling at the end), and it also happens naturally early in the launch whenever a buy's fee exceeds the manager's IMD (fee 200 vs manager 148.5 in the natural reproduction). The final accounting is correct once redeemed and the attacker never profits (the fee costs more than the victim's shortfall), so impact is a transient broken guarantee plus griefing of ask()/tender. Suggested fix: track the burn halves of unredeemed claims (e.g. `deferredBurn += fee / 2` on deferral, decreased pro rata on `_payPending` and on the flush in `_collect`), have `_burn` send only `min(burnOwed - deferredBurn, balance)` and `floatBalance()` subtract only `burnOwed - deferredBurn`. This keeps the brief's counting (burnOwed still includes the deferred half) and the one-time split; alternatively split at redemption instead of at deferral, which is a scope decision against the brief's wording.","line":510,"path":"src/PerkVaultHook.sol","reproduction":"Fixture: PoolManager, PerkToken, mock IMD, hook bound by the factory, full-range liquidity 1e24 at price 1:1. (1) Buy exact-in 1000 IMD via PoolSwapTest: fee 10 paid, dead = 5 IMD, hook balance 5, floatBalance 5, bookValuePerPerk = 5e18*1e18/1e27 = 5e9, quoteTender(1e24) = 5e21. (2) A contract calls poolManager.unlock, inside it takes the manager's whole IMD balance to itself, swaps exact-in 1000 IMD (same direction), then settles its IMD and takes its PERK. In afterSwap `available` = 0 < fee 10, so the fee is minted as a claim; `_account` sets burnOwed = 5; `_burn` sends min(5, hook balance 5) = 5 of the FLOAT to dEaD. Actual state after (2): pendingFloat 10, burnOwed 0, hook balance 0, dead 10, floatBalance 0, bookValuePerPerk 0, tender(1e24) reverts ZeroPayout, quoteTender(1e24) = 0. Expected: floatBalance still 5, book value unchanged (5e9) until the claim is redeemed, since a fee must never lower book value and the deferred IMD is not in the hook yet. After redeemPendingFees(): hook balance 10, floatBalance 10, i.e. the dip was transient. Natural variant (no attacker): PERK-only liquidity 1e24; buy 100 (deferred), buy 50 (paid, flushes; float 0.75, manager 148.5); buy 20000: fee 200 > 148.5 so deferred, burnOwed 100 -> _burn sends the whole 0.75 float; floatBalance 0, pendingFloat 200, burnOwed 99.25; ask() reverts FloatTooSmall(0) although 100.75 IMD of float is owed to the hook. Run: forge test --match-path test/scratch/DeferredFeeBookValue.t.sol (fails on this code at 'the float that backed tenders was spent on the burn: 0 < 5000000000000000000').","severity":"low","snippet":"        uint256 balance = imd.balanceOfSelf();\n        amount = owed < balance ? owed : balance;","title":"Deferred fee's burn half is paid out of the float the hook already holds, so a fee lowers book value and underpays tenders until the claim is redeemed"},{"citation":"resolved","description":"`feeFor` truncates: any swap whose IMD leg is below 100 wei pays no hook fee, and every swap underpays by up to 1 wei. This is #1330's measurement (the brief asks for it unchanged) and the README documents 'rounding is down', so it is recorded for completeness only: splitting a trade into sub-100-wei pieces costs far more gas than the fee saved, and the exact-in basis (1% of the gross input, the pool moving input minus fee) is also #1330's. No action needed unless the requester wants fees rounded up.","line":272,"path":"src/PerkVaultHook.sol","reproduction":"Full-range liquidity 1e24. _buyExactIn(99): FloatCollected not emitted, totalFloat/burnOwed/totalBurned unchanged (fee = 99*100/10000 = 0); _sellExactOut(99): same. Verified in test/scratch/Probe.t.sol::test_subHundredWeiSwapPaysNoFee. Exact-in buy of 10000 wei: fee 100, pool moves 9900 (test_exactInFeeBasisIsGross).","severity":"info","snippet":"        return imdAmount * FEE_BPS / BPS_DENOMINATOR;","title":"Fee rounds down to zero for IMD legs below 100 wei (fee rounds in the swapper's favour, not up)"}],"hash":"f42c69e27e35db8e10919d4be00b5c1310b527bdd1881cb9e1364a10da402c03","nodeId":"373a3a24-4ef9-482e-b730-12806ae585de","outcome":"completed","summary":"The review is complete. Both extra probes passed, so no further defects surfaced. The workspace holds only the findings file and the scratch directory.\n\n**Outcome.** The math in the PERK hook is sound in every place I traced except one seam at the fee-deferral boundary. The findings file at `.imd-findings.json` holds 2 findings and 15 coverage rows covering all 12 listed entry points plus three invariants.\n\n**Finding 1, low: a deferred fee burns from the float.** When the PoolManager cannot pay the fee during the swap, the hook mints an ERC-6909 claim but still adds half the fee to the burn debt, then immediately pays that debt from whatever IMD it already holds, which is the float backing tenders. Book value and the tender quote fall by half the deferred fee until anyone redeems the claim. In the reproduction the float goes from 5 IMD to 0 and a tender that would have paid 5e21 wei reverts with ZeroPayout. In the natural early-launch variant a 20000 IMD buy on a pool holding 148.5 IMD wipes a 0.75 IMD float and makes ask() revert. Any swapper can force deferral by taking the manager's IMD inside its own unlock before swapping. The accounting is correct once redeemed and the attacker never profits, so I rated it low with a self-contained proof test that fails on the current code. The suggested fix tracks burn halves of unredeemed claims and excludes them from both the burn and the float, keeping the brief's counting.\n\n**Finding 2, info: fees truncate to zero under 100 wei.** This matches #1330 and the README, recorded for completeness only.\n\n**What held.** The simulation-based quote for IMD-specified swaps is consistent with the real swap for full and short fills in both directions, which I verified by reasoning about the floor arithmetic and by fuzzing all four modes with price limits from 1 to 20000 ticks. The exact-in fee basis is the gross input, identical to #1330's code, which I downloaded and compared. Tender rounding never pays above pro-rata and never lowers book value. The split is applied exactly once across paid, deferred, flushed and redeemed paths. Donated ERC-6909 claims and max-uint tender amounts cause no breakage. Casts and intermediate products stay within bounds given the pool's int128 limits.\n\n**Not reached.** The live IMD token's exact transfer semantics on Robinhood Chain and the live Intake's pricing behaviour, which sit outside the math area and need a fork.","treeHash":null,"usage":{"cachedInputTokens":2712281,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":58730,"runtime":"claude","turns":35,"wallClockMs":838864}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8f08088e7a7f557f","findings":[{"citation":"resolved","description":"When the PoolManager cannot pay a fee inside the swap (its IMD balance is below the fee: the launch pool's first buys, any buy whose fee exceeds the manager's IMD, or IMD refusing the hook), _collect mints the whole fee as an ERC-6909 claim and still calls _account(key, fee, false), which adds fee/2 to burnOwed at once (line 524). afterSwap then runs _burn(), which sends min(burnOwed, balance) of the IMD the hook already holds to 0x...dEaD (lines 510-513). That IMD is the insurance float earned earlier (or donated), not the fee that was just deferred, which is still inside the PoolManager. floatBalance() (line 255) and bookValuePerPerk() therefore fall by min(fee/2, float) the moment the fee is taken, and only recover when someone calls redeemPendingFees() or a later paid swap flushes the claim. In that window tender() pays amount*(balance-burnOwed)/circulating, below the vault's real book value (the shortfall stays with the remaining holders for good), or reverts ZeroPayout when the deferred burn half covers the whole float, and ask() reverts FloatTooSmall although the float is owed to the hook. The brief says book value rises only from the float and that donations only raise it; here a swap lowers it. Deferral is reachable at will by any swapper, not only on a fresh manager: inside its own unlock it can take the manager's entire IMD balance to itself before swapping and settle everything at the end, so afterSwap sees imd.balanceOf(poolManager) == 0 and defers. The final accounting is correct once redeemed and the attacker gains nothing directly, so this is a broken guarantee plus front-run griefing of tenders and asks rather than a drain. Reported by audit_flow, audit_economics, audit_math and the test writer with the same root cause; merged here. Minimal fix that keeps the brief's one-time split: track the burn halves of unredeemed claims separately (deferredBurn += fee/2 on the mint path, moved into burnOwed in _payPending and in _collect's flush), have _burn() send only min(burnOwed - deferredBurn, balance) and floatBalance() subtract only burnOwed - deferredBurn.","line":614,"path":"src/PerkVaultHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {ModifyLiquidityParams, SwapParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {PoolSwapTest} from \"v4-core/src/test/PoolSwapTest.sol\";\nimport {PoolModifyLiquidityTest} from \"v4-core/src/test/PoolModifyLiquidityTest.sol\";\n\nimport {PerkVaultHook} from \"src/PerkVaultHook.sol\";\nimport {PerkToken} from \"src/PerkToken.sol\";\n\ncontract Imd {\n    string public constant name = \"IMD\";\n    string public constant symbol = \"IMD\";\n    uint8 public constant decimals = 18;\n    uint256 public totalSupply;\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    constructor(uint256 supply) {\n        totalSupply = supply;\n        balanceOf[msg.sender] = supply;\n    }\n\n    function approve(address s, uint256 a) external returns (bool) {\n        allowance[msg.sender][s] = a;\n        return true;\n    }\n\n    function transfer(address to, uint256 a) external returns (bool) {\n        return _move(msg.sender, to, a);\n    }\n\n    function transferFrom(address f, address to, uint256 a) external returns (bool) {\n        if (allowance[f][msg.sender] != type(uint256).max) allowance[f][msg.sender] -= a;\n        return _move(f, to, a);\n    }\n\n    function _move(address f, address to, uint256 a) internal returns (bool) {\n        require(balanceOf[f] >= a, \"bal\");\n        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\n/// @notice A deferred fee lowers `bookValuePerPerk` until someone redeems the pending claim.\ncontract DeferredFeeLowersBookValueTest is Test {\n    uint160 constant SQRT_PRICE_1_1 = 79228162514264337593543950336;\n    uint160 constant FLAGS = (1 << 13) | (1 << 7) | (1 << 6) | (1 << 3) | (1 << 2);\n    address constant DEAD = 0x000000000000000000000000000000000000dEaD;\n\n    PoolManager manager;\n    PoolSwapTest swapRouter;\n    PoolModifyLiquidityTest lpRouter;\n    Imd imd;\n    PerkToken perk;\n    PerkVaultHook hook;\n    PoolKey key;\n    address factory = makeAddr(\"factory\");\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        swapRouter = new PoolSwapTest(manager);\n        lpRouter = new PoolModifyLiquidityTest(manager);\n        imd = new Imd(1e30);\n        perk = new PerkToken();\n\n        bytes memory code = abi.encodePacked(\n            type(PerkVaultHook).creationCode, abi.encode(address(manager), address(perk), address(imd), factory)\n        );\n        bytes32 initHash = keccak256(code);\n        for (uint256 i = 0;; i++) {\n            address predicted =\n                address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash)))));\n            if (uint160(predicted) & ((1 << 14) - 1) == FLAGS) {\n                bytes32 salt = bytes32(i);\n                address at;\n                assembly {\n                    at := create2(0, add(code, 0x20), mload(code), salt)\n                }\n                hook = PerkVaultHook(at);\n                break;\n            }\n        }\n        bool perkIsZero = address(perk) < address(imd);\n        key = PoolKey({\n            currency0: Currency.wrap(perkIsZero ? address(perk) : address(imd)),\n            currency1: Currency.wrap(perkIsZero ? address(imd) : address(perk)),\n            fee: 12_500,\n            tickSpacing: 60,\n            hooks: IHooks(address(hook))\n        });\n        vm.prank(factory);\n        manager.initialize(key, SQRT_PRICE_1_1);\n\n        imd.approve(address(swapRouter), type(uint256).max);\n        perk.approve(address(lpRouter), type(uint256).max);\n        imd.approve(address(lpRouter), type(uint256).max);\n        // Launch shape: PERK-only liquidity, no IMD in the manager.\n        lpRouter.modifyLiquidity(\n            key,\n            ModifyLiquidityParams(\n                perkIsZero ? int24(60) : TickMath.minUsableTick(60),\n                perkIsZero ? TickMath.maxUsableTick(60) : int24(-60),\n                1e24,\n                bytes32(0)\n            ),\n            \"\"\n        );\n        assertEq(imd.balanceOf(address(manager)), 0);\n    }\n\n    function test_deferredFeeDoesNotLowerBookValue() public {\n        // The float holds 10 IMD (a donation; the same happens with float from earlier paid fees).\n        imd.transfer(address(hook), 10 ether);\n        uint256 bvBefore = hook.bookValuePerPerk();\n        assertEq(bvBefore, 10 ether * 1e18 / perk.totalSupply());\n\n        // First buy on the launch pool: the manager holds no IMD yet, so the 1 IMD fee is deferred.\n        bool zeroForOne = address(perk) > address(imd);\n        swapRouter.swap(\n            key,\n            SwapParams(zeroForOne, -100 ether, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        assertEq(hook.pendingFloat(), 1 ether, \"fee deferred\");\n        assertEq(imd.balanceOf(DEAD), 0.5 ether, \"burn half paid out of the pre-existing float\");\n        assertEq(hook.burnOwed(), 0);\n        assertEq(imd.balanceOf(address(hook)), 9.5 ether);\n\n        // Expected: book value never falls from a fee. Actual: it fell by 0.5 IMD / supply.\n        emit log_named_uint(\"book value before the swap\", bvBefore);\n        emit log_named_uint(\"book value after the swap \", hook.bookValuePerPerk());\n        assertGe(hook.bookValuePerPerk(), bvBefore, \"a deferred fee lowered book value\");\n    }\n\n    function test_afterRedeemBookValueIsAboveTheStart() public {\n        imd.transfer(address(hook), 10 ether);\n        uint256 bvBefore = hook.bookValuePerPerk();\n        bool zeroForOne = address(perk) > address(imd);\n        swapRouter.swap(\n            key,\n            SwapParams(zeroForOne, -100 ether, zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),\n            PoolSwapTest.TestSettings(false, false),\n            \"\"\n        );\n        hook.redeemPendingFees();\n        assertEq(imd.balanceOf(address(hook)), 10.5 ether);\n        assertGt(hook.bookValuePerPerk(), bvBefore, \"once redeemed the float is whole again\");\n    }\n}","reproduction":"Fresh PoolManager, PERK/IMD pool bound by the factory, PERK-only liquidity 1e24 (manager IMD balance 0). (1) imd.transfer(hook, 10 ether): bookValuePerPerk() = 10e18*1e18/1e27 = 10000000000, floatBalance() = 10e18. (2) buy exact-in 100 ether IMD through PoolSwapTest: the 1 IMD fee cannot be taken (manager IMD is 0 at afterSwap), so pendingFloat() = 1e18, burnOwed is set to 0.5e18 and _burn immediately sends 0.5 IMD of the donated float to 0x...dEaD. Expected: bookValuePerPerk() >= 10000000000 (a fee never lowers book value). Actual: hook holds 9.5 IMD, burnOwed 0, bookValuePerPerk() = 9500000000; quoteTender(1e24) = 0.0095e18 instead of 0.01e18. (3) redeemPendingFees(): hook holds 10.5 IMD, book value 10500000000. The proof test fails on this code with 'a deferred fee lowered book value: 9500000000 < 10000000000'. At-will variant (test/scratch/ForcedDeferral.t.sol, passes on this code): full-range liquidity 1e24, buy exact-in 1000 IMD (fee 10 paid, hook float 5 IMD, book value > 0); a contract unlocks the manager, takes its whole IMD balance, swaps exact-in 1000 IMD, then settles. Result: pendingFloat 10e18, hook IMD balance 0, floatBalance 0, bookValuePerPerk 0, tender(1e24) reverts ZeroPayout, and ask() would revert FloatTooSmall(0); after redeemPendingFees() the hook holds 10 IMD and book value is above its pre-attack value. Natural variant without a donation: PERK-only liquidity, buy 100 (deferred), buy 50 (paid, flushes; float 0.75, manager 148.5 IMD), buy 20000 (fee 200 > 148.5, deferred): _burn sends the whole 0.75 float, floatBalance 0, ask() reverts FloatTooSmall(0).","severity":"medium","snippet":"            poolManager.mint(address(this), imd.toId(), fee);\n            _account(key, fee, false);","title":"Deferred fee's burn half is paid out of the float the hook already holds, so a swap lowers book value, underpays or blocks tenders and can block ask() until the claim is redeemed"},{"citation":"resolved","description":"INTAKE, ACTION and ASK_PRICE are constants (lines 68-74) and the hook has no owner, as the brief requires. The oracle-consumer reference states the Intake's action id, asset and price are owner-settable on the Intake side ('a moved price must not need a redeploy') and that the live Intake pulls exactly `amount`, so a request with the wrong amount is refused. If Intake.priceOf(ACTION, imd) ever differs from 0.5e18 or the action is superseded, every ask() reverts for the life of the hook: lastAsk is never set, totalAsked stays at its value, and the 'paying customer of the swarm' feature ends while the float keeps accruing. No IMD is lost and tender and burn are unaffected, so this is a trust assumption the brief chose (fixed price, no admin) recorded for the author to decide knowingly, not a coding slip. Reported by audit_permissions, audit_flow and audit_economics; merged here. A fix that keeps 'no admin' and the 0.5 IMD ceiling: read INTAKE.priceOf(ACTION, Currency.unwrap(imd)) in ask(), revert only if it exceeds a ceiling the float can cover, and approve and pay that price; or document in the README that a repriced oracle ends the asks.","line":317,"path":"src/PerkVaultHook.sol","reproduction":"Fixture test/utils/PerkVaultBase.sol. Etch at 0xa43e6F75ee006411F79Ac1C84120606C2330DE82 an Intake whose priceOf(action, imd) returns 0.6e18 and whose request requires amount == price (the live Intake pulls exactly amount). imd.transfer(hook, 5 ether) so floatBalance() >= 0.6 IMD. Call hook.ask(). Expected: the vault buys an answer at the current price. Actual: _approve(imd, INTAKE, 0.5e18) then INTAKE.request(..., imd, 0.5e18) reverts 'Intake: wrong price' and ask() reverts. vm.warp(+30 days), hook.ask() reverts again; totalAsked() == 0 and lastAsk() == 0. test/scratch/AskPrice.t.sol test_askIsDeadForeverOnceTheIntakeReprices passes on this code, reproducing the dead end.","severity":"low","snippet":"        _approve(imd, address(INTAKE), ASK_PRICE);\n        requestId = INTAKE.request(\n            ACTION, bytes(BODY), IIntake.Callback(address(0), bytes4(0)), Currency.unwrap(imd), ASK_PRICE\n        );","title":"ask() hard-codes the Intake, action and 0.5 IMD price; a repriced or moved Intake leaves the vault unable to buy answers for good, with no owner or fallback path"},{"citation":"resolved","description":"pendingFloat() is the hook's ERC-6909 IMD balance in the PoolManager. Anyone who pays IMD into the manager inside an unlock can mint such a claim to the hook; it then shows in pendingFloat(), is redeemed by redeemPendingFees() or flushed by _collect like a deferred fee, and sits in the hook as float with totalFloat, burnOwed and totalBurned unchanged, so it behaves exactly like a direct IMD donation and only raises book value. The brief's 'pendingFloat is the deferred part' is therefore only true when nobody pushes claims. No value is at risk and the existing test test_claimsPushedOntoTheHookBehaveAsADonation pins the behaviour. If the counter is meant to be exact, track the deferred fee amount in storage (incremented on the mint path, decremented in _payPending and the flush) instead of reading the ERC-6909 balance. Reported by the test writer; kept as information only.","line":248,"path":"src/PerkVaultHook.sol","reproduction":"Fixture test/utils/PerkVaultBase.sol with a ClaimMinter contract: inside poolManager.unlock it syncs IMD, transfers 3e18 IMD to the manager, settles, and calls manager.mint(hook, imd.toId(), 3e18). Afterwards hook.pendingFloat() == 3e18 with no fee taken and bookValuePerPerk() unchanged. hook.redeemPendingFees() returns 3e18, the hook's IMD balance rises by 3e18, totalFloat, burnOwed and totalBurned are unchanged and bookValuePerPerk() rises. Expected per the brief's wording: pendingFloat counts only deferred fees. Actual: it includes the pushed claim. See test/PerkVaultHookAdversarial.t.sol test_claimsPushedOntoTheHookBehaveAsADonation (passes on this code).","severity":"info","snippet":"        return poolManager.balanceOf(address(this), imd.toId());","title":"pendingFloat() reports any IMD ERC-6909 claim pushed onto the hook, not only deferred fees"}],"hash":"f5347250146019e62b9e84273bd0f804a169349ba9e8a9104bea9670e378e3ab","nodeId":"c9e5ce03-c602-4670-af9b-f2c4fd903213","outcome":"completed","summary":"The review is complete. The findings file is at `.imd-findings.json` and no tracked file was changed.\n\n**Outcome.** The six specialist reports collapse into two real defects and one note. I reproduced each against the code with Foundry before keeping it, and the existing suite of 68 tests passes unchanged.\n\n**Findings kept**\n\n- **Medium, merged from four reports:** a deferred fee's burn half is burned out of the float the hook already holds. When the PoolManager cannot pay a fee inside the swap, the hook mints a claim but still adds half the fee to burnOwed, and the burn that follows spends existing float. Book value falls, a tender in that window underpays or reverts, and ask can revert, until someone redeems. The specialist proof fails on this code as stated. I also showed the deferral is reachable at will: any swapper can take the manager's IMD inside its own unlock before swapping, which zeroes book value for the window. The proof file is attached to the finding.\n- **Low, merged from three reports:** ask hard-codes the Intake price at 0.5 IMD with no owner, so a repriced or moved Intake ends the weekly report permanently. Verified with a repriced Intake stand-in. This follows the brief's no-admin rule, so it is recorded as a trust decision for the author.\n- **Info:** pendingFloat counts any IMD claim pushed onto the hook, not only deferred fees. Harmless, behaves as a donation.\n\n**Dropped.** The sub-100-wei fee rounding note matches launch #1330's measurement and the README, so it is not a defect.\n\n**Coverage.** All 12 entry points are answered, plus three invariant rows and the manifest. Nine hold. afterSwap and tender carry finding 1, ask carries finding 2. The manifest's constructor arguments, permissions, fee, and notes length check out, and the hook's runtime size is within the limit.","treeHash":null,"usage":{"cachedInputTokens":1891232,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":25967,"runtime":"claude","turns":32,"wallClockMs":398341}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"077d2937780a81bc","findings":[{"citation":"resolved","description":"When the PoolManager cannot pay a fee at once (its IMD balance is below the fee, as on a fresh manager or any buy larger than 100x the IMD the manager holds), _collect mints an ERC-6909 claim and _account still adds fee/2 to burnOwed. afterSwap then runs _burn(), which sends min(burnOwed, balance) of the IMD the hook already holds to 0x...dEaD. That IMD is the insurance float earned by earlier swaps, not the fee that was just deferred. Until redeemPendingFees() (or the next fee-paying swap's flush) brings the claim in, floatBalance(), bookValuePerPerk() and quoteTender() are understated by up to fee/2, tender() pays less than the book value the vault will have one call later or reverts ZeroPayout when the float was smaller than fee/2, and ask() can read FloatTooSmall. No IMD is lost and redeem is permissionless, so this is a temporary mis-statement of book value rather than a drain; but 'a tender leaves book value unchanged' and 'book value rises only from the float' are both visibly false inside the window: book value falls after the deferring swap and jumps after the redeem. The brief's ordering (count both halves at deferral, burn whenever the hook holds IMD) produces this; a fix that keeps the brief would make _burn() pay only from IMD that has actually arrived, e.g. cap the burn at balance minus the float share of pending claims, or defer the burn half together with the float half and account both on redeem.","line":614,"path":"src/PerkVaultHook.sol","reproduction":"Fresh PoolManager, launch pool, full-range liquidity of 1000e18. (1) sell 100 PERK exact-in: fee paid directly; hook float = 449374288964732651 wei (0.449 IMD), burnOwed = 0, manager IMD = 910125142207053469799. (2) buy exact-in with IMD = manager IMD * 100 + 100e18 = 91112514220705346979900: fee = 911125142207053469799 > manager balance, so it is deferred: pendingFloat = 911125142207053469799, burnOwed = 455113196814562002248 after _burn() sent the hook's whole 0.449 IMD to 0x...dEaD. Now floatBalance() = 0, bookValuePerPerk() = 0, quoteTender(1000e18) = 0 and tender(1000e18) reverts ZeroPayout, although the vault is owed 455.5 IMD of float. (3) anyone calls redeemPendingFees(): quoteTender(1000e18) = 456011945392491 and bookValuePerPerk() is above its pre-swap value. Expected per the brief: book value never falls, and a holder tendering between (2) and (3) is paid at book value; actual: it falls to zero and the tender reverts or underpays. Scratch test: test/scratch/Econ.t.sol test_deferredFeeBurnHalfIsPaidFromExistingFloat.","severity":"low","snippet":"            poolManager.mint(address(this), imd.toId(), fee);\n            _account(key, fee, false);","title":"Deferred fee: the burn half is paid out of the float that already exists, so book value and tender payouts drop until someone redeems the pending claim"},{"citation":"resolved","description":"The oracle-consumer reference the launch is checked against says the intake, action id, asset and price must be constructor input and owner-settable, never a constant, because a new action version (oracle-2) or a moved price must not need a redeploy. The hook follows the brief instead: INTAKE, ACTION and ASK_PRICE are constants and there is no owner. If the Intake ever prices oracle.request@oracle-1 in IMD above 0.5, stops selling that action, or is superseded, every ask() reverts for the life of the hook: lastAsk is never set, the 7-day window never opens, and the float accumulates with no buyer. Nothing is lost (tender and burn keep working), but the product's 'paying customer of the swarm' feature is dead with no recovery. This is a design trade-off the brief chose (no admin keys); it is reported so the author decides it knowingly. A fix that keeps 'no admin' and the fixed 0.5 ceiling: read INTAKE.priceOf(ACTION, imd) in ask(), revert only if it exceeds a ceiling, and approve and pay that price; or document that a repriced oracle ends the asks.","line":318,"path":"src/PerkVaultHook.sol","reproduction":"State: pool funded, hook floatBalance() >= 0.6 IMD. Replace the code at 0xa43e6F75ee006411F79Ac1C84120606C2330DE82 with an Intake whose priceOf(action, imd) returns 0.6e18 and whose request requires amount >= priceOf (the live intake pulls exactly `amount`; an underpaid request is refused). Call ask(): reverts (Underpaid). Warp 30 days, call ask(): reverts again; totalAsked stays 0 forever. Expected: a consumer can follow a price move; actual: no code path can. Scratch test: test/scratch/Econ.t.sol test_askBreaksForeverWhenIntakePriceMoves.","severity":"low","snippet":"        requestId = INTAKE.request(\n            ACTION, bytes(BODY), IIntake.Callback(address(0), bytes4(0)), Currency.unwrap(imd), ASK_PRICE\n        );","title":"ask() hard-codes the Intake, action and 0.5 IMD price; any change at the oracle side breaks the weekly report permanently, with no owner or redeploy path"}],"hash":"fae8b0aa55be2d3355108c25b4f4c7c0f4a729fb2f80c3881330c3e4d5fd3b07","nodeId":"962b3a35-0da7-4371-bf3b-201961b7c5a4","outcome":"completed","summary":"The review is complete. The findings file is at `.imd-findings.json` in the repository root, with two low findings and a coverage row for all twelve entry points plus four invariants. No source files were changed. The only files I wrote are that file and `test/scratch/Econ.t.sol`, which the gitignore excludes.\n\n**What I covered.** I read the hook against the Economic Security, Invariant and Asymmetry (flow gap) guides, traced every IMD and PERK movement, and checked the fee netting in all four swap modes, the simulation path, the ERC-6909 deferral and redemption, tender rounding, the ask flow, and the binding. The existing suite of 63 tests passes. I added a scratch fuzz for exact-output sells against a price limit, which confirmed the fee is 1% of the specified amount on a full fill and 1% of the moved amount on a partial fill, and found no gap.\n\n**Findings, both low.**\n\n- **Deferred fee burns pre-existing float.** When the manager cannot pay a fee at once, half of it is added to burnOwed and the burn then sends IMD the vault already held to the dead address. In my reproduction the vault's prior float dropped to zero, book value read zero, and a tender would have reverted until someone called redeem. No IMD is lost and redeem is permissionless, but book value visibly falls and then jumps, against the brief's guarantee. The brief's own ordering produces this, so I framed the fix as an author decision.\n- **Hard-coded Intake price.** The oracle-consumer reference says intake, action and price must never be constants because a repriced oracle must not need a redeploy. The hook follows the brief instead and has no owner. With a mock intake priced at 0.6 IMD, every ask reverts forever. Nothing is lost, but the weekly report feature cannot be revived. Reported as a knowing trade-off.\n\n**What I did not find.** No extractable value: tender never pays above pro rata and never lowers book value, donations only raise it, fees cannot be bypassed by any caller including the hook itself, swaps cannot be blocked by token refusals, and the request body matches the brief byte for byte. Neither finding carries a proof file, since neither is critical or high.","treeHash":null,"usage":{"cachedInputTokens":1372097,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":46423,"runtime":"claude","turns":26,"wallClockMs":682722}}],"verification":[{"checks":[{"durationMs":3513,"exitCode":0,"name":"build","output":"Compiling 85 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.42s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> test/mocks/MockIntake.sol:30:5:\n   |\n30 |     function priceOf(bytes32, address) external view returns (uint256) {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":3189,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/PerkToken.t.sol:PerkTokenTest\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 70169, ~: 70722)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 115230)\n[PASS] test_metadata() (gas: 23878)\n[PASS] test_mintsTheWholeSupplyToTheDeployerOnce() (gas: 18436)\n[PASS] test_noMintOrAdminPath() (gas: 244863)\n[PASS] test_transferFromRespectsAllowance() (gas: 242791)\n[PASS] test_transferMovesExactlyWhatItWasAsked() (gas: 74895)\n[PASS] test_transferRevertsOnInsufficientBalanceAndZeroRecipient() (gas: 60645)\n[PASS] test_transferToDeadIsAnOrdinaryTransfer() (gas: 64992)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 4.58ms (3.11ms CPU time)\n\nRan 2 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookReentrancyTest\n[PASS] test_askRewardCannotReenterTender() (gas: 1327808)\n[PASS] test_tenderPayoutCannotReenterTender() (gas: 880878)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.54ms (1.73ms CPU time)\n\nRan 3 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookBlockedDeadTest\n[PASS] test_askRunsTheBurnFirstAndCountsOnlyTheFloat() (gas: 1679712)\n[PASS] test_burnRefusedByImdStaysOwedAndDoesNotRevertTheSwap() (gas: 1220252)\n[PASS] test_feeDeferredWhenImdRefusesTheHook() (gas: 1063403)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 47.76ms (3.22ms CPU time)\n\nRan 4 tests for test/PerkVaultHookAdversarial.t.sol:PerkVaultHookAdversarialTest\n[PASS] test_claimsPushedOntoTheHookBehaveAsADonation() (gas: 1485853)\n[PASS] test_doctoredKeysAreRefusedBySwapCallbacks() (gas: 75295580)\n[PASS] test_managerAlwaysCoversPendingClaimsSoRedeemIsWhole() (gas: 1535809)\n[PASS] test_perkClaimsPushedOntoTheHookAreIgnored() (gas: 994348)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 148.20ms (51.23ms CPU time)\n\nRan 49 tests for test/PerkVaultHook.t.sol:PerkVaultHookTest\n[PASS] testFuzz_bookValueNeverFallsFromDonationsOrFees(uint256,uint256) (runs: 256, μ: 1038559, ~: 1036711)\n[PASS] testFuzz_buyExactInputWithLimit_feeIsOnePercentOfTheFill(uint256,uint24) (runs: 256, μ: 877660, ~: 880695)\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 866946, ~: 869815)\n[PASS] testFuzz_buyExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 816460, ~: 819266)\n[PASS] testFuzz_imdLeavesOnlyByTenderBurnAndAsk(uint8[12],uint256) (runs: 256, μ: 4101609, ~: 4047487)\n[PASS] testFuzz_sellExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 810350, ~: 815951)\n[PASS] testFuzz_sellExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 854906, ~: 857750)\n[PASS] testFuzz_tenderNeverPaysAboveProRataAndNeverLowersBookValue(uint256,uint256,uint256) (runs: 256, μ: 874683, ~: 941527)\n[PASS] test_askCountsOnlyFloatNotTheBurnHalf() (gas: 740514)\n[PASS] test_askPaysTheIntakeAndTheCallerAndLeavesNoAllowance() (gas: 1479338)\n[PASS] test_askRevertsInsideSevenDaysThenWorks() (gas: 1423214)\n[PASS] test_askRevertsUnderZeroPointSixAndPassesAtExactlyIt() (gas: 718477)\n[PASS] test_askRevertsWhenTheIntakeRefusesAndChangesNothing() (gas: 857308)\n[PASS] test_bindingEmitsPoolBound() (gas: 75000972)\n[PASS] test_buyExactInput_takesOnePercentIntoTheVault() (gas: 897099)\n[PASS] test_buyExactOutput_takesOnePercentIntoTheVault() (gas: 828982)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 241791)\n[PASS] test_constructorRejectsAnAddressWithoutTheRightFlags() (gas: 62750551)\n[PASS] test_constructorRejectsZeroAndEqualAddresses() (gas: 165867517)\n[PASS] test_donationsOnlyRaiseBookValue() (gas: 954752)\n[PASS] test_dustTenderPaysTheRoundedDownWeiAndRaisesBookValue() (gas: 819248)\n[PASS] test_factoryBindsTheLaunchPool() (gas: 22854)\n[PASS] test_firstBuyOnPerkOnlyPool_defersTheFeeAndCountsBothHalvesOnce() (gas: 1118948)\n[PASS] test_firstExactOutputBuyOnPerkOnlyPool_defersTheFee() (gas: 873539)\n[PASS] test_firstTenderPaysBookValueAndBurnsThePerk() (gas: 1008032)\n[PASS] test_halfOfEveryFeeReachesTheDeadAddressOnceAndOnlyOnce() (gas: 2237422)\n[PASS] test_immutablesAndConstants() (gas: 171958)\n[PASS] test_initializeRefusesAFakeTokenWithImd() (gas: 75499898)\n[PASS] test_initializeRefusesANonFactorySender() (gas: 75072325)\n[PASS] test_initializeRefusesAPoolWithoutImdOrWithEth() (gas: 75562352)\n[PASS] test_initializeRefusesASecondPoolAndReinitialization() (gas: 105491)\n[PASS] test_initializeRefusesWrongAndDynamicFees() (gas: 75015215)\n[PASS] test_lastTenderDrainsTheFloatProRata() (gas: 1271944)\n[PASS] test_lpFeeIsUntouchedAndNoPerkIsEverTaken() (gas: 1329362)\n[PASS] test_noPrivilegedFunctions() (gas: 876962)\n[PASS] test_oddFeeRoundsTheBurnDown() (gas: 781064)\n[PASS] test_pendingFeesAreFlushedByTheNextSwapThatFindsImd_withoutDoubleCounting() (gas: 1036000)\n[PASS] test_permissionsMatchTheAddressFlags() (gas: 11228)\n[PASS] test_redeemPendingFeesRevertsWhenNothingIsPending() (gas: 45821)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6118648)\n[PASS] test_sellExactInput_takesOnePercentIntoTheVault() (gas: 818127)\n[PASS] test_sellExactOutput_takesOnePercentIntoTheVault() (gas: 867351)\n[PASS] test_simulateSwapRefusesEveryoneButTheHook() (gas: 68970)\n[PASS] test_swapCallbacksRefuseAnUnboundPool() (gas: 78137)\n[PASS] test_tenderNeedsAllowanceAndBalance() (gas: 974926)\n[PASS] test_tenderRevertsOnZeroPayout() (gas: 792139)\n[PASS] test_tinySwapWithZeroFeeStillSucceeds() (gas: 654655)\n[PASS] test_unusedCallbacksRevertEvenFromTheManager() (gas: 126393)\n[PASS] test_zeroLiquidityChangeGoesThrough() (gas: 772703)\nSuite result: ok. 49 passed; 0 failed; 0 skipped; finished in 343.02ms (1.68s CPU time)\n\nRan 1 test for test/invariants/PerkVaultInvariants.t.sol:PerkVaultInvariantsTest\n[PASS]\nPerkVaultInvariantsTest invariants:\n[PASS] invariant_askAccounting\n[PASS] invariant_bookValueFallsOnlyByAsk\n[PASS] invariant_burnOwedNeverExceedsWhatCouldNotBePaid\n[PASS] invariant_deadHoldsExactlyTotalBurnedAndTotalTendered\n[PASS] invariant_halfOfEveryFeeIsOwedToTheBurnOnce\n[PASS] invariant_imdIsConserved\n[PASS] invariant_imdLeavesOnlyByTenderBurnAndAsk\n[PASS] invariant_managerCoversPendingClaims\n[PASS] invariant_perkSupplyIsFixed\n PerkVaultInvariantsTest invariants (runs: 128, calls: 6400, reverts: 0)\n\n╭------------------+-----------------------+-------+---------+----------╮\n| Contract         | Selector              | Calls | Reverts | Discards |\n+=======================================================================+\n| PerkVaultHandler | addFullRangeLiquidity | 489   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | ask                   | 447   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | burn                  | 487   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | buyExactIn            | 1009  | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | buyExactOut           | 470   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | donate                | 466   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | redeemPendingFees     | 525   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | sellExactIn           | 490   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | sellExactOut          | 492   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | setDeadBlocked        | 518   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | setHookBlocked        | 506   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | tender                | 501   | 0       | 0        |\n╰------------------+-----------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.09s (3.08s CPU time)\n\nRan 6 test suites in 3.09s (3.64s CPU time): 68 tests passed, 0 failed, 0 skipped (68 total tests)\n","passed":true},{"durationMs":52,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PerkToken.approve(address,uint256)\",\"PerkToken.transfer(address,uint256)\",\"PerkToken.transferFrom(address,address,uint256)\",\"PerkVaultHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"PerkVaultHook.ask()\",\"PerkVaultHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"PerkVaultHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"PerkVaultHook.burn()\",\"PerkVaultHook.redeemPendingFees()\",\"PerkVaultHook.simulateSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"PerkVaultHook.tender(uint256)\",\"PerkVaultHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"README.md\":281,\"foundry.toml\":15,\"remappings.txt\":3,\"src/HookFlags.sol\":37,\"src/IIntake.sol\":20,\"src/PerkToken.sol\":72,\"src/PerkVaultHook.sol\":693,\"test/PerkToken.t.sol\":97,\"test/PerkVaultHook.t.sol\":1005,\"test/PerkVaultHookAdversarial.t.sol\":139,\"test/PerkVaultHookImdBehaviour.t.sol\":123,\"test/invariants/PerkVaultHandler.sol\":470,\"test/invariants/PerkVaultInvariants.t.sol\":194,\"test/mocks/BlocklistERC20.sol\":51,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MockIntake.sol\":53,\"test/utils/HookDeployer.sol\":45,\"test/utils/PerkVaultBase.sol\":269},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"0f2b945b16868344c4be5681aa9bd48de5c95f1c34a001c584869bada9fead46","verifiedTreeHash":"d406c274456357ff03e5bb7673493d1756065f44","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":3833,"exitCode":0,"name":"build","output":"Compiling 86 files with Solc 0.8.26\nSolc 0.8.26 finished in 3.75s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> test/mocks/MockIntake.sol:30:5:\n   |\n30 |     function priceOf(bytes32, address) external view returns (uint256) {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":3405,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/PerkToken.t.sol:PerkTokenTest\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 70092, ~: 70722)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 115230)\n[PASS] test_metadata() (gas: 23878)\n[PASS] test_mintsTheWholeSupplyToTheDeployerOnce() (gas: 18436)\n[PASS] test_noMintOrAdminPath() (gas: 244863)\n[PASS] test_transferFromRespectsAllowance() (gas: 242791)\n[PASS] test_transferMovesExactlyWhatItWasAsked() (gas: 74895)\n[PASS] test_transferRevertsOnInsufficientBalanceAndZeroRecipient() (gas: 60645)\n[PASS] test_transferToDeadIsAnOrdinaryTransfer() (gas: 64992)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 2.38ms (3.17ms CPU time)\n\nRan 3 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookBlockedDeadTest\n[PASS] test_askRunsTheBurnFirstAndCountsOnlyTheFloat() (gas: 1693037)\n[PASS] test_burnRefusedByImdStaysOwedAndDoesNotRevertTheSwap() (gas: 1233809)\n[PASS] test_feeDeferredWhenImdRefusesTheHook() (gas: 1083269)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 11.74ms (2.75ms CPU time)\n\nRan 2 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookReentrancyTest\n[PASS] test_askRewardCannotReenterTender() (gas: 1332383)\n[PASS] test_tenderPayoutCannotReenterTender() (gas: 885453)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 39.22ms (2.06ms CPU time)\n\nRan 4 tests for test/PerkVaultHookAdversarial.t.sol:PerkVaultHookAdversarialTest\n[PASS] test_claimsPushedOntoTheHookBehaveAsADonation() (gas: 1497086)\n[PASS] test_doctoredKeysAreRefusedBySwapCallbacks() (gas: 5679392)\n[PASS] test_managerAlwaysCoversPendingClaimsSoRedeemIsWhole() (gas: 1555882)\n[PASS] test_perkClaimsPushedOntoTheHookAreIgnored() (gas: 996654)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 142.20ms (9.35ms CPU time)\n\nRan 55 tests for test/PerkVaultHook.t.sol:PerkVaultHookTest\n[PASS] testFuzz_bookValueNeverFallsFromDonationsOrFees(uint256,uint256) (runs: 256, μ: 1051558, ~: 1049819)\n[PASS] testFuzz_buyExactInputWithLimit_feeIsOnePercentOfTheFill(uint256,uint24) (runs: 256, μ: 879919, ~: 883109)\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 869528, ~: 872207)\n[PASS] testFuzz_buyExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 819152, ~: 821629)\n[PASS] testFuzz_imdLeavesOnlyByTenderBurnAndAsk(uint8[12],uint256) (runs: 256, μ: 4144509, ~: 4064048)\n[PASS] testFuzz_sellExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 812991, ~: 818371)\n[PASS] testFuzz_sellExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 857509, ~: 860164)\n[PASS] testFuzz_swapsNeverLowerBookValueAndDeferredBurnStaysBounded(uint8[12],uint256) (runs: 256, μ: 4663945, ~: 4550881)\n[PASS] testFuzz_tenderNeverPaysAboveProRataAndNeverLowersBookValue(uint256,uint256,uint256) (runs: 256, μ: 885443, ~: 950545)\n[PASS] test_askCountsOnlyFloatNotTheBurnHalf() (gas: 747144)\n[PASS] test_askPaysTheIntakeAndTheCallerAndLeavesNoAllowance() (gas: 1488221)\n[PASS] test_askRevertsInsideSevenDaysThenWorks() (gas: 1430192)\n[PASS] test_askRevertsUnderZeroPointSixAndPassesAtExactlyIt() (gas: 723239)\n[PASS] test_askRevertsWhenTheIntakeRefusesAndChangesNothing() (gas: 861839)\n[PASS] test_bindingEmitsPoolBound() (gas: 5384769)\n[PASS] test_buyExactInput_takesOnePercentIntoTheVault() (gas: 899580)\n[PASS] test_buyExactOutput_takesOnePercentIntoTheVault() (gas: 831485)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 241726)\n[PASS] test_constructorRejectsAnAddressWithoutTheRightFlags() (gas: 77274708)\n[PASS] test_constructorRejectsZeroAndEqualAddresses() (gas: 45588819)\n[PASS] test_deferredFeeDoesNotSpendTheFloatOnHand() (gas: 1158500)\n[PASS] test_donationsOnlyRaiseBookValue() (gas: 965800)\n[PASS] test_dustTenderPaysTheRoundedDownWeiAndRaisesBookValue() (gas: 828065)\n[PASS] test_factoryBindsTheLaunchPool() (gas: 22832)\n[PASS] test_firstBuyOnPerkOnlyPool_defersTheFeeAndCountsBothHalvesOnce() (gas: 1141252)\n[PASS] test_firstExactOutputBuyOnPerkOnlyPool_defersTheFee() (gas: 893549)\n[PASS] test_firstTenderPaysBookValueAndBurnsThePerk() (gas: 1019168)\n[PASS] test_forcedDeferralCannotLowerBookValueOrBlockTendersAndAsks() (gas: 2852391)\n[PASS] test_halfOfEveryFeeReachesTheDeadAddressOnceAndOnlyOnce() (gas: 2251643)\n[PASS] test_immutablesAndConstants() (gas: 174446)\n[PASS] test_initializeRefusesAFakeTokenWithImd() (gas: 5883762)\n[PASS] test_initializeRefusesANonFactorySender() (gas: 5456093)\n[PASS] test_initializeRefusesAPoolWithoutImdOrWithEth() (gas: 5946210)\n[PASS] test_initializeRefusesASecondPoolAndReinitialization() (gas: 105380)\n[PASS] test_initializeRefusesWrongAndDynamicFees() (gas: 5398993)\n[PASS] test_largeDeferredBuyAfterPaidFeesLeavesTheFloatWhole() (gas: 1883695)\n[PASS] test_lastTenderDrainsTheFloatProRata() (gas: 1283044)\n[PASS] test_lpFeeIsUntouchedAndNoPerkIsEverTaken() (gas: 1338184)\n[PASS] test_noPrivilegedFunctions() (gas: 877094)\n[PASS] test_oddFeeRoundsTheBurnDown() (gas: 783412)\n[PASS] test_partialRedemptionReleasesTheBurnFirstAndKeepsTheInvariants() (gas: 1255975)\n[PASS] test_pendingFeesAreFlushedByTheNextSwapThatFindsImd_withoutDoubleCounting() (gas: 1055859)\n[PASS] test_permissionsMatchTheAddressFlags() (gas: 11250)\n[PASS] test_redeemPendingFeesRevertsWhenNothingIsPending() (gas: 45821)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6231151)\n[PASS] test_sellExactInput_takesOnePercentIntoTheVault() (gas: 820675)\n[PASS] test_sellExactOutput_takesOnePercentIntoTheVault() (gas: 869765)\n[PASS] test_simulateSwapRefusesEveryoneButTheHook() (gas: 68792)\n[PASS] test_swapCallbacksRefuseAnUnboundPool() (gas: 78005)\n[PASS] test_tenderAndAskInsideTheDeferralWindowUseTheWholeFloat() (gas: 1464851)\n[PASS] test_tenderNeedsAllowanceAndBalance() (gas: 984175)\n[PASS] test_tenderRevertsOnZeroPayout() (gas: 801594)\n[PASS] test_tinySwapWithZeroFeeStillSucceeds() (gas: 654654)\n[PASS] test_unusedCallbacksRevertEvenFromTheManager() (gas: 126525)\n[PASS] test_zeroLiquidityChangeGoesThrough() (gas: 774875)\nSuite result: ok. 55 passed; 0 failed; 0 skipped; finished in 433.31ms (2.27s CPU time)\n\nRan 1 test for test/invariants/PerkVaultInvariants.t.sol:PerkVaultInvariantsTest\n[PASS]\nPerkVaultInvariantsTest invariants:\n[PASS] invariant_askAccounting\n[PASS] invariant_bookValueFallsOnlyByAsk\n[PASS] invariant_burnOwedNeverExceedsWhatCouldNotBePaid\n[PASS] invariant_deadHoldsExactlyTotalBurnedAndTotalTendered\n[PASS] invariant_halfOfEveryFeeIsOwedToTheBurnOnce\n[PASS] invariant_imdIsConserved\n[PASS] invariant_imdLeavesOnlyByTenderBurnAndAsk\n[PASS] invariant_managerCoversPendingClaims\n[PASS] invariant_perkSupplyIsFixed\n PerkVaultInvariantsTest invariants (runs: 128, calls: 6400, reverts: 0)\n\n╭------------------+-----------------------+-------+---------+----------╮\n| Contract         | Selector              | Calls | Reverts | Discards |\n+=======================================================================+\n| PerkVaultHandler | addFullRangeLiquidity | 489   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | ask                   | 447   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | burn                  | 487   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | buyExactIn            | 1009  | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | buyExactOut           | 470   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | donate                | 466   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | redeemPendingFees     | 525   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | sellExactIn           | 490   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | sellExactOut          | 492   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | setDeadBlocked        | 518   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | setHookBlocked        | 506   | 0       | 0        |\n|------------------+-----------------------+-------+---------+----------|\n| PerkVaultHandler | tender                | 501   | 0       | 0        |\n╰------------------+-----------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.30s (3.28s CPU time)\n\nRan 6 test suites in 3.30s (3.93s CPU time): 74 tests passed, 0 failed, 0 skipped (74 total tests)\n","passed":true},{"durationMs":76,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PerkToken.approve(address,uint256)\",\"PerkToken.transfer(address,uint256)\",\"PerkToken.transferFrom(address,address,uint256)\",\"PerkVaultHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"PerkVaultHook.ask()\",\"PerkVaultHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"PerkVaultHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"PerkVaultHook.burn()\",\"PerkVaultHook.redeemPendingFees()\",\"PerkVaultHook.simulateSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"PerkVaultHook.tender(uint256)\",\"PerkVaultHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"README.md\":310,\"foundry.toml\":15,\"remappings.txt\":3,\"src/HookFlags.sol\":37,\"src/IIntake.sol\":20,\"src/PerkToken.sol\":72,\"src/PerkVaultHook.sol\":723,\"test/PerkToken.t.sol\":97,\"test/PerkVaultHook.t.sol\":1222,\"test/PerkVaultHookAdversarial.t.sol\":139,\"test/PerkVaultHookImdBehaviour.t.sol\":123,\"test/invariants/PerkVaultHandler.sol\":475,\"test/invariants/PerkVaultInvariants.t.sol\":201,\"test/mocks/BlocklistERC20.sol\":51,\"test/mocks/ForcedDeferral.sol\":64,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MockIntake.sol\":53,\"test/utils/HookDeployer.sol\":45,\"test/utils/PerkVaultBase.sol\":269},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"12901a7bfd6df887f6b6935f2997374a3ea706af0a3091f7652741cfce8ef360","verifiedTreeHash":"2d2b8e7953a986bef53ff17e0cda0f93cac7ce3a","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":3071,"exitCode":0,"name":"build","output":"Compiling 83 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.99s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> test/mocks/MockIntake.sol:30:5:\n   |\n30 |     function priceOf(bytes32, address) external view returns (uint256) {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":491,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/PerkToken.t.sol:PerkTokenTest\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 70118, ~: 70734)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 115230)\n[PASS] test_metadata() (gas: 23878)\n[PASS] test_mintsTheWholeSupplyToTheDeployerOnce() (gas: 18436)\n[PASS] test_noMintOrAdminPath() (gas: 244863)\n[PASS] test_transferFromRespectsAllowance() (gas: 242791)\n[PASS] test_transferMovesExactlyWhatItWasAsked() (gas: 74895)\n[PASS] test_transferRevertsOnInsufficientBalanceAndZeroRecipient() (gas: 60645)\n[PASS] test_transferToDeadIsAnOrdinaryTransfer() (gas: 64992)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 2.43ms (2.98ms CPU time)\n\nRan 2 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookReentrancyTest\n[PASS] test_askRewardCannotReenterTender() (gas: 1332383)\n[PASS] test_tenderPayoutCannotReenterTender() (gas: 885453)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 12.14ms (1.34ms CPU time)\n\nRan 3 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookBlockedDeadTest\n[PASS] test_askRunsTheBurnFirstAndCountsOnlyTheFloat() (gas: 1693037)\n[PASS] test_burnRefusedByImdStaysOwedAndDoesNotRevertTheSwap() (gas: 1233809)\n[PASS] test_feeDeferredWhenImdRefusesTheHook() (gas: 1083269)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 13.62ms (2.85ms CPU time)\n\nRan 55 tests for test/PerkVaultHook.t.sol:PerkVaultHookTest\n[PASS] testFuzz_bookValueNeverFallsFromDonationsOrFees(uint256,uint256) (runs: 256, μ: 1051700, ~: 1049819)\n[PASS] testFuzz_buyExactInputWithLimit_feeIsOnePercentOfTheFill(uint256,uint24) (runs: 256, μ: 879705, ~: 883108)\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 869530, ~: 872207)\n[PASS] testFuzz_buyExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 819159, ~: 821769)\n[PASS] testFuzz_imdLeavesOnlyByTenderBurnAndAsk(uint8[12],uint256) (runs: 256, μ: 4108311, ~: 4053249)\n[PASS] testFuzz_sellExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 812997, ~: 818499)\n[PASS] testFuzz_sellExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 857513, ~: 860164)\n[PASS] testFuzz_swapsNeverLowerBookValueAndDeferredBurnStaysBounded(uint8[12],uint256) (runs: 256, μ: 4592729, ~: 4507824)\n[PASS] testFuzz_tenderNeverPaysAboveProRataAndNeverLowersBookValue(uint256,uint256,uint256) (runs: 256, μ: 888010, ~: 951094)\n[PASS] test_askCountsOnlyFloatNotTheBurnHalf() (gas: 747144)\n[PASS] test_askPaysTheIntakeAndTheCallerAndLeavesNoAllowance() (gas: 1488221)\n[PASS] test_askRevertsInsideSevenDaysThenWorks() (gas: 1430192)\n[PASS] test_askRevertsUnderZeroPointSixAndPassesAtExactlyIt() (gas: 723239)\n[PASS] test_askRevertsWhenTheIntakeRefusesAndChangesNothing() (gas: 861839)\n[PASS] test_bindingEmitsPoolBound() (gas: 5384769)\n[PASS] test_buyExactInput_takesOnePercentIntoTheVault() (gas: 899580)\n[PASS] test_buyExactOutput_takesOnePercentIntoTheVault() (gas: 831485)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 241726)\n[PASS] test_constructorRejectsAnAddressWithoutTheRightFlags() (gas: 77274708)\n[PASS] test_constructorRejectsZeroAndEqualAddresses() (gas: 45588819)\n[PASS] test_deferredFeeDoesNotSpendTheFloatOnHand() (gas: 1158500)\n[PASS] test_donationsOnlyRaiseBookValue() (gas: 965800)\n[PASS] test_dustTenderPaysTheRoundedDownWeiAndRaisesBookValue() (gas: 828065)\n[PASS] test_factoryBindsTheLaunchPool() (gas: 22832)\n[PASS] test_firstBuyOnPerkOnlyPool_defersTheFeeAndCountsBothHalvesOnce() (gas: 1141252)\n[PASS] test_firstExactOutputBuyOnPerkOnlyPool_defersTheFee() (gas: 893549)\n[PASS] test_firstTenderPaysBookValueAndBurnsThePerk() (gas: 1019168)\n[PASS] test_forcedDeferralCannotLowerBookValueOrBlockTendersAndAsks() (gas: 2852391)\n[PASS] test_halfOfEveryFeeReachesTheDeadAddressOnceAndOnlyOnce() (gas: 2251643)\n[PASS] test_immutablesAndConstants() (gas: 174446)\n[PASS] test_initializeRefusesAFakeTokenWithImd() (gas: 5883762)\n[PASS] test_initializeRefusesANonFactorySender() (gas: 5456093)\n[PASS] test_initializeRefusesAPoolWithoutImdOrWithEth() (gas: 5946210)\n[PASS] test_initializeRefusesASecondPoolAndReinitialization() (gas: 105380)\n[PASS] test_initializeRefusesWrongAndDynamicFees() (gas: 5398993)\n[PASS] test_largeDeferredBuyAfterPaidFeesLeavesTheFloatWhole() (gas: 1883695)\n[PASS] test_lastTenderDrainsTheFloatProRata() (gas: 1283044)\n[PASS] test_lpFeeIsUntouchedAndNoPerkIsEverTaken() (gas: 1338184)\n[PASS] test_noPrivilegedFunctions() (gas: 877094)\n[PASS] test_oddFeeRoundsTheBurnDown() (gas: 783412)\n[PASS] test_partialRedemptionReleasesTheBurnFirstAndKeepsTheInvariants() (gas: 1255975)\n[PASS] test_pendingFeesAreFlushedByTheNextSwapThatFindsImd_withoutDoubleCounting() (gas: 1055859)\n[PASS] test_permissionsMatchTheAddressFlags() (gas: 11250)\n[PASS] test_redeemPendingFeesRevertsWhenNothingIsPending() (gas: 45821)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6231151)\n[PASS] test_sellExactInput_takesOnePercentIntoTheVault() (gas: 820675)\n[PASS] test_sellExactOutput_takesOnePercentIntoTheVault() (gas: 869765)\n[PASS] test_simulateSwapRefusesEveryoneButTheHook() (gas: 68792)\n[PASS] test_swapCallbacksRefuseAnUnboundPool() (gas: 78005)\n[PASS] test_tenderAndAskInsideTheDeferralWindowUseTheWholeFloat() (gas: 1464851)\n[PASS] test_tenderNeedsAllowanceAndBalance() (gas: 984175)\n[PASS] test_tenderRevertsOnZeroPayout() (gas: 801594)\n[PASS] test_tinySwapWithZeroFeeStillSucceeds() (gas: 654654)\n[PASS] test_unusedCallbacksRevertEvenFromTheManager() (gas: 126525)\n[PASS] test_zeroLiquidityChangeGoesThrough() (gas: 774875)\nSuite result: ok. 55 passed; 0 failed; 0 skipped; finished in 396.86ms (2.06s CPU time)\n\nRan 4 test suites in 397.92ms (425.06ms CPU time): 69 tests passed, 0 failed, 0 skipped (69 total tests)\n","passed":true},{"durationMs":61,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PerkToken.approve(address,uint256)\",\"PerkToken.transfer(address,uint256)\",\"PerkToken.transferFrom(address,address,uint256)\",\"PerkVaultHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"PerkVaultHook.ask()\",\"PerkVaultHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"PerkVaultHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"PerkVaultHook.burn()\",\"PerkVaultHook.redeemPendingFees()\",\"PerkVaultHook.simulateSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"PerkVaultHook.tender(uint256)\",\"PerkVaultHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"README.md\":310,\"foundry.toml\":15,\"launch.json\":32,\"remappings.txt\":3,\"src/HookFlags.sol\":37,\"src/IIntake.sol\":20,\"src/PerkToken.sol\":72,\"src/PerkVaultHook.sol\":723,\"test/PerkToken.t.sol\":97,\"test/PerkVaultHook.t.sol\":1222,\"test/PerkVaultHookImdBehaviour.t.sol\":123,\"test/mocks/BlocklistERC20.sol\":51,\"test/mocks/ForcedDeferral.sol\":64,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MockIntake.sol\":53,\"test/utils/HookDeployer.sol\":45,\"test/utils/PerkVaultBase.sol\":269},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"20da37c72b13b4d2d5666115a9d70434c5c27ffa1b1a3e61e078b0b9555dcaf7","verifiedTreeHash":"0e3fb8ff8255e9edec91b78c44ea5ac12fbfa95a","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":3003,"exitCode":0,"name":"build","output":"Compiling 83 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.92s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> test/mocks/MockIntake.sol:30:5:\n   |\n30 |     function priceOf(bytes32, address) external view returns (uint256) {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":514,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/PerkToken.t.sol:PerkTokenTest\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 70118, ~: 70734)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 115230)\n[PASS] test_metadata() (gas: 23878)\n[PASS] test_mintsTheWholeSupplyToTheDeployerOnce() (gas: 18436)\n[PASS] test_noMintOrAdminPath() (gas: 244863)\n[PASS] test_transferFromRespectsAllowance() (gas: 242791)\n[PASS] test_transferMovesExactlyWhatItWasAsked() (gas: 74895)\n[PASS] test_transferRevertsOnInsufficientBalanceAndZeroRecipient() (gas: 60645)\n[PASS] test_transferToDeadIsAnOrdinaryTransfer() (gas: 64992)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 2.76ms (3.44ms CPU time)\n\nRan 3 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookBlockedDeadTest\n[PASS] test_askRunsTheBurnFirstAndCountsOnlyTheFloat() (gas: 1693037)\n[PASS] test_burnRefusedByImdStaysOwedAndDoesNotRevertTheSwap() (gas: 1233809)\n[PASS] test_feeDeferredWhenImdRefusesTheHook() (gas: 1083269)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 11.04ms (2.48ms CPU time)\n\nRan 2 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookReentrancyTest\n[PASS] test_askRewardCannotReenterTender() (gas: 1332383)\n[PASS] test_tenderPayoutCannotReenterTender() (gas: 885453)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 421.79ms (3.13ms CPU time)\n\nRan 55 tests for test/PerkVaultHook.t.sol:PerkVaultHookTest\n[PASS] testFuzz_bookValueNeverFallsFromDonationsOrFees(uint256,uint256) (runs: 256, μ: 1051700, ~: 1049819)\n[PASS] testFuzz_buyExactInputWithLimit_feeIsOnePercentOfTheFill(uint256,uint24) (runs: 256, μ: 879705, ~: 883108)\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 869530, ~: 872207)\n[PASS] testFuzz_buyExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 819159, ~: 821769)\n[PASS] testFuzz_imdLeavesOnlyByTenderBurnAndAsk(uint8[12],uint256) (runs: 256, μ: 4108311, ~: 4053249)\n[PASS] testFuzz_sellExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 812997, ~: 818499)\n[PASS] testFuzz_sellExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 857513, ~: 860164)\n[PASS] testFuzz_swapsNeverLowerBookValueAndDeferredBurnStaysBounded(uint8[12],uint256) (runs: 256, μ: 4592729, ~: 4507824)\n[PASS] testFuzz_tenderNeverPaysAboveProRataAndNeverLowersBookValue(uint256,uint256,uint256) (runs: 256, μ: 888010, ~: 951094)\n[PASS] test_askCountsOnlyFloatNotTheBurnHalf() (gas: 747144)\n[PASS] test_askPaysTheIntakeAndTheCallerAndLeavesNoAllowance() (gas: 1488221)\n[PASS] test_askRevertsInsideSevenDaysThenWorks() (gas: 1430192)\n[PASS] test_askRevertsUnderZeroPointSixAndPassesAtExactlyIt() (gas: 723239)\n[PASS] test_askRevertsWhenTheIntakeRefusesAndChangesNothing() (gas: 861839)\n[PASS] test_bindingEmitsPoolBound() (gas: 5384769)\n[PASS] test_buyExactInput_takesOnePercentIntoTheVault() (gas: 899580)\n[PASS] test_buyExactOutput_takesOnePercentIntoTheVault() (gas: 831485)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 241726)\n[PASS] test_constructorRejectsAnAddressWithoutTheRightFlags() (gas: 77274708)\n[PASS] test_constructorRejectsZeroAndEqualAddresses() (gas: 45588819)\n[PASS] test_deferredFeeDoesNotSpendTheFloatOnHand() (gas: 1158500)\n[PASS] test_donationsOnlyRaiseBookValue() (gas: 965800)\n[PASS] test_dustTenderPaysTheRoundedDownWeiAndRaisesBookValue() (gas: 828065)\n[PASS] test_factoryBindsTheLaunchPool() (gas: 22832)\n[PASS] test_firstBuyOnPerkOnlyPool_defersTheFeeAndCountsBothHalvesOnce() (gas: 1141252)\n[PASS] test_firstExactOutputBuyOnPerkOnlyPool_defersTheFee() (gas: 893549)\n[PASS] test_firstTenderPaysBookValueAndBurnsThePerk() (gas: 1019168)\n[PASS] test_forcedDeferralCannotLowerBookValueOrBlockTendersAndAsks() (gas: 2852391)\n[PASS] test_halfOfEveryFeeReachesTheDeadAddressOnceAndOnlyOnce() (gas: 2251643)\n[PASS] test_immutablesAndConstants() (gas: 174446)\n[PASS] test_initializeRefusesAFakeTokenWithImd() (gas: 5883762)\n[PASS] test_initializeRefusesANonFactorySender() (gas: 5456093)\n[PASS] test_initializeRefusesAPoolWithoutImdOrWithEth() (gas: 5946210)\n[PASS] test_initializeRefusesASecondPoolAndReinitialization() (gas: 105380)\n[PASS] test_initializeRefusesWrongAndDynamicFees() (gas: 5398993)\n[PASS] test_largeDeferredBuyAfterPaidFeesLeavesTheFloatWhole() (gas: 1883695)\n[PASS] test_lastTenderDrainsTheFloatProRata() (gas: 1283044)\n[PASS] test_lpFeeIsUntouchedAndNoPerkIsEverTaken() (gas: 1338184)\n[PASS] test_noPrivilegedFunctions() (gas: 877094)\n[PASS] test_oddFeeRoundsTheBurnDown() (gas: 783412)\n[PASS] test_partialRedemptionReleasesTheBurnFirstAndKeepsTheInvariants() (gas: 1255975)\n[PASS] test_pendingFeesAreFlushedByTheNextSwapThatFindsImd_withoutDoubleCounting() (gas: 1055859)\n[PASS] test_permissionsMatchTheAddressFlags() (gas: 11250)\n[PASS] test_redeemPendingFeesRevertsWhenNothingIsPending() (gas: 45821)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6231151)\n[PASS] test_sellExactInput_takesOnePercentIntoTheVault() (gas: 820675)\n[PASS] test_sellExactOutput_takesOnePercentIntoTheVault() (gas: 869765)\n[PASS] test_simulateSwapRefusesEveryoneButTheHook() (gas: 68792)\n[PASS] test_swapCallbacksRefuseAnUnboundPool() (gas: 78005)\n[PASS] test_tenderAndAskInsideTheDeferralWindowUseTheWholeFloat() (gas: 1464851)\n[PASS] test_tenderNeedsAllowanceAndBalance() (gas: 984175)\n[PASS] test_tenderRevertsOnZeroPayout() (gas: 801594)\n[PASS] test_tinySwapWithZeroFeeStillSucceeds() (gas: 654654)\n[PASS] test_unusedCallbacksRevertEvenFromTheManager() (gas: 126525)\n[PASS] test_zeroLiquidityChangeGoesThrough() (gas: 774875)\nSuite result: ok. 55 passed; 0 failed; 0 skipped; finished in 421.88ms (2.18s CPU time)\n\nRan 4 test suites in 422.97ms (857.47ms CPU time): 69 tests passed, 0 failed, 0 skipped (69 total tests)\n","passed":true},{"durationMs":53,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PerkToken.approve(address,uint256)\",\"PerkToken.transfer(address,uint256)\",\"PerkToken.transferFrom(address,address,uint256)\",\"PerkVaultHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"PerkVaultHook.ask()\",\"PerkVaultHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"PerkVaultHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"PerkVaultHook.burn()\",\"PerkVaultHook.redeemPendingFees()\",\"PerkVaultHook.simulateSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"PerkVaultHook.tender(uint256)\",\"PerkVaultHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"README.md\":310,\"foundry.toml\":15,\"remappings.txt\":3,\"src/HookFlags.sol\":37,\"src/IIntake.sol\":20,\"src/PerkToken.sol\":72,\"src/PerkVaultHook.sol\":723,\"test/PerkToken.t.sol\":97,\"test/PerkVaultHook.t.sol\":1222,\"test/PerkVaultHookImdBehaviour.t.sol\":123,\"test/mocks/BlocklistERC20.sol\":51,\"test/mocks/ForcedDeferral.sol\":64,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MockIntake.sol\":53,\"test/utils/HookDeployer.sol\":45,\"test/utils/PerkVaultBase.sol\":269},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1080,"exitCode":0,"name":"slither","output":"[medium/medium] reentrancy-no-eth at src/PerkVaultHook.sol:427: Reentrancy in PerkVaultHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/PerkVaultHook.sol#427-446):\n[medium/medium] reentrancy-no-eth at src/PerkVaultHook.sol:525: Reentrancy in PerkVaultHook._burn() (src/PerkVaultHook.sol#525-535):\n[medium/medium] reentrancy-no-eth at src/PerkVaultHook.sol:632: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#632-646):\n[medium/medium] reentrancy-no-eth at src/PerkVaultHook.sol:326: Reentrancy in PerkVaultHook.ask() (src/PerkVaultHook.sol#326-341):\n[medium/medium] uninitialized-local at src/PerkVaultHook.sol:434: PerkVaultHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).hookDeltaUnspecified (src/PerkVaultHook.sol#434) is a local variable never initialized\n[medium/medium] unused-return at src/PerkVaultHook.sol:350: PerkVaultHook.redeemPendingFees() (src/PerkVaultHook.sol#350-355) ignores return value by poolManager.unlock(abi.encode(msg.sender)) (src/PerkVaultHook.sol#353)\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:632: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#632-646):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:632: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#632-646):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:310: Reentrancy in PerkVaultHook.tender(uint256) (src/PerkVaultHook.sol#310-321):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:525: Reentrancy in PerkVaultHook._burn() (src/PerkVaultHook.sol#525-535):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:326: Reentrancy in PerkVaultHook.ask() (src/PerkVaultHook.sol#326-341):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:659: Reentrancy in PerkVaultHook._payPending(uint256,address) (src/PerkVaultHook.sol#659-664):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:659: Reentrancy in PerkVaultHook._payPending(uint256,address) (src/PerkVaultHook.sol#659-664):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:427: Reentrancy in PerkVaultHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/PerkVaultHook.sol#427-446):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:525: Reentrancy in PerkVaultHook._burn() (src/PerkVaultHook.sol#525-535):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:632: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#632-646):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:632: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#632-646):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:632: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#632-646):\n[low/medium] timestamp at src/PerkVaultHook.sol:326: PerkVaultHook.ask() (src/PerkVaultHook.sol#326-341) uses timestamp for comparisons","passed":true},{"durationMs":342,"exitCode":0,"name":"aderyn","output":"[low] internal-function-used-once at src/HookFlags.sol:29: Internal Function Used Only Once\n[low] large-numeric-literal at src/PerkToken.sol:20: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/PerkVaultHook.sol:563: Literal Instead of Constant (2 places)\n[low] missing-inheritance at src/PerkToken.sol:14: Missing Inheritance\n[low] unchecked-return at src/PerkVaultHook.sol:311: Unchecked Return (5 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"3454e6c2a1e77df2e35073ed2475d6a48c1bb18d366c46f67a5c7ebdc7c92a5b","verifiedTreeHash":"ca659a32c291be2a308457cfc562fbab89de12ea","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":2878,"exitCode":0,"name":"build","output":"Compiling 82 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.79s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> test/mocks/MockIntake.sol:30:5:\n   |\n30 |     function priceOf(bytes32, address) external view returns (uint256) {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":429,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 3 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookBlockedDeadTest\n[PASS] test_askRunsTheBurnFirstAndCountsOnlyTheFloat() (gas: 1679712)\n[PASS] test_burnRefusedByImdStaysOwedAndDoesNotRevertTheSwap() (gas: 1220252)\n[PASS] test_feeDeferredWhenImdRefusesTheHook() (gas: 1063403)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 5.76ms (4.30ms CPU time)\n\nRan 2 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookReentrancyTest\n[PASS] test_askRewardCannotReenterTender() (gas: 1327808)\n[PASS] test_tenderPayoutCannotReenterTender() (gas: 880878)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.62ms (1.26ms CPU time)\n\nRan 9 tests for test/PerkToken.t.sol:PerkTokenTest\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 70062, ~: 70734)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 115230)\n[PASS] test_metadata() (gas: 23878)\n[PASS] test_mintsTheWholeSupplyToTheDeployerOnce() (gas: 18436)\n[PASS] test_noMintOrAdminPath() (gas: 244863)\n[PASS] test_transferFromRespectsAllowance() (gas: 242791)\n[PASS] test_transferMovesExactlyWhatItWasAsked() (gas: 74895)\n[PASS] test_transferRevertsOnInsufficientBalanceAndZeroRecipient() (gas: 60645)\n[PASS] test_transferToDeadIsAnOrdinaryTransfer() (gas: 64992)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 25.44ms (23.13ms CPU time)\n\nRan 49 tests for test/PerkVaultHook.t.sol:PerkVaultHookTest\n[PASS] testFuzz_bookValueNeverFallsFromDonationsOrFees(uint256,uint256) (runs: 256, μ: 1038601, ~: 1036711)\n[PASS] testFuzz_buyExactInputWithLimit_feeIsOnePercentOfTheFill(uint256,uint24) (runs: 256, μ: 877598, ~: 880694)\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 867535, ~: 869815)\n[PASS] testFuzz_buyExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 817046, ~: 818986)\n[PASS] testFuzz_imdLeavesOnlyByTenderBurnAndAsk(uint8[12],uint256) (runs: 256, μ: 4046365, ~: 3953351)\n[PASS] testFuzz_sellExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 811417, ~: 815696)\n[PASS] testFuzz_sellExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 855495, ~: 857744)\n[PASS] testFuzz_tenderNeverPaysAboveProRataAndNeverLowersBookValue(uint256,uint256,uint256) (runs: 256, μ: 877624, ~: 941598)\n[PASS] test_askCountsOnlyFloatNotTheBurnHalf() (gas: 740514)\n[PASS] test_askPaysTheIntakeAndTheCallerAndLeavesNoAllowance() (gas: 1479338)\n[PASS] test_askRevertsInsideSevenDaysThenWorks() (gas: 1423214)\n[PASS] test_askRevertsUnderZeroPointSixAndPassesAtExactlyIt() (gas: 718477)\n[PASS] test_askRevertsWhenTheIntakeRefusesAndChangesNothing() (gas: 857308)\n[PASS] test_bindingEmitsPoolBound() (gas: 75000972)\n[PASS] test_buyExactInput_takesOnePercentIntoTheVault() (gas: 897099)\n[PASS] test_buyExactOutput_takesOnePercentIntoTheVault() (gas: 828982)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 241791)\n[PASS] test_constructorRejectsAnAddressWithoutTheRightFlags() (gas: 62750551)\n[PASS] test_constructorRejectsZeroAndEqualAddresses() (gas: 165867517)\n[PASS] test_donationsOnlyRaiseBookValue() (gas: 954752)\n[PASS] test_dustTenderPaysTheRoundedDownWeiAndRaisesBookValue() (gas: 819248)\n[PASS] test_factoryBindsTheLaunchPool() (gas: 22854)\n[PASS] test_firstBuyOnPerkOnlyPool_defersTheFeeAndCountsBothHalvesOnce() (gas: 1118948)\n[PASS] test_firstExactOutputBuyOnPerkOnlyPool_defersTheFee() (gas: 873539)\n[PASS] test_firstTenderPaysBookValueAndBurnsThePerk() (gas: 1008032)\n[PASS] test_halfOfEveryFeeReachesTheDeadAddressOnceAndOnlyOnce() (gas: 2237422)\n[PASS] test_immutablesAndConstants() (gas: 171958)\n[PASS] test_initializeRefusesAFakeTokenWithImd() (gas: 75499898)\n[PASS] test_initializeRefusesANonFactorySender() (gas: 75072325)\n[PASS] test_initializeRefusesAPoolWithoutImdOrWithEth() (gas: 75562352)\n[PASS] test_initializeRefusesASecondPoolAndReinitialization() (gas: 105491)\n[PASS] test_initializeRefusesWrongAndDynamicFees() (gas: 75015215)\n[PASS] test_lastTenderDrainsTheFloatProRata() (gas: 1271944)\n[PASS] test_lpFeeIsUntouchedAndNoPerkIsEverTaken() (gas: 1329362)\n[PASS] test_noPrivilegedFunctions() (gas: 876962)\n[PASS] test_oddFeeRoundsTheBurnDown() (gas: 781064)\n[PASS] test_pendingFeesAreFlushedByTheNextSwapThatFindsImd_withoutDoubleCounting() (gas: 1036000)\n[PASS] test_permissionsMatchTheAddressFlags() (gas: 11228)\n[PASS] test_redeemPendingFeesRevertsWhenNothingIsPending() (gas: 45821)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6118648)\n[PASS] test_sellExactInput_takesOnePercentIntoTheVault() (gas: 818127)\n[PASS] test_sellExactOutput_takesOnePercentIntoTheVault() (gas: 867351)\n[PASS] test_simulateSwapRefusesEveryoneButTheHook() (gas: 68970)\n[PASS] test_swapCallbacksRefuseAnUnboundPool() (gas: 78137)\n[PASS] test_tenderNeedsAllowanceAndBalance() (gas: 974926)\n[PASS] test_tenderRevertsOnZeroPayout() (gas: 792139)\n[PASS] test_tinySwapWithZeroFeeStillSucceeds() (gas: 654655)\n[PASS] test_unusedCallbacksRevertEvenFromTheManager() (gas: 126393)\n[PASS] test_zeroLiquidityChangeGoesThrough() (gas: 772703)\nSuite result: ok. 49 passed; 0 failed; 0 skipped; finished in 340.29ms (1.62s CPU time)\n\nRan 4 test suites in 342.07ms (378.11ms CPU time): 63 tests passed, 0 failed, 0 skipped (63 total tests)\n","passed":true},{"durationMs":55,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PerkToken.approve(address,uint256)\",\"PerkToken.transfer(address,uint256)\",\"PerkToken.transferFrom(address,address,uint256)\",\"PerkVaultHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"PerkVaultHook.ask()\",\"PerkVaultHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"PerkVaultHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"PerkVaultHook.burn()\",\"PerkVaultHook.redeemPendingFees()\",\"PerkVaultHook.simulateSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"PerkVaultHook.tender(uint256)\",\"PerkVaultHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"README.md\":281,\"foundry.toml\":15,\"remappings.txt\":3,\"src/HookFlags.sol\":37,\"src/IIntake.sol\":20,\"src/PerkToken.sol\":72,\"src/PerkVaultHook.sol\":693,\"test/PerkToken.t.sol\":97,\"test/PerkVaultHook.t.sol\":1005,\"test/PerkVaultHookImdBehaviour.t.sol\":123,\"test/mocks/BlocklistERC20.sol\":51,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MockIntake.sol\":53,\"test/utils/HookDeployer.sol\":45,\"test/utils/PerkVaultBase.sol\":269},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1113,"exitCode":0,"name":"slither","output":"[medium/medium] reentrancy-no-eth at src/PerkVaultHook.sol:411: Reentrancy in PerkVaultHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/PerkVaultHook.sol#411-430):\n[medium/medium] reentrancy-no-eth at src/PerkVaultHook.sol:507: Reentrancy in PerkVaultHook._burn() (src/PerkVaultHook.sol#507-517):\n[medium/medium] reentrancy-no-eth at src/PerkVaultHook.sol:310: Reentrancy in PerkVaultHook.ask() (src/PerkVaultHook.sol#310-325):\n[medium/medium] uninitialized-local at src/PerkVaultHook.sol:418: PerkVaultHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes).hookDeltaUnspecified (src/PerkVaultHook.sol#418) is a local variable never initialized\n[medium/medium] unused-return at src/PerkVaultHook.sol:334: PerkVaultHook.redeemPendingFees() (src/PerkVaultHook.sol#334-339) ignores return value by poolManager.unlock(abi.encode(msg.sender)) (src/PerkVaultHook.sol#337)\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:310: Reentrancy in PerkVaultHook.ask() (src/PerkVaultHook.sol#310-325):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:294: Reentrancy in PerkVaultHook.tender(uint256) (src/PerkVaultHook.sol#294-305):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:507: Reentrancy in PerkVaultHook._burn() (src/PerkVaultHook.sol#507-517):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:604: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#604-617):\n[low/medium] reentrancy-benign at src/PerkVaultHook.sol:604: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#604-617):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:604: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#604-617):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:507: Reentrancy in PerkVaultHook._burn() (src/PerkVaultHook.sol#507-517):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:604: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#604-617):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:411: Reentrancy in PerkVaultHook.afterSwap(address,PoolKey,SwapParams,BalanceDelta,bytes) (src/PerkVaultHook.sol#411-430):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:604: Reentrancy in PerkVaultHook._collect(PoolKey,uint256) (src/PerkVaultHook.sol#604-617):\n[low/medium] reentrancy-events at src/PerkVaultHook.sol:630: Reentrancy in PerkVaultHook._payPending(uint256,address) (src/PerkVaultHook.sol#630-634):\n[low/medium] timestamp at src/PerkVaultHook.sol:310: PerkVaultHook.ask() (src/PerkVaultHook.sol#310-325) uses timestamp for comparisons","passed":true},{"durationMs":378,"exitCode":0,"name":"aderyn","output":"[low] internal-function-used-once at src/HookFlags.sol:29: Internal Function Used Only Once\n[low] large-numeric-literal at src/PerkToken.sol:20: Large Numeric Literal (2 places)\n[low] literal-instead-of-constant at src/PerkVaultHook.sol:535: Literal Instead of Constant (2 places)\n[low] missing-inheritance at src/PerkToken.sol:14: Missing Inheritance\n[low] unchecked-return at src/PerkVaultHook.sol:295: Unchecked Return (5 places)","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"a2a606dccdbb63d69a2f620d5d239b31f2be40109ccb0fe003f83d84abe2d0cc","verifiedTreeHash":"5813eafcc1967e6ed87df1ff0f7f91b079038002","verifierVersion":"0.1.0+7129bf9d"},{"checks":[{"durationMs":2931,"exitCode":0,"name":"build","output":"Compiling 82 files with Solc 0.8.26\nSolc 0.8.26 finished in 2.85s\nCompiler run successful with warnings:\nWarning (2018): Function state mutability can be restricted to pure\n  --> test/mocks/MockIntake.sol:30:5:\n   |\n30 |     function priceOf(bytes32, address) external view returns (uint256) {\n   |     ^ (Relevant source part starts here and spans across multiple lines).\n\n","passed":true},{"durationMs":488,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 9 tests for test/PerkToken.t.sol:PerkTokenTest\n[PASS] testFuzz_transferConservesSupply(address,uint256) (runs: 256, μ: 70062, ~: 70734)\n[PASS] test_infiniteAllowanceIsNotDecremented() (gas: 115230)\n[PASS] test_metadata() (gas: 23878)\n[PASS] test_mintsTheWholeSupplyToTheDeployerOnce() (gas: 18436)\n[PASS] test_noMintOrAdminPath() (gas: 244863)\n[PASS] test_transferFromRespectsAllowance() (gas: 242791)\n[PASS] test_transferMovesExactlyWhatItWasAsked() (gas: 74895)\n[PASS] test_transferRevertsOnInsufficientBalanceAndZeroRecipient() (gas: 60645)\n[PASS] test_transferToDeadIsAnOrdinaryTransfer() (gas: 64992)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 3.08ms (4.11ms CPU time)\n\nRan 3 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookBlockedDeadTest\n[PASS] test_askRunsTheBurnFirstAndCountsOnlyTheFloat() (gas: 1679712)\n[PASS] test_burnRefusedByImdStaysOwedAndDoesNotRevertTheSwap() (gas: 1220252)\n[PASS] test_feeDeferredWhenImdRefusesTheHook() (gas: 1063403)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 100.76ms (3.14ms CPU time)\n\nRan 2 tests for test/PerkVaultHookImdBehaviour.t.sol:PerkVaultHookReentrancyTest\n[PASS] test_askRewardCannotReenterTender() (gas: 1327808)\n[PASS] test_tenderPayoutCannotReenterTender() (gas: 880878)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 377.89ms (1.69ms CPU time)\n\nRan 49 tests for test/PerkVaultHook.t.sol:PerkVaultHookTest\n[PASS] testFuzz_bookValueNeverFallsFromDonationsOrFees(uint256,uint256) (runs: 256, μ: 1038601, ~: 1036711)\n[PASS] testFuzz_buyExactInputWithLimit_feeIsOnePercentOfTheFill(uint256,uint24) (runs: 256, μ: 877598, ~: 880694)\n[PASS] testFuzz_buyExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 867535, ~: 869815)\n[PASS] testFuzz_buyExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 817046, ~: 818986)\n[PASS] testFuzz_imdLeavesOnlyByTenderBurnAndAsk(uint8[12],uint256) (runs: 256, μ: 4046365, ~: 3953351)\n[PASS] testFuzz_sellExactInput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 811417, ~: 815696)\n[PASS] testFuzz_sellExactOutput_feeIsExactlyOnePercent(uint256) (runs: 256, μ: 855495, ~: 857744)\n[PASS] testFuzz_tenderNeverPaysAboveProRataAndNeverLowersBookValue(uint256,uint256,uint256) (runs: 256, μ: 877624, ~: 941598)\n[PASS] test_askCountsOnlyFloatNotTheBurnHalf() (gas: 740514)\n[PASS] test_askPaysTheIntakeAndTheCallerAndLeavesNoAllowance() (gas: 1479338)\n[PASS] test_askRevertsInsideSevenDaysThenWorks() (gas: 1423214)\n[PASS] test_askRevertsUnderZeroPointSixAndPassesAtExactlyIt() (gas: 718477)\n[PASS] test_askRevertsWhenTheIntakeRefusesAndChangesNothing() (gas: 857308)\n[PASS] test_bindingEmitsPoolBound() (gas: 75000972)\n[PASS] test_buyExactInput_takesOnePercentIntoTheVault() (gas: 897099)\n[PASS] test_buyExactOutput_takesOnePercentIntoTheVault() (gas: 828982)\n[PASS] test_callbacksRefuseCallersOtherThanThePoolManager() (gas: 241791)\n[PASS] test_constructorRejectsAnAddressWithoutTheRightFlags() (gas: 62750551)\n[PASS] test_constructorRejectsZeroAndEqualAddresses() (gas: 165867517)\n[PASS] test_donationsOnlyRaiseBookValue() (gas: 954752)\n[PASS] test_dustTenderPaysTheRoundedDownWeiAndRaisesBookValue() (gas: 819248)\n[PASS] test_factoryBindsTheLaunchPool() (gas: 22854)\n[PASS] test_firstBuyOnPerkOnlyPool_defersTheFeeAndCountsBothHalvesOnce() (gas: 1118948)\n[PASS] test_firstExactOutputBuyOnPerkOnlyPool_defersTheFee() (gas: 873539)\n[PASS] test_firstTenderPaysBookValueAndBurnsThePerk() (gas: 1008032)\n[PASS] test_halfOfEveryFeeReachesTheDeadAddressOnceAndOnlyOnce() (gas: 2237422)\n[PASS] test_immutablesAndConstants() (gas: 171958)\n[PASS] test_initializeRefusesAFakeTokenWithImd() (gas: 75499898)\n[PASS] test_initializeRefusesANonFactorySender() (gas: 75072325)\n[PASS] test_initializeRefusesAPoolWithoutImdOrWithEth() (gas: 75562352)\n[PASS] test_initializeRefusesASecondPoolAndReinitialization() (gas: 105491)\n[PASS] test_initializeRefusesWrongAndDynamicFees() (gas: 75015215)\n[PASS] test_lastTenderDrainsTheFloatProRata() (gas: 1271944)\n[PASS] test_lpFeeIsUntouchedAndNoPerkIsEverTaken() (gas: 1329362)\n[PASS] test_noPrivilegedFunctions() (gas: 876962)\n[PASS] test_oddFeeRoundsTheBurnDown() (gas: 781064)\n[PASS] test_pendingFeesAreFlushedByTheNextSwapThatFindsImd_withoutDoubleCounting() (gas: 1036000)\n[PASS] test_permissionsMatchTheAddressFlags() (gas: 11228)\n[PASS] test_redeemPendingFeesRevertsWhenNothingIsPending() (gas: 45821)\n[PASS] test_runtimeCodeHasNoEscapeHatch() (gas: 6118648)\n[PASS] test_sellExactInput_takesOnePercentIntoTheVault() (gas: 818127)\n[PASS] test_sellExactOutput_takesOnePercentIntoTheVault() (gas: 867351)\n[PASS] test_simulateSwapRefusesEveryoneButTheHook() (gas: 68970)\n[PASS] test_swapCallbacksRefuseAnUnboundPool() (gas: 78137)\n[PASS] test_tenderNeedsAllowanceAndBalance() (gas: 974926)\n[PASS] test_tenderRevertsOnZeroPayout() (gas: 792139)\n[PASS] test_tinySwapWithZeroFeeStillSucceeds() (gas: 654655)\n[PASS] test_unusedCallbacksRevertEvenFromTheManager() (gas: 126393)\n[PASS] test_zeroLiquidityChangeGoesThrough() (gas: 772703)\nSuite result: ok. 49 passed; 0 failed; 0 skipped; finished in 378.00ms (1.61s CPU time)\n\nRan 4 test suites in 378.75ms (859.72ms CPU time): 63 tests passed, 0 failed, 0 skipped (63 total tests)\n","passed":true},{"durationMs":51,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"PerkToken.approve(address,uint256)\",\"PerkToken.transfer(address,uint256)\",\"PerkToken.transferFrom(address,address,uint256)\",\"PerkVaultHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"PerkVaultHook.ask()\",\"PerkVaultHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"PerkVaultHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"PerkVaultHook.burn()\",\"PerkVaultHook.redeemPendingFees()\",\"PerkVaultHook.simulateSwap((address,address,uint24,int24,address),(bool,int256,uint160))\",\"PerkVaultHook.tender(uint256)\",\"PerkVaultHook.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"README.md\":281,\"foundry.toml\":15,\"launch.json\":32,\"remappings.txt\":3,\"src/HookFlags.sol\":37,\"src/IIntake.sol\":20,\"src/PerkToken.sol\":72,\"src/PerkVaultHook.sol\":693,\"test/PerkToken.t.sol\":97,\"test/PerkVaultHook.t.sol\":1005,\"test/PerkVaultHookImdBehaviour.t.sol\":123,\"test/mocks/BlocklistERC20.sol\":51,\"test/mocks/MockERC20.sol\":60,\"test/mocks/MockIntake.sol\":53,\"test/utils/HookDeployer.sol\":45,\"test/utils/PerkVaultBase.sol\":269},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"cd2e40092addf0a1d0e40ebfa6b14db79259e9052b4d2e44a446129e179a05f3","verifiedTreeHash":"f5750fa390176c3e2b7cc3acb642dc62a593999f","verifierVersion":"0.1.0+7129bf9d"}]}