{"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":"09d157d4-92ff-4035-859e-0187f4cc6d02","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"f420c385a84eb6f3ea7ec5385aabfa310b9d8b33eb87a1880e4f98039ec9b429","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_economics","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"86b1a34b671b40500508890c5fee6ec6f94340712eb2cb1ad5468102318f33fa","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_flow","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"030250e735d60b290b45c5e991c06675a1adaab5a810950c22f61967a850f659","dependsOn":["build_contract_project","write_foundry_tests","manifest","audit_math","audit_permissions","audit_economics","audit_flow"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","tools":[]},"key":"audit_judge","kind":"code","role":"review","skillHash":"3014f1ea5961918ca059453a484bf4c8bcbbfc2248dbe31d94ac7c5cdf8f50bd","skillId":"audit-judge","state":"accepted"},{"acceptedSubmissionHash":"950ec150a9b3b08532920ccbbb07ee949a20e278901bde2b32a03f575e2a60e1","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_math","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"e8d035ed38646ae5e470885cbfcec300b825dd8e9abb9a595ad01fa0e5d7f92a","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","tools":[]},"key":"audit_permissions","kind":"code","role":"review","skillHash":"e5ac2cb1fd91a56aa40b16487fc230c48de0d317c8266140331dd3219bb40a85","skillId":"audit-specialist","state":"accepted"},{"acceptedSubmissionHash":"6b292f65e9a1cdf792e8a0f9a73ece028cdaeeb6e7d24772aebce5cb526bde58","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"587567a5ef71546b5973ceaf6ef1dbbd590f49503b5ac9a6973ed86b89584857","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"587567a5ef71546b5973ceaf6ef1dbbd590f49503b5ac9a6973ed86b89584857","skillId":"build-contract-project","state":"accepted"},{"acceptedSubmissionHash":"d7fcab857b24d825bdf39a400e03b4b5d1b3b2a81e48672a15b2c86afe00d56a","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"tools":[]},"key":"manifest","kind":"code","role":"integrate","skillHash":null,"skillId":null,"state":"accepted"},{"acceptedSubmissionHash":"aebbb4b51c23ea72396b20c60a414c3a2cfd19c6ee612ab44ab02557585aa129","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","tools":[]},"key":"write_foundry_tests","kind":"code","role":"tests","skillHash":"73431852439ad3a343f3d2b7db1c43cd363b9f48f9bb51497374a0cf6d50b223","skillId":"write-foundry-tests","state":"accepted"}],"objective":"Token name: Work\nToken symbol: WORK\nWrite these files EXACTLY as given (tested; formatting only, never change logic; launch.json must keep its notes string). foundry.toml: solc 0.8.26, evm_version cancun, optimizer true, optimizer_runs 200, via_ir true, bytecode_hash \"none\"; vendor Uniswap v4-core v4.0.0 at lib/v4-core and OpenZeppelin v5 at lib/openzeppelin-contracts, remappings v4-core/=lib/v4-core/ and @openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/. Hook deploys at a CREATE2 address with flags 0x2044. Fees: snipe guard 50% sliding to 2% over 15 min after pool init, then 2%, to the treasury 0xc9eafe33a510a3a3d95a94c4f85adaf6a3ea12a0 via permissionless sweep. Admin: that address may only set the standing fee within 0-10%. Tests against the real PoolManager.\nlaunch.json:\n{\"kind\":\"univ4_hook\",\"hook\":{\"contract\":\"WorkLaunchHook3\",\"constructorArgs\":[\"$poolManager\",\"$token\"],\"permissions\":[\"beforeInitialize\",\"afterSwap\",\"afterSwapReturnDelta\"]},\"token\":{\"contract\":\"Work\",\"name\":\"Work\",\"symbol\":\"WORK\",\"decimals\":18},\"pool\":{\"pairedCurrency\":\"0x5f7bb59365ce557c26dbcaa4ee9d39a4b95b7127\",\"fee\":12500,\"tickSpacing\":60,\"initialPrice\":\"79228162514264337593543950336\"},\"notes\":\"Work (WORK) is a plain fixed-supply OpenZeppelin ERC-20: 1,000,000,000 with 18 decimals minted once to msg.sender, no admin. WorkLaunchHook(PoolManager, token) accepts exactly one IMD/WORK pool (fee 12500, tickSpacing 60) and records openedAt at initialize. Every swap pays a hook fee equal to the same share of what the trader pays or receives in both swap modes (exact-out fees are grossed up): 50% at openedAt sliding linearly to the standing fee over 900 s, then the standing fee (2%; 0xc9eafe33a510a3a3d95a94c4f85adaf6a3ea12a0 may set 0-10%). Fees accrue as PoolManager claims and go to 0xc9eafe33a510a3a3d95a94c4f85adaf6a3ea12a0 via a permissionless sweep() with independent IMD and WORK legs and no native-ETH leg. Flags 0x2044. No other admin, no upgrades.\"}\nsrc/Work.sol:\npragma solidity 0.8.26;import{ERC20}from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";contract Work is ERC20{constructor()ERC20(\"Work\",\"WORK\"){_mint(msg.sender,1_000_000_000 ether);}}\nsrc/WorkLaunchHook3.sol:\n// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;import{IPoolManager}from \"v4-core/src/interfaces/IPoolManager.sol\";import{IHooks}from \"v4-core/src/interfaces/IHooks.sol\";import{Hooks}from \"v4-core/src/libraries/Hooks.sol\";import{PoolKey}from \"v4-core/src/types/PoolKey.sol\";import{Currency}from \"v4-core/src/types/Currency.sol\";import{BalanceDelta}from \"v4-core/src/types/BalanceDelta.sol\";contract WorkLaunchHook3{address public constant IMD=0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;address public constant B=0xc9EAFE33A510a3a3d95A94c4f85AdaF6a3EA12a0;IPoolManager public immutable poolManager;address public immutable token;uint256 public openedAt;uint256 public standingFee=200;event StandingFee(uint256 fee);event SweepFailed(address token);constructor(IPoolManager m,address t){require(t!=IMD&&t!=address(0));poolManager=m;token=t;Hooks.validateHookPermissions(IHooks(address(this)),getHookPermissions());}function getHookPermissions()public pure returns(Hooks.Permissions memory p){p.beforeInitialize=true;p.afterSwap=true;p.afterSwapReturnDelta=true;}modifier onlyPM(){require(msg.sender==address(poolManager));_;}function beforeInitialize(address,PoolKey calldata k,uint160)external onlyPM returns(bytes4){address a=Currency.unwrap(k.currency0);address b=Currency.unwrap(k.currency1);require(openedAt==0&&((a==IMD&&b==token)||(a==token&&b==IMD))&&k.fee==12500&&k.tickSpacing==60&&address(k.hooks)==address(this));openedAt=block.timestamp;return IHooks.beforeInitialize.selector;}function feeNow()public view returns(uint256){uint256 t=openedAt;uint256 s=standingFee;if(t==0||block.timestamp<=t)return 5000;t=block.timestamp-t;return t<900?s+(5000-s)*(900-t)/900:s;}function setStandingFee(uint256 f)external{require(msg.sender==B&&f<=1000);standingFee=f;emit StandingFee(f);}function afterSwap(address,PoolKey calldata k,IPoolManager.SwapParams calldata p,BalanceDelta d,bytes calldata)external onlyPM returns(bytes4,int128){bool u1=(p.amountSpecified<0)==p.zeroForOne;int128 a=u1?d.amount1():d.amount0();uint256 r=feeNow();uint256 f=a<0?uint256(int256(-a))*r/(10000-r):uint256(int256(a))*r/10000;if(f>0)poolManager.mint(address(this),(u1?k.currency1:k.currency0).toId(),f);return(IHooks.afterSwap.selector,int128(int256(f)));}function sweep()external{for(uint256 i;i<2;++i){address x=i==0?IMD:token;try poolManager.unlock(abi.encode(x)){}catch{emit SweepFailed(x);}uint256 v=Currency.wrap(x).balanceOfSelf();if(v>0){(bool ok,bytes memory r)=x.call(abi.encodeWithSignature(\"transfer(address,uint256)\",B,v));if(!ok||(r.length>0&&!abi.decode(r,(bool))))emit SweepFailed(x);}}}function unlockCallback(bytes calldata d)external onlyPM returns(bytes memory){Currency x=Currency.wrap(abi.decode(d,(address)));uint256 v=poolManager.balanceOf(address(this),x.toId());if(v>0){poolManager.burn(address(this),x.toId(),v);poolManager.take(x,B,v);}return \"\";}}\n","parentJobId":null,"planHash":"e66aa7d06122b17624f72ab77fccd399a76f4664198a26a49c360be4517be1a3","previousHash":"0000000000000000000000000000000000000000000000000000000000000000","projectId":"09d157d4-92ff-4035-859e-0187f4cc6d02","publication":{"commit":null,"deliveredAt":null,"repoUrl":"https://github.com/identity-md-launches/launch-1015-src-work-sol-src-worklaunchhook3-sol"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[{"agentId":"52391","feedbackHash":"e44d70d23e33c80e4cddb16159abc0b542684245be92fe895a3310cd4501f911","nodeKey":"audit_economics","submissionHash":"f420c385a84eb6f3ea7ec5385aabfa310b9d8b33eb87a1880e4f98039ec9b429","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50981","feedbackHash":"0b815e10bcaca1f6ea5daa66b162dfe52dada5a0155b1631b25717004cbbb4d1","nodeKey":"audit_flow","submissionHash":"86b1a34b671b40500508890c5fee6ec6f94340712eb2cb1ad5468102318f33fa","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"51154","feedbackHash":"918f9342d09785052a70eca532c1b55ce8e691b58b0101a91c3183c61d33aa00","nodeKey":"audit_judge","submissionHash":"030250e735d60b290b45c5e991c06675a1adaab5a810950c22f61967a850f659","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52396","feedbackHash":"ce5c5266f6993a62f52cea2fdd7a9018f97aa82a560736ce2261a213aca70605","nodeKey":"audit_math","submissionHash":"950ec150a9b3b08532920ccbbb07ee949a20e278901bde2b32a03f575e2a60e1","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"50955","feedbackHash":"90d878892f1fa4c84346f8f95762ba1f4d1aca22f25ff48598cb4557f4fd70a3","nodeKey":"audit_permissions","submissionHash":"e8d035ed38646ae5e470885cbfcec300b825dd8e9abb9a595ad01fa0e5d7f92a","tag1":"review:submission","tag2":"acceptance-v2","value":1},{"agentId":"52185","feedbackHash":"cfc6e1fc48db9fcda2a9cbeaf6187dc9c53cbb8dd389cdc5200b45783bb86264","nodeKey":"build_contract_project","submissionHash":"6b292f65e9a1cdf792e8a0f9a73ece028cdaeeb6e7d24772aebce5cb526bde58","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51080","feedbackHash":"d83d6cbfd522e56a960b2c6b1a730fca623f339344475204382a3bd671e2de25","nodeKey":"manifest","submissionHash":"d7fcab857b24d825bdf39a400e03b4b5d1b3b2a81e48672a15b2c86afe00d56a","tag1":"verification:checks","tag2":"acceptance-v2","value":1},{"agentId":"51060","feedbackHash":"92a970c0caef799c215b4cbe4695babbc9275bf996bff6c8ab556f6f4f494d91","nodeKey":"write_foundry_tests","submissionHash":"aebbb4b51c23ea72396b20c60a414c3a2cfd19c6ee612ab44ab02557585aa129","tag1":"verification:checks","tag2":"acceptance-v2","value":1}],"site":null,"snapshotHash":"632117078d8a89a6081db2aec19982e29e99b32c1ae5f3ff5aa23b0374e6acb3","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":null,"device":"27168b1ff68f84d2","findings":[{"citation":"resolved","description":"Merged from write_foundry_tests (medium) and audit_flow (low). The manifest notes promise 'a permissionless sweep() with independent IMD and WORK legs'. Only poolManager.unlock is inside try/catch. Two calls on each leg run unguarded: Currency.wrap(x).balanceOfSelf() at line 82 (a high-level IERC20Minimal.balanceOf call that reverts if IMD reverts or returns no data) and abi.decode(r,(bool)) at line 85 on the raw transfer return data, which reverts when the data is non-empty and shorter than 32 bytes or not a clean 0/1 word. Because the IMD leg (i == 0) runs first, either condition on IMD reverts the whole sweep before the WORK leg executes, so WORK claims and direct WORK balances cannot reach the treasury while IMD misbehaves. Recalibrated to low rather than medium: the IMD token at 0x5F7B...7127 is a fixed external contract that cannot be inspected here; a standard ERC-20 never triggers either path; and under both trigger conditions the PoolManager itself (sync() calls balanceOfSelf, take() requires a 32-byte true or empty return) would already fail to settle IMD, so the pool is broken for IMD before the sweep independence matters. No funds are lost; the WORK leg is delayed until IMD recovers. The README already records this limitation. Minimal fix that preserves design: read the balance with a staticcall and treat failure as a failed leg, and require r.length >= 32 before decoding, emitting SweepFailed(x) instead of reverting.","line":82,"path":"src/WorkLaunchHook3.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Work} from \"src/Work.sol\";\nimport {WorkLaunchHook3} from \"src/WorkLaunchHook3.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\ncontract FailingSweepIMD is ERC20 {\n    uint256 public mode;\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n    function setMode(uint256 value) external { mode = value; }\n    function balanceOf(address who) public view override returns (uint256) {\n        require(mode != 2, \"IMD balance query unavailable\");\n        return super.balanceOf(who);\n    }\n    function transfer(address to, uint256 amount) public override returns (bool) {\n        bool result = super.transfer(to, amount);\n        if (mode == 1) {\n            assembly (\"memory-safe\") { mstore(0, 1) return(31, 1) }\n        }\n        return result;\n    }\n}\n\ncontract SweepIsolationProof is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant TREASURY = 0xc9EAFE33A510a3a3d95A94c4f85AdaF6a3EA12a0;\n    Work work;\n    WorkLaunchHook3 hook;\n\n    function setUp() public {\n        PoolManager manager = new PoolManager(address(this));\n        work = new Work();\n        FailingSweepIMD implementation = new FailingSweepIMD();\n        vm.etch(IMD, address(implementation).code);\n        bytes32 hash = keccak256(abi.encodePacked(type(WorkLaunchHook3).creationCode, abi.encode(manager, address(work))));\n        for (uint256 i; i < 200_000; ++i) {\n            address predicted = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), hash)))));\n            if (uint160(predicted) & 0x3fff == 0x2044) {\n                hook = new WorkLaunchHook3{salt: bytes32(i)}(manager, address(work));\n                break;\n            }\n        }\n        require(address(hook).code.length > 0, \"salt search failed\");\n        FailingSweepIMD(IMD).mint(address(hook), 3 ether);\n        work.transfer(address(hook), 7 ether);\n    }\n\n    function testMalformedIMDTransferMustNotBlockWorkSweep() public {\n        FailingSweepIMD(IMD).setMode(1);\n        _assertIndependentWorkLeg();\n    }\n\n    function testRevertingIMDBalanceQueryMustNotBlockWorkSweep() public {\n        FailingSweepIMD(IMD).setMode(2);\n        _assertIndependentWorkLeg();\n    }\n\n    function _assertIndependentWorkLeg() internal {\n        (bool success,) = address(hook).call(abi.encodeCall(hook.sweep, ()));\n        // Check delivery first so the failure describes the economic consequence.\n        assertEq(work.balanceOf(TREASURY), 7 ether, \"independent WORK leg must reach treasury\");\n        assertEq(work.balanceOf(address(hook)), 0);\n        assertTrue(success, \"sweep must isolate a failed IMD leg\");\n    }\n}","reproduction":"State: real PoolManager v4.0.0, Work deployed, WorkLaunchHook3 at a mined 0x2044 CREATE2 address, a local IMD model etched at 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127; hook holds 3e18 IMD and 7e18 WORK directly (no claims needed). Case A: IMD.transfer executes the transfer then returns one byte (mstore(0,1); return(31,1)). Case B: IMD.balanceOf reverts. Call hook.sweep() from any address. Expected per manifest: SweepFailed(IMD) emitted, WORK leg still delivers 7e18 WORK to 0xc9EAFE33A510a3a3d95A94c4f85AdaF6a3EA12a0. Actual: the call reverts (Case A in abi.decode at line 85 after the IMD transfer already ran, Case B at line 82); treasury WORK stays 0 and the hook keeps 7e18 WORK. Verified by running the attached proof: forge test --match-path test/scratch/Proof_Sweep.t.sol fails both tests with 'independent WORK leg must reach treasury: 0 != 7000000000000000000'. The existing test testDirectTokenFalseAndRevertResponsesKeepOtherLegUsable shows the clean-revert and return-false paths are handled; only these two paths escape the guard.","severity":"low","snippet":"            uint256 v = Currency.wrap(x).balanceOfSelf();","title":"sweep() legs are not independent: an IMD balanceOf revert or malformed IMD transfer return data aborts the WORK leg too"},{"citation":"resolved","description":"Merged from audit_economics (low), audit_flow (low) and audit_permissions (info); same root cause. The callback is correctly restricted to the PoolManager, enforces the single IMD/WORK pool (fee 12500, tickSpacing 60, hooks == this) and rejects a second initialization, but it ignores its first argument (the initializer) and third argument (sqrtPriceX96) and permanently sets openedAt on the first success. PoolManager.initialize is permissionless, so from the moment the hook has code any address can be the one that opens the pool. Consequences: (1) the opener chooses the opening price and the factory's own initialize at the manifest price 79228162514264337593543950336 then reverts inside the openedAt == 0 guard; because openedAt is never reset the hook can never accept the intended pool and the launch must be redone with a new salt; (2) the 50% -> 2% ramp starts at the opener's block, so if seed liquidity lands 900 s or more later the first real buyer pays 2% instead of 50%. The service reference states the launch factory deploys the hook and initializes its pool, and the README requires that to be atomic; if both happen in one transaction no window exists. The hook itself enforces nothing, so this is a deployment-ordering dependency, rated low (griefing, no fund loss, needs a non-atomic deployment). Minimal fix preserving the no-admin design: require the sqrtPriceX96 argument to equal the manifest price, or take $factory as a constructor argument and require the sender argument to equal it.","line":37,"path":"src/WorkLaunchHook3.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {Work} from \"src/Work.sol\";\nimport {WorkLaunchHook3} from \"src/WorkLaunchHook3.sol\";\n\ncontract StandIn is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n}\n\n/// @dev Finding: the only accepted pool can be initialized by anyone, at any price, and the\n/// snipe ramp starts at that moment. Fails on the current code; passes once beforeInitialize\n/// restricts the initializer (factory) or pins sqrtPriceX96 to the manifest price.\ncontract InitFrontRunTest is Test {\n    address internal constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    uint160 internal constant MANIFEST_PRICE = 79228162514264337593543950336;\n    uint256 internal constant T0 = 1_800_000_000;\n\n    PoolManager internal manager;\n    Work internal work;\n    WorkLaunchHook3 internal hook;\n    PoolKey internal key;\n    address internal factory = makeAddr(\"factory\");\n    address internal sniper = makeAddr(\"sniper\");\n\n    function setUp() public {\n        vm.warp(T0);\n        manager = new PoolManager(address(this));\n        vm.etch(IMD, address(new StandIn()).code);\n        work = new Work();\n        bytes32 initHash = keccak256(abi.encodePacked(type(WorkLaunchHook3).creationCode, abi.encode(manager, address(work))));\n        for (uint256 i; i < 300_000; ++i) {\n            address p = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash))))\n            );\n            if (uint160(p) & 0x3fff == 0x2044) {\n                hook = new WorkLaunchHook3{salt: bytes32(i)}(manager, address(work));\n                break;\n            }\n        }\n        require(address(hook) != address(0), \"no salt\");\n        (address a, address b) = address(work) < IMD ? (address(work), IMD) : (IMD, address(work));\n        key = PoolKey(Currency.wrap(a), Currency.wrap(b), 12500, 60, IHooks(address(hook)));\n    }\n\n    function testStrangerCannotPreInitializeAtArbitraryPrice() public {\n        // Hook is deployed; factory has not yet initialized. A stranger opens the pool at the\n        // minimum price and 15 minutes early.\n        vm.prank(sniper);\n        (bool ok,) = address(manager).call(\n            abi.encodeCall(IPoolManager.initialize, (key, TickMath.MIN_SQRT_PRICE + 1))\n        );\n        assertFalse(ok, \"stranger initialized the launch pool at a non-manifest price\");\n\n        // The factory's own initialization at the manifest price must still succeed afterwards,\n        // and the ramp must start when the factory opens the pool.\n        vm.warp(T0 + 900);\n        vm.prank(factory);\n        manager.initialize(key, MANIFEST_PRICE);\n        assertEq(hook.openedAt(), T0 + 900, \"ramp must start at the factory's initialization\");\n        assertEq(hook.feeNow(), 5000, \"snipe guard must be at 50% when the factory opens the pool\");\n    }\n}","reproduction":"State: PoolManager and Work deployed, WorkLaunchHook3 deployed at a 0x2044 address, pool not yet initialized. Step 1: an unprivileged account calls manager.initialize(PoolKey(sorted(IMD, WORK), 12500, 60, hook), TickMath.MIN_SQRT_PRICE + 1) at time T0. Actual: succeeds, hook.openedAt() == T0. Step 2: the factory calls manager.initialize(sameKey, 79228162514264337593543950336). Actual: reverts (hook require openedAt == 0 fails; PoolManager also reports PoolAlreadyInitialized). Variant: stranger opens at the correct price at T0, liquidity is added at T0 + 900: hook.feeNow() == 200, so the first buyer pays 2%, not 50% (reproduced in scratch test testRampConsumedBeforeLiquidity). Expected: only the factory can open the pool, at the manifest price, with the ramp starting then. The attached proof (forge test --match-path test/scratch/Proof_Init.t.sol) fails on this code with 'stranger initialized the launch pool at a non-manifest price' and passes once beforeInitialize pins the price (a factory-sender fix instead would also require updating the proof's constructor call).","severity":"low","snippet":"    function beforeInitialize(address, PoolKey calldata k, uint160) external onlyPM returns (bytes4) {","title":"beforeInitialize accepts any initializer and any sqrtPriceX96: a stranger can open the only accepted pool at an arbitrary price and start the 900 s ramp before liquidity exists"},{"citation":"resolved","description":"From audit_permissions (low), reproduced. The hook enables no liquidity callbacks, so modifyLiquidity on the launch pool never enters the hook and never pays a fee. A range placed entirely on the IMD side of the current tick holds only IMD; when other traders sell WORK through that range (they pay the 50% fee on their IMD output), the position converts to WORK at pool price plus the 1.25% LP fee, and the owner withdraws WORK having paid no hook fee. At openedAt this yields roughly twice the WORK per IMD that a swapper gets. It is passive (needs WORK sell flow through the range) and inherent to a fee-on-swap-only design; the notes only promise that every swap pays the fee. Rated low: the treasury forgoes fee on that flow and the guard's stated purpose is weakened, but nothing is lost from the pool. Any fix (a beforeAddLiquidity gate during the ramp, or charging on liquidity adds) changes the permission set and the mined 0x2044 address, which is a design decision for the requester rather than a silent change.","line":27,"path":"src/WorkLaunchHook3.sol","reproduction":"State: real PoolManager, pool initialized at sqrtPriceX96 = 2^96 (tick 0) at T0 = openedAt, factory liquidity 10_000_000e18 in [-600, 600], feeNow() == 5000. (1) Sniper calls modifyLiquidity with the IMD-only range [-120, -60] when WORK is currency0 (or [60, 120] when WORK is currency1), liquidityDelta 10_000_000e18: pays 29,863.83 IMD and 0 WORK; hook claims stay 0. (2) A seller swaps exact-in 300_000e18 WORK; the price crosses the sniper's range; the hook mints IMD claims from the seller's output. (3) Sniper removes the position (liquidityDelta -10_000_000e18) and receives 30,515.24 WORK and 0 IMD; hook IMD claims unchanged since step 2, WORK claims 0. Comparison at the same moment: a swapper spending 60,000e18 IMD exact-in receives 31,033.95 WORK and the hook mints 31,033.95 WORK of claims (50%). Reproduced in scratch test testLiquidityAcquiresWorkWithoutHookFee against lib/v4-core PoolManager.","severity":"low","snippet":"    function getHookPermissions() public pure returns (Hooks.Permissions memory p) {","title":"Snipe guard is charged only in afterSwap: single-sided IMD liquidity acquires WORK during the 50% window with no hook fee"},{"citation":"resolved","description":"From audit_math (low), reproduced. Both fee formulas use floor division, so the fee is always rounded in the trader's favour, where the usual rule for protocol fees is to round toward the fee recipient. Exact-input: when raw AMM output is below 10000 / r units the fee is 0 (at 2%, any output below 50 wei). Exact-output: when rawInput * r < 10000 - r the grossed-up fee is 0. The ramp in feeNow() at line 53 also floors each second, under-charging by up to 1 bp. Impact is dust that does not compound: avoiding the fee needs one swap per < 50 wei of 18-decimal output, so gas dwarfs the saving, and the per-swap shortfall never exceeds one unit of the fee currency. Minimal fix: ceiling division, e.g. (x * r + 9999) / 10000 and (x * r + (10000 - r) - 1) / (10000 - r).","line":70,"path":"src/WorkLaunchHook3.sol","reproduction":"State: real PoolManager, pool initialized at sqrtPrice 2^96 with 10_000_000e18 liquidity in [-600, 600], warp to openedAt + 900 so feeNow() == 200. Swap exact-in 50 wei IMD -> WORK (amountSpecified = -50, limit MIN_SQRT_PRICE + 1 or MAX_SQRT_PRICE - 1 by direction): raw AMM output is 48 wei, 48 * 200 / 10000 = 0, the hook mints no claim and the trader receives all 48 wei. Expected under round-up: 1 wei fee, 47 wei to the trader. With amountSpecified = -100 the raw output is 96 and the fee is 1 (ideal 1.92). Reproduced in scratch test testDustRounding; the existing test testDustExactInputRoundsFeeToZero documents the same behaviour.","severity":"low","snippet":"        uint256 f = a < 0 ? uint256(int256(-a)) * r / (10000 - r) : uint256(int256(a)) * r / 10000;","title":"Hook fee rounds down against the treasury and truncates to zero on dust swaps in both modes"},{"citation":"resolved","description":"Merged from audit_permissions (info) and audit_economics (info); same observation. Access control is correct (only the hard-coded treasury B, bounded 0..1000 bps) and matches the brief, so this is not a defect but the one privileged power in the system, recorded as a trust assumption. The write takes effect on the next afterSwap with no delay, so B can land setStandingFee(1000) ahead of a pending swap and take 10% instead of the 2% the trader quoted; it also alters an in-progress ramp (fee at t = 450 s goes from 2600 to 3000 bps). The trader's only protection is their router's slippage bound. B is a single externally held key that is also the sole fee recipient; loss of that key freezes the fee at its last value with no transfer, two-step or recovery path, consistent with the notes' 'no other admin, no upgrades'.","line":57,"path":"src/WorkLaunchHook3.sol","reproduction":"State: pool open with 10_000_000e18 liquidity, block.timestamp = openedAt + 900, standingFee = 200. Trade: exact-in 100e18 IMD -> WORK. At 2% the trader receives 96.774044356311981419 WORK. If B calls setStandingFee(1000) first, the identical trade receives 88.874122368041615589 WORK, 7.9 WORK less, the difference minted as hook claims for B. Reproduced in scratch test testStandingFeeChangeHitsPendingTrade. A stranger calling setStandingFee reverts and B calling with 1001 reverts, as the existing test testTreasuryCanOnlySetBoundedStandingFee shows.","severity":"info","snippet":"        require(msg.sender == B && f <= 1000);","title":"Trust assumption: the treasury key can move the standing fee between 0 and 10% instantly, so an in-flight trade pays whatever rate is live at execution"},{"citation":"resolved","description":"From audit_economics (info), reproduced. unlockCallback always redeems claims to the hard-coded treasury B and sweep() swallows the revert and leaves the claims in place, which is the intended behaviour for transient failures. If the IMD token at 0x5F7B...7127 ever permanently blocks transfers to B (blacklist, pause, or B becoming a contract the token refuses), every IMD fee ever accrued stays as ERC-6909 claims in the PoolManager with no admin, no alternate recipient and no upgrade to move them. For the plain Work token this cannot happen. This is the deliberate trade-off of the no-admin design; recorded so the author confirms IMD has no transfer restrictions on the launch chain.","line":95,"path":"src/WorkLaunchHook3.sol","reproduction":"State: pool opened, one exact-in swap in each direction so the hook holds IMD and WORK claims. Make IMD.transfer(B, v) revert permanently (existing MockIMD: imd.setTransferMode(2)). Call sweep() from any address: emits SweepFailed(IMD), IMD claims unchanged, WORK leg succeeds (existing test testIMDClaimFailureKeepsClaimsAndStillSweepsWorkThenRetries). Repeating at any later time gives the same result; no function sends the IMD claims anywhere other than B.","severity":"info","snippet":"            poolManager.take(x, B, v);","title":"Dependency assumption: accrued claims in a currency the treasury cannot receive have no alternative exit"}],"hash":"030250e735d60b290b45c5e991c06675a1adaab5a810950c22f61967a850f659","nodeId":"fdd90063-3e89-4fdb-9eec-e183991a6a3c","outcome":"completed","summary":"The review is complete. The findings file is written and validated, and no source, config or dependency file was changed.\n\n**Outcome.** No critical, high or medium defect survived reproduction. Six findings are recorded in `.imd-findings.json`: four low and two info, with all 8 entry points covered plus three invariant rows.\n\n**What I kept, after reproducing each against the real PoolManager v4.0.0**\n\n- **sweep() legs not independent (low).** Merged the test writer's medium and the flow specialist's low. The balance read and the transfer return-data decode sit outside the try/catch, so a reverting IMD balanceOf or a one-byte IMD transfer return aborts the WORK leg. The supplied proof fails on this code with the stated message. I lowered it to low because a standard ERC-20 never triggers it, and under either trigger the PoolManager's own sync and take would already fail to settle IMD, so the pool is broken before sweep independence matters.\n- **beforeInitialize accepts any initializer and price (low).** Merged three specialists. A stranger can open the only accepted pool at MIN_SQRT_PRICE+1, the factory's initialize then reverts permanently, and the ramp can run down before liquidity exists. The supplied proof fails on this code. It needs a non-atomic deploy-and-initialize, which the service says does not happen, so it is a deployment-ordering dependency.\n- **Single-sided IMD liquidity bypasses the snipe fee (low).** Reproduced in a scratch test. The sniper paid about 29,864 IMD and withdrew about 30,515 WORK with zero hook fee, while a swapper at the same moment got about 31,034 WORK for 60,000 IMD. Passive and inherent to a fee-on-swap-only design.\n- **Fee rounds down to zero on dust (low).** A 50 wei exact-in swap at 2% produced 48 wei output and no fee.\n- **Two info rows** record the trust assumptions: the treasury can change the standing fee instantly ahead of a pending trade, which I measured as 96.77 versus 88.87 WORK on a 100 IMD trade, and IMD claims strand if the treasury can never receive IMD.\n\n**What I checked myself beyond the specialists.** The hook delta accounting nets to zero in both swap modes, claims stay backed by manager balances across mixed swaps and a sweep, the unlock callback can only be reached by the hook's own unlock, the 0x2044 flags match the permissions and manifest, and the manifest constructor arguments and permissions match the compiled contract. The existing 47-test suite passes.\n\n**Proofs.** The two supplied proof files are attached to findings 1 and 2. Both ran from `test/scratch/` and fail on the current code for the reason each finding states.","treeHash":null,"usage":{"cachedInputTokens":1166690,"inputTokens":418,"model":"claude-fable-5-1","outputTokens":32464,"runtime":"claude","turns":30,"wallClockMs":528890}},{"artifacts":[],"attempt":2,"bundleHash":"c96dd85f5349af1a4a250cc698da4a36aa61962aeb27db7652b4c05b69ea736c","device":"2c985fa233fefc9e","findings":[],"hash":"6b292f65e9a1cdf792e8a0f9a73ece028cdaeeb6e7d24772aebce5cb526bde58","nodeId":"50561ed9-7638-435e-9e7e-9bd44b2ac738","outcome":"completed","summary":"Implemented the project with supplied contract logic and launch notes preserved, vendored dependencies, CREATE2 salt helper, and deployment documentation.\n\nVerified with Solidity 0.8.26:\n\n- `forge build` — passed\n- `forge test` — 36 passed, including real PoolManager tests\n- `forge fmt --check` — passed\n\nDeployment responsibilities and supplied-code limitations are documented in `README.md`.","treeHash":"f55a83418e2041fce2d3b10638d423ca56863d06","usage":{"cachedInputTokens":1124224,"inputTokens":83473,"model":"gpt-6-astra","outputTokens":22489,"runtime":"codex","turns":7,"wallClockMs":740156}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"5739ce0d803a43cd","findings":[{"citation":"resolved","description":"Execution-trace / first-principles: the hook's only initialization guard checks the pair, fee, tick spacing, hook address and that openedAt is still zero. It does not check who initializes (PoolManager.initialize is permissionless) or the opening sqrtPriceX96, and it sets openedAt = block.timestamp at that moment. Once the hook has code, any account can call PoolManager.initialize(key, anyPrice) for the one key the hook accepts. Consequences: (1) the launch factory's own initialize at the manifest price 79228162514264337593543950336 then reverts with PoolAlreadyInitialized, so the launch transaction fails or must proceed on the stranger's price; (2) the pool opens at a price the stranger chose (e.g. TickMath.MIN_SQRT_PRICE+1 or MAX_SQRT_PRICE-1), which can make the factory's liquidity seed mispriced; (3) feeNow() starts decaying from the stranger's initialization, so if liquidity is seeded 900 s or more later the 50% snipe guard is already at the 2% standing fee. The manifest says the factory deploys the hook and initializes its pool; the defect is reachable only if those two steps are not in one transaction (or if a transaction between them is possible), which is why this is rated low rather than medium. The README documents the requirement for atomic deploy+initialize but the contract does not enforce it. Minimal fix that preserves the design: either take the factory ($factory) as a constructor argument and require the beforeInitialize sender argument to equal it, or require the sqrtPriceX96 argument to equal the manifest initial price; either keeps the single-pool, no-admin design.","line":37,"path":"src/WorkLaunchHook3.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {IPoolManager} from \"v4-core/src/interfaces/IPoolManager.sol\";\nimport {IHooks} from \"v4-core/src/interfaces/IHooks.sol\";\nimport {TickMath} from \"v4-core/src/libraries/TickMath.sol\";\nimport {PoolKey} from \"v4-core/src/types/PoolKey.sol\";\nimport {Currency} from \"v4-core/src/types/Currency.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\nimport {Work} from \"src/Work.sol\";\nimport {WorkLaunchHook3} from \"src/WorkLaunchHook3.sol\";\n\ncontract StandIn is ERC20 {\n    constructor() ERC20(\"IMD\", \"IMD\") {}\n}\n\n/// @dev Finding: the only accepted pool can be initialized by anyone, at any price, and the\n/// snipe ramp starts at that moment. Fails on the current code; passes once beforeInitialize\n/// restricts the initializer (factory) or pins sqrtPriceX96 to the manifest price.\ncontract InitFrontRunTest is Test {\n    address internal constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    uint160 internal constant MANIFEST_PRICE = 79228162514264337593543950336;\n    uint256 internal constant T0 = 1_800_000_000;\n\n    PoolManager internal manager;\n    Work internal work;\n    WorkLaunchHook3 internal hook;\n    PoolKey internal key;\n    address internal factory = makeAddr(\"factory\");\n    address internal sniper = makeAddr(\"sniper\");\n\n    function setUp() public {\n        vm.warp(T0);\n        manager = new PoolManager(address(this));\n        vm.etch(IMD, address(new StandIn()).code);\n        work = new Work();\n        bytes32 initHash = keccak256(abi.encodePacked(type(WorkLaunchHook3).creationCode, abi.encode(manager, address(work))));\n        for (uint256 i; i < 300_000; ++i) {\n            address p = address(\n                uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), initHash))))\n            );\n            if (uint160(p) & 0x3fff == 0x2044) {\n                hook = new WorkLaunchHook3{salt: bytes32(i)}(manager, address(work));\n                break;\n            }\n        }\n        require(address(hook) != address(0), \"no salt\");\n        (address a, address b) = address(work) < IMD ? (address(work), IMD) : (IMD, address(work));\n        key = PoolKey(Currency.wrap(a), Currency.wrap(b), 12500, 60, IHooks(address(hook)));\n    }\n\n    function testStrangerCannotPreInitializeAtArbitraryPrice() public {\n        // Hook is deployed; factory has not yet initialized. A stranger opens the pool at the\n        // minimum price and 15 minutes early.\n        vm.prank(sniper);\n        (bool ok,) = address(manager).call(\n            abi.encodeCall(IPoolManager.initialize, (key, TickMath.MIN_SQRT_PRICE + 1))\n        );\n        assertFalse(ok, \"stranger initialized the launch pool at a non-manifest price\");\n\n        // The factory's own initialization at the manifest price must still succeed afterwards,\n        // and the ramp must start when the factory opens the pool.\n        vm.warp(T0 + 900);\n        vm.prank(factory);\n        manager.initialize(key, MANIFEST_PRICE);\n        assertEq(hook.openedAt(), T0 + 900, \"ramp must start at the factory's initialization\");\n        assertEq(hook.feeNow(), 5000, \"snipe guard must be at 50% when the factory opens the pool\");\n    }\n}","reproduction":"State: PoolManager deployed, WORK deployed, WorkLaunchHook3 deployed at a 0x2044-flagged CREATE2 address, pool not yet initialized by the factory. Input: an unprivileged account calls manager.initialize(PoolKey(sorted(IMD,WORK), 12500, 60, hook), TickMath.MIN_SQRT_PRICE + 1) at time T0. Actual: succeeds, hook.openedAt() == T0; the factory's later manager.initialize(key, 79228162514264337593543950336) reverts with PoolAlreadyInitialized; at T0+900 hook.feeNow() == 200 although no liquidity was ever seeded. Expected: the stranger's initialize reverts and the factory's initialize at the manifest price succeeds with openedAt equal to the factory's block time and feeNow() == 5000. The scratch test test/scratch/InitFrontRun.t.sol fails on the current code with 'stranger initialized the launch pool at a non-manifest price'.","severity":"low","snippet":"    function beforeInitialize(address, PoolKey calldata k, uint160) external onlyPM returns (bytes4) {\n        address a = Currency.unwrap(k.currency0);\n        address b = Currency.unwrap(k.currency1);\n        require(\n            openedAt == 0 && ((a == IMD && b == token) || (a == token && b == IMD)) && k.fee == 12500\n                && k.tickSpacing == 60 && address(k.hooks) == address(this)\n        );\n        openedAt = block.timestamp;","title":"beforeInitialize accepts any initializer and any sqrtPriceX96, so the single accepted pool can be opened by a stranger at an arbitrary price and the 15-minute snipe ramp starts then"},{"citation":"resolved","description":"Periphery / boundary: the manifest promises 'a permissionless sweep() with independent IMD and WORK legs'. Only the PoolManager.unlock claim redemption is inside try/catch. Two calls per leg are outside it: Currency.balanceOfSelf() (a high-level IERC20Minimal.balanceOf call that reverts if the token reverts or returns no data) and abi.decode(r,(bool)) on the raw transfer return data, which reverts when the data is non-empty but shorter than 32 bytes or is not a clean 0/1 word. Either condition on the IMD leg (i == 0) reverts before the WORK leg runs, so WORK claims and direct WORK balances cannot be swept until IMD recovers, contrary to the stated independence. With standard OpenZeppelin-style tokens this does not trigger; it matters only if the fixed IMD contract at 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127 pauses balanceOf or returns non-standard data, which the project cannot verify locally (the test IMD is a mock etched at that address). No funds are lost; they are delayed. Minimal fix: wrap the balanceOf read in a low-level staticcall with length check and require r.length >= 32 before decoding (treat anything else as a failed leg and emit SweepFailed), so each leg fails on its own.","line":82,"path":"src/WorkLaunchHook3.sol","reproduction":"State: hook holds 3 IMD and 7 WORK directly (or any WORK ERC-6909 claims). Case A: IMD.transfer returns 1 byte of data (mstore(0,1); return(31,1)) -> hook.sweep() reverts in abi.decode(r,(bool)) at line 85 after the IMD transfer already executed; WORK stays in the hook (balance 7 ether, treasury 0). Case B: IMD.balanceOf reverts -> hook.sweep() reverts at line 82; WORK leg never runs. Expected per manifest: the IMD leg emits SweepFailed(IMD) and the WORK leg still delivers 7 WORK to 0xc9EAFE33A510a3a3d95A94c4f85AdaF6a3EA12a0. Reproduced in test/scratch/SweepLegs.t.sol (both cases) and acknowledged by the existing test testMalformedDirectTransferResponseRevertsEntireSweep.","severity":"low","snippet":"            uint256 v = Currency.wrap(x).balanceOfSelf();\n            if (v > 0) {\n                (bool ok, bytes memory r) = x.call(abi.encodeWithSignature(\"transfer(address,uint256)\", B, v));\n                if (!ok || (r.length > 0 && !abi.decode(r, (bool)))) emit SweepFailed(x);","title":"sweep() legs are not independent: a revert in the IMD leg's balanceOf or malformed transfer return data aborts the whole sweep including the WORK leg"}],"hash":"86b1a34b671b40500508890c5fee6ec6f94340712eb2cb1ad5468102318f33fa","nodeId":"93aa675f-fee7-423b-8e11-0ad14b48d156","outcome":"completed","summary":"The findings file is in place and is excluded from git by the repository's own exclude rules, which is the expected collection path. No tracked file was changed.\n\n## Review result\n\nThe hook's control flow is sound for its core paths. I traced the swap fee against the vendored v4-core: the unspecified-currency selection in `afterSwap` matches the manager's own `Hooks.afterSwap` branch, the claim mint and the returned delta net to zero for the hook, the exact-out gross-up is algebraically correct, and `unlockCallback` is reachable only through the hook's own `sweep`. Reentrancy through token callbacks cannot double-redeem because the manager's claim balance is the source of truth and a nested unlock is caught.\n\nTwo defects survived the validation gates, both low:\n\n1. **Unrestricted pool initialization** at `src/WorkLaunchHook3.sol:37`. The hook checks the key but not the initializer or the opening price. Once the hook has code, any account can open the one accepted pool at any price, and the snipe ramp starts at that moment. The factory's own initialize then reverts. This is reachable only if the factory does not deploy and initialize in one transaction. A fail-now proof test is attached.\n2. **Sweep legs are not independent** at `src/WorkLaunchHook3.sol:82`. A reverting `balanceOf` or short transfer return data on the IMD leg aborts the whole sweep before the WORK leg runs. No funds are lost, only delayed, and only a non-standard IMD contract triggers it.\n\nCoverage records all 8 listed entry points plus 5 invariant rows. Six entry points hold, two carry the findings above. The static-analysis leads were each traced and none became a finding on its own. One open item is recorded in coverage rather than as a finding: I could not verify that the fee tier 12500 is in the launch policy's tier list, since that list was not supplied.\n\nThe project's 36 tests, `forge build` and `forge fmt --check` still pass. The only failing test is the proof under test/scratch, which is deleted before submission.","treeHash":null,"usage":{"cachedInputTokens":1003117,"inputTokens":386,"model":"claude-fable-5-1","outputTokens":30634,"runtime":"claude","turns":25,"wallClockMs":495674}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"f9a9196633a6ffa8","findings":[{"citation":"resolved","description":"Both fee formulas use floor division, so the fee is always rounded in the trader's favour (the Math Precision guide's rule is that fees round UP). Exact-input: when the raw AMM output is below 10000/r units the fee is 0 (at the 2% standing fee, any output below 50 wei; at the 50% opening rate, 1 wei). Exact-output: when raw input*r < 10000-r the grossed-up fee is 0 (at 2%, any raw input below 49 wei). The ramp in feeNow() (line 53, `(5000 - s) * (900 - t) / 900`) also floors every second, under-charging by up to 1 bp per second (sum of the truncated numerators over the 899 ramp seconds is 270000/900 = 300 bp-seconds). Impact is dust and does not compound: avoiding the fee requires one swap per <50 wei of 18-decimal output, so gas dwarfs the saving, and the per-swap under-charge never exceeds one unit of the fee currency. Reported for completeness of the rounding-direction audit; a `mulDivUp`-style ceiling (`(x * r + 9999) / 10000`, `(x * r + (10000 - r) - 1) / (10000 - r)`) would make the fee round toward the treasury without changing any other behaviour.","line":70,"path":"src/WorkLaunchHook3.sol","reproduction":"State: real PoolManager, pool initialized at sqrtPrice 2^96 with 10_000_000e18 liquidity in [-600,600], warp to openedAt+900 so feeNow()==200. Call swap(zeroForOne=true, amountSpecified=-50, limit=MIN_SQRT_PRICE+1): raw AMM output is 48 wei, 48*200/10000 = 0, so the hook mints no claim and the trader receives all 48 wei; expected a 1 wei fee under round-up. With amountSpecified=-100 the raw output is 96, fee 96*200/10000 = 1 (ideal 1.92). Exact-output at opening rate 5000: swap(zeroForOne=true, amountSpecified=1) -> raw input 3, fee 3*5000/5000 = 3 (exact, no loss); at t=+1 (rate 4994) raw input 3 -> fee 3*4994/5006 = 2 instead of 2.99. feeNow() at t=1 returns 4994 while the exact value is 4994.67. Verified with a scratch Foundry test against lib/v4-core PoolManager (test removed; the existing test testDustExactInputRoundsFeeToZero documents the same behaviour).","severity":"low","snippet":"        uint256 f = a < 0 ? uint256(int256(-a)) * r / (10000 - r) : uint256(int256(a)) * r / 10000;","title":"Hook fee rounds down against the treasury and truncates to zero on dust swaps in both modes"}],"hash":"950ec150a9b3b08532920ccbbb07ee949a20e278901bde2b32a03f575e2a60e1","nodeId":"583ed441-59f7-4ba7-9467-3bd83f878476","outcome":"completed","summary":"The review is complete and the findings file is written.\n\n**Result:** the fee math in `WorkLaunchHook3` holds under the Math Precision, Boundary and Numerical Gap checks. One low-severity rounding-direction defect was recorded. No critical, high or medium findings.\n\n**What I verified, against the real PoolManager from `lib/v4-core`:**\n- The unspecified-currency selection in afterSwap matches the PoolManager's own convention in all four mode and direction combinations, and the hook's positive delta is netted to zero by its claim mint.\n- The exact-output gross-up produces the same effective price as exact-input, so the \"same share in both modes\" claim is true. The fuzzed share deficit never exceeds one unit across rates 0 to 50% and amounts from 1 wei to 1e24.\n- No division by zero, no overflow, and no reachable int128 wrap. The int128 cast would need a single-swap delta of 2^127, far above the 1e27 supply.\n- The ramp has no underflow, is continuous at the 900-second boundary, and stays within the standing fee and 50% for every admin setting, including mid-ramp changes.\n- The Slither strict-equality lead on `openedAt == 0` is not a defect. The timestamp is never zero on a live chain.\n- Sweep legs are independent and reentrancy-safe. Claims are burned before the take, and the direct balance is read after the unlock.\n\n**The one finding (low):** every fee division floors in the trader's favour. At the 2% standing fee, an exact-input swap whose raw output is under 50 wei pays no fee at all, and the ramp under-charges by up to one basis point per second. The impact is dust at 18 decimals and does not compound, so it is reported for completeness with a ceiling-division fix suggested.\n\n**Coverage:** all eight listed entry points have rows, plus three invariant rows. One note outside my area is recorded in the beforeInitialize row: the callback does not check the caller or the initial price, which only matters if the factory ever deploys the hook and initializes the pool in separate transactions.\n\nThe scratch test I used was removed, and no project file was changed.","treeHash":null,"usage":{"cachedInputTokens":722426,"inputTokens":322,"model":"claude-fable-5-1","outputTokens":28950,"runtime":"claude","turns":18,"wallClockMs":433801}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"4f40ad362162f51a","findings":[],"hash":"a9db3cda0755ec7ac852847e7fd687915ed166310cdbdb04635cdd2efb49db36","nodeId":"50561ed9-7638-435e-9e7e-9bd44b2ac738","outcome":"failed","summary":"the task produced no changes; the agent's last message was: Blocked by environment permissions. The workspace is read-only, and execution policy rejected file reads and `forge --version`.\n\nNo files changed. I couldn’t create `.imd-blocked.json`, implement the project, or run `forge build` and `forge test`.\n\nPlease rerun with repository write access and command execution enabled.","treeHash":null,"usage":{"cachedInputTokens":39168,"inputTokens":21228,"model":"gpt-6-astra","outputTokens":912,"runtime":"codex","turns":2,"wallClockMs":54857}},{"artifacts":[],"attempt":1,"bundleHash":"685416d608b324d6ebf967d82c25cb08496a5f3739e5bf1617664ccb663693a4","device":"312a95392d3e8a75","findings":[{"description":"The brief promises independent IMD and WORK sweep legs. Only PoolManager.unlock is protected by try/catch. The subsequent IMD balanceOf call (line 82) and abi.decode of transfer returndata (line 85) can revert outside that protection. A reverting balanceOf, a one-byte transfer response, or an invalid ABI bool therefore rolls back the entire sweep and prevents otherwise transferable WORK from reaching the treasury. The reproducer uses a local IMD fault model at the mandated address and a real PoolManager; it does not establish that the deployed IMD currently exhibits these behaviors. Isolate all external interactions and decoding for each currency so a failed IMD leg cannot stop WORK. The prior test that asserted a whole-sweep revert was removed rather than retaining that behavior as correct.","line":82,"path":"src/WorkLaunchHook3.sol","proof":"// SPDX-License-Identifier: MIT\npragma solidity 0.8.26;\n\nimport {Test} from \"forge-std/Test.sol\";\nimport {Work} from \"src/Work.sol\";\nimport {WorkLaunchHook3} from \"src/WorkLaunchHook3.sol\";\nimport {PoolManager} from \"v4-core/src/PoolManager.sol\";\nimport {ERC20} from \"@openzeppelin/contracts/token/ERC20/ERC20.sol\";\n\ncontract FailingSweepIMD is ERC20 {\n    uint256 public mode;\n    constructor() ERC20(\"Test IMD\", \"IMD\") {}\n    function mint(address to, uint256 amount) external { _mint(to, amount); }\n    function setMode(uint256 value) external { mode = value; }\n    function balanceOf(address who) public view override returns (uint256) {\n        require(mode != 2, \"IMD balance query unavailable\");\n        return super.balanceOf(who);\n    }\n    function transfer(address to, uint256 amount) public override returns (bool) {\n        bool result = super.transfer(to, amount);\n        if (mode == 1) {\n            assembly (\"memory-safe\") { mstore(0, 1) return(31, 1) }\n        }\n        return result;\n    }\n}\n\ncontract SweepIsolationProof is Test {\n    address constant IMD = 0x5F7Bb59365ce557C26dbcAa4EE9d39A4b95B7127;\n    address constant TREASURY = 0xc9EAFE33A510a3a3d95A94c4f85AdaF6a3EA12a0;\n    Work work;\n    WorkLaunchHook3 hook;\n\n    function setUp() public {\n        PoolManager manager = new PoolManager(address(this));\n        work = new Work();\n        FailingSweepIMD implementation = new FailingSweepIMD();\n        vm.etch(IMD, address(implementation).code);\n        bytes32 hash = keccak256(abi.encodePacked(type(WorkLaunchHook3).creationCode, abi.encode(manager, address(work))));\n        for (uint256 i; i < 200_000; ++i) {\n            address predicted = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), bytes32(i), hash)))));\n            if (uint160(predicted) & 0x3fff == 0x2044) {\n                hook = new WorkLaunchHook3{salt: bytes32(i)}(manager, address(work));\n                break;\n            }\n        }\n        require(address(hook).code.length > 0, \"salt search failed\");\n        FailingSweepIMD(IMD).mint(address(hook), 3 ether);\n        work.transfer(address(hook), 7 ether);\n    }\n\n    function testMalformedIMDTransferMustNotBlockWorkSweep() public {\n        FailingSweepIMD(IMD).setMode(1);\n        _assertIndependentWorkLeg();\n    }\n\n    function testRevertingIMDBalanceQueryMustNotBlockWorkSweep() public {\n        FailingSweepIMD(IMD).setMode(2);\n        _assertIndependentWorkLeg();\n    }\n\n    function _assertIndependentWorkLeg() internal {\n        (bool success,) = address(hook).call(abi.encodeCall(hook.sweep, ()));\n        // Check delivery first so the failure describes the economic consequence.\n        assertEq(work.balanceOf(TREASURY), 7 ether, \"independent WORK leg must reach treasury\");\n        assertEq(work.balanceOf(address(hook)), 0);\n        assertTrue(success, \"sweep must isolate a failed IMD leg\");\n    }\n}","reproduction":"Deploy Work, the real PoolManager, and a WorkLaunchHook3 at a mined CREATE2 address with flags 0x2044. Give the hook 3e18 IMD and 7e18 WORK. In the local IMD model, either (1) make transfer move tokens but return the single byte 0x01, or (2) make balanceOf revert. Call permissionless sweep(). Expected: the WORK leg transfers 7e18 WORK to 0xc9EAFE33A510a3a3d95A94c4f85AdaF6a3EA12a0 despite the IMD fault. Actual in both cases: sweep reverts, treasury WORK remains 0, and the hook retains 7e18 WORK. Verified with forge test --match-path test/scratch/SweepIsolationProof.t.sol -vv: both tests fail with \"independent WORK leg must reach treasury: 0 != 7000000000000000000\". Save the proof source as a test/*.t.sol file to replay.","severity":"medium","title":"Unisolated IMD balance query and return decoding can block the independent WORK sweep"}],"hash":"aebbb4b51c23ea72396b20c60a414c3a2cfd19c6ee612ab44ab02557585aa129","nodeId":"b34919dd-de41-4f0b-9322-d93ac444c82f","outcome":"completed","summary":"Added failure-path, fuzz, and stateful invariant tests against the real PoolManager.\n\nValidation passed: `forge build`, 47 tests, and 24,576 randomized calls.\n\nReported one medium-severity sweep-isolation defect with reproduced failing proofs in [.imd-findings.json](/root/.identitymd/work/09d157d4-92ff-4035-859e-0187f4cc6d02/b34919dd-de41-4f0b-9322-d93ac444c82f/.imd-findings.json): certain IMD failures block WORK sweeping. Live IMD behavior remains unverified.","treeHash":"80a1da52533d752907ce83bc852a419421ac3416","usage":{"cachedInputTokens":1783808,"inputTokens":112004,"model":"gpt-6-astra","outputTokens":17122,"runtime":"codex","turns":5,"wallClockMs":683138}},{"artifacts":[],"attempt":1,"bundleHash":"297b157e57a30f60762792308d718c2e55311f0cc8d5a226fdd189909e9d67a6","device":"19e9c602f9934c65","findings":[],"hash":"d7fcab857b24d825bdf39a400e03b4b5d1b3b2a81e48672a15b2c86afe00d56a","nodeId":"f1ff43f8-daeb-42d6-9e53-84ac9cd49c66","outcome":"completed","summary":"Updated only [launch.json](/home/imd-worker/.identitymd/work/09d157d4-92ff-4035-859e-0187f4cc6d02/f1ff43f8-daeb-42d6-9e53-84ac9cd49c66/launch.json), preserving all supplied values and the exact notes string.\n\nSchema validation passed; permissions and constructor arguments match the implementation. `forge build` succeeded with lint warnings; `forge test` passed all 36 tests.","treeHash":"526ffbd45bca332949f11fb7f66951b449731c0a","usage":{"cachedInputTokens":296960,"inputTokens":34440,"model":"gpt-6-astra","outputTokens":3303,"runtime":"codex","turns":3,"wallClockMs":153741}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"3c7630b22a73c1fb","findings":[{"citation":"resolved","description":"Outside my assigned lenses (economics), reported because it undercuts the stated purpose of the 50% opening fee. The hook has no liquidity callbacks, so modifyLiquidity on the launch pool never touches the hook and never pays a fee. Price in the pool is currency1/currency0, and a range entirely on one side of the current tick holds only one token. Anyone can therefore place IMD-only liquidity one tick-spacing below the current price at openedAt and let other traders' WORK sales (which do pay the 50% fee on their IMD output) convert that IMD into WORK; removing the position returns WORK plus the 1.25% LP fee, and afterSwap is never entered for the acquirer. The acquirer gets roughly twice the WORK per IMD that a swapper gets at the same moment. It is passive (needs counterflow through the range) and is inherent to a fee-only-in-afterSwap design, so severity is low. Any fix (e.g., a beforeAddLiquidity gate or fee during the 900 s ramp) changes the permission set and the mined 0x2044 address, which is a design decision for the requester, not something to apply silently.","line":27,"path":"src/WorkLaunchHook3.sol","reproduction":"State: pool initialized at sqrtPriceX96 = 2^96 (tick 0) at time T = openedAt, deployer liquidity 10_000_000e18 units in [-600,600], feeNow() == 5000. Actors hold IMD only. Sequence against the real v4.0.0 PoolManager, verified in a scratch test: (1) sniper calls modifyLiquidity on the launch pool with range [-120,-60] when WORK is currency0 (or [60,120] when WORK is currency1), liquidityDelta 10_000_000e18, paying 29,863.83 IMD and 0 WORK; hook ERC-6909 claims stay 0. (2) a seller swaps exactIn 300_000e18 WORK through the pool; the price crosses the sniper's range; the hook mints IMD claims from the seller's output (seller keeps 50%). (3) sniper calls modifyLiquidity with liquidityDelta -10_000_000e18 and receives 30,515.24 WORK, 0 IMD back; hook IMD claims unchanged since step 2 and WORK claims == 0. Expected under the manifest notes' intent (50% snipe guard at open): acquiring WORK at openedAt costs 50% to the treasury. Actual: 0 hook fee. Comparison in the same state: a swapper spending 60,000e18 IMD exactIn at T receives 29,450.51 WORK and the hook mints 29,450.51 WORK of claims (50%).","severity":"low","snippet":"    function getHookPermissions() public pure returns (Hooks.Permissions memory p) {\n        p.beforeInitialize = true;\n        p.afterSwap = true;\n        p.afterSwapReturnDelta = true;\n    }","title":"Snipe-guard fee is charged only on swaps: WORK can be acquired during the 50% window through single-sided liquidity with no hook fee"},{"citation":"resolved","description":"Access control is correct (only the hard-coded treasury B, bounded 0..1000 bps) and matches the brief, so this is not a defect but the one privileged power in the system, recorded per the review rules. Because the write takes effect on the next afterSwap with no delay, B can front-run any pending swap with setStandingFee(1000) and take 10% instead of the 2% the trader observed, and it also alters an in-progress ramp (fee at t=450 s jumps from 2600 to 3000 bps). The trader's only protection is their router's slippage bound. B is a single EOA that is also the only fee recipient; loss of that key freezes the fee at its last value with no recovery path (no transfer, no two-step, no renounce), which is consistent with the notes' 'no other admin, no upgrades'.","line":56,"path":"src/WorkLaunchHook3.sol","reproduction":"State: pool open for >= 900 s, standingFee == 200, feeNow() == 200. Trader broadcasts exactIn 100e18 IMD -> WORK expecting ~98% of raw output. B sends setStandingFee(1000) with higher priority in the same block. Trader's afterSwap computes f = rawOut * 1000 / 10000; trader receives 90% of raw output instead of 98%, the 8-point difference is minted as hook claims for B. From a stranger address the same call reverts (require msg.sender == B), and B calling with f = 1001 reverts; both are already covered by test/WorkLaunchHook3.t.sol testTreasuryCanOnlySetBoundedStandingFee.","severity":"info","snippet":"    function setStandingFee(uint256 f) external {\n        require(msg.sender == B && f <= 1000);\n        standingFee = f;\n        emit StandingFee(f);\n    }","title":"Trust assumption: treasury key can change the standing fee by up to 8 points instantly and ahead of a pending trade (no timelock)"},{"citation":"resolved","description":"The callback is correctly restricted to the PoolManager, enforces the single IMD/WORK pool (fee 12500, spacing 60, hooks == this) and rejects a second initialization, so the pool cannot be created before the hook exists. However the first parameter (initializer) and the third (sqrtPriceX96) are unused: once the hook has code, any address may call PoolManager.initialize at any price, and openedAt is set from that call. The README already states the launch factory must deploy and initialize atomically at the manifest price, and the reference says the factory does so, so this is a dependency on that process rather than a code defect. Two consequences to keep in mind: if deploy and initialize were ever split, a stranger picks the opening price; and if initialize and liquidity seeding were split, the 50%->2% ramp runs down before any liquidity exists.","line":37,"path":"src/WorkLaunchHook3.sol","reproduction":"State: hook deployed at its 0x2044 CREATE2 address, pool not yet initialized. A stranger calls manager.initialize(key, 4295128740) (near MIN_SQRT_PRICE) from an unrelated EOA. Expected if the deployment is non-atomic: refused. Actual: beforeInitialize passes (only key fields are checked), openedAt = block.timestamp, the pool exists at the stranger's price and the factory's later initialize reverts (openedAt != 0). test/WorkLaunchHook3.t.sol testInitializationByUnprivilegedCallerAndDuplicateRejected already shows the stranger path succeeding at the manifest price.","severity":"info","snippet":"    function beforeInitialize(address, PoolKey calldata k, uint160) external onlyPM returns (bytes4) {","title":"Trust assumption: beforeInitialize accepts any initializer and any starting price, and starts the 900 s ramp clock"}],"hash":"e8d035ed38646ae5e470885cbfcec300b825dd8e9abb9a595ad01fa0e5d7f92a","nodeId":"26177ad5-c4dc-4bd5-b521-71a62ac57a5f","outcome":"completed","summary":"Review complete. The findings file is written and verified, the scratch test is removed, and no tracked file changed.\n\n**Outcome:** no critical, high, or medium defect in the assigned area. The permission model of `WorkLaunchHook3` is sound: every PoolManager callback is caller-restricted, the one privileged setter is bounded and has a single writer, and every value path out of the hook ends at the hard-coded treasury. The existing 36-test suite against the real v4.0.0 PoolManager passes.\n\n**What I reported in `.imd-findings.json`:**\n\n- **Low (economics, outside my lenses but substantiated):** the 50% snipe-guard fee only exists in afterSwap, and the hook has no liquidity callbacks. I confirmed against the real PoolManager that an IMD-only position placed one tick-spacing below the opening price acquired about 30,515 WORK for about 29,864 IMD with zero hook fee once a seller pushed the price through it. A swapper at the same moment received about 29,450 WORK for 60,000 IMD. Any fix changes the permission flags and the mined address, so I flagged it as a design decision rather than a silent change.\n- **Info (trust assumption):** the treasury key can move the standing fee between 0 and 10% instantly, ahead of a pending trade and mid-ramp, with no timelock or key rotation. Access control is correct and matches the brief.\n- **Info (trust assumption):** beforeInitialize ignores the initializer and the starting price, so atomic deploy-plus-initialize by the factory is what protects the opening price and ramp clock. The README already documents this.\n\n**Coverage:** all 8 ABI entry points have rows (6 hold, 2 carry the info findings), plus 7 invariant and manifest rows. Exact-in versus exact-out fee symmetry, both sweep legs, claim custody, single-pool enforcement, and launch.json consistency with the constructor and flags 0x2044 all hold.\n\n**Not reached:** nothing within the assigned guides. The static-analysis leads (strict equality on `openedAt`, unused unlock return, timestamp use) were traced and are benign, so they are omitted.","treeHash":null,"usage":{"cachedInputTokens":745060,"inputTokens":290,"model":"claude-fable-5-1","outputTokens":29286,"runtime":"claude","turns":24,"wallClockMs":420667}},{"artifacts":[],"attempt":2,"bundleHash":null,"device":"743b05d8a235188d","findings":[{"citation":"resolved","description":"beforeInitialize validates the pair, fee, spacing and hook, but ignores both the caller (first parameter) and the initial sqrtPriceX96 (third parameter), and it permanently records openedAt on the first success. PoolManager.initialize is permissionless, so from the moment the hook contract exists, any address can be the one that opens the IMD/WORK pool. Two economic consequences follow. (1) The opener chooses the starting price: an attacker opens at TickMath.MIN_SQRT_PRICE+1 (or any price), the factory's own initialize at the manifest price 79228162514264337593543950336 then reverts inside the hook's openedAt==0 guard, and because openedAt is never reset the hook can never accept the intended pool; the launch must be redone with a new CREATE2 salt. (2) Even an opener who uses the correct price starts the 900 s snipe-guard ramp at their block, not at the block liquidity arrives; if seed liquidity lands 900 s or more later, the first-ever buyer pays the 2% standing fee instead of 50%, so the guard the launch advertises never applies. Both require a window between hook deployment and the factory's initialize call. The service description says the launch factory deploys the hook and initializes the pool, and if those are in one transaction no window exists; the hook itself provides no protection, so this is a deployment-ordering dependency the author should either pin in code (accept only the manifest price, or only a factory/owner opener) or document as a hard requirement on the deployer. Severity is low because it is griefing with no fund loss and depends on a non-atomic deployment.","line":37,"path":"src/WorkLaunchHook3.sol","reproduction":"State: PoolManager deployed, Work deployed, WorkLaunchHook3 deployed at a 0x2044 address, pool NOT yet initialized. Step 1 (stranger): manager.initialize(key{IMD/WORK sorted, fee 12500, tickSpacing 60, hooks=hook}, TickMath.MIN_SQRT_PRICE+1) -> succeeds, hook.openedAt()==block.timestamp. Step 2 (factory): manager.initialize(sameKey, 79228162514264337593543950336) -> reverts (hook require openedAt==0 fails). Step 3: any later beforeInitialize for any key reverts; openedAt is permanent. Expected: only the launch factory can open the pool, at the manifest price. Actual: first caller wins and fixes the price and ramp start. Variant for (2): stranger opens at the correct price at T; factory adds 10,000,000 liquidity at T+900; hook.feeNow()==200, so the first buyer pays 2% not 50%. Both reproduced in test/scratch/Economics.t.sol (testStrangerCanPreemptInitializeAtArbitraryPrice, testEarlyInitializeConsumesRampBeforeLiquidity) against the real PoolManager.","severity":"low","snippet":"    function beforeInitialize(address, PoolKey calldata k, uint160) external onlyPM returns (bytes4) {\n        address a = Currency.unwrap(k.currency0);\n        address b = Currency.unwrap(k.currency1);\n        require(\n            openedAt == 0 && ((a == IMD && b == token) || (a == token && b == IMD)) && k.fee == 12500\n                && k.tickSpacing == 60 && address(k.hooks) == address(this)\n        );\n        openedAt = block.timestamp;","title":"Anyone can initialize the single accepted pool at any price once the hook exists; the ramp starts and the hook's one-shot slot is consumed"},{"citation":"resolved","description":"afterSwap reads the live standingFee through feeNow() at execution time with no timelock, announcement or per-swap cap, so a setStandingFee(1000) that lands ahead of a pending swap raises that swap's hook fee from 2% to 10% of the unspecified amount. This is within the 0-10% power the brief explicitly grants to 0xc9ea...12a0, so it is not a defect in the hook; it is recorded as the privileged-power risk the review guide asks to document separately. The only protection a trader has is their own router's minimum-output / maximum-input bound; the hook offers none. No unprivileged actor can trigger it.","line":69,"path":"src/WorkLaunchHook3.sol","reproduction":"State: pool opened at price 1:1 with 10,000,000 liquidity in [-600,600], block.timestamp = openedAt + 900 (ramp finished), standingFee = 200. Trade: exact-in 100 IMD for WORK. Case A (fee 2%): trader receives 96.774044356311981419 WORK. Case B: treasury calls setStandingFee(1000) in the block before the trade executes; the same trade now receives 88.874122368041615589 WORK, 7.9 WORK (8.2%) less than the quote the trader saw. Reproduced in test/scratch/Economics.t.sol testStandingFeeChangeHitsInFlightTrade. Expected per brief: the treasury may set 0-10% (holds); noted so the author can decide whether a delay or cap on changes is wanted.","severity":"info","snippet":"        uint256 r = feeNow();","title":"Trust assumption: the treasury can move the standing fee between 0% and 10% instantly, and an in-flight trade pays whatever rate is live at execution"},{"citation":"resolved","description":"unlockCallback always redeems claims to the hard-coded treasury B, and sweep() swallows the revert and leaves the claims in place. That is the intended independent-leg behavior for transient failures, and the existing test testIMDClaimFailureKeepsClaimsAndStillSweepsWorkThenRetries shows claims are retained and later recovered. The economic edge case is a permanent failure: if the IMD token at 0x5F7B...7127 ever blocks transfers to B (blacklist, pause, or B being a contract that the token refuses), every IMD fee ever accrued stays as ERC-6909 claims inside the PoolManager with no admin, no alternate recipient and no upgrade to move them. For the plain Work token this cannot happen. The hook's 'no other admin' design makes this the deliberate trade-off; recorded so the author confirms IMD has no transfer restrictions on the launch chain.","line":95,"path":"src/WorkLaunchHook3.sol","reproduction":"State: pool opened, one exact-in swap in each direction so the hook holds IMD and WORK claims. Make IMD.transfer(B, v) revert permanently (test: imd.setTransferMode(2) in the existing MockIMD). Call sweep() from any address: emits SweepFailed(IMD), IMD claims unchanged, WORK leg succeeds. Repeat at any later time: same result, no function exists that sends the IMD claims anywhere other than B. Expected per notes: fees go to B (holds while IMD transfers to B work). Actual under a permanent IMD-side block: IMD fees are stranded indefinitely.","severity":"info","snippet":"            poolManager.take(x, B, v);","title":"Dependency assumption: accrued claims in a currency the treasury cannot receive have no alternative exit"}],"hash":"f420c385a84eb6f3ea7ec5385aabfa310b9d8b33eb87a1880e4f98039ec9b429","nodeId":"09a5dc84-59be-4816-8fac-241e53fcc499","outcome":"completed","summary":"The findings file is in place and excluded from git by the repository's own exclude list, the scratch test is gitignored, and no tracked file changed. The review is complete.\n\n**Outcome:** the economics of the hook hold. The fee share, the ramp, the claim accounting and the sweep all behave as the notes describe, and I found no fund-loss defect. I reported one low finding and two documented trust assumptions, all with reproductions in a scratch test run against the real PoolManager.\n\n**Findings written to `.imd-findings.json`:**\n\n- **Low, pool opening is permissionless and unpriced.** The hook validates pair, fee, spacing and hook address in beforeInitialize but ignores the caller and the initial price. Once the hook contract exists, the first address to call initialize fixes the starting price and starts the 900 s ramp, and openedAt can never be reset. A stranger opening at an extreme price bricks the launch for that salt, and a stranger opening at the correct price early lets seed liquidity arrive after the ramp has already decayed to 2%. Both require a gap between hook deployment and the factory's initialize call, which the service description says is a single transaction, so this is a deployment-ordering dependency rather than a loss path.\n- **Info, standing fee applies instantly.** A trade pays whatever rate is live when it executes. The treasury moving from 2% to 10% ahead of a pending 100 IMD trade cuts the output by about 8%. That is inside the power the brief grants, so it is recorded as a trust assumption, not a defect.\n- **Info, no alternative exit for stranded claims.** If IMD ever permanently refuses transfers to the treasury, IMD fee claims stay inside the PoolManager with no admin or fallback recipient. Not reachable with a plain token, recorded as a dependency assumption on the real IMD contract.\n\n**Coverage:** all eight listed entry points have rows, plus five invariant and manifest rows. afterSwap, sweep, unlockCallback and the token functions hold. I traced the four swap mode and direction cases through the vendored Hooks and PoolManager code and confirmed the hook's net delta is zero and the fee share is exact in both modes. The Slither leads (strict equality, unused return, timestamp use) were checked and are not defects. Nothing in the assigned guides was left unreached.","treeHash":null,"usage":{"cachedInputTokens":1290674,"inputTokens":514,"model":"claude-fable-5-1","outputTokens":37133,"runtime":"claude","turns":23,"wallClockMs":549403}}],"verification":[{"checks":[{"durationMs":13570,"exitCode":0,"name":"build","output":"Compiling 74 files with Solc 0.8.26\nSolc 0.8.26 finished in 13.41s\nCompiler run successful!\nwarning[missing-events-arithmetic]: `openedAt` is changed without an event but is used in arithmetic\n   ╭▸ src/WorkLaunchHook3.sol:44:9\n   │\n44 │         openedAt = block.timestamp;\n   │         ━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-events-arithmetic\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/WorkLaunchHook3.sol:51:23\n   │\n51 │         if (t == 0 || block.timestamp <= t) return 5000;\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/WorkLaunchHook3.sol:53:16\n   │\n53 │         return t < 900 ? s + (5000 - s) * (900 - t) / 900 : s;\n   │                ━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/WorkLaunchHook3.sol:70:29\n   │\n70 │         uint256 f = a < 0 ? uint256(int256(-a)) * r / (10000 - r) : uint256(int256(a)) * r / 10000;\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/WorkLaunchHook3.sol:70:69\n   │\n70 │         uint256 f = a < 0 ? uint256(int256(-a)) * r / (10000 - r) : uint256(int256(a)) * r / 10000;\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/WorkLaunchHook3.sol:72:44\n   │\n72 │         return (IHooks.afterSwap.selector, int128(int256(f)));\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/WorkLaunchHook3.sol:72:51\n   │\n72 │         return (IHooks.afterSwap.selector, int128(int256(f)));\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[calls-loop]: external call inside a loop\n   ╭▸ src/WorkLaunchHook3.sol:78:17\n   │\n78 │             try poolManager.unlock(abi.encode(x)) {}\n   │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/WorkLaunchHook3.sol:84:45\n   │\n84 │                 (bool ok, bytes memory r) = x.call(abi.encodeWithSignature(\"transfer(address,uint256)\", B, v));\n   │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/WorkLaunchHook3.sol:80:17\n   │\n80 │                 emit SweepFailed(x);\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/WorkLaunchHook3.sol:85:70\n   │\n85 │                 if (!ok || (r.length > 0 && !abi.decode(r, (bool)))) emit SweepFailed(x);\n   │                                                                      ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":187,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Work.t.sol:WorkTest\n[PASS] testAllowanceAndTransferFrom() (gas: 130309)\n[PASS] testFuzzTransferPreservesSupply(uint256) (runs: 256, μ: 104491, ~: 105074)\nLogs:\n  Bound result 3971\n\n[PASS] testInsufficientBalanceAndAllowanceRevert() (gas: 72409)\n[PASS] testMetadataAndEntireFixedSupplyToDeployer() (gas: 49010)\n[PASS] testNoMintOwnerPauseOrUpgradeEntrypoints() (gas: 101761)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 7.65ms (8.19ms CPU time)\n\nRan 31 tests for test/WorkLaunchHook3.t.sol:WorkLaunchHook3Test\n[PASS] testAllCallbacksRejectNonManager() (gas: 91126)\n[PASS] testConstructorRejectsIMDAndZeroToken() (gas: 6347)\n[PASS] testConstructorRejectsIncorrectFlags() (gas: 9046)\n[PASS] testCreate2AddressPermissionsAndImmutables() (gas: 63822)\n[PASS] testDirectTokenFalseAndRevertResponsesKeepOtherLegUsable() (gas: 752045)\n[PASS] testDustExactInputRoundsFeeToZero() (gas: 753901)\n[PASS] testEmptySweepAndNoNativeLeg() (gas: 110995)\n[PASS] testFeeScheduleAtBoundaries() (gas: 477887)\n[PASS] testFuzzFeeMonotonicallyDecays(uint16,uint16) (runs: 256, μ: 155598, ~: 156033)\nLogs:\n  Bound result 1\n  Bound result 18\n\n[PASS] testFuzzRealSwaps(bool,bool,uint96,uint16,uint16) (runs: 256, μ: 803182, ~: 801651)\nLogs:\n  Bound result 15717\n  Bound result 1601\n  Bound result 899\n\n[PASS] testIMDClaimFailureKeepsClaimsAndStillSweepsWorkThenRetries() (gas: 1121798)\n[PASS] testInitializationAcceptsBothTokenSortOrders() (gas: 199523792)\n[PASS] testInitializationByUnprivilegedCallerAndDuplicateRejected() (gas: 187094)\n[PASS] testInvalidSwapAndUnfundedSettlementRevertAtomically() (gas: 957673)\n[PASS] testMalformedDirectTransferResponseRevertsEntireSweep() (gas: 266153)\n[PASS] testNoReturnTokenTransfersSupportedForClaimsAndDirectBalances() (gas: 1033211)\n[PASS] testOneForZeroExactInputFees() (gas: 2420395)\n[PASS] testOneForZeroExactOutputGrossUp() (gas: 2428359)\n[PASS] testPermissionlessSweepRedeemsBothClaimsAndDirectDonationsExactlyOnce() (gas: 1178301)\n[PASS] testPriceLimitedPartialFillsInBothModesAndDirections() (gas: 1828336)\n[PASS] testReentrantTokenCannotRedeemClaimsOrDirectBalancesTwice() (gas: 1167026)\n[PASS] testRejectsWrongFeeIncludingDynamicFee() (gas: 521903)\n[PASS] testRejectsWrongPairAndNativePair() (gas: 488985)\n[PASS] testRejectsWrongSpacingAndHook() (gas: 87854)\n[PASS] testSaltSearchExhaustion() (gas: 16518)\n[PASS] testStandingFeeUpdateChangesRampWithoutRestartingIt() (gas: 486723)\n[PASS] testSweepWhileManagerUnlockedPreservesClaimsButForwardsDirectBalances() (gas: 1097623)\n[PASS] testTreasuryCanOnlySetBoundedStandingFee() (gas: 579431)\n[PASS] testWorkClaimFailureDoesNotUndoIMDSweep() (gas: 1051069)\n[PASS] testZeroForOneExactInputFees() (gas: 2442622)\n[PASS] testZeroForOneExactOutputGrossUp() (gas: 2443480)\nSuite result: ok. 31 passed; 0 failed; 0 skipped; finished in 91.57ms (135.91ms CPU time)\n\nRan 2 test suites in 92.21ms (99.22ms CPU time): 36 tests passed, 0 failed, 0 skipped (36 total tests)\n","passed":true},{"durationMs":44,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Work.approve(address,uint256)\",\"Work.transfer(address,uint256)\",\"Work.transferFrom(address,address,uint256)\",\"WorkLaunchHook3.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WorkLaunchHook3.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WorkLaunchHook3.setStandingFee(uint256)\",\"WorkLaunchHook3.sweep()\",\"WorkLaunchHook3.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"DEPENDENCIES.md\":32,\"README.md\":181,\"foundry.toml\":21,\"launch.json\":1,\"script/MineWorkHook.s.sol\":26,\"src/Work.sol\":8,\"src/WorkLaunchHook3.sol\":99,\"test/Work.t.sol\":60,\"test/WorkLaunchHook3.t.sol\":550,\"test/helpers/MockIMD.sol\":52,\"test/helpers/TestRouter.sol\":61},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true},{"durationMs":1372,"exitCode":0,"name":"slither","output":"[medium/high] incorrect-equality at src/WorkLaunchHook3.sol:48: WorkLaunchHook3.feeNow() (src/WorkLaunchHook3.sol#48-54) uses a dangerous strict equality:\n[medium/medium] unused-return at src/WorkLaunchHook3.sol:75: WorkLaunchHook3.sweep() (src/WorkLaunchHook3.sol#75-88) ignores return value by poolManager.unlock(abi.encode(x)) (src/WorkLaunchHook3.sol#78-81)\n[low/medium] calls-loop at src/WorkLaunchHook3.sol:75: WorkLaunchHook3.sweep() (src/WorkLaunchHook3.sol#75-88) has external calls inside a loop: poolManager.unlock(abi.encode(x)) (src/WorkLaunchHook3.sol#78-81)\n[low/medium] calls-loop at src/WorkLaunchHook3.sol:75: WorkLaunchHook3.sweep() (src/WorkLaunchHook3.sol#75-88) has external calls inside a loop: (ok,r) = x.call(abi.encodeWithSignature(transfer(address,uint256),B,v)) (src/WorkLaunchHook3.sol#84)\n[low/medium] reentrancy-events at src/WorkLaunchHook3.sol:75: Reentrancy in WorkLaunchHook3.sweep() (src/WorkLaunchHook3.sol#75-88):\n[low/medium] timestamp at src/WorkLaunchHook3.sol:48: WorkLaunchHook3.feeNow() (src/WorkLaunchHook3.sol#48-54) uses timestamp for comparisons\n[low/medium] timestamp at src/WorkLaunchHook3.sol:62: WorkLaunchHook3.afterSwap(address,PoolKey,IPoolManager.SwapParams,BalanceDelta,bytes) (src/WorkLaunchHook3.sol#62-73) uses timestamp for comparisons\n[low/medium] timestamp at src/WorkLaunchHook3.sol:37: WorkLaunchHook3.beforeInitialize(address,PoolKey,uint160) (src/WorkLaunchHook3.sol#37-46) uses timestamp for comparisons","passed":true},{"durationMs":310,"exitCode":0,"name":"aderyn","output":"[low] empty-require-revert at src/WorkLaunchHook3.sol:21: Empty `require()` / `revert()` Statement (4 places)\n[low] large-numeric-literal at src/Work.sol:6: Large Numeric Literal (3 places)\n[low] literal-instead-of-constant at src/WorkLaunchHook3.sol:51: Literal Instead of Constant (7 places)\n[low] state-change-without-event at src/WorkLaunchHook3.sol:37: State Change Without Event\n[low] uninitialized-local-variable at src/WorkLaunchHook3.sol:76: Uninitialized Local Variable","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"6b292f65e9a1cdf792e8a0f9a73ece028cdaeeb6e7d24772aebce5cb526bde58","verifiedTreeHash":"f55a83418e2041fce2d3b10638d423ca56863d06","verifierVersion":"0.1.0+ad90ce4c"},{"checks":[{"durationMs":21153,"exitCode":0,"name":"build","output":"Compiling 77 files with Solc 0.8.26\nSolc 0.8.26 finished in 21.01s\nCompiler run successful!\nwarning[missing-events-arithmetic]: `openedAt` is changed without an event but is used in arithmetic\n   ╭▸ src/WorkLaunchHook3.sol:44:9\n   │\n44 │         openedAt = block.timestamp;\n   │         ━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-events-arithmetic\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/WorkLaunchHook3.sol:51:23\n   │\n51 │         if (t == 0 || block.timestamp <= t) return 5000;\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/WorkLaunchHook3.sol:53:16\n   │\n53 │         return t < 900 ? s + (5000 - s) * (900 - t) / 900 : s;\n   │                ━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/WorkLaunchHook3.sol:70:29\n   │\n70 │         uint256 f = a < 0 ? uint256(int256(-a)) * r / (10000 - r) : uint256(int256(a)) * r / 10000;\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/WorkLaunchHook3.sol:70:69\n   │\n70 │         uint256 f = a < 0 ? uint256(int256(-a)) * r / (10000 - r) : uint256(int256(a)) * r / 10000;\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/WorkLaunchHook3.sol:72:44\n   │\n72 │         return (IHooks.afterSwap.selector, int128(int256(f)));\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/WorkLaunchHook3.sol:72:51\n   │\n72 │         return (IHooks.afterSwap.selector, int128(int256(f)));\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[calls-loop]: external call inside a loop\n   ╭▸ src/WorkLaunchHook3.sol:78:17\n   │\n78 │             try poolManager.unlock(abi.encode(x)) {}\n   │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/WorkLaunchHook3.sol:84:45\n   │\n84 │                 (bool ok, bytes memory r) = x.call(abi.encodeWithSignature(\"transfer(address,uint256)\", B, v));\n   │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/WorkLaunchHook3.sol:80:17\n   │\n80 │                 emit SweepFailed(x);\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/WorkLaunchHook3.sol:85:70\n   │\n85 │                 if (!ok || (r.length > 0 && !abi.decode(r, (bool)))) emit SweepFailed(x);\n   │                                                                      ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":7053,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 10 tests for test/Work.t.sol:WorkTest\n[PASS] testAllowanceAndTransferFrom() (gas: 130381)\n[PASS] testFuzzTransferPreservesSupply(uint256) (runs: 1000, μ: 104790, ~: 105182)\nLogs:\n  Bound result 259956788803460871350074267\n\n[PASS] testInsufficientBalanceAndAllowanceRevert() (gas: 72519)\n[PASS] testMaximumTransferAndAbsentBurnCannotAlterFixedSupply() (gas: 83969)\n[PASS] testMetadataAndEntireFixedSupplyToDeployer() (gas: 49010)\n[PASS] testNoMintOwnerPauseOrUpgradeEntrypoints() (gas: 101790)\n[PASS] testRevertingTransferFromRollsBackAllowanceSpending() (gas: 174998)\n[PASS] testUnlimitedApprovalOverwriteAndRevocation() (gas: 303459)\n[PASS] testZeroOneAndFullSupplyTransfersAndSelfTransfers() (gas: 266346)\n[PASS] testZeroRecipientAndSpenderRevertWithoutBurningSupply() (gas: 99256)\nSuite result: ok. 10 passed; 0 failed; 0 skipped; finished in 10.65ms (12.38ms CPU time)\n\nRan 35 tests for test/WorkLaunchHook3.t.sol:WorkLaunchHook3Test\n[PASS] testAllCallbacksRejectNonManager() (gas: 91227)\n[PASS] testConstructorRejectsIMDAndZeroToken() (gas: 6397)\n[PASS] testConstructorRejectsIncorrectFlags() (gas: 9140)\n[PASS] testCreate2AddressPermissionsAndImmutables() (gas: 63851)\n[PASS] testDirectTokenFalseAndRevertResponsesKeepOtherLegUsable() (gas: 752139)\n[PASS] testDustExactInputRoundsFeeToZero() (gas: 754021)\n[PASS] testEmptyLiquiditySwapsCannotCreateClaims() (gas: 1749824)\n[PASS] testEmptySweepAndNoNativeLeg() (gas: 111067)\n[PASS] testFeeScheduleAtBoundaries() (gas: 477953)\n[PASS] testFuzzFeeMonotonicallyDecays(uint16,uint16) (runs: 1000, μ: 155538, ~: 156093)\nLogs:\n  Bound result 839\n  Bound result 1501\n\n[PASS] testFuzzRealSwaps(bool,bool,uint96,uint16,uint16) (runs: 1000, μ: 803064, ~: 801776)\nLogs:\n  Bound result 5488\n  Bound result 2000\n  Bound result 985\n\n[PASS] testFuzzRepeatedSwapRoundTripsCannotCreateTokens(bool,uint96,uint8,uint16) (runs: 1000, μ: 1676068, ~: 1478981)\nLogs:\n  Bound result 183\n  Bound result 437483923\n  Bound result 1\n\n[PASS] testIMDClaimFailureKeepsClaimsAndStillSweepsWorkThenRetries() (gas: 1121842)\n[PASS] testInitializationAcceptsBothTokenSortOrders() (gas: 199523865)\n[PASS] testInitializationByUnprivilegedCallerAndDuplicateRejected() (gas: 187166)\n[PASS] testInvalidInitialPriceRollsBackLaunchTimestamp() (gas: 489034)\n[PASS] testInvalidSwapAndUnfundedSettlementRevertAtomically() (gas: 957777)\n[PASS] testNoReturnTokenTransfersSupportedForClaimsAndDirectBalances() (gas: 1033278)\n[PASS] testOneForZeroExactInputFees() (gas: 2420593)\n[PASS] testOneForZeroExactOutputGrossUp() (gas: 2428911)\n[PASS] testOneWeiSwapsInBothModesAndDirections() (gas: 1686164)\n[PASS] testPermissionlessSweepRedeemsBothClaimsAndDirectDonationsExactlyOnce() (gas: 1178367)\n[PASS] testPriceLimitedPartialFillsInBothModesAndDirections() (gas: 1828856)\n[PASS] testReentrantTokenCannotRedeemClaimsOrDirectBalancesTwice() (gas: 1167070)\n[PASS] testRejectsWrongFeeIncludingDynamicFee() (gas: 521925)\n[PASS] testRejectsWrongPairAndNativePair() (gas: 489007)\n[PASS] testRejectsWrongSpacingAndHook() (gas: 87926)\n[PASS] testSaltSearchExhaustion() (gas: 16590)\n[PASS] testStandingFeeUpdateChangesRampWithoutRestartingIt() (gas: 486767)\n[PASS] testSweepWhileManagerUnlockedPreservesClaimsButForwardsDirectBalances() (gas: 1097711)\n[PASS] testTreasuryCanOnlySetBoundedStandingFee() (gas: 579519)\n[PASS] testUninitializedPoolCannotSwapOrAccrueClaims() (gas: 843316)\n[PASS] testWorkClaimFailureDoesNotUndoIMDSweep() (gas: 1051069)\n[PASS] testZeroForOneExactInputFees() (gas: 2442908)\n[PASS] testZeroForOneExactOutputGrossUp() (gas: 2444054)\nSuite result: ok. 35 passed; 0 failed; 0 skipped; finished in 204.89ms (519.62ms CPU time)\n\nRan 2 tests for test/WorkLaunchInvariant.t.sol:WorkLaunchInvariantTest\n[PASS]\nWorkLaunchInvariantTest invariants:\n[PASS] invariantAdminAndLaunchStateRemainBounded\n[PASS] invariantFeesAndDonationsAreConservedAndBacked\n[PASS] invariantWorkAllowancesFollowActorAuthorization\n WorkLaunchInvariantTest invariants (runs: 256, calls: 24576, reverts: 0)\n\n╭-------------------+------------------------+-------+---------+----------╮\n| Contract          | Selector               | Calls | Reverts | Discards |\n+=========================================================================+\n| WorkLaunchHandler | advanceTime            | 2217  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | approve                | 2224  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | donate                 | 2242  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | rejectFeeChange        | 2218  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | rejectReinitialization | 2236  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | setFee                 | 2287  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | swap                   | 2222  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | sweep                  | 2261  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | sweepWhileUnlocked     | 2210  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | transfer               | 2229  | 0       | 0        |\n|-------------------+------------------------+-------+---------+----------|\n| WorkLaunchHandler | transferFrom           | 2230  | 0       | 0        |\n╰-------------------+------------------------+-------+---------+----------╯\n\nLogs:\n  Bound result 10000000000000000000\n  Bound result 10000000000000000000\n  Bound result 10000000000000000000\n  Bound result 10000000000000000000\n  Bound result 3000000000000000000\n  Bound result 7000000000000000000\n  Bound result 300\n  Bound result 96\n  Bound result 0\n  Bound result 496\n  Bound result 31317951092\n  Bound result 97\n  Bound result 8724\n  Bound result 151\n  Bound result 0\n  Bound result 43048288593880946975\n  Bound result 100000000000000000000\n  Bound result 99999999999999999901\n  Bound result 11792\n  Bound result 3\n  Bound result 7\n  Bound result 1\n  Bound result 78\n  Bound result 899\n  Bound result 12500\n  Bound result 12744621492878418\n  Bound result 117300738\n  Bound result 229\n  Bound result 220\n  Bound result 1\n  Bound result 0\n  Bound result 0\n  Bound result 10\n  Bound result 953\n  Bound result 10\n  Bound result 115792089237316195423570985008687907853269984665640564039457584007913129638944\n  Bound result 51269371723886438954\n  Bound result 500\n  Bound result 16916\n  Bound result 1000\n  Bound result 346\n  Bound result 38\n  Bound result 8539336372276876322\n  Bound result 1\n  Bound result 13633\n  Bound result 115792089237316195423570985008687907853269984665640564039457584007913129639835\n  Bound result 1000000000000000000\n  Bound result 449\n  Bound result 2\n  Bound result 0\n  Bound result 100\n  Bound result 3000\n  Bound result 1800000000\n  Bound result 63603998624831160614\n  Bound result 211\n  Bound result 2\n  Bound result 67154896172180557500\n  Bound result 5649248361144249\n  Bound result 8983\n  Bound result 0\n  Bound result 2\n  Bound result 12279\n  Bound result 45\n  Bound result 96\n  Bound result 911\n  Bound result 1809\n  Bound result 5\n  Bound result 1490894\n  Bound result 0\n  Bound result 72356883355589697709\n  Bound result 8745\n  Bound result 0\n  Bound result 82\n  Bound result 3514649358\n  Bound result 94432811001888296924\n  Bound result 223\n  Bound result 133\n  Bound result 0\n  Bound result 814\n  Bound result 0\n  Bound result 9495\n  Bound result 0\n\n[PASS] testHandlerSequenceExercisesRecoveryAndAuthorization() (gas: 1837472)\nLogs:\n  Bound result 10000000000000000000\n  Bound result 10000000000000000000\n  Bound result 10000000000000000000\n  Bound result 10000000000000000000\n  Bound result 3000000000000000000\n  Bound result 7000000000000000000\n  Bound result 1\n  Bound result 149\n  Bound result 1000\n  Bound result 115792089237316195423570985008687907853269984665640564039457584007913129639935\n  Bound result 10000000000000000000\n  Bound result 10000000000000000000\n  Bound result 10000000000000000000\n  Bound result 1\n  Bound result 2\n  Bound result 3\n  Bound result 0\n\nSuite result: ok. 2 passed; 0 failed; 0 skipped; finished in 6.97s (6.94s CPU time)\n\nRan 3 test suites in 6.97s (7.19s CPU time): 47 tests passed, 0 failed, 0 skipped (47 total tests)\n","passed":true},{"durationMs":31,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Work.approve(address,uint256)\",\"Work.transfer(address,uint256)\",\"Work.transferFrom(address,address,uint256)\",\"WorkLaunchHook3.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WorkLaunchHook3.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WorkLaunchHook3.setStandingFee(uint256)\",\"WorkLaunchHook3.sweep()\",\"WorkLaunchHook3.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"DEPENDENCIES.md\":32,\"README.md\":181,\"foundry.toml\":21,\"launch.json\":1,\"script/MineWorkHook.s.sol\":26,\"src/Work.sol\":8,\"src/WorkLaunchHook3.sol\":99,\"test/TESTING.md\":63,\"test/Work.t.sol\":145,\"test/WorkLaunchHook3.t.sol\":538,\"test/WorkLaunchInvariant.t.sol\":131,\"test/helpers/MockIMD.sol\":52,\"test/helpers/TestRouter.sol\":61,\"test/helpers/WorkLaunchHandler.sol\":249,\"test/helpers/WorkPoolFixture.sol\":80},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"aebbb4b51c23ea72396b20c60a414c3a2cfd19c6ee612ab44ab02557585aa129","verifiedTreeHash":"80a1da52533d752907ce83bc852a419421ac3416","verifierVersion":"0.1.0+ad90ce4c"},{"checks":[{"durationMs":14559,"exitCode":0,"name":"build","output":"Compiling 74 files with Solc 0.8.26\nSolc 0.8.26 finished in 14.39s\nCompiler run successful!\nwarning[missing-events-arithmetic]: `openedAt` is changed without an event but is used in arithmetic\n   ╭▸ src/WorkLaunchHook3.sol:44:9\n   │\n44 │         openedAt = block.timestamp;\n   │         ━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/missing-events-arithmetic\n\nwarning[block-timestamp]: usage of `block.timestamp` in a comparison may be manipulated by validators\n   ╭▸ src/WorkLaunchHook3.sol:51:23\n   │\n51 │         if (t == 0 || block.timestamp <= t) return 5000;\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/WorkLaunchHook3.sol:53:16\n   │\n53 │         return t < 900 ? s + (5000 - s) * (900 - t) / 900 : s;\n   │                ━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/block-timestamp\n\nwarning[unsafe-typecast]: typecast can truncate values\n   ╭▸ src/WorkLaunchHook3.sol:70:29\n   │\n70 │         uint256 f = a < 0 ? uint256(int256(-a)) * r / (10000 - r) : uint256(int256(a)) * r / 10000;\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/WorkLaunchHook3.sol:70:69\n   │\n70 │         uint256 f = a < 0 ? uint256(int256(-a)) * r / (10000 - r) : uint256(int256(a)) * r / 10000;\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/WorkLaunchHook3.sol:72:44\n   │\n72 │         return (IHooks.afterSwap.selector, int128(int256(f)));\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/WorkLaunchHook3.sol:72:51\n   │\n72 │         return (IHooks.afterSwap.selector, int128(int256(f)));\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[calls-loop]: external call inside a loop\n   ╭▸ src/WorkLaunchHook3.sol:78:17\n   │\n78 │             try poolManager.unlock(abi.encode(x)) {}\n   │                 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[calls-loop]: external call inside a loop\n   ╭▸ src/WorkLaunchHook3.sol:84:45\n   │\n84 │                 (bool ok, bytes memory r) = x.call(abi.encodeWithSignature(\"transfer(address,uint256)\", B, v));\n   │                                             ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/calls-loop\n\nwarning[reentrancy-events]: event emitted after an external call; reentrancy can reorder or fabricate logs that off-chain consumers rely on\n   ╭▸ src/WorkLaunchHook3.sol:80:17\n   │\n80 │                 emit SweepFailed(x);\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/WorkLaunchHook3.sol:85:70\n   │\n85 │                 if (!ok || (r.length > 0 && !abi.decode(r, (bool)))) emit SweepFailed(x);\n   │                                                                      ━━━━━━━━━━━━━━━━━━━━\n   │\n   ╰ help: https://getfoundry.sh/forge/linting/reentrancy-events\n\n","passed":true},{"durationMs":209,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 5 tests for test/Work.t.sol:WorkTest\n[PASS] testAllowanceAndTransferFrom() (gas: 130309)\n[PASS] testFuzzTransferPreservesSupply(uint256) (runs: 256, μ: 104763, ~: 105062)\nLogs:\n  Bound result 788448532863721063259148606\n\n[PASS] testInsufficientBalanceAndAllowanceRevert() (gas: 72409)\n[PASS] testMetadataAndEntireFixedSupplyToDeployer() (gas: 49010)\n[PASS] testNoMintOwnerPauseOrUpgradeEntrypoints() (gas: 101761)\nSuite result: ok. 5 passed; 0 failed; 0 skipped; finished in 6.23ms (6.57ms CPU time)\n\nRan 31 tests for test/WorkLaunchHook3.t.sol:WorkLaunchHook3Test\n[PASS] testAllCallbacksRejectNonManager() (gas: 91126)\n[PASS] testConstructorRejectsIMDAndZeroToken() (gas: 6347)\n[PASS] testConstructorRejectsIncorrectFlags() (gas: 9046)\n[PASS] testCreate2AddressPermissionsAndImmutables() (gas: 63822)\n[PASS] testDirectTokenFalseAndRevertResponsesKeepOtherLegUsable() (gas: 752045)\n[PASS] testDustExactInputRoundsFeeToZero() (gas: 753901)\n[PASS] testEmptySweepAndNoNativeLeg() (gas: 110995)\n[PASS] testFeeScheduleAtBoundaries() (gas: 477887)\n[PASS] testFuzzFeeMonotonicallyDecays(uint16,uint16) (runs: 256, μ: 155533, ~: 156021)\nLogs:\n  Bound result 233\n  Bound result 10\n\n[PASS] testFuzzRealSwaps(bool,bool,uint96,uint16,uint16) (runs: 256, μ: 802653, ~: 801396)\nLogs:\n  Bound result 1022583891\n  Bound result 1428\n  Bound result 4\n\n[PASS] testIMDClaimFailureKeepsClaimsAndStillSweepsWorkThenRetries() (gas: 1121798)\n[PASS] testInitializationAcceptsBothTokenSortOrders() (gas: 199523792)\n[PASS] testInitializationByUnprivilegedCallerAndDuplicateRejected() (gas: 187094)\n[PASS] testInvalidSwapAndUnfundedSettlementRevertAtomically() (gas: 957673)\n[PASS] testMalformedDirectTransferResponseRevertsEntireSweep() (gas: 266153)\n[PASS] testNoReturnTokenTransfersSupportedForClaimsAndDirectBalances() (gas: 1033211)\n[PASS] testOneForZeroExactInputFees() (gas: 2420395)\n[PASS] testOneForZeroExactOutputGrossUp() (gas: 2428359)\n[PASS] testPermissionlessSweepRedeemsBothClaimsAndDirectDonationsExactlyOnce() (gas: 1178301)\n[PASS] testPriceLimitedPartialFillsInBothModesAndDirections() (gas: 1828336)\n[PASS] testReentrantTokenCannotRedeemClaimsOrDirectBalancesTwice() (gas: 1167026)\n[PASS] testRejectsWrongFeeIncludingDynamicFee() (gas: 521903)\n[PASS] testRejectsWrongPairAndNativePair() (gas: 488985)\n[PASS] testRejectsWrongSpacingAndHook() (gas: 87854)\n[PASS] testSaltSearchExhaustion() (gas: 16518)\n[PASS] testStandingFeeUpdateChangesRampWithoutRestartingIt() (gas: 486723)\n[PASS] testSweepWhileManagerUnlockedPreservesClaimsButForwardsDirectBalances() (gas: 1097623)\n[PASS] testTreasuryCanOnlySetBoundedStandingFee() (gas: 579431)\n[PASS] testWorkClaimFailureDoesNotUndoIMDSweep() (gas: 1051069)\n[PASS] testZeroForOneExactInputFees() (gas: 2442622)\n[PASS] testZeroForOneExactOutputGrossUp() (gas: 2443480)\nSuite result: ok. 31 passed; 0 failed; 0 skipped; finished in 111.20ms (149.88ms CPU time)\n\nRan 2 test suites in 111.85ms (117.44ms CPU time): 36 tests passed, 0 failed, 0 skipped (36 total tests)\n","passed":true},{"durationMs":43,"exitCode":0,"name":"source-index","output":"{\"v\":1,\"entryPoints\":[\"Work.approve(address,uint256)\",\"Work.transfer(address,uint256)\",\"Work.transferFrom(address,address,uint256)\",\"WorkLaunchHook3.afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes)\",\"WorkLaunchHook3.beforeInitialize(address,(address,address,uint24,int24,address),uint160)\",\"WorkLaunchHook3.setStandingFee(uint256)\",\"WorkLaunchHook3.sweep()\",\"WorkLaunchHook3.unlockCallback(bytes)\"],\"files\":{\".gitignore\":4,\"DEPENDENCIES.md\":32,\"README.md\":181,\"foundry.toml\":21,\"launch.json\":21,\"script/MineWorkHook.s.sol\":26,\"src/Work.sol\":8,\"src/WorkLaunchHook3.sol\":99,\"test/Work.t.sol\":60,\"test/WorkLaunchHook3.t.sol\":550,\"test/helpers/MockIMD.sol\":52,\"test/helpers/TestRouter.sol\":61},\"excluded\":[\"lib/\",\"node_modules/\"],\"truncated\":false}","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"d7fcab857b24d825bdf39a400e03b4b5d1b3b2a81e48672a15b2c86afe00d56a","verifiedTreeHash":"526ffbd45bca332949f11fb7f66951b449731c0a","verifierVersion":"0.1.0+ad90ce4c"}]}