{"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":"422329b6-504e-4b38-90a7-d68f0ccb7f0c","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"883cd6db18c10e6af494cd95c60516275ad45363a2dc7bed0a4db59dede4ba33","dependsOn":["manifest"],"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":"538a2d18e695021d901a7721ced510273321d10a97d26dcad8e8f4f7e2c56acb","dependsOn":["manifest"],"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":"e01ce51158679ac1407f2998fc49309cade8ea483eec0c5c2731838818b8f26c","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":"a831ff9f96a8412c1f67ec2e17a47352bcb6506d93e49916302e4bd356ea5a98","dependsOn":["manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"af3aa01159bbf354f621cafa5c0006f6169e0938a8b015be508b7377dbf165bc","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"5d92ff97ad3c5ba477984ef60cac212e0a655735e5c299d53ceceb3c2ecc8a2c","dependsOn":["manifest"],"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":"ae007e7e3c4142f6ad3d5079b44c858816565d398c827845ea8a9f0179e6cc9c","dependsOn":["manifest"],"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":"980bed5cd20776fb4fbec831160825fd9df9e9969c3a549aa29baa1e80cec89a","dependsOn":["audit_imported_code"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"}],"objective":"Token name: SOVRN.ONE (nine characters: S O V R N . O N E, one full stop). Token symbol: SVO. Total supply 1,000,000,000 with 18 decimals. Chain id 4663 (Robinhood Chain), paired with IMD (an ERC-20, not native ETH): IMD = 0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127, 18 decimals.\n\nLAUNCH THIS REPOSITORY AS IT IS. The workspace is a finished, already-built and already-reviewed project at the given base commit: src/SovrnToken.sol, src/SovrnHook.sol, src/LifeForceVault.sol, src/HookFlags.sol, src/Interfaces.sol, script/PrepareLaunch.s.sol, launch.json (the launch manifest), launch-attestation.json, foundry.toml, README.md and test/. Do NOT rewrite, redesign, refactor or reformat any of it. Do not modify src/, test/, lib/, foundry.toml, launch.json or launch-attestation.json. The deliverable is the launch of this exact code, not new code.\n\nWHAT TO DO. (1) Verify that launch.json is a valid manifest for the launch factory: hook SovrnHook with the flat three-argument constructor ($poolManager, $token, $factory), flags 10444, token SovrnToken with the exact name and symbol above, pool fee 12500, tick spacing 60, paired currency IMD. (2) Mine and verify the hook address for flags 10444 with script/PrepareLaunch.s.sol. (3) Run the existing test suite (forge test; the fork test test/Fork4663.t.sol runs only when FORK_4663_RPC is set) and report the result. (4) Review the code as it is and report findings; if you find a defect, report it with a reproducing test instead of changing the source.\n\nKEEP THESE FACTS UNCHANGED. Fees are paid in IMD to the immutable LifeForceVault: sells 3.5%; buys 50% at pool opening decaying linearly to 3.5% over 3,600 seconds. The vault has no owner or setters; only the fixed Safe 0xEb57c52272B90F989C41B739e2ccc5f00bF7697C can withdraw, and burn() sends SVO only to the dead address. The token is plain: no owner, mint, tax, pause or blacklist. CONTEXT: earlier launches of this code were parked or blocked. #1173 (commit 0baa120): constructor chain-id and IMD-code gates broke the admission floor, and no liquidity callbacks let an IMD-only range skip the opening buy fee. Commit a6137d4 fixed both, and was parked for one finding: the opening-hour liquidity exemption covered the whole opening timestamp. Commit 48d35f2 tracks the initializing transaction with transient storage in beforeInitialize, and was blocked at the manifest node because the test fixtures initialized and seeded the pool in separate top-level calls, and forge 1.8.3 clears transient storage between them (26 setUps failed with LiquidityLocked). This commit (214f5a5) changes only the test fixtures: the pool router is the hook's factory and does both the initialize and the liquidity calls, and test/LiquidityLock.t.sol covers the initializing-transaction exemption with a single-call opener. src/ is identical to 48d35f2. Hook permissions are beforeInitialize, beforeAddLiquidity, beforeSwap, afterSwap and the two swap return deltas (flags 10444). Report anything that still blocks admission with a reproducing test; do not edit the source. The launch factory sets the opening price, cap and currency order; never hard-code them. Do not claim the code is audited or secure.","parentJobId":null,"planHash":"ee633cc7aa4108e1c71e8fd382d3a337efc4bb578a97b5f6853a2d1b4832678d","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"422329b6-504e-4b38-90a7-d68f0ccb7f0c","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1205-sovrn-one-nine-characters-s-o-v-r-n-o-n"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52169","feedbackHash":"a547480baa3b66b9f918b72313210edbbe34f1f616acc51398648548b117dabb","nodeKey":"audit_economics","submissionHash":"883cd6db18c10e6af494cd95c60516275ad45363a2dc7bed0a4db59dede4ba33","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52482","feedbackHash":"28de28ea02883c08b77f68b33ded60fd5d52d36b04ea5bb52bb466e33bc455c6","nodeKey":"audit_flow","submissionHash":"538a2d18e695021d901a7721ced510273321d10a97d26dcad8e8f4f7e2c56acb","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51082","feedbackHash":"3c3de5608d2f4275c44b34a2a0160e058fb359ec5a23135b3c289364d6421095","nodeKey":"audit_imported_code","submissionHash":"e01ce51158679ac1407f2998fc49309cade8ea483eec0c5c2731838818b8f26c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52289","feedbackHash":"66d1488f220621050d313b511ce8691ad39ca0c643f67bbc55083288c7788e1e","nodeKey":"audit_judge","submissionHash":"a831ff9f96a8412c1f67ec2e17a47352bcb6506d93e49916302e4bd356ea5a98","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52150","feedbackHash":"5328d1457d1f9728a9520110e6fb80e07120b486e2dd986d9ab2607e9a607ffc","nodeKey":"audit_math","submissionHash":"5d92ff97ad3c5ba477984ef60cac212e0a655735e5c299d53ceceb3c2ecc8a2c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51142","feedbackHash":"4d731465d7b551573cb1f3d1b8978d71f67d5ce4e02cbef27b882b2c1db67382","nodeKey":"audit_permissions","submissionHash":"ae007e7e3c4142f6ad3d5079b44c858816565d398c827845ea8a9f0179e6cc9c","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52136","feedbackHash":"4295f548f4d3ee510e8440546d4763c9f29041c157651087ef1e3ea4da50dcfa","nodeKey":"manifest","submissionHash":"980bed5cd20776fb4fbec831160825fd9df9e9969c3a549aa29baa1e80cec89a","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"fb82a286b9619d2d603e6b2740875c3564fd3bdf1e22c87e796c84f4b36bfbc8","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e8e860c7f2306473","findings":[{"citation":"resolved","description":"Execution trace, buy with IMD as input. A router that pays before it swaps does poolManager.sync(IMD) (reserves R recorded), transfers its payment P to the manager (balance R+P), then poolManager.swap(). Inside that swap SovrnHook.afterSwap finds the manager's IMD balance >= fee and runs poolManager.take(IMD, vault, fee), which moves fee out of the manager while IMD is still the synced currency (PoolManager.take does not touch CurrencyReserves). The router's poolManager.settle() then computes paid = balanceNow - R = P - fee and credits the router that much. The hook's own delta nets to zero (+fee from the hook delta, -fee from the take), but the router's IMD delta is short by exactly fee however large P is, and the unlock ends with CurrencyNotSettled. Sending more IMD does not help: the shortfall is always the fee, because every wei above the debt is refunded by the router's own take and the fee was removed from the measured reserves. Sells are unaffected (SVO is the synced currency while the IMD fee is taken) and swap-then-settle routers (v4 Router, Universal Router with SWAP, SETTLE_ALL, TAKE_ALL) work, which the two control tests show. The claim path (mint) is only used when the manager's balance is below the fee, so a pre-paid buy, which raises the balance, always goes through the direct take. Impact: every buy through a pre-paying integration (a router or aggregator adapter that settles its input first, or a contract that holds IMD claims-free and settles before swapping) reverts; no funds are lost. The author notes this as a router constraint (manifest notes item 3 and the README) and it is reported here with a reproducing test because the reviewed area is the swap execution path and nothing in the suite exercises a sync-before-swap order.","line":250,"path":"src/SovrnHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {ERC20} from \"solmate/src/tokens/ERC20.sol\";\nimport {SovrnToken} from \"src/SovrnToken.sol\";\nimport {SovrnHook} from \"src/SovrnHook.sol\";\n\ncontract ProofMockIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\", 18) {\n        _mint(msg.sender, 1e33);\n    }\n}\n\n/// @dev Acts as the launch factory (initializes and seeds) and as a router. `swapPrepay` is the\n///      \"sync, transfer, swap, settle\" order: the input, fee included, is paid before the swap and\n///      settled after it. `swapPostpay` is the ordinary \"swap, sync, transfer, settle\" order.\ncontract ProofFactory {\n    IPoolManager public immutable m;\n\n    constructor(IPoolManager m_) {\n        m = m_;\n    }\n\n    function initialize(PoolKey memory key, uint160 p) external {\n        m.initialize(key, p);\n    }\n\n    function liquidity(PoolKey memory key, ModifyLiquidityParams memory p) external {\n        m.unlock(abi.encode(uint8(0), key, abi.encode(p)));\n    }\n\n    function swapPrepay(PoolKey memory key, SwapParams memory p, uint256 pay) external {\n        m.unlock(abi.encode(uint8(1), key, abi.encode(p, pay)));\n    }\n\n    function swapPostpay(PoolKey memory key, SwapParams memory p) external {\n        m.unlock(abi.encode(uint8(2), key, abi.encode(p)));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(m));\n        (uint8 mode, PoolKey memory key, bytes memory params) = abi.decode(data, (uint8, PoolKey, bytes));\n        BalanceDelta d;\n        if (mode == 0) {\n            (d,) = m.modifyLiquidity(key, abi.decode(params, (ModifyLiquidityParams)), \"\");\n        } else if (mode == 1) {\n            (SwapParams memory p, uint256 pay) = abi.decode(params, (SwapParams, uint256));\n            Currency input = p.zeroForOne ? key.currency0 : key.currency1;\n            Currency output = p.zeroForOne ? key.currency1 : key.currency0;\n            m.sync(input);\n            ERC20(Currency.unwrap(input)).transfer(address(m), pay);\n            d = m.swap(key, p, \"\");\n            m.settle();\n            int128 inDelta = p.zeroForOne ? d.amount0() : d.amount1();\n            int128 outDelta = p.zeroForOne ? d.amount1() : d.amount0();\n            uint256 owed = uint256(-int256(inDelta));\n            if (pay > owed) m.take(input, address(this), pay - owed);\n            m.take(output, address(this), uint256(uint128(outDelta)));\n            return \"\";\n        } else {\n            d = m.swap(key, abi.decode(params, (SwapParams)), \"\");\n        }\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return \"\";\n    }\n\n    function _settle(Currency c, int128 a) private {\n        if (a < 0) {\n            m.sync(c);\n            ERC20(Currency.unwrap(c)).transfer(address(m), uint256(-int256(a)));\n            m.settle();\n        } else if (a > 0) {\n            m.take(c, address(this), uint256(uint128(a)));\n        }\n    }\n}\n\n/// @notice A router that pays the swap input before the swap (sync, transfer, swap, settle) cannot buy SVO:\n///         SovrnHook.afterSwap takes the IMD fee out of the PoolManager while IMD is the synced currency, so\n///         the router's settle() credits it `paid - fee` and the unlock ends with CurrencyNotSettled. Paying\n///         the fee in advance does not help: the shortfall is always exactly the fee. Sells and swap-then-settle\n///         routers work, which the control tests show. Fails on the current code; passes once a buy taken through\n///         a pre-synced payment settles (for example by minting the fee as a claim whenever IMD is the synced\n///         currency, or by taking the fee only after settlement).\ncontract PreSyncRouterProofTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant TOKEN_AT = 0xF000000000000000000000000000000000000001;\n    PoolManager manager;\n    SovrnToken token;\n    SovrnHook hook;\n    ProofFactory factory;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        deployCodeTo(\"PreSyncRouterProof.t.sol:ProofMockIMD\", \"\", IMD);\n        factory = new ProofFactory(manager);\n        deployCodeTo(\"SovrnToken.sol:SovrnToken\", \"\", TOKEN_AT);\n        token = SovrnToken(TOKEN_AT);\n        address at = address(uint160(0x28cc));\n        deployCodeTo(\"SovrnHook.sol:SovrnHook\", abi.encode(IPoolManager(address(manager)), token, address(factory)), at);\n        hook = SovrnHook(at);\n        key = PoolKey(Currency.wrap(IMD), Currency.wrap(TOKEN_AT), 12500, 60, IHooks(at));\n        factory.initialize(key, 79228162514264337593543950336000);\n        token.transfer(address(factory), 100_000_000 ether);\n        ERC20(IMD).transfer(address(factory), 1_000_000 ether);\n        factory.liquidity(key, ModifyLiquidityParams(-887220, 887220, 1e22, bytes32(0)));\n        vm.warp(block.timestamp + 2 hours); // past the decay: a buy pays the normal 3.5%\n    }\n\n    /// @dev Control: the ordinary order works and pays the vault.\n    function test_control_swapThenSettleBuyPaysTheVault() public {\n        factory.swapPostpay(key, SwapParams(true, -1 ether, TickMath.MIN_SQRT_PRICE + 1));\n        assertGt(ERC20(IMD).balanceOf(address(hook.vault())), 0);\n    }\n\n    /// @dev Control: a pre-paid SELL works, because the fee is taken in IMD while SVO is the synced currency.\n    function test_control_prepaidSellPaysTheVault() public {\n        factory.swapPrepay(key, SwapParams(false, -1000 ether, TickMath.MAX_SQRT_PRICE - 1), 1000 ether);\n        assertGt(ERC20(IMD).balanceOf(address(hook.vault())), 0);\n    }\n\n    /// @dev The finding: exact-input buy of 1 IMD, paid in advance with the 3.5% fee included (2 IMD sent, the\n    ///      unused part refunded by take). Expected: the swap settles and the vault holds the fee. Actual on this\n    ///      code: PoolManager reverts with CurrencyNotSettled, because afterSwap's take(IMD) moved the fee out\n    ///      between sync and settle, so settle credited the router 2 IMD - fee.\n    function test_prepaidBuyWithFeeIncludedSettles() public {\n        uint256 vaultBefore = ERC20(IMD).balanceOf(address(hook.vault()));\n        factory.swapPrepay(key, SwapParams(true, -1 ether, TickMath.MIN_SQRT_PRICE + 1), 2 ether);\n        assertGt(ERC20(IMD).balanceOf(address(hook.vault())), vaultBefore, \"fee did not reach the vault\");\n    }\n}","reproduction":"State: local PoolManager, SovrnToken at 0xF000...0001 (IMD is currency0), hook at 0x...28cc built with (manager, token, factory); the factory initialized the pool at sqrtPrice 79228162514264337593543950336000 and seeded a full range of liquidity 1e22; block.timestamp two hours after opening (fee 3.5%). Call from the router inside poolManager.unlock: sync(IMD); IMD.transfer(manager, 2e18); swap(key, {zeroForOne: true, amountSpecified: -1e18, sqrtPriceLimitX96: MIN_SQRT_PRICE+1}, \"\"); settle(); take(IMD, router, 2e18 - owed); take(SVO, router, out). Expected: the swap settles, the router pays 1e18 + 0.035e18 IMD and the vault receives 0.035e18 IMD. Actual: PoolManager.unlock reverts with CurrencyNotSettled(); the vault balance is unchanged. The same sequence with zeroForOne=false (a sell of 1000e18 SVO) succeeds and funds the vault, and the same buy with swap first and sync/transfer/settle after succeeds. Run: forge test --match-path test/scratch/PreSyncRouterProof.t.sol -vv (test_prepaidBuyWithFeeIncludedSettles fails with CurrencyNotSettled(); the two control tests pass).","severity":"low","snippet":"                poolManager.take(Currency.wrap(IMD), address(vault), fee);","title":"afterSwap takes the IMD fee mid-swap, so a router that syncs IMD before the swap can never buy (CurrencyNotSettled)"},{"citation":"resolved","description":"Cross-transaction execution trace of beforeAddLiquidity. The exemption has exactly two legs: the modifyLiquidity caller is the constructor's factory address, or the transient flag written by beforeInitialize is still set, which is only true inside the transaction that called PoolManager.initialize. Any other (sender, transaction) pair is refused with LiquidityLocked for 3600 seconds after openedAt. The launch factory's seeding flow is not in this repository. If it adds the opening liquidity from its own address, or in the same transaction as initialize through any contract, the lock is transparent. If it initializes in one transaction and seeds in a later one through a position manager or any helper whose address is not the $factory argument, that seeding reverts and the pool stays empty for an hour, during which any swap against the empty pool moves sqrtPriceX96 to the caller's price limit at zero cost (both deltas zero, no fee), so the opening price the factory set is not preserved. Removal is never blocked. The author documents this constraint (README, manifest notes item 2); it is listed here because it is the one remaining path by which this exact source can stall at the deployer, and whether it is hit depends on the factory's order of calls, which must be confirmed from the factory before the launch transaction is sent.","line":138,"path":"src/SovrnHook.sol","reproduction":"Existing test test/LiquidityLock.t.sol, contract AtomicOpenTest (and AtomicOpenReversedTest for the other currency order). State: hook built with factory = AtomicOpener; opener.open() calls PoolManager.initialize and then seeds through a separate PoolRouter contract in the same call, which succeeds (test_seedingThroughAnotherContractInTheInitializingCallWorked). Then, in a later top-level call at the same block.timestamp, opener.addLater(seeder, key, {-887220, 887220, 1e20}) makes the same seeder call modifyLiquidity again. Expected by a factory that delegates seeding: the add succeeds. Actual: SovrnHook.beforeAddLiquidity reverts with LiquidityLocked() (test_aLaterTransactionThroughTheSameContractIsLockedOut), and LiquidityLockTest.test_othersCannotAddInTheOpeningSecondEither shows the same for any outsider. Only after vm.warp(openedAt + 3600) does the add go through (test_anyoneCanAddOnceTheDecayIsOver). Run: forge test --match-path test/LiquidityLock.t.sol -vv.","severity":"low","snippet":"        if (sender != factory && !openingTx && block.timestamp < openedAt + DECAY) revert LiquidityLocked();","title":"Opening-hour liquidity lock refuses every non-factory seeder in any transaction after the initializing one; a factory that seeds through a helper later stalls the launch"},{"citation":"resolved","description":"Commit 20c673e (the manifest assignment) changed launch.json pool.initialPrice from 45742400955009932534161870629490 to 79228162514264337593543950336 (line 29) and rewrote the notes. The committed launch-attestation.json (line 23) and the assertion in script/attest.py build_record (lines 37-41) still hold the old value, so the record the README step 4 points reviewers at describes a pool block that no longer matches the manifest, and the repository's own reproducibility check fails before it compares any artifact. Source hashes in the record still match src/ (SovrnHook 512aae25..., LifeForceVault 7060791f..., SovrnToken 3632f774..., Interfaces aca1fa33...). No deployment impact: the service signs its own ReleaseAttestation and the hook accepts any opening price (the manifest price is indicative; the factory sets the real one). Fix outside src/: regenerate launch-attestation.json and the attest.py assertion for the new manifest, or restore the previous price in launch.json.","line":40,"path":"script/attest.py","reproduction":"From the repository root run: python3 -I script/attest.py --check. Expected: the record is reproduced and the check passes. Actual: AssertionError raised at script/attest.py line 37 (assert manifest[\"pool\"] == {... \"initialPrice\": \"45742400955009932534161870629490\"}) because launch.json line 29 reads \"initialPrice\": \"79228162514264337593543950336\". git diff 214f5a5 HEAD -- launch.json shows the change.","severity":"info","snippet":"        \"initialPrice\": \"45742400955009932534161870629490\",","title":"Committed attestation record and script/attest.py still carry the previous manifest initialPrice; the repository's own check fails"}],"hash":"538a2d18e695021d901a7721ced510273321d10a97d26dcad8e8f4f7e2c56acb","nodeId":"120bf701-fe6c-4bec-977f-99e89a0392ab","outcome":"completed","summary":"The review is complete. `.imd-findings.json` holds three substantiated findings and 18 coverage rows, and no tracked file was changed.\n\n**What I verified**\n\n- **Manifest.** `launch.json` has only the five schema keys, notes of 3,924 characters, the flat `$poolManager`/`$token`/`$factory` constructor in declaration order, six permissions that sum to 10444 and match `getHookPermissions`, fee 12500, tick spacing 60, lowercase IMD address, decimal price below 2^256.\n- **Hook address.** `PrepareLaunch.mine` finds a 10444 salt for the real initcode in both currency orders, and the deployed hook's constructor accepts it.\n- **Tests.** Build clean on forge 1.8.3. Suite: 190 passed, 0 failed, 2 skipped. Fork rehearsal against the public Robinhood RPC: 12 passed on the real IMD token and PoolManager, protocol fee currently 0.\n- **Live dependencies.** The Safe at the fixed address is a 2-of-3 proxy on chain 4663 and IMD there reports symbol IMD with 18 decimals.\n\n**Findings**\n\n1. **Low, `src/SovrnHook.sol:250`.** A router that syncs IMD and pays before the swap can never buy. `afterSwap` takes the fee while IMD is the synced currency, so the router's settle is short by exactly the fee and the unlock ends with `CurrencyNotSettled`, even when the fee is paid in advance. Sells and swap-then-settle routers work. Proof test under `test/scratch/PreSyncRouterProof.t.sol` fails on the current code with both controls passing. The author already notes this as a router constraint.\n2. **Low, `src/SovrnHook.sol:138`.** The opening-hour lock refuses any non-factory seeder in a transaction after the initializing one. If the real factory seeds through a helper in a later transaction, the launch stalls for an hour while the empty pool's price can be moved at no cost. Reproduced by the existing `AtomicOpenTest`. Documented by the author; whether it is hit depends on the factory's call order, which is not in the tree.\n3. **Info, `script/attest.py:40`.** The last commit changed the manifest's indicative price, but the committed attestation record and the script's assertion still carry the old value, so the repository's own `attest.py --check` fails. No deployment impact since the service signs its own attestation.\n\n**Coverage.** All 14 entry points have rows. Everything else in the swap execution path, the vault accounting, the token, the quote mechanism and the claim redemption traced as intended. The one unreached item is the launch factory's own seeding order and public surface, which is not in this repository. The source is not claimed audited or secure.","treeHash":null,"usage":{"cachedInputTokens":2658651,"inputTokens":546,"model":"claude-fable-5-1","outputTokens":58724,"runtime":"claude","turns":47,"wallClockMs":1325152}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e764f15311426447","findings":[{"citation":"resolved","description":"All four fee formulas (lines 179, 182-191, 239) use floor division, and the README states this (\"Integer divisions round down\"). The Math Precision guide asks that fees round up; here the vault under-collects by up to 1 wei of IMD per swap and collects nothing when the IMD leg is below 1/rate wei: 1 wei at the opening rate (0.5e18), 28 wei at the normal rate (0.035e18). The exact-output sell quote at line 182 (gross = floor(requested * WAD / (WAD - rate))) and the charge at line 191 compose so that the trader always receives exactly the requested net (proved for requested in [1, 1e18] and every elapsed in [0, 7200] s, both currency orders, 2000 fuzz runs), so the only leak is the floor itself. Per-swap loss is < 1 wei of IMD (1e-18 IMD) and a sub-29-wei swap costs far more in gas than the fee it avoids, so there is no exploitable value; reported for the record because it is the one place the arithmetic departs from the guide's rounding rule. No change is recommended for this launch.","line":239,"path":"src/SovrnHook.sol","reproduction":"Fixture: SystemBase._system(true) (real PoolManager, mock IMD at its real address, full-range liquidity 1e22, price 1e6 SVO per IMD). (a) At openedAt, buy exact input 1 wei IMD: fee = 1 * 0.5e18 / 1e18 = 0, vault IMD unchanged; buy exact input 2 wei: fee 1. (b) At openedAt + 3600 s, sell exact output 27 wei IMD: gross = floor(27e18/0.965e18) = 27, fee = floor(27*0.035) = 0, trader receives 27, vault unchanged; sell exact output 28 wei: gross 29, fee 1. (c) At openedAt + 3600 s, buy exact input 28 wei: fee = floor(28*0.035) = 0; 29 wei: fee 1. Expected under a round-up rule: 1 wei of fee in each zero case. Actual: 0. Verified with test/scratch/MathEdges.t.sol test_dustFeesAreZero in both currency orders (passes on the current code, i.e. the behaviour is as described).","severity":"info","snippet":"            fee = buy ? actual * rate / (WAD - rate) : actual * rate / WAD;","title":"Every hook fee rounds down, so swaps whose IMD leg is below 1/rate wei pay no fee (boundary x precision, documented, negligible value)"}],"hash":"5d92ff97ad3c5ba477984ef60cac212e0a655735e5c299d53ceceb3c2ecc8a2c","nodeId":"b08ceb8a-b96b-4ffe-b495-5b6626bbe55d","outcome":"completed","summary":"The review is complete. Findings and coverage are written to `.imd-findings.json`; the scratch tests are removed and the tree is otherwise unchanged.\n\n**Launch checks**\n\n- **Manifest**: launch.json is valid. It has exactly the five schema keys, the flat three-argument constructor in declaration order, six permissions that encode flags 10444 and match `getHookPermissions`, name SOVRN.ONE with nine bytes and one dot, symbol SVO, 18 decimals, lowercase IMD as paired currency, fee 12500, tick spacing 60, initialPrice 2^96 as a string, and notes of 3,924 characters.\n- **Address mining**: PrepareLaunch mined a 10444 address from the real initcode in a local run and the CREATE2 deployment landed there. The real salt depends on the deployer's address and the resolved constructor arguments, so only the method is verified, not the final salt.\n- **Tests**: forge 1.8.5 reports 190 passed, 0 failed, 2 skipped. The skipped ones are the fork suites, since no RPC was set.\n\n**Math review result**\n\nNothing in the Math Precision, Boundary or Numerical Gap guides turned up a defect that blocks admission. I traced all four swap modes and both partial-fill paths: the exact-output sell quote composes with its charge so the trader always receives exactly the requested net, partial fills cannot trigger QuoteMismatch, products fit uint256, int128 bounds are checked, and the vault's 70/30 split is exact with no overflowing intermediate. Fuzz probes near the current price in both currency orders passed 2,000 runs each.\n\nOne **info** finding is recorded. Every fee uses floor division, which the README documents, so swaps whose IMD leg is below 1/rate wei pay nothing. Examples: a 1 wei opening buy, a 27 wei exact-output sell, or a 28 wei buy after the hour. The loss is under 1 wei per swap and costs more in gas than it saves, so it is not exploitable. No source change is recommended.\n\n**Coverage**: all 14 entry points have rows, plus six invariant and launch-check rows. The int128-max exact-output buy reverts inside v4's own swap math after the hook returns, which is not a hook defect. The fork test against Robinhood Chain remains unrun here and should be rerun before launch.","treeHash":null,"usage":{"cachedInputTokens":1805664,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":43241,"runtime":"claude","turns":38,"wallClockMs":1034590}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e8816d4386532a66","findings":[{"citation":"resolved","description":"Flow-gap seam (execution x first principles). `openedAt` is written once in `beforeInitialize` (src/SovrnHook.sol:115) and both `launchFeeNow()` (lines 143-147: `elapsed = block.timestamp - openedAt`, `elapsed >= DECAY ? NORMAL_FEE : ...`) and the liquidity lock (line 138: `block.timestamp < openedAt + DECAY`) count from it. Nothing in the hook ties the clock to the first liquidity. The brief's guarantee 'buys 50% at pool opening decaying linearly to 3.5% over 3,600 seconds' therefore holds only if the launch factory seeds the pool in the initializing transaction (or the same second). If the factory initializes in one transaction and seeds from its own address later (which the lock permits: `sender == factory`), the opening window is partly or fully consumed before any buy can be filled: seeding 1800 s later gives the first buyer a 26.75% fee, seeding 3600 s later gives 3.5%, and the vault collects correspondingly less. The repository cannot verify the real factory's flow; the manifest notes already require atomic seeding for the lock, and the same requirement is needed for the fee schedule. No unprivileged actor can trigger it, no funds are lost or misdirected, so this is informational: a deployment-flow precondition to record, not a source defect. Reproduced with a scratch test (src unchanged): both assertions below pass on the current code.","line":115,"path":"src/SovrnHook.sol","reproduction":"State: fresh PoolManager, SovrnToken deployed by factory F, SovrnHook(manager, token, F) at a 0x28cc-flag address. 1) At t=1_000_000 F calls manager.initialize(key{IMD, SVO, 12500, 60, hook}, 2^96). launchFeeNow() == 0.5e18. 2) vm.warp(1_000_000 + 3600); F (msg.sender == factory, so beforeAddLiquidity passes) adds full-range liquidity 1e22 via modifyLiquidity. 3) First buy ever: swap zeroForOne (IMD in), amountSpecified -1 ether, limit MIN_SQRT_PRICE+1. Expected under the brief: fee 0.5 ether to the vault (first buy at pool opening). Actual: launchFeeNow() == 0.035e18 and the vault receives exactly 0.035 ether. Variant: seed at +1800 s -> launchFeeNow() == 0.2675e18 and the vault receives 0.2675 ether on the same 1 ether buy. If the factory seeds inside the initializing transaction (the documented flow), the first buy pays 0.5 ether as intended.","severity":"info","snippet":"        openedAt = block.timestamp;","title":"Opening buy fee and liquidity lock are timed from beforeInitialize, not from the moment liquidity exists"}],"hash":"883cd6db18c10e6af494cd95c60516275ad45363a2dc7bed0a4db59dede4ba33","nodeId":"c3790796-5036-447c-ba4e-a18023cccb05","outcome":"completed","summary":"The findings file is written and valid, the scratch directory is removed, and the working tree holds no other changes.\n\n**Result: no blocking or fund-affecting defect found in the economics. One informational finding reported.**\n\n**What I verified**\n\n- **Manifest** `launch.json` is valid: schema keys only, constructor args in declaration order, permissions match `getHookPermissions` (flags 10444), name SOVRN.ONE and symbol SVO, fee 12500 accepted by beforeInitialize, spacing 60, lowercase IMD paired currency, notes under the limit.\n- **Hook address mining** for 10444 works through PrepareLaunch against the real initcode, and the deployed hook's permissions match the mined address in both currency orders.\n- **Test suite** on forge 1.8.5: 190 passed, 0 failed, 2 skipped. With the public Robinhood RPC, the chain 4663 fork suite passed 12 of 12 against the real IMD token and PoolManager.\n- **Live state probed**: IMD has code, 18 decimals, EOA owner, `blocked()` false for the manager and the Safe. The Safe is a 2-of-3 with code. Protocol fee on the pool is 0.\n- **Economics traced** across all four swap modes: fee is exactly the stated rate of gross IMD, partial fills recompute from the actual delta, the quoted exact-output sell equals the real swap by integer proof, the hook's net delta is always zero, and the vault's 70/30 accounting always sums to its balance with no underflow in shortfall. Non-zero protocol fees and both currency orders are covered by the existing reference-pool tests.\n\n**The one finding (info)**\n\nThe buy-fee decay and the liquidity lock both count from `openedAt`, written in beforeInitialize. If the factory initializes and then seeds from its own address in a later transaction, the opening window is spent before any buy can fill. Seeding 1800 seconds later makes the first buyer pay 26.75%, and 3600 seconds later 3.5%. I reproduced both with a scratch test and recorded the exact sequence in the finding. It is a deployment-flow precondition, not a source defect, and the documented atomic-seeding flow avoids it.\n\n**Already documented behaviors I did not re-report**: vault refusing IMD halts fee-bearing swaps, transfer-fee IMD breaks buys, hookless pools escape the fee, empty-pool price moves, IMD owner gate and OFT powers.\n\nThe coverage record answers all 14 entry points, plus rows for the key invariants, the manifest, mining, and the suite results.","treeHash":null,"usage":{"cachedInputTokens":1511761,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":54938,"runtime":"claude","turns":33,"wallClockMs":963224}},{"artifacts":[],"attempt":2,"bundleHash":"0a508953a1f32df649ec8a313374e9daa81e97c02b660eece2f548c6e7e8642c","device":"00920b27421b9a80","findings":[],"hash":"980bed5cd20776fb4fbec831160825fd9df9e9969c3a549aa29baa1e80cec89a","nodeId":"873309f6-9b50-49e0-b312-3681238ee1cf","outcome":"completed","summary":"Done. Only `launch.json` changed (two lines: the initial price and the notes). Everything else was verified in place.\n\n**Manifest.** The existing manifest already had the right hook, constructor args, flags, token name and symbol, fee 12500, spacing 60 and the IMD pair. I changed two things: `pool.initialPrice` now reads the reference's one-to-one value instead of the old indicative 3,000 IMD cap price, and the notes were rewritten to record this run's check results and the status of the six imported audit findings. Notes are 3924 characters, all addresses lowercase, no audit or security claim. One consequence to flag: `launch-attestation.json` binds the manifest by hash, so its manifest digest is stale until the attestation step refreshes it. That file is out of my write scope.\n\n**Checks run on 2026-10-09, forge 1.8.3:**\n\n| Check | Result |\n| --- | --- |\n| forge build | ok, lint warnings only |\n| forge test | 190 passed, 0 failed, 2 skipped (fork suites without RPC) |\n| Fork4663 with the public Robinhood RPC | 12 passed, both currency orders |\n| PrepareLaunch.mine for flags 10444 | address found, CREATE2 deployment matched the prediction, getHookPermissions agrees |\n| Protected hook and token floors against the real creation code | 4 of 4 and 7 of 7 passed, including factory initialization at fee 12500 / spacing 60 |\n\n**Imported findings.** All six reproduce. None were fixed because the brief launches this exact source and only the manifest may change. Each is recorded in the notes with its status:\n\n- Opening-transaction exemption covers any sender in the initializing transaction, so code the factory calls after initialize could add liquidity. Narrowing it is a source change.\n- Non-factory seeding in a later transaction is refused for an hour. This is the one remaining way the launch can stall and depends on the real factory's seeding flow, which is not in this tree.\n- A router that syncs IMD before the swap gets CurrencyNotSettled on buys. Sells and sync-after routers work.\n- After the hour, an IMD-only range acquires SVO from sellers with no hook fee. Inherent to the design.\n- README still lists five permissions and denies transient storage. README is out of scope.\n- The LiquidityLock test's named IMD-only range is actually SVO-only. The lock is unconditional so the assertions still hold.\n\n**My own review** found nothing that blocks admission beyond the factory-flow dependency above. Reproduction tests live in `test/scratch/`, which is discarded.","treeHash":"f6211bf67b55d0fc513e72e16c842070846c82e8","usage":{"cachedInputTokens":1272278,"inputTokens":388,"model":"claude-fable-5-1","outputTokens":22523,"runtime":"claude","turns":27,"wallClockMs":987355}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"540f6378761dba26","findings":[],"hash":"9fa12fb2b600d746c64241fa7446dd85da45006b9477dc2d06466416a0c213a4","nodeId":"873309f6-9b50-49e0-b312-3681238ee1cf","outcome":"failed","summary":"This content was flagged for possible cybersecurity risk. If this seems wrong, try rephrasing your request. If you’re doing authorized security work that requires more cyber permissive safeguards, apply for Daybreak access via https://platform.openai.com/settings/organization/status-and-access before retrying.","treeHash":null,"usage":{"cachedInputTokens":0,"inputTokens":0,"model":"gpt-6-astra","outputTokens":0,"runtime":"codex","turns":5,"wallClockMs":329038}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"ee9fbaf2480d1034","findings":[{"citation":"resolved","description":"Merged from audit_flow (06830bf5). A router that pays its input before swapping does sync(IMD), transfers P to the manager, then swap(). Inside that swap afterSwap finds the manager's IMD balance >= fee and runs take(IMD, vault, fee) while IMD is still the synced currency; PoolManager.take does not touch CurrencyReserves, so the router's later settle() measures paid = balance - reserves = P - fee. The hook's own delta nets to zero (+fee hook delta, -fee take) but the router's IMD delta is short by exactly the fee however large P is, and the unlock ends with CurrencyNotSettled. Sells are unaffected (SVO is the synced currency while the IMD fee is taken) and swap-then-settle routers (v4 Router, Universal Router SWAP/SETTLE_ALL/TAKE_ALL) work. The claim (mint) fallback is only chosen when the manager's balance is below the fee, which a pre-paid buy never is. Impact: every buy through a pre-paying integration reverts; no funds are lost, and the author documents it as a router constraint (manifest notes item 3, README). It is a compatibility limitation of the accepted design, not a loss path, so it is kept at low; it does not block admission. A fix outside this launch would mint the fee as a claim whenever IMD is the synced currency, or take it after settlement.","line":250,"path":"src/SovrnHook.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {SwapParams, ModifyLiquidityParams} from \"v4-core/src/types/PoolOperation.sol\";\nimport {BalanceDelta} from \"v4-core/src/types/BalanceDelta.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {ERC20} from \"solmate/src/tokens/ERC20.sol\";\nimport {SovrnToken} from \"src/SovrnToken.sol\";\nimport {SovrnHook} from \"src/SovrnHook.sol\";\n\ncontract ProofMockIMD is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\", 18) {\n        _mint(msg.sender, 1e33);\n    }\n}\n\n/// @dev Acts as the launch factory (initializes and seeds) and as a router. `swapPrepay` is the\n///      \"sync, transfer, swap, settle\" order: the input, fee included, is paid before the swap and\n///      settled after it. `swapPostpay` is the ordinary \"swap, sync, transfer, settle\" order.\ncontract ProofFactory {\n    IPoolManager public immutable m;\n\n    constructor(IPoolManager m_) {\n        m = m_;\n    }\n\n    function initialize(PoolKey memory key, uint160 p) external {\n        m.initialize(key, p);\n    }\n\n    function liquidity(PoolKey memory key, ModifyLiquidityParams memory p) external {\n        m.unlock(abi.encode(uint8(0), key, abi.encode(p)));\n    }\n\n    function swapPrepay(PoolKey memory key, SwapParams memory p, uint256 pay) external {\n        m.unlock(abi.encode(uint8(1), key, abi.encode(p, pay)));\n    }\n\n    function swapPostpay(PoolKey memory key, SwapParams memory p) external {\n        m.unlock(abi.encode(uint8(2), key, abi.encode(p)));\n    }\n\n    function unlockCallback(bytes calldata data) external returns (bytes memory) {\n        require(msg.sender == address(m));\n        (uint8 mode, PoolKey memory key, bytes memory params) = abi.decode(data, (uint8, PoolKey, bytes));\n        BalanceDelta d;\n        if (mode == 0) {\n            (d,) = m.modifyLiquidity(key, abi.decode(params, (ModifyLiquidityParams)), \"\");\n        } else if (mode == 1) {\n            (SwapParams memory p, uint256 pay) = abi.decode(params, (SwapParams, uint256));\n            Currency input = p.zeroForOne ? key.currency0 : key.currency1;\n            Currency output = p.zeroForOne ? key.currency1 : key.currency0;\n            m.sync(input);\n            ERC20(Currency.unwrap(input)).transfer(address(m), pay);\n            d = m.swap(key, p, \"\");\n            m.settle();\n            int128 inDelta = p.zeroForOne ? d.amount0() : d.amount1();\n            int128 outDelta = p.zeroForOne ? d.amount1() : d.amount0();\n            uint256 owed = uint256(-int256(inDelta));\n            if (pay > owed) m.take(input, address(this), pay - owed);\n            m.take(output, address(this), uint256(uint128(outDelta)));\n            return \"\";\n        } else {\n            d = m.swap(key, abi.decode(params, (SwapParams)), \"\");\n        }\n        _settle(key.currency0, d.amount0());\n        _settle(key.currency1, d.amount1());\n        return \"\";\n    }\n\n    function _settle(Currency c, int128 a) private {\n        if (a < 0) {\n            m.sync(c);\n            ERC20(Currency.unwrap(c)).transfer(address(m), uint256(-int256(a)));\n            m.settle();\n        } else if (a > 0) {\n            m.take(c, address(this), uint256(uint128(a)));\n        }\n    }\n}\n\n/// @notice A router that pays the swap input before the swap (sync, transfer, swap, settle) cannot buy SVO:\n///         SovrnHook.afterSwap takes the IMD fee out of the PoolManager while IMD is the synced currency, so\n///         the router's settle() credits it `paid - fee` and the unlock ends with CurrencyNotSettled. Paying\n///         the fee in advance does not help: the shortfall is always exactly the fee. Sells and swap-then-settle\n///         routers work, which the control tests show. Fails on the current code; passes once a buy taken through\n///         a pre-synced payment settles (for example by minting the fee as a claim whenever IMD is the synced\n///         currency, or by taking the fee only after settlement).\ncontract PreSyncRouterProofTest is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant TOKEN_AT = 0xF000000000000000000000000000000000000001;\n    PoolManager manager;\n    SovrnToken token;\n    SovrnHook hook;\n    ProofFactory factory;\n    PoolKey key;\n\n    function setUp() public {\n        manager = new PoolManager(address(this));\n        // Run the mock's constructor in place at IMD's address (independent of this file's name), minting to this test.\n        vm.etch(IMD, type(ProofMockIMD).creationCode);\n        (bool built, bytes memory runtime) = IMD.call(\"\");\n        require(built, \"mock IMD constructor reverted\");\n        vm.etch(IMD, runtime);\n        factory = new ProofFactory(manager);\n        deployCodeTo(\"SovrnToken.sol:SovrnToken\", \"\", TOKEN_AT);\n        token = SovrnToken(TOKEN_AT);\n        address at = address(uint160(0x28cc));\n        deployCodeTo(\"SovrnHook.sol:SovrnHook\", abi.encode(IPoolManager(address(manager)), token, address(factory)), at);\n        hook = SovrnHook(at);\n        key = PoolKey(Currency.wrap(IMD), Currency.wrap(TOKEN_AT), 12500, 60, IHooks(at));\n        factory.initialize(key, 79228162514264337593543950336000);\n        token.transfer(address(factory), 100_000_000 ether);\n        ERC20(IMD).transfer(address(factory), 1_000_000 ether);\n        factory.liquidity(key, ModifyLiquidityParams(-887220, 887220, 1e22, bytes32(0)));\n        vm.warp(block.timestamp + 2 hours); // past the decay: a buy pays the normal 3.5%\n    }\n\n    /// @dev Control: the ordinary order works and pays the vault.\n    function test_control_swapThenSettleBuyPaysTheVault() public {\n        factory.swapPostpay(key, SwapParams(true, -1 ether, TickMath.MIN_SQRT_PRICE + 1));\n        assertGt(ERC20(IMD).balanceOf(address(hook.vault())), 0);\n    }\n\n    /// @dev Control: a pre-paid SELL works, because the fee is taken in IMD while SVO is the synced currency.\n    function test_control_prepaidSellPaysTheVault() public {\n        factory.swapPrepay(key, SwapParams(false, -1000 ether, TickMath.MAX_SQRT_PRICE - 1), 1000 ether);\n        assertGt(ERC20(IMD).balanceOf(address(hook.vault())), 0);\n    }\n\n    /// @dev The finding: exact-input buy of 1 IMD, paid in advance with the 3.5% fee included (2 IMD sent, the\n    ///      unused part refunded by take). Expected: the swap settles and the vault holds the fee. Actual on this\n    ///      code: PoolManager reverts with CurrencyNotSettled, because afterSwap's take(IMD) moved the fee out\n    ///      between sync and settle, so settle credited the router 2 IMD - fee.\n    function test_prepaidBuyWithFeeIncludedSettles() public {\n        uint256 vaultBefore = ERC20(IMD).balanceOf(address(hook.vault()));\n        factory.swapPrepay(key, SwapParams(true, -1 ether, TickMath.MIN_SQRT_PRICE + 1), 2 ether);\n        assertGt(ERC20(IMD).balanceOf(address(hook.vault())), vaultBefore, \"fee did not reach the vault\");\n    }\n}","reproduction":"State: local PoolManager; SovrnToken at 0xF000...0001 (IMD is currency0); hook at 0x...28cc built with (manager, token, factory); the factory initialized the pool at sqrtPrice 79228162514264337593543950336000 and seeded full-range liquidity 1e22; block.timestamp two hours after opening (fee 3.5%). Inside the router's unlock: sync(IMD); IMD.transfer(manager, 2e18); swap(key, {zeroForOne:true, amountSpecified:-1e18, limit MIN_SQRT_PRICE+1}, \"\"); settle(); take(IMD, router, 2e18 - owed); take(SVO, router, out). Expected: the swap settles, the router pays 1e18 + 0.035e18 IMD and the vault receives 0.035e18 IMD. Actual: PoolManager.unlock reverts with CurrencyNotSettled(); the vault balance is unchanged. The same sequence as a sell (zeroForOne=false, -1000e18 SVO) succeeds, and the same buy with swap first and sync/transfer/settle after succeeds. Ran: forge test --match-path test/scratch/PreSyncRouterProof.t.sol -> test_prepaidBuyWithFeeIncludedSettles FAIL: CurrencyNotSettled(), both control tests PASS.","severity":"low","snippet":"                poolManager.take(Currency.wrap(IMD), address(vault), fee);","title":"afterSwap takes the IMD fee mid-swap, so a router that syncs IMD before the swap can never buy (CurrencyNotSettled)"},{"citation":"resolved","description":"Merged from audit_permissions (479c17e8), audit_flow (c12a95f1) and audit_economics (1aafa3f7): one root cause, three consequences. beforeInitialize writes openedAt = block.timestamp (line 115) and sets a transient flag; beforeAddLiquidity admits an add during the first 3,600 s only when the modifyLiquidity caller is exactly the constructor's factory address or the transient flag is still live (same transaction as PoolManager.initialize). (a) A factory that initializes in one transaction and seeds in a later one through a position manager or helper whose address is not $factory is refused with LiquidityLocked for an hour, while the empty pool's price can be moved to any limit by a zero-delta, zero-fee swap. (b) launchFeeNow() and the lock both count from openedAt, not from the first liquidity, so if the factory seeds from its own address later, the opening window is consumed before any buy can be filled (seeding at +1800 s gives the first buyer 26.75%, at +3600 s 3.5%). (c) Inside the initializing transaction every sender is exempt, so any contract the factory calls after initialize (a contributor callback, a helper) could add an IMD-only range beside the price. None of this is triggered by an unprivileged actor and no funds are lost or misdirected; whether any of it is hit depends solely on the launch factory's order of calls, which the repository cannot show and the manifest cannot express. The author documents the constraint (README line 86 'Deployment, initialization and initial liquidity seeding must be atomic', manifest notes items 1 and 2). Kept at low as a deployment precondition to confirm from the factory before the launch transaction is sent; if the factory initializes and seeds from its own address in one transaction, as the reference describes, every consequence is moot. No source change proposed.","line":138,"path":"src/SovrnHook.sol","reproduction":"(a) Existing test test/LiquidityLock.t.sol AtomicOpenTest and AtomicOpenReversedTest: hook factory = AtomicOpener; opener.open() initializes and seeds through a separate PoolRouter in the same call -> succeeds (test_seedingThroughAnotherContractInTheInitializingCallWorked). Same opener, same seeder, later top-level call at the same timestamp: opener.addLater(seeder, key, {-887220, 887220, 1e20}) -> reverts LiquidityLocked (test_aLaterTransactionThroughTheSameContractIsLockedOut); only after vm.warp(openedAt + 3600) does an outsider add succeed (test_anyoneCanAddOnceTheDecayIsOver). (b) Scratch test test/scratch/JudgeChecks.t.sol test_feeClockRunsFromInitializeNotFirstLiquidity: _system(false) initializes at t0 with no liquidity, launchFeeNow()==0.5e18; vm.warp(t0+1800); the factory (router) adds full-range 1e22 (allowed: sender == factory); launchFeeNow()==0.2675e18; first buy ever, exact input 1 ether IMD: vault receives exactly 0.2675 ether, not 0.5 ether. Expected under the brief: the first buy at pool opening pays 50%. Actual: 26.75%. (c) test_emptyPoolPriceMovesAtZeroCostDuringTheLock: with no liquidity a 1 ether exact-input buy to MIN_SQRT_PRICE+1 completes with zero deltas and the vault receives 0. All three scratch tests pass on the current code (the behaviour is as described).","severity":"low","snippet":"        if (sender != factory && !openingTx && block.timestamp < openedAt + DECAY) revert LiquidityLocked();","title":"Opening-hour liquidity lock and buy-fee clock are anchored on the factory's seeding flow, which is outside the repository: a non-factory seeder in a later transaction is locked out for 3,600 s, late s"},{"citation":"resolved","description":"Merged from audit_permissions (0847dc2c) and audit_flow (adb223b3). Commit 20c673e changed launch.json (pool.initialPrice from 45742400955009932534161870629490 to 79228162514264337593543950336 and the notes text) without regenerating the attestation or the check script. launch-attestation.json line 55 binds launch.json to sha256 77c83561..., but the committed file hashes to d3e79b3b5f16a242e236981ff5207f00fa22bec4bbcc7320eb817159d297f400; line 23 records the old initialPrice; script/attest.py line 40 hard-asserts the old pool block, so the verification step README line 103 tells the deployer to run raises AssertionError before comparing any artifact. Everything else in the record still matches the tree: the five src/ sha256 hashes and the three creationBytecodeKeccak256 values (SovrnToken 0xd0c3bc18..., SovrnHook 0xf78b7a97..., LifeForceVault 0x6e583423...) equal forge inspect bytecode | cast keccak on the current build. The service signs its own ReleaseAttestation, so nothing deployed changes and the hook accepts any opening price; this is a provenance/documentation inconsistency in files this task may not edit. Fix outside src/: regenerate launch-attestation.json and the asserted pool block in script/attest.py for the committed manifest.","line":55,"path":"launch-attestation.json","reproduction":"HEAD 20c673e. sha256sum launch.json -> d3e79b3b5f16a242e236981ff5207f00fa22bec4bbcc7320eb817159d297f400 versus launch-attestation.json line 55 (77c8356158ad...). jq .pool.initialPrice launch.json -> 79228162514264337593543950336 versus launch-attestation.json line 23 (45742400955009932534161870629490). python3 -I script/attest.py --check -> 'File script/attest.py, line 37, in build_record: assert manifest[\"pool\"] == {...}  AssertionError'. Expected: the check passes and the record matches the committed manifest. Actual: AssertionError; bytecode and source hashes verified to match.","severity":"low","snippet":"    \"launch.json\": \"77c8356158ad823d6b8c00a1afa96e1be2e9c31427cdae59b234aa3478f8d2d7\",","title":"launch-attestation.json and script/attest.py still describe the pre-20c673e manifest: the repository's own provenance check fails on the tree being launched"},{"citation":"resolved","description":"From audit_permissions (7a680c9b), extended with README line 17. src/SovrnHook.sol getHookPermissions (lines 73-80) enables six permissions including beforeAddLiquidity (flags 10444, matching launch.json and HookFlags.SOVRN_FLAGS) and lines 116-119 / 134-137 use tstore/tload. README line 96 says five and denies transient storage; README lines 19 and 50 are correct, so the document contradicts itself. README line 17 still describes launch.json's indicative price as a 3,000 IMD cap with IMD as currency0, while the committed manifest price 79228162514264337593543950336 is a 1:1 sqrt price. Documentation only; already item (5) of the manifest notes; README is outside this task's write scope.","line":96,"path":"README.md","reproduction":"Read README.md line 96 and compare with src/SovrnHook.sol lines 73-80 (p.beforeAddLiquidity = true; six flags) and launch.json hook.permissions (six entries) and lines 116-119 (tstore). Read README.md line 17 against launch.json pool.initialPrice 79228162514264337593543950336. Expected: README lists six permissions, mentions the transient flag and describes the committed price. Actual: five permissions, 'no transient hook storage', 3,000 IMD cap.","severity":"info","snippet":"The unchanged `foundry.toml` pins **Solidity 0.8.26**, **Cancun**, optimizer **200 runs**, **via_ir = true**, and **`bytecode_hash = \"none\"`**. Dependencies are already ordinary vendored files in `lib/`; no new packages are needed. The hook is the base's direct v4 callback implementation, with no added base-class dependency, share tokens, transient hook storage or role framework. Enabled permissions are exactly `beforeInitialize`, `beforeSwap`, `afterSwap`, `beforeSwapReturnDelta`, `afterSwapReturnDelta`; all nine others are false.","title":"README still documents five permissions, no transient hook storage and a 3,000 IMD indicative price; source enables six (flags 10444) and uses tstore/tload, and the manifest price is 2^96"},{"citation":"resolved","description":"From audit_math (c0490259). All four fee formulas (lines 179, 182-191, 239) use floor division; the README states 'Integer divisions round down'. The Math Precision guide asks fees to round up; here the vault under-collects by at most 1 wei of IMD per swap and collects nothing when the IMD leg is below 1/rate wei: 1 wei at the opening rate, 28 wei at the normal rate. The exact-output sell quote (gross = floor(requested*WAD/(WAD-rate)), fee = floor(actual*rate/WAD)) composes so requested + fee == gross for full fills, so the trader always receives exactly the requested net; the only leak is the floor itself. Per-swap loss is under 1e-18 IMD and a sub-29-wei swap costs far more in gas than the fee it avoids. No change recommended for this launch; recorded because it is the one departure from the guide's rounding rule.","line":239,"path":"src/SovrnHook.sol","reproduction":"Scratch test test/scratch/JudgeChecks.t.sol test_dustFeesRoundToZero (SystemBase, real PoolManager, mock IMD at its real address, full-range liquidity 1e22): at openedAt, buy exact input 1 wei IMD -> vault IMD unchanged; 2 wei -> +1. At openedAt+3600: buy exact input 28 wei -> unchanged; 29 wei -> +1. Sell exact output 27 wei IMD -> unchanged; 28 wei -> +1 (gross 29). Expected under a round-up rule: 1 wei of fee in each zero case. Actual: 0. The test passes on the current code, confirming the behaviour.","severity":"info","snippet":"            fee = buy ? actual * rate / (WAD - rate) : actual * rate / WAD;","title":"Every hook fee rounds down, so swaps whose IMD leg is below 1/rate wei pay no fee (sub-29-wei dust, no exploitable value)"}],"hash":"a831ff9f96a8412c1f67ec2e17a47352bcb6506d93e49916302e4bd356ea5a98","nodeId":"45850965-591d-4b8d-aabc-3e89464c5689","outcome":"completed","summary":"The review is complete and `.imd-findings.json` holds the final report. No tracked file was changed.\n\n**Verdict: nothing blocks admission.** The manifest, the mined hook address, the bytecode sizes and the project's own suite all check out. Five findings are kept, none above low, and each was reproduced against the tree.\n\nWhat I verified:\n- **launch.json** has exactly the five schema keys, flat constructor arguments in declaration order, six permissions matching getHookPermissions (flags 10444), name SOVRN.ONE, symbol SVO, decimals 18, lowercase IMD paired currency, fee 12500, tick spacing 60, a 29-digit decimal price, and notes under 4,000 characters.\n- **Hook mining** with script/PrepareLaunch.s.sol found a 10444 address, CREATE2 deployed it, and the deployed hook's permissions match. Runtime is 7,199 bytes and creation code with arguments is 10,862 bytes, both inside the protocol limits. The vault child deploys from the constructor.\n- **Test suite** on forge 1.8.3: 192 passed, 1 failed, 2 skipped. The two skips are the fork suites without an RPC. The one failure is the imported specialist proof, which fails with CurrencyNotSettled exactly as its finding states. The project's own 190 tests pass.\n- **Attestation bytecode linkage**: the five source hashes and three creation-bytecode keccaks match the current build.\n\nFindings kept, merged across specialists:\n1. **Low:** afterSwap takes the IMD fee while IMD may be the synced currency, so any router that pays before swapping cannot buy. Reproduced with the specialist's proof, which I made filename-independent and re-ran; it carries the proof.\n2. **Low:** the opening-hour lock and the buy-fee clock both hang on the factory seeding inside the initializing transaction. Three specialist reports collapsed into one root cause, reproduced with the existing LiquidityLock tests plus a scratch test showing a seed at 1,800 seconds gives the first buyer a 26.75% fee.\n3. **Low:** launch-attestation.json and script/attest.py still describe the pre-20c673e manifest; the repository's own check raises AssertionError. Bytecode hashes still match.\n4. **Info:** README line 96 lists five permissions and denies transient storage, and line 17 describes the old price.\n5. **Info:** fees round down, so sub-29-wei IMD legs pay nothing. Reproduced with a scratch test.\n\nCoverage answers all 14 entry points plus four invariant rows. afterSwap and beforeAddLiquidity carry finding references; everything else holds. Scratch files live only under test/scratch and are not kept.","treeHash":null,"usage":{"cachedInputTokens":1981001,"inputTokens":484,"model":"claude-fable-5-1","outputTokens":39237,"runtime":"claude","turns":36,"wallClockMs":1303118}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"e57a8e639cccfbab","findings":[{"citation":"resolved","description":"Commit 20c673e changed launch.json (pool.initialPrice from 45742400955009932534161870629490 to 79228162514264337593543950336, and the notes text) without regenerating the delivered attestation or the script that checks it. Three things now disagree with the tree: (a) launch-attestation.json line 55 binds launch.json to sha256 77c83561..., while the committed file hashes to d3e79b3b5f16a242e236981ff5207f00fa22bec4bbcc7320eb817159d297f400; (b) launch-attestation.json line 23 records pool.initialPrice 45742400955009932534161870629490 while launch.json says 79228162514264337593543950336; (c) script/attest.py line 40 hard-asserts the old initialPrice, so `python3 script/attest.py --check` (the verification step README line 103 tells the deployer to run) raises AssertionError at build_record() before it even compares hashes. The source hashes and the three creation-bytecode keccaks in the attestation DO match the current build (checked with forge inspect + cast keccak), so this is a provenance/documentation inconsistency, not a bytecode one. README line 17 is also stale: it still describes the manifest's indicative price as a 3,000 IMD cap with IMD as currency0, but 2^96 is a 1:1 sqrt price. The service builds its own signed ReleaseAttestation, so this does not change what is deployed; it does mean the repository's own provenance check fails on the tree being launched, and anyone following README to verify the build gets a failure. Fix: regenerate launch-attestation.json and update the asserted pool block in script/attest.py (and README line 17) for the committed manifest, or revert the manifest price to the attested value.","line":55,"path":"launch-attestation.json","reproduction":"State: HEAD 20c673e. Run `sha256sum launch.json` -> d3e79b3b5f16a242e236981ff5207f00fa22bec4bbcc7320eb817159d297f400; compare with launch-attestation.json line 55 (77c8356158ad...). Read launch.json pool.initialPrice (79228162514264337593543950336) against launch-attestation.json line 23 (45742400955009932534161870629490). Run `python3 script/attest.py --check`: line 40 asserts manifest[\"pool\"] == {... \"initialPrice\": \"45742400955009932534161870629490\"} and raises AssertionError (python3 is not installed on this machine, so the assertion was read from the script rather than executed; the hash and price mismatches were verified directly). Expected: the attestation and the check script match the committed manifest. Actual: both still describe the pre-20c673e manifest.","severity":"low","snippet":"    \"launch.json\": \"77c8356158ad823d6b8c00a1afa96e1be2e9c31427cdae59b234aa3478f8d2d7\",","title":"launch-attestation.json and script/attest.py no longer describe the committed launch.json (stale after commit 20c673e)"},{"citation":"resolved","description":"Access-control trust gap between the hook and the launch factory, which is outside this repository. beforeAddLiquidity admits an add during the first 3,600 s only when (a) the PoolManager's msg.sender is exactly the constructor's `factory` address, or (b) the transient flag set in beforeInitialize is still live, i.e. the add happens inside the initializing transaction. `sender` is the contract that calls PoolManager.modifyLiquidity, so a factory that seeds through a position manager or a helper in a separate transaction is refused as if it were an attacker, while the pool sits initialized and empty for an hour (an empty v4 pool's price can be moved to any limit by a zero-delta swap, README line 107). Nothing in the repository can verify the real factory's seeding flow, and the manifest cannot express it. This is the same condition the manifest notes list as imported finding (2); it is restated here because it is the one remaining access-control path by which admission's deployer simulation can revert with LiquidityLocked on correct code. Needed evidence before launch: confirmation that the launch factory adds the seed liquidity either from its own address or within the transaction that calls PoolManager.initialize. No source change is proposed; narrowing or widening the exemption is a design decision.","line":138,"path":"src/SovrnHook.sol","reproduction":"Existing test test/LiquidityLock.t.sol AtomicOpenTest (lines 123-145): a factory stand-in (AtomicOpener) initializes the pool and seeds through a different contract (PoolRouter `seeder`) in the same call -> succeeds (test_seedingThroughAnotherContractInTheInitializingCallWorked). The same opener calling the same seeder in a later top-level call -> reverts LiquidityLocked (test_aLaterTransactionThroughTheSameContractIsLockedOut). Concrete state: hook.factory() = opener, PoolManager.modifyLiquidity msg.sender = seeder != opener, tload(OPENING_TX) = 0, block.timestamp < openedAt + 3600. Expected by a factory that seeds in two steps: liquidity added. Actual: revert LiquidityLocked for 3,600 s.","severity":"low","snippet":"        if (sender != factory && !openingTx && block.timestamp < openedAt + DECAY) revert LiquidityLocked();","title":"Opening-hour liquidity lock refuses any non-factory sender in a later transaction: launch stalls if the real factory seeds through another contract or a second transaction"},{"citation":"resolved","description":"Documentation-only inconsistency in the permission model the launch publishes. src/SovrnHook.sol lines 73-80 enable six permissions including beforeAddLiquidity (flags 10444, matching launch.json and HookFlags.SOVRN_FLAGS), and lines 116-119 / 134-137 use tstore/tload for the opening-transaction flag. README line 96 says five permissions and 'no ... transient hook storage'. README line 19 and line 50 are correct, so the file contradicts itself. A reviewer reading only line 96 would conclude the manifest's permission list is wrong. Already noted as item (5) in the manifest notes; recorded here with a file and line so it can be fixed when README is in scope.","line":96,"path":"README.md","reproduction":"Read README.md line 96 and compare with src/SovrnHook.sol getHookPermissions() (lines 73-80: beforeAddLiquidity = true) and launch.json hook.permissions (six entries). Expected: README lists the six enabled permissions and mentions the transient flag. Actual: lists five and denies transient storage.","severity":"info","snippet":"The unchanged `foundry.toml` pins **Solidity 0.8.26**, **Cancun**, optimizer **200 runs**, **via_ir = true**, and **`bytecode_hash = \"none\"`**. Dependencies are already ordinary vendored files in `lib/`; no new packages are needed. The hook is the base's direct v4 callback implementation, with no added base-class dependency, share tokens, transient hook storage or role framework. Enabled permissions are exactly `beforeInitialize`, `beforeSwap`, `afterSwap`, `beforeSwapReturnDelta`, `afterSwapReturnDelta`; all nine others are false.","title":"README still documents five permissions and no transient hook storage; source enables beforeAddLiquidity (flags 10444) and uses tstore/tload"}],"hash":"ae007e7e3c4142f6ad3d5079b44c858816565d398c827845ea8a9f0179e6cc9c","nodeId":"f3dc1b59-6aa9-4d1c-ab9a-9cf1392aa024","outcome":"completed","summary":"Review complete. The findings file is valid, every snippet matches its line, and `git status --porcelain` shows nothing but the required `.imd-findings.json` (no scratch files were created).\n\n**Outcome.** No access-control, trust-gap or asymmetry defect was found in the source. Two low findings and one info finding are recorded, all about provenance and documentation around the manifest, not the contracts.\n\n**What was verified**\n\n- `launch.json` is a valid manifest: exactly the five schema keys, hook `SovrnHook` with `[\"$poolManager\",\"$token\",\"$factory\"]` in constructor order, six permissions encoding flags 10444 (matches `getHookPermissions`), token `SovrnToken` named SOVRN.ONE / SVO with 18 decimals, fee 12500 and tick spacing 60 as numbers, lowercase IMD paired currency, decimal `initialPrice`, notes under 4,000 characters.\n- Hook address mining: `test/Launch.t.sol` mines with `PrepareLaunch.mine`, deploys the real initcode by CREATE2 and asserts the 10444 bits, vault linkage and factory-only initialization in both currency orders.\n- Test results on forge 1.8.3: 190 passed, 0 failed, 2 skipped locally. With the public Robinhood RPC set, the fork suite passed 12 of 12 against the real IMD and PoolManager (protocol fee currently 0, LP fee 12500). `forge fmt --check` is clean.\n- Permission model traced for all 14 entry points: every hook callback is PoolManager-only and pool-bound, the quote path is self-only and always reverts, redemption pays only the immutable vault, both vault withdrawals share one guard and pay only the fixed Safe, and the token is plain.\n\n**Findings**\n\n1. **Low, stale attestation.** Commit 20c673e changed `launch.json` (price and notes) but `launch-attestation.json` still binds the old file hash and old price, and `script/attest.py` hard-asserts the old price, so the README's own `attest.py --check` step fails. Bytecode and source hashes still match the build. Python is not installed here, so the assertion was read, not executed.\n2. **Low, factory trust gap.** During the first hour only the constructor's factory address, or a caller inside the initializing transaction, may add liquidity. A real factory that seeds through another contract in a later transaction gets `LiquidityLocked`. The repository cannot verify the factory's flow. The existing `AtomicOpenTest` reproduces both sides.\n3. **Info, README line 96** still lists five permissions and denies transient storage.\n\n**Not run.** `test_attestation.py` and `attest.py --check` need python3, which this machine lacks.","treeHash":null,"usage":{"cachedInputTokens":1695222,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":33501,"runtime":"claude","turns":45,"wallClockMs":724170}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f47f953f8e35921e","findings":[{"citation":"resolved","description":"beforeInitialize sets the transient flag OPENING_TX and beforeAddLiquidity waives the 60-minute lock for every sender while that flag is set, i.e. for the whole remainder of the transaction that initialized the pool, not only for the factory or for the seeding step. The design accepts this so a factory that seeds through another contract works. The residual exposure: if the launch factory's transaction hands execution to any address the payer controls after PoolManager.initialize (an ETH refund by call to the payer, a token callback, a post-launch notification), that code can add an IMD-only range beside the opening price inside the same transaction. That is exactly the route the lock was added to close in launch #1173 (sellers then convert the attacker's IMD into SVO at the opening price with no 50% buy fee). Whether the real factory makes such a call could not be verified here (factory source not in this tree), so the severity is bounded to low and this is for the adapter and panel to confirm against the factory. A narrower exemption (factory sender, or a seeder the factory names in hookData) would remove the dependence on the factory's call graph; no source change was made.","line":138,"path":"src/SovrnHook.sol","reproduction":"State: pool not yet initialized; a contract X that is NOT the factory (test/LiquidityLock.t.sol AtomicOpenTest: `seeder` is a fresh PoolRouter, factory is `opener`). Call sequence in ONE transaction: opener.open(manager, seeder, key, price, ModifyLiquidityParams(-887220, 887220, 1e20, 0)) which does manager.initialize(key, price) then seeder.liquidity(key, p). Actual: the add succeeds although sender == seeder != factory and block.timestamp < openedAt + 3600 (test_seedingThroughAnotherContractInTheInitializingCallWorked passes, liquidity > 0). Expected under a strict factory-only lock: LiquidityLocked. The same call from X in the next transaction reverts LiquidityLocked (test_aLaterTransactionThroughTheSameContractIsLockedOut), which shows the boundary is the transaction, not the caller. Attack precondition to confirm: the factory's launch transaction performs any call into payer-controlled code after initialize.","severity":"low","snippet":"        if (sender != factory && !openingTx && block.timestamp < openedAt + DECAY) revert LiquidityLocked();","title":"Opening-hour liquidity lock exempts any contract that gains control inside the factory's launch transaction"},{"citation":"resolved","description":"The lock admits only sender == factory or a call inside the initializing transaction. The hook therefore assumes the launch factory either seeds from its own address or seeds atomically in the initialize transaction. If the real factory seeds in a separate transaction through another contract (a position manager, a seeding helper), every add reverts LiquidityLocked until openedAt + 3600. During that hour an initialized pool with no liquidity lets anyone move sqrtPriceX96 to their own limit at zero cost (already recorded in test/RevisionBoundaries.t.sol, finding d3c40d8222ad), so the opening price would be set by whoever swaps first, not by the factory. This is an operational dependency on the factory's flow, not a reachable defect on its own; it is reported because it is the one way the hook can still block or distort admission and the factory's flow was not available to verify.","line":138,"path":"src/SovrnHook.sol","reproduction":"State: pool initialized by `router` (the factory) at timestamp T, no liquidity yet or any liquidity. Input: a different contract `outsider` (PoolRouter) calls manager.unlock -> modifyLiquidity(key, ModifyLiquidityParams(-60, 60, 1e18, 0)) at any timestamp in [T, T+3599] in a transaction after the initializing one. Actual: revert LiquidityLocked (test/LiquidityLock.t.sol test_othersCannotAddInTheOpeningSecondEither and test_othersCannotAddLiquidityDuringTheDecay). Expected for an atomic factory flow: not applicable; expected if the factory's seeding is a second transaction via a periphery contract: the launch's own liquidity is refused for an hour. At T+3600 the same call succeeds (test_anyoneCanAddOnceTheDecayIsOver).","severity":"low","snippet":"        if (sender != factory && !openingTx && block.timestamp < openedAt + DECAY) revert LiquidityLocked();","title":"Liquidity lock reverts any non-factory seeding made in a later transaction; launch liquidity then cannot be added for 3,600 s while the empty pool's price is freely movable"},{"citation":"resolved","description":"afterSwap pays the fee by poolManager.take(IMD, vault, fee) in the middle of the swap, which lowers the manager's IMD balance. PoolManager.settle() credits balanceNow - balanceAtLastSync. A router that calls sync(IMD) and transfers its input before the swap and settles after it is credited input - fee and the unlock reverts CurrencyNotSettled, while the identical swap on a pool without this hook succeeds. Sells (fee on the output side) and routers that sync after the swap (Uniswap's V4Router / Universal Router pattern) are unaffected. The README discloses the condition and states no delivered test exercises it; this finding supplies the reproduction. Impact is availability for that router class only, no loss of funds. No source change was made; a fix that preserves the design is to document the required ordering in the manifest notes for integrators or to settle the fee via claims when a pre-synced balance is detected (a design decision for the author).","line":250,"path":"src/SovrnHook.sol","reproduction":"Fixture: SystemBase._system(true), vm.warp(openedAt + 3600) so the buy fee is 3.5%, manager holds >= 1 IMD so the direct take path runs. Router R.unlockCallback does: manager.sync(IMD); IMD.transferFrom(payer, manager, 1e18); d = manager.swap(key, SwapParams(zeroForOne = imdIsCurrency0, -1e18, MIN_SQRT_PRICE+1), \"\"); manager.settle(); take outputs. Actual: R.trade reverts IPoolManager.CurrencyNotSettled (the hook took 0.035e18 IMD to the vault between sync and settle, so settle credited 0.965e18 against a 1e18 debt). Same router selling 1,000,000 SVO succeeds; the delivered PoolRouter (sync after swap) buying 1 IMD succeeds. Verified in both currency orders (scratch test test_syncFirstRouterBuyRevertsSellWorks, IMD as currency0 and currency1).","severity":"low","snippet":"                poolManager.take(Currency.wrap(IMD), address(vault), fee);","title":"Buys through a router that syncs IMD before the swap revert with CurrencyNotSettled on this pool"},{"citation":"resolved","description":"The lock only covers the first 3,600 s. After that, anyone may add liquidity, and a liquidity position is not a swap: an IMD-only range placed just above the current tick (IMD as currency0; mirrored otherwise) is converted into SVO by ordinary sell flow at the range's prices, then withdrawn (removal has no callback). The provider pays no hook fee; only the sellers' 3.5% reaches the vault. This is inherent to taxing swaps but not liquidity, and the author judged it acceptable once the buy fee is 3.5% (README). It is recorded so the economics are explicit: the 3.5% 'buy' fee is avoidable by anyone willing to act as a maker, with price risk and gas as the only costs.","line":124,"path":"src/SovrnHook.sol","reproduction":"Fixture: SystemBase._system(true) (full-range liquidity 1e22, sqrt price 1000*2^96, current tick ~138162), vm.warp(openedAt + 3600). Outsider adds ModifyLiquidityParams(138180, 138480, 1e24, 0) via a PoolRouter: takes 14,873,939,550,374,996,442 wei IMD and 0 SVO. ALICE sells 9,000,000e18 SVO through the delivered router (pays 3.5% of her IMD out to the vault). Outsider removes the range (liquidityDelta -1e24): receives back 6,175,534,401,120,829,956 wei IMD and 8,901,870,424,541,740,935,765,073 wei SVO. Vault IMD increased only by Alice's fee; the outsider's conversion of ~8.7 IMD into ~8.9M SVO paid 0 to the vault. Expected under the brief's 'buys pay 3.5%': a buyer of 8.9M SVO would have paid ~0.3 IMD. Same result mirrored with IMD as currency1 (range -138480..-138180).","severity":"info","snippet":"    /// @notice During the opening decay only the launch factory may add liquidity, plus anyone inside the pool's\n    ///         initializing transaction (so an atomic seeding works whichever contract performs it). An IMD-only\n    ///         range placed beside the price would otherwise turn IMD into SVO through sell flow and skip the buy fee.","title":"After the opening hour, providing an IMD-only range and letting sellers cross it acquires SVO with no hook fee"},{"citation":"resolved","description":"README line 96 says the enabled permissions are exactly beforeInitialize, beforeSwap, afterSwap, beforeSwapReturnDelta, afterSwapReturnDelta ('all nine others are false') and that the hook has 'no ... transient hook storage'. Since commit a6137d4 the hook also enables beforeAddLiquidity (flags 10444, as launch.json, HookFlags.SOVRN_FLAGS and getHookPermissions all say) and since 48d35f2 it uses transient storage (tstore in beforeInitialize, tload in beforeAddLiquidity). The rest of the README (lines 19, 50) is already updated, so this is one stale sentence, but a reviewer or integrator reading line 96 gets the wrong permission set.","line":96,"path":"README.md","reproduction":"Read README.md line 96 and compare with src/SovrnHook.sol lines 73-80 (six fields true, including p.beforeAddLiquidity) and lines 116-119 / 134-137 (tstore / tload of OPENING_TX). Expected: documentation lists beforeAddLiquidity and the transient flag. Actual: it lists five permissions and denies transient storage.","severity":"info","snippet":"The unchanged `foundry.toml` pins **Solidity 0.8.26**, **Cancun**, optimizer **200 runs**, **via_ir = true**, and **`bytecode_hash = \"none\"`**. Dependencies are already ordinary vendored files in `lib/`; no new packages are needed. The hook is the base's direct v4 callback implementation, with no added base-class dependency, share tokens, transient hook storage or role framework. Enabled permissions are exactly `beforeInitialize`, `beforeSwap`, `afterSwap`, `beforeSwapReturnDelta`, `afterSwapReturnDelta`; all nine others are false.","title":"README states five hook permissions and 'no transient hook storage'; the code has six permissions and uses tstore/tload"},{"citation":"resolved","description":"With IMD as currency0 the fixture opens at sqrt price 1000*2^96 (tick ~ +138162). The range -184200..-166200 lies entirely below the current tick, so a position there holds only currency1, which is SVO, not IMD. The mirrored range 166200..184200 with IMD as currency1 lies above tick ~ -138162 and holds only currency0, again SVO. The lock itself is unconditional for non-factory senders during the hour, so every LiquidityLock assertion still holds and the protection is real; but the tests' description ('a range holding only IMD, sitting beside the price on the side sellers push toward') is wrong and test_bypassDoesNotWork would also pass against a hook that only blocked SVO-only ranges. An IMD-only range for this fixture is one above the current tick when IMD is currency0 (e.g. 138180..138480).","line":52,"path":"test/LiquidityLock.t.sol","reproduction":"Fixture: SystemBase._system(true), vm.warp(openedAt + 3600) so the add is allowed. Add _imdOnlyRange() (ModifyLiquidityParams(-184200, -166200, 1e21, 0) with IMD as currency0) through a PoolRouter. Actual: IMD taken 0, SVO taken 146,066,172,882,120,873 wei. Mirrored orientation: identical numbers. Expected for an 'IMD-only range': IMD taken > 0, SVO taken 0.","severity":"info","snippet":"    function _imdOnlyRange() internal view returns (ModifyLiquidityParams memory) {\n        return _imdIsCurrency0()\n            ? ModifyLiquidityParams(-184200, -166200, 1e21, bytes32(0))\n            : ModifyLiquidityParams(166200, 184200, 1e21, bytes32(0));","title":"LiquidityLock tests' '_imdOnlyRange' is an SVO-only range in both orientations, so test_bypassDoesNotWork does not model the bypass it names"}],"hash":"e01ce51158679ac1407f2998fc49309cade8ea483eec0c5c2731838818b8f26c","nodeId":"a2fddd07-e912-4df7-9d98-51624d1fd376","outcome":"completed","summary":"Review complete. The findings are in `.imd-findings.json` (six entries, none above low). No source, test, manifest or configuration file was modified; my only other artifacts are three scratch tests under `test/scratch/`, which is disposable.\n\n**Verification results**\n\n| Check | Result |\n| --- | --- |\n| launch.json | Valid: five schema keys, hook SovrnHook with `[\"$poolManager\",\"$token\",\"$factory\"]` matching the constructor order, six permissions encoding 10444, token SOVRN.ONE / SVO / 18, pool fee 12500, tick spacing 60, lowercase IMD paired currency, decimal sqrtPriceX96 in range, notes 3879 characters |\n| Attestation | `python3 -I script/attest.py --check` reports manifest and attestation match the build |\n| Address mining | PrepareLaunch mined a salt for a concrete initcode (real manager address, fresh token, factory probe); predicted address carries flags 10444 and a CREATE2 deploy landed on the prediction with vault linkage intact. The salt is initcode-specific, so it must be re-mined once the factory's token address is known |\n| forge test | 190 passed, 0 failed, 2 skipped (the fork suites, no `FORK_4663_RPC`); forge fmt clean |\n| Protected floor | All 11 floor tests pass against the real creation bytecode with the manager built in place at the real PoolManager address, a token probe on either side of IMD, and the factory probe initializing the pool |\n\n**Findings (all low or informational)**\n\n- **Low, hook line 138:** the opening-hour lock exemption covers the whole initializing transaction, so any payer-controlled code the factory calls after `initialize` in that transaction could place the IMD-only range the lock exists to block. Whether the factory does such a call is unverifiable here.\n- **Low, hook line 138:** the same lock refuses any non-factory seeding made in a later transaction for an hour, during which an empty pool's price is freely movable. This holds only if the factory's seeding is not atomic.\n- **Low, hook line 250:** a router that syncs IMD before the swap has every buy revert with `CurrencyNotSettled`, because the fee take lowers the manager's balance between sync and settle. Reproduced in both currency orders; sells and sync-after routers are fine.\n- **Info:** after the hour, anyone can acquire SVO with no hook fee by providing an IMD-only range and letting sellers cross it. Reproduced with concrete amounts.\n- **Info:** README line 96 still lists five permissions and denies transient storage.\n- **Info:** the LiquidityLock tests' \"IMD-only range\" is actually SVO-only in both orientations, so the bypass test does not model the bypass it names. The lock still holds because it is unconditional.\n\n**Scope**\n\nRead in full: all five `src/` contracts, `script/PrepareLaunch.s.sol`, `launch.json`, `README.md`, `test/REVIEW.md`, the fixtures and routers in `test/`, `LiquidityLock.t.sol`, `Launch.t.sol`, `Fork4663.t.sol`, the mocks, both protected floor tests, and the v4-core `Hooks` library paths the quote and delta logic depend on. The fee math, quote-and-match mechanism, delta accounting, vault reserve arithmetic and token were traced by hand and against the existing fuzz and invariant suites, and I found no loss-of-funds or accounting defect. Not reached: the real IMD token source and the launch factory's code (neither is in the tree and no network was used), the fork rehearsal (no RPC), and the remainder of the vendored v4-core beyond the Hooks library. Static analyzers were not run. This review does not make the code audited or secure.","treeHash":null,"usage":{"cachedInputTokens":1882781,"inputTokens":450,"model":"claude-fable-5-1","outputTokens":49738,"runtime":"claude","turns":42,"wallClockMs":1528976}}],"verification":[{"checks":[{"durationMs":151258,"exitCode":0,"name":"build","output":"Compiling 93 files with Solc 0.8.26\nSolc 0.8.26 finished in 151.06s\nCompiler run successful with warnings:\nWarning (5159): \"selfdestruct\" has been deprecated. Note that, starting from the Cancun hard fork, the underlying opcode no longer deletes the code and data associated with an account and only transfers its Ether to the beneficiary, unless executed in the same transaction in which the contract was created (see EIP-6780). Any use in newly deployed contracts is strongly discouraged even if the new behavior is taken into account. Future changes to the EVM might further reduce the functionality of the opcode.\n  --> test/Vault.t.sol:40:9:\n   |\n40 |         selfdestruct(to);\n   |         ^^^^^^^^^^^^\n\nWarning (2018): Function state mutability can be restricted to view\n   --> src/SovrnHook.sol:127:5:\n    |\n127 |     function beforeAddLiquidity(address sender, PoolKey calldata key, ModifyLiquidityParams calldata, bytes calldata)\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to pure\n   --> test/LaunchPolicy.t.sol:113:5:\n    |\n113 |     function _imdIsCurrency0() internal view override returns (bool) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to pure\n   --> test/LaunchPolicy.t.sol:230:5:\n    |\n230 |     function _imdIsCurrency0() internal view override returns (bool) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to pure\n   --> test/LifecycleInvariants.t.sol:184:5:\n    |\n184 |     function _imdIsCurrency0() internal view override returns (bool) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to pure\n   --> test/RevisionBoundaries.t.sol:158:5:\n    |\n158 |     function _imdIsCurrency0() internal view override returns (bool) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to pure\n   --> test/Security.t.sol:240:5:\n    |\n240 |     function _imdIsCurrency0() internal view override returns (bool) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to pure\n   --> test/SettlementFailures.t.sol:313:5:\n    |\n313 |     function _imdIsCurrency0() internal view override returns (bool) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nWarning (2018): Function state mutability can be restricted to pure\n   --> test/Vault.t.sol:467:5:\n    |\n467 |     function _imdIsCurrency0() internal view override returns (bool) {\n    |     ^ (Relevant source part starts here and spans across multiple lines).\n\nwarning[reentrancy-no-eth]: external call can be reentered before `entered` is updated\n    ╭▸ src/LifeForceVault.sol:133:40\n    │\n133 │         (bool ok, bytes memory data) = IMD.call(abi.encodeWithSignature(\"transfer(address,uint256)\", to, amount));\n    │                                        ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[non-reentrant-not-first]: `nonReentrant` is not the first modifier\n   ╭▸ src/LifeForceVault.sol:66:66\n   │\n66 │     function withdrawInference(uint256 amount) external onlySafe nonReentrant {\n   │                                                                  ━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/LifeForceVault.sol:72:9\n   │\n72 │         emit InferenceWithdrawn(amount);\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/LifeForceVault.sol:75:64\n   │\n75 │     function withdrawBuyback(uint256 amount) external onlySafe nonReentrant {\n   │                                                                ━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/non-reentrant-not-first\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/LifeForceVault.sol:81:9\n   │\n81 │         emit BuybackWithdrawn(amount);\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/LifeForceVault.sol:89:9\n   │\n89 │         emit Burned(amount);\n   │         ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[divide-before-multiply]: division before multiplication may lose precision\n    ╭▸ src/LifeForceVault.sol:122:16\n    │\n122 │         return (amount / 10_000) * BUYBACK_BPS + (amount % 10_000) * BUYBACK_BPS / 10_000;\n    │                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/divide-before-multiply\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/SovrnHook.sol:120:9\n    │\n120 │         emit PoolOpened(block.timestamp);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/SovrnHook.sol:138:48\n    │\n138 │         if (sender != factory && !openingTx && block.timestamp < openedAt + DECAY) revert LiquidityLocked();\n    │                                                ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/SovrnHook.sol:146:16\n    │\n146 │         return elapsed >= DECAY ? NORMAL_FEE : NORMAL_FEE + 0.465e18 * (DECAY - elapsed) / DECAY;\n    │                ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n    ╭▸ src/SovrnHook.sol:152:16\n    │\n152 │         return elapsed >= DECAY ? 0 : (DECAY - elapsed + 59) / 60;\n    │                ━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/SovrnHook.sol:194:25\n    │\n194 │             quotedFee = fee;\n    │                         ━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/SovrnHook.sol:177:33\n    │\n177 │             uint256 requested = uint256(params.amountSpecified < 0 ? -params.amountSpecified : params.amountSpecified);\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/SovrnHook.sol:180:48\n    │\n180 │                 quoteParams.amountSpecified = -int256(requested - fee);\n    │                                                ━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' 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/SovrnHook.sol:183:29\n    │\n183 │                 if (gross > uint256(uint128(type(int128).max))) revert InvalidAmount();\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/SovrnHook.sol:183:37\n    │\n183 │                 if (gross > uint256(uint128(type(int128).max))) revert InvalidAmount();\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/SovrnHook.sol:184:47\n    │\n184 │                 quoteParams.amountSpecified = int256(gross);\n    │                                               ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' 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/SovrnHook.sol:189:31\n    │\n189 │                 if (actual != uint256(-quoteParams.amountSpecified)) fee = actual * rate / (WAD - rate);\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/SovrnHook.sol:203:40\n    │\n203 │             if (reason.length != 36 || bytes4(reason) != QuoteResult.selector) {\n    │                                        ━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'bytes4' 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[reentrancy-no-eth]: external call can be reentered before `busy` is updated\n    ╭▸ src/SovrnHook.sol:247:17\n    │\n247 │                 poolManager.mint(address(this), CurrencyLibrary.toId(Currency.wrap(IMD)), fee);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[reentrancy-no-eth]: external call can be reentered before `busy` is updated\n    ╭▸ src/SovrnHook.sol:250:17\n    │\n250 │                 poolManager.take(Currency.wrap(IMD), address(vault), fee);\n    │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[uninitialized-local]: local variable is read before being initialized\n    ╭▸ src/SovrnHook.sol:253:47\n    │\n253 │         emit FeePaid(sender, buy, gross, fee, asClaim);\n    │                                               ━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/uninitialized-local\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/SovrnHook.sol:253:9\n    │\n253 │         emit FeePaid(sender, buy, gross, fee, asClaim);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\nwarning[unsafe-typecast]: typecast can truncate values\n    ╭▸ src/SovrnHook.sol:259:16\n    │\n259 │         return uint256(value < 0 ? -int256(value) : int256(value));\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/SovrnHook.sol:263:21\n    │\n263 │         if (value > uint256(uint128(type(int128).max))) revert InvalidAmount();\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/SovrnHook.sol:263:29\n    │\n263 │         if (value > uint256(uint128(type(int128).max))) revert InvalidAmount();\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/SovrnHook.sol:264:16\n    │\n264 │         return int128(int256(value));\n    │                ━━━━━━━━━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int128' 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/SovrnHook.sol:264:23\n    │\n264 │         return int128(int256(value));\n    │                       ━━━━━━━━━━━━━\n    │\n    ├ note: consider disabling this lint if you're certain the cast is safe\n    │       \n    │       // casting to 'int256' 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[reentrancy-no-eth]: external call can be reentered before `busy` is updated\n    ╭▸ src/SovrnHook.sol:273:9\n    │\n273 │         poolManager.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-no-eth\n\nwarning[unused-return]: return value of an external call is not used\n    ╭▸ src/SovrnHook.sol:273:9\n    │\n273 │         poolManager.unlock(abi.encode(amount));\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/unused-return\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n    ╭▸ src/SovrnHook.sol:282:9\n    │\n282 │         emit ClaimsRedeemed(amount);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":3684,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 1 test for test/Fork4663.t.sol:Fork4663ImdHighTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 1.00ms (0.00ns CPU time)\n\nRan 1 test for test/Fork4663.t.sol:Fork4663ImdLowTest\n[SKIP: skipped] setUp() (gas: 0)\nSuite result: ok. 0 passed; 0 failed; 1 skipped; finished in 983.74µs (0.00ns CPU time)\n\nRan 2 tests for test/LiquidityLock.t.sol:AtomicOpenReversedTest\n[PASS] test_aLaterTransactionThroughTheSameContractIsLockedOut() (gas: 72602)\n[PASS] test_seedingThroughAnotherContractInTheInitializingCallWorked() (gas: 21883)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.50ms (110.21µs CPU time)\n\nRan 7 tests for test/LiquidityLock.t.sol:LiquidityLockReversedTest\n[PASS] test_anyoneCanAddOnceTheDecayIsOver() (gas: 436723)\n[PASS] test_bypassDoesNotWork() (gas: 89680)\n[PASS] test_othersCannotAddInTheOpeningSecondEither() (gas: 114301)\n[PASS] test_othersCannotAddLiquidityDuringTheDecay() (gas: 185482)\n[PASS] test_removingLiquidityIsNeverBlocked() (gas: 155162)\n[PASS] test_theFactoryCanAddDuringTheDecay() (gas: 224502)\n[PASS] test_theFactorySeededThePool() (gas: 22442)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 2.33ms (1.03ms CPU time)\n\nRan 5 tests for test/RevisionBoundaries.t.sol:RevisionBoundariesTest\n[PASS] test_hooklessPoolBypassesTheHookFee() (gas: 1612952)\n[PASS] test_managerRoutedIMDAndUnsolicitedClaimsAreNotFeeDeposits() (gas: 784353)\n[PASS] test_safeCanWithdrawBothReservesWithoutBuyingOrBurning() (gas: 356838)\n[PASS] test_strayClaimsDoNotDivertRecordedFeeRedemption() (gas: 1005234)\n[PASS] test_zeroLiquidityPriceMoveIsFree() (gas: 151420)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 3.44ms (2.34ms CPU time)\n\nRan 2 tests for test/LiquidityLock.t.sol:AtomicOpenTest\n[PASS] test_aLaterTransactionThroughTheSameContractIsLockedOut() (gas: 72541)\n[PASS] test_seedingThroughAnotherContractInTheInitializingCallWorked() (gas: 21897)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 3.25ms (145.32µs CPU time)\n\nRan 5 tests for test/RevisionBoundaries.t.sol:RevisionBoundariesReversedTest\n[PASS] test_hooklessPoolBypassesTheHookFee() (gas: 1641888)\n[PASS] test_managerRoutedIMDAndUnsolicitedClaimsAreNotFeeDeposits() (gas: 784365)\n[PASS] test_safeCanWithdrawBothReservesWithoutBuyingOrBurning() (gas: 356838)\n[PASS] test_strayClaimsDoNotDivertRecordedFeeRedemption() (gas: 1004236)\n[PASS] test_zeroLiquidityPriceMoveIsFree() (gas: 150993)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 4.81ms (4.02ms CPU time)\n\nRan 7 tests for test/LiquidityLock.t.sol:LiquidityLockTest\n[PASS] test_anyoneCanAddOnceTheDecayIsOver() (gas: 447483)\n[PASS] test_bypassDoesNotWork() (gas: 90332)\n[PASS] test_othersCannotAddInTheOpeningSecondEither() (gas: 115601)\n[PASS] test_othersCannotAddLiquidityDuringTheDecay() (gas: 187429)\n[PASS] test_removingLiquidityIsNeverBlocked() (gas: 155157)\n[PASS] test_theFactoryCanAddDuringTheDecay() (gas: 229671)\n[PASS] test_theFactorySeededThePool() (gas: 22461)\nSuite result: ok. 7 passed; 0 failed; 0 skipped; finished in 6.16ms (2.17ms CPU time)\n\nRan 9 tests for test/Security.t.sol:SecurityTest\n[PASS] test_constructorCodeChecksAndInvalidFlags() (gas: 6338)\n[PASS] test_directFeeRejectionRollsBackSwapAndBusyGuard() (gas: 583259)\n[PASS] test_exactPermissionsAndFlags() (gas: 29661)\n[PASS] test_feeCallbackCannotReenterRedemption() (gas: 1379733)\n[PASS] test_firstInitEveryFieldAndSecondInitRejected() (gas: 1016350)\n[PASS] test_hookCallbacksRejectEveryoneButThePoolManager() (gas: 435710)\n[PASS] test_imdReturningFalseRollsBackSwap() (gas: 572121)\n[PASS] test_noAdministrationEvenForFactoryOrSafe() (gas: 1686186)\n[PASS] test_runtimeHasNoEscapeOpcodes() (gas: 1738167)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 7.64ms (6.09ms CPU time)\n\nRan 9 tests for test/Security.t.sol:SecurityReversedTest\n[PASS] test_constructorCodeChecksAndInvalidFlags() (gas: 6316)\n[PASS] test_directFeeRejectionRollsBackSwapAndBusyGuard() (gas: 581891)\n[PASS] test_exactPermissionsAndFlags() (gas: 29704)\n[PASS] test_feeCallbackCannotReenterRedemption() (gas: 1379049)\n[PASS] test_firstInitEveryFieldAndSecondInitRejected() (gas: 1016914)\n[PASS] test_hookCallbacksRejectEveryoneButThePoolManager() (gas: 436964)\n[PASS] test_imdReturningFalseRollsBackSwap() (gas: 570744)\n[PASS] test_noAdministrationEvenForFactoryOrSafe() (gas: 1685898)\n[PASS] test_runtimeHasNoEscapeOpcodes() (gas: 1738167)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 10.88ms (9.95ms CPU time)\n\nRan 9 tests for test/SettlementFailures.t.sol:SettlementFailuresReversedTest\n[PASS] test_dustFeesAccumulateAsClaimsOrPayDirectAndSplitOnTheBalance() (gas: 2792750)\n[PASS] test_failedQuoteBubblesUpAndDoesNotKeepBusyState() (gas: 735534)\n[PASS] test_failedSettlementPreservesEarlierClaimsInBothPaymentModes() (gas: 2365101)\n[PASS] test_managerBalanceAtFeeThresholdPaysEntireFeeOneWay() (gas: 2735268)\n[PASS] test_nestedUnlockRedemptionFailsAtomicallyAndCanRetry() (gas: 1384018)\n[PASS] test_underpaidSwapRollsBackDirectFeesAndClaims() (gas: 2257654)\n[PASS] test_unpaidTokenInputRollsBackSellAndRestoresAllowance() (gas: 1135861)\n[PASS] test_vaultRefusingIMDBlocksDirectTakeButNotClaimsPath() (gas: 1332703)\n[PASS] test_zeroRoundedFeePreservesPendingClaims() (gas: 733660)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 21.72ms (17.11ms CPU time)\n\nRan 3 tests for test/Token.t.sol:TokenTest\n[PASS] testFuzz_plainTransfers(uint256) (runs: 256, μ: 69700, ~: 69774)\n[PASS] test_infiniteAllowanceSelfTransferAndInsufficientBalance() (gas: 214138)\n[PASS] test_supplyPlainTransferAllowanceAndBurn() (gas: 317436)\nSuite result: ok. 3 passed; 0 failed; 0 skipped; finished in 22.62ms (22.54ms CPU time)\n\nRan 2 tests for test/LaunchPolicy.t.sol:LaunchPolicyTest\n[PASS] test_allFeeIMDIsReleasableAtLaunchPrice() (gas: 6936513)\n[PASS] test_launchPriceSupportsFourModesAndFullVaultFunding() (gas: 7555582)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.96ms (2.71ms CPU time)\n\nRan 16 tests for test/Hook.t.sol:HookReversedTest\n[PASS] testFuzz_feeBuyAndSell(uint96,bool,bool) (runs: 256, μ: 260851, ~: 280805)\n[PASS] test_beforeInitializeRejectsWrongKeys() (gas: 782050)\n[PASS] test_constructorHasNoChainGate() (gas: 553978)\n[PASS] test_decayAndFullFeeToVault() (gas: 365705)\n[PASS] test_exactModesRespectAmount() (gas: 738043)\n[PASS] test_feeAllFourModes() (gas: 918373)\n[PASS] test_hookHoldsNothingAfterEveryMode() (gas: 860158)\n[PASS] test_partialBuyExactInput() (gas: 284507)\n[PASS] test_partialBuyExactOutput() (gas: 230398)\n[PASS] test_partialSellExactInput() (gas: 233543)\n[PASS] test_partialSellExactOutput() (gas: 291213)\n[PASS] test_permissionsAndUnauthorizedCallbacks() (gas: 148414)\n[PASS] test_poolKeyMatchesInitializedKey() (gas: 34416)\n[PASS] test_sellsStayAtNormalFee() (gas: 463914)\n[PASS] test_wrongPoolRejected() (gas: 78109)\n[PASS] test_zeroFeeSwapDoesNotReadIMDBalance() (gas: 146574)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 74.34ms (94.04ms CPU time)\n\nRan 2 tests for test/LaunchPolicy.t.sol:LaunchPolicyImdHigherTest\n[PASS] test_allFeeIMDIsReleasableAtLaunchPrice() (gas: 6935596)\n[PASS] test_launchPriceSupportsFourModesAndFullVaultFunding() (gas: 7553016)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 4.22ms (3.96ms CPU time)\n\nRan 16 tests for test/Hook.t.sol:HookTest\n[PASS] testFuzz_feeBuyAndSell(uint96,bool,bool) (runs: 256, μ: 262175, ~: 282897)\n[PASS] test_beforeInitializeRejectsWrongKeys() (gas: 781765)\n[PASS] test_constructorHasNoChainGate() (gas: 553956)\n[PASS] test_decayAndFullFeeToVault() (gas: 366407)\n[PASS] test_exactModesRespectAmount() (gas: 740114)\n[PASS] test_feeAllFourModes() (gas: 920558)\n[PASS] test_hookHoldsNothingAfterEveryMode() (gas: 862228)\n[PASS] test_partialBuyExactInput() (gas: 284313)\n[PASS] test_partialBuyExactOutput() (gas: 230161)\n[PASS] test_partialSellExactInput() (gas: 234083)\n[PASS] test_partialSellExactOutput() (gas: 292688)\n[PASS] test_permissionsAndUnauthorizedCallbacks() (gas: 148412)\n[PASS] test_poolKeyMatchesInitializedKey() (gas: 34265)\n[PASS] test_sellsStayAtNormalFee() (gas: 464656)\n[PASS] test_wrongPoolRejected() (gas: 78093)\n[PASS] test_zeroFeeSwapDoesNotReadIMDBalance() (gas: 146837)\nSuite result: ok. 16 passed; 0 failed; 0 skipped; finished in 79.57ms (83.08ms CPU time)\n\nRan 2 tests for test/ReviewRegression.t.sol:ReviewRegressionTest\n[PASS] testFuzz_tinyFeeRounding(uint16,bool,bool,uint16) (runs: 256, μ: 245995, ~: 233023)\n[PASS] test_fourModesInSameUnlockSettleNetAmounts() (gas: 1242516)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 85.09ms (84.15ms CPU time)\n\nRan 6 tests for test/Launch.t.sol:LaunchTest\n[PASS] test_codeSizes() (gas: 11716853)\nLogs:\n  hook runtime: 7199\n  vault runtime: 2326\n\n[PASS] test_deploymentGas() (gas: 12015515)\nLogs:\n  gas: token deployment (CREATE via factory): 24693\n  gas: hook + vault deployment (CREATE2 via factory): 2175232\n  hook initcode bytes: 10862\n  hook runtime bytes: 7199\n\n[PASS] test_deploymentNeedsNeitherChainIdNorIMDCode() (gas: 21657177)\n[PASS] test_deploymentWhereTokenIsIMDReverts() (gas: 13192109)\n[PASS] test_realCreate2DeploymentInitializesAllContracts_tokenAboveIMD() (gas: 12001341)\n[PASS] test_realCreate2DeploymentInitializesAllContracts_tokenBelowIMD() (gas: 15422728)\nSuite result: ok. 6 passed; 0 failed; 0 skipped; finished in 86.08ms (50.07ms CPU time)\n\nRan 5 tests for test/LaunchPolicy.t.sol:FreshManagerTest\n[PASS] testFuzz_firstBuyClaimsBothExactModesAndDecay(bool,uint16) (runs: 256, μ: 567435, ~: 608917)\n[PASS] test_allFourModesMintClaimsInOneUnlockOnEmptyManager() (gas: 1461419)\n[PASS] test_claimAndDirectFeesMixedBeforeRedemption() (gas: 700818)\n[PASS] test_failedRedemptionRestoresClaimsAndCanRetry() (gas: 664739)\n[PASS] test_refusingVaultDoesNotBlockClaimMintButRedemptionReverts() (gas: 607278)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 94.57ms (95.82ms CPU time)\n\nRan 1 test for test/Hook.t.sol:HookClaimsTest\n[PASS] testFuzz_firstBuyClaimsThenRedeemReachesVault(bool,uint16) (runs: 256, μ: 564769, ~: 606229)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 95.13ms (93.87ms CPU time)\n\nRan 9 tests for test/SettlementFailures.t.sol:SettlementFailuresTest\n[PASS] test_dustFeesAccumulateAsClaimsOrPayDirectAndSplitOnTheBalance() (gas: 2797243)\n[PASS] test_failedQuoteBubblesUpAndDoesNotKeepBusyState() (gas: 736156)\n[PASS] test_failedSettlementPreservesEarlierClaimsInBothPaymentModes() (gas: 2299584)\n[PASS] test_managerBalanceAtFeeThresholdPaysEntireFeeOneWay() (gas: 2738759)\n[PASS] test_nestedUnlockRedemptionFailsAtomicallyAndCanRetry() (gas: 1385583)\n[PASS] test_underpaidSwapRollsBackDirectFeesAndClaims() (gas: 2192513)\n[PASS] test_unpaidTokenInputRollsBackSellAndRestoresAllowance() (gas: 1149631)\n[PASS] test_vaultRefusingIMDBlocksDirectTakeButNotClaimsPath() (gas: 1334978)\n[PASS] test_zeroRoundedFeePreservesPendingClaims() (gas: 733859)\nSuite result: ok. 9 passed; 0 failed; 0 skipped; finished in 106.34ms (15.96ms CPU time)\n\nRan 2 tests for test/ReviewRegression.t.sol:ReviewRegressionReversedTest\n[PASS] testFuzz_tinyFeeRounding(uint16,bool,bool,uint16) (runs: 256, μ: 245579, ~: 234010)\n[PASS] test_fourModesInSameUnlockSettleNetAmounts() (gas: 1239814)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 114.34ms (113.89ms CPU time)\n\nRan 5 tests for test/AdversarialFees.t.sol:AdversarialFeesTest\n[PASS] testFuzz_actualSettlementAndEvents(uint96,uint16,bool,bool,bool) (runs: 1000, μ: 358370, ~: 336973)\n[PASS] test_callbackRejectsZeroAndNarrowingOverflow() (gas: 516320)\n[PASS] test_decaySecondAndMinuteBoundaries() (gas: 256155)\n[PASS] test_quoteLeavesSamePriceLiquidityAndLPGrowthAsUnhookedPool() (gas: 1195022)\n[PASS] test_wrongPoolEveryBoundFieldAndUnpairedAfterSwap() (gas: 376922)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 120.22ms (120.73ms CPU time)\n\nRan 5 tests for test/AdversarialFees.t.sol:AdversarialFeesReversedTest\n[PASS] testFuzz_actualSettlementAndEvents(uint96,uint16,bool,bool,bool) (runs: 256, μ: 358774, ~: 342351)\n[PASS] test_callbackRejectsZeroAndNarrowingOverflow() (gas: 515853)\n[PASS] test_decaySecondAndMinuteBoundaries() (gas: 255969)\n[PASS] test_quoteLeavesSamePriceLiquidityAndLPGrowthAsUnhookedPool() (gas: 1190919)\n[PASS] test_wrongPoolEveryBoundFieldAndUnpairedAfterSwap() (gas: 377620)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 126.12ms (127.21ms CPU time)\n\nRan 5 tests for test/TokenFailurePaths.t.sol:TokenFailurePathsTest\n[PASS] testFuzz_allowanceCannotBeBypassedOrBorrowed(uint96) (runs: 1000, μ: 190176, ~: 190524)\n[PASS] testFuzz_failedTransferFromRestoresFiniteAllowance(uint96,bool) (runs: 1000, μ: 251279, ~: 245394)\n[PASS] test_approvalOverwriteRevocationAndSelfSpend() (gas: 259407)\n[PASS] test_infiniteAllowanceSurvivesBurnAndFailedBalanceCheck() (gas: 241137)\n[PASS] test_zeroTransferFromNeedsNoAllowanceButCannotTargetZero() (gas: 98636)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 126.05ms (227.68ms CPU time)\n\nRan 5 tests for test/LaunchPolicy.t.sol:FreshManagerImdHigherTest\n[PASS] testFuzz_firstBuyClaimsBothExactModesAndDecay(bool,uint16) (runs: 256, μ: 567238, ~: 607994)\n[PASS] test_allFourModesMintClaimsInOneUnlockOnEmptyManager() (gas: 1459056)\n[PASS] test_claimAndDirectFeesMixedBeforeRedemption() (gas: 699159)\n[PASS] test_failedRedemptionRestoresClaimsAndCanRetry() (gas: 663815)\n[PASS] test_refusingVaultDoesNotBlockClaimMintButRedemptionReverts() (gas: 606368)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 125.08ms (127.25ms CPU time)\n\nRan 19 tests for test/Vault.t.sol:VaultTestImdHigh\n[PASS] testFuzz_clampNeverExceedsBalance(uint96,uint96,bool) (runs: 256, μ: 390370, ~: 402981)\n[PASS] testFuzz_fundWithdrawRoundtrip(uint96,bool) (runs: 256, μ: 400922, ~: 405739)\n[PASS] testFuzz_sameAndCrossWithdrawalReentryBlocked(bool) (runs: 256, μ: 958673, ~: 958766)\n[PASS] testFuzz_unsolicitedIMDSplitsFloor(uint96) (runs: 256, μ: 97938, ~: 98185)\n[PASS] test_balanceOfRevertingBlocksViewsAndWithdrawalsOnly() (gas: 241451)\n[PASS] test_burnEntireBalanceToDeadWithoutIMDMovement() (gas: 458203)\n[PASS] test_clampTransferFeeAndSeizedIMDShortfallReducesBuybackFirst() (gas: 773302)\n[PASS] test_constructorDoesNotRequireIMDCode() (gas: 3826046)\n[PASS] test_constructorRevertsForMissingManagerTokenOrHook() (gas: 3820441)\n[PASS] test_falseReturningIMDRollsBackBothWithdrawals() (gas: 561562)\n[PASS] test_noTokenRescueApprovalOrAlternateDestinationEvenForSafe() (gas: 234147)\n[PASS] test_onlySafeAndMatchingReserve() (gas: 737708)\n[PASS] test_plainETHToVaultReverts() (gas: 65140)\n[PASS] test_rejectingSafeRollsBackBothWithdrawalsAndCanRetry() (gas: 558591)\n[PASS] test_splitRoundingAndEvents() (gas: 2419324)\n[PASS] test_syncCheckpointsAndEmitsOnlyForNewIMD() (gas: 434070)\n[PASS] test_syncDuringShortfallDoesNotRewriteSplit() (gas: 482346)\n[PASS] test_unsolicitedIMDIncludedAndWithdrawableOnlyBySafe() (gas: 586246)\n[PASS] test_withdrawDuringShortfallKeepsUnbackedCheckpoint() (gas: 515212)\nSuite result: ok. 19 passed; 0 failed; 0 skipped; finished in 136.90ms (218.22ms CPU time)\n\nRan 2 tests for test/FeeDifferential.t.sol:FeeDifferentialReversedTest\n[PASS] testFuzz_referencePoolAllModes(uint8,uint96,uint16,uint16,bool) (runs: 256, μ: 652509, ~: 650545)\n[PASS] test_allFourModesAtEveryRequiredDecayTime() (gas: 7948836)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 136.97ms (145.32ms CPU time)\n\nRan 19 tests for test/Vault.t.sol:VaultTest\n[PASS] testFuzz_clampNeverExceedsBalance(uint96,uint96,bool) (runs: 256, μ: 389874, ~: 391189)\n[PASS] testFuzz_fundWithdrawRoundtrip(uint96,bool) (runs: 1000, μ: 402553, ~: 405726)\n[PASS] testFuzz_sameAndCrossWithdrawalReentryBlocked(bool) (runs: 256, μ: 958645, ~: 958738)\n[PASS] testFuzz_unsolicitedIMDSplitsFloor(uint96) (runs: 256, μ: 97938, ~: 98173)\n[PASS] test_balanceOfRevertingBlocksViewsAndWithdrawalsOnly() (gas: 241415)\n[PASS] test_burnEntireBalanceToDeadWithoutIMDMovement() (gas: 458195)\n[PASS] test_clampTransferFeeAndSeizedIMDShortfallReducesBuybackFirst() (gas: 773258)\n[PASS] test_constructorDoesNotRequireIMDCode() (gas: 3826086)\n[PASS] test_constructorRevertsForMissingManagerTokenOrHook() (gas: 3820463)\n[PASS] test_falseReturningIMDRollsBackBothWithdrawals() (gas: 561480)\n[PASS] test_noTokenRescueApprovalOrAlternateDestinationEvenForSafe() (gas: 233975)\n[PASS] test_onlySafeAndMatchingReserve() (gas: 737621)\n[PASS] test_plainETHToVaultReverts() (gas: 65140)\n[PASS] test_rejectingSafeRollsBackBothWithdrawalsAndCanRetry() (gas: 558509)\n[PASS] test_splitRoundingAndEvents() (gas: 2419260)\n[PASS] test_syncCheckpointsAndEmitsOnlyForNewIMD() (gas: 434027)\n[PASS] test_syncDuringShortfallDoesNotRewriteSplit() (gas: 482328)\n[PASS] test_unsolicitedIMDIncludedAndWithdrawableOnlyBySafe() (gas: 586112)\n[PASS] test_withdrawDuringShortfallKeepsUnbackedCheckpoint() (gas: 515162)\nSuite result: ok. 19 passed; 0 failed; 0 skipped; finished in 136.99ms (211.77ms CPU time)\n\nRan 2 tests for test/FeeDifferential.t.sol:FeeDifferentialTest\n[PASS] testFuzz_referencePoolAllModes(uint8,uint96,uint16,uint16,bool) (runs: 1000, μ: 653954, ~: 651487)\n[PASS] test_allFourModesAtEveryRequiredDecayTime() (gas: 7966336)\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 137.01ms (144.94ms CPU time)\n\nRan 1 test for test/VaultModelInvariants.t.sol:VaultModelInvariantTest\n[PASS] invariant_independentReserveAndAssetLedgers() (runs: 256, calls: 24576, reverts: 0)\n\n╭-------------------+--------------+-------+---------+----------╮\n| Contract          | Selector     | Calls | Reverts | Discards |\n+===============================================================+\n| VaultModelHandler | burn         | 3111  | 0       | 0        |\n|-------------------+--------------+-------+---------+----------|\n| VaultModelHandler | fund         | 3064  | 0       | 0        |\n|-------------------+--------------+-------+---------+----------|\n| VaultModelHandler | overdraw     | 3081  | 0       | 0        |\n|-------------------+--------------+-------+---------+----------|\n| VaultModelHandler | receiver     | 3063  | 0       | 0        |\n|-------------------+--------------+-------+---------+----------|\n| VaultModelHandler | sendTokens   | 3111  | 0       | 0        |\n|-------------------+--------------+-------+---------+----------|\n| VaultModelHandler | syncOnly     | 3050  | 0       | 0        |\n|-------------------+--------------+-------+---------+----------|\n| VaultModelHandler | unauthorized | 3078  | 0       | 0        |\n|-------------------+--------------+-------+---------+----------|\n| VaultModelHandler | withdraw     | 3018  | 0       | 0        |\n╰-------------------+--------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.94s (1.94s CPU time)\n\nRan 1 test for test/LifecycleInvariants.t.sol:LifecycleInvariantTest\n[PASS]\nLifecycleInvariantTest invariants:\n[PASS] invariant_IMDAccountingAndOnlySafeReceivesWithdrawals\n[PASS] invariant_fixedSupplyAndEveryBurnConserved\n LifecycleInvariantTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭------------------+----------------------+-------+---------+----------╮\n| Contract         | Selector             | Calls | Reverts | Discards |\n+======================================================================+\n| LifecycleHandler | advance              | 3041  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | donate               | 3072  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | moveAndBurn          | 3125  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | receiver             | 3058  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | redeem               | 3083  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | swap                 | 3065  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | unauthorizedWithdraw | 3058  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | withdraw             | 3074  | 0       | 0        |\n╰------------------+----------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.41s (3.41s CPU time)\n\nRan 5 tests for test/VaultCallbackBoundaries.t.sol:VaultCallbackBoundariesTest\n[PASS] testFuzz_refundAndBurnDuringEitherWithdrawal(bool) (runs: 256, μ: 1527632, ~: 1527737)\n[PASS] testFuzz_rejectionRollsBackRefundBurnAndBothLedgers(bool) (runs: 256, μ: 1616279, ~: 1616351)\n[PASS] testFuzz_uint256ReceiptAndExit(uint256,bool) (runs: 1000, μ: 323345, ~: 325927)\n[PASS] test_counterfactualPrefundingAndUnsolicitedDustRemainWithdrawable() (gas: 1000062)\n[PASS] test_maxUint256ReceiptDoesNotOverflowSplit() (gas: 321733)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 3.50s (3.70s CPU time)\n\nRan 1 test for test/LifecycleInvariants.t.sol:LifecycleInvariantImdHigherTest\n[PASS]\nLifecycleInvariantImdHigherTest invariants:\n[PASS] invariant_IMDAccountingAndOnlySafeReceivesWithdrawals\n[PASS] invariant_fixedSupplyAndEveryBurnConserved\n LifecycleInvariantImdHigherTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭------------------+----------------------+-------+---------+----------╮\n| Contract         | Selector             | Calls | Reverts | Discards |\n+======================================================================+\n| LifecycleHandler | advance              | 3041  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | donate               | 3072  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | moveAndBurn          | 3125  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | receiver             | 3058  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | redeem               | 3083  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | swap                 | 3065  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | unauthorizedWithdraw | 3058  | 0       | 0        |\n|------------------+----------------------+-------+---------+----------|\n| LifecycleHandler | withdraw             | 3074  | 0       | 0        |\n╰------------------+----------------------+-------+---------+----------╯\n\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.51s (3.51s CPU time)\n\nRan 1 test for test/Hook.t.sol:HookClaimsReversedTest\n[PASS] testFuzz_firstBuyClaimsThenRedeemReachesVault(bool,uint16) (runs: 256, μ: 564536, ~: 605254)\nSuite result: ok. 1 passed; 0 failed; 0 skipped; finished in 3.57s (3.57s CPU time)\n\nRan 35 test suites in 3.57s (17.81s CPU time): 190 tests passed, 0 failed, 2 skipped (192 total tests)\n","passed":true},{"durationMs":85,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"LifeForceVault.burn()\",\"LifeForceVault.sync()\",\"LifeForceVault.withdrawBuyback(uint256)\",\"LifeForceVault.withdrawInference(uint256)\",\"SovrnHook.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"SovrnHook.beforeAddLiquidity(address,(address,address,uint24,int24,address),(int24,int24,int256,bytes32),bytes)\",\"SovrnHook.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"SovrnHook.beforeSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),bytes)\",\"SovrnHook.quoteIMD((address,address,uint24,int24,address),(bool,int256,uint160))\",\"SovrnHook.redeemFees()\",\"SovrnHook.unlockCallback(bytes)\",\"SovrnToken.approve(address,uint256)\",\"SovrnToken.transfer(address,uint256)\",\"SovrnToken.transferFrom(address,address,uint256)\"],\"files\":{\".gitignore\":3,\"README.md\":123,\"foundry.toml\":19,\"launch-attestation.json\":1515,\"launch.json\":32,\"script/PrepareLaunch.s.sol\":32,\"script/attest.py\":110,\"src/HookFlags.sol\":30,\"src/Interfaces.sol\":13,\"src/LifeForceVault.sol\":136,\"src/SovrnHook.sol\":291,\"src/SovrnToken.sol\":48,\"test/AdversarialFees.t.sol\":188,\"test/FeeDifferential.t.sol\":123,\"test/Fork4663.t.sol\":178,\"test/Hook.t.sol\":266,\"test/Launch.t.sol\":187,\"test/LaunchPolicy.t.sol\":233,\"test/LifecycleInvariants.t.sol\":187,\"test/LiquidityLock.t.sol\":158,\"test/PoolRouter.sol\":68,\"test/README.md\":66,\"test/REVIEW.md\":62,\"test/ReviewRegression.t.sol\":89,\"test/RevisionBoundaries.t.sol\":161,\"test/Security.t.sol\":243,\"test/SettlementFailures.t.sol\":316,\"test/SystemBase.sol\":113,\"test/Token.t.sol\":60,\"test/TokenFailurePaths.t.sol\":114,\"test/Vault.t.sol\":470,\"test/VaultCallbackBoundaries.t.sol\":190,\"test/VaultModelInvariants.t.sol\":175,\"test/__pycache__/test_attestation.cpython-314.pyc\":29,\"test/mocks/MockERC20.sol\":73,\"test/test_attestation.py\":98},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"980bed5cd20776fb4fbec831160825fd9df9e9969c3a549aa29baa1e80cec89a","verifiedTreeHash":"f6211bf67b55d0fc513e72e16c842070846c82e8","verifierVersion":"0.1.0+fdeb4d4a"}]}