{"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":"e649920a-ff71-4537-b7ec-95f9ad7aec6e","kind":"shape:chain","nodes":[{"acceptedSubmissionHash":"fbbce3ac051af75a474591416487fe1df50495159a11ace8f7edb0d309c63f19","dependsOn":["build_contract_project"],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"c26edc76dc7a76e09a42c3634054429c1679c6e247747d647a03f77e5b332d7e","skillId":"adversarial-review","tools":[]},"key":"adversarial_review","kind":"code","role":"review","skillHash":"c26edc76dc7a76e09a42c3634054429c1679c6e247747d647a03f77e5b332d7e","skillId":"adversarial-review","state":"accepted"},{"acceptedSubmissionHash":"38536a343841ddd20231cce383f3f337e7d8a6ef3053ee1a3907d8b0980937da","dependsOn":[],"execution":{"network":false,"profile":"foundry","requires":[],"skillHash":"fb6887b34514bcb194265fa403f0195c6a83eaef269da1e6189e1e9c4b372a7d","skillId":"build-contract-project","tools":[]},"key":"build_contract_project","kind":"code","role":"implement","skillHash":"fb6887b34514bcb194265fa403f0195c6a83eaef269da1e6189e1e9c4b372a7d","skillId":"build-contract-project","state":"accepted"}],"objective":"Build LiquidityLockHook, a simple, creative Uniswap v4 hook: a hook that enforces a minimum liquidity hold: beforeAddLiquidity records the block per (PoolId, owner), and beforeRemoveLiquidity reverts unless at least N blocks (fixed at construction) have passed since that owner's most recent add in that pool. Tests cover the lock boundary, a second add extending the lock, and independence across pools. Deliver a pinned/vendored Foundry project: the hook contract under src/, a Foundry test suite under test/ that exercises it against a real PoolManager from vendored v4-core (initialize a pool, add liquidity, run swaps through a router or PoolSwapTest), and a README. Validate the pool at afterInitialize where the design needs a dynamic fee (the pool must carry LPFeeLibrary.DYNAMIC_FEE_FLAG) and revert otherwise. Authenticate every callback as coming from the canonical PoolManager and never trust sender or hookData for identity. Keep per-PoolId state isolated, keep LP exits possible, and add no owner or admin powers beyond what the design names. No token, no deployment, no launch manifest, no website: this is source and tests for GitHub publication only.","parentJobId":null,"planHash":"85a3fa94fd53aa9389bcafe6820422f5c1069d008c363612ff8f0e3425dec63f","previousHash":"105351ad590ef56b7300a79ab133573a96b2a1301f3b1f092105a49b3dcf7930","projectId":"e649920a-ff71-4537-b7ec-95f9ad7aec6e","publication":{"commit":"d3e9f5da0295352051454ee839b3cdb318258a64","deliveredAt":"2026-09-21T04:09:28.369Z","repoUrl":"https://github.com/Identity-md/launch-70-build-liquiditylockhook-simple-creative"},"receiptIdentity":{"adapter":"0xde152afb7db5373f34876e1499fbd893a82dd336","chainId":1,"collection":"0x0000ec93127baa929e58e97dd0095a2bfb38ec1d","registry":"0x8004a169fb4a3325136eb29fa0ceb6d2e539a432"},"registry":"0xb6d0a187b050fa5bb0b87033a203f37becf4a775","research":[],"schema":"identitymd-work-v1","signals":[],"site":null,"snapshotHash":"9635117817f4dd052ebc168a8be7280be47fda1b8315b4a9338aac4d9723851a","state":"completed","submissions":[{"artifacts":[],"attempt":1,"bundleHash":"bfe445006743763656f0a5d4494e204f8909e528a6d3a27495ed9414783fb5e3","device":"62761ac059990ff7","findings":[],"hash":"38536a343841ddd20231cce383f3f337e7d8a6ef3053ee1a3907d8b0980937da","nodeId":"86ceb5a1-fb16-499d-af9f-d545e5e63801","outcome":"completed","summary":"`LiquidityLockHook` is built as a vendored Foundry project. `forge build`, `forge test` (14 tests pass) and `forge fmt --check` all pass. I ran them with network access, and I did not re-run them with the network off. I did not run the protected suites from `.imd/reads`. Nothing is committed.\n\n- **Hook (`src/LiquidityLockHook.sol`):** `beforeAddLiquidity` records the block for the sender in that pool. `beforeRemoveLiquidity` reverts with `LiquidityLocked` until `lastAdd + MIN_HOLD_BLOCKS`, and the exact boundary block is allowed. `MIN_HOLD_BLOCKS` is fixed at construction. Only those two permission bits are set.\n- **Callbacks and identity:** every callback is restricted to the PoolManager. Identity is the `sender` the PoolManager passes; `hookData` is never read.\n- **No admin powers:** there are no owner or admin functions.\n- **Exits:** a zero-delta `modifyLiquidity` (fee collection) is never blocked.\n- **`afterInitialize`:** I skipped the dynamic-fee check because this design doesn't need one.\n- **Tests (`test/LiquidityLockHook.t.sol`):** they run against a real `PoolManager` with the vendored `PoolModifyLiquidityTest` and `PoolSwapTest` routers. They cover the boundary block, a second add extending the lock, and independence across pools and across owners. They also cover swaps being unaffected, `hookData` not spoofing identity, non-manager callers being rejected, and a fuzzed boundary.\n- **Vendoring:** `lib/` holds plain files with no submodules. v4-core is at commit `46c6834`, plus forge-std and solmate at their latest. I added `test/mocks/MockERC20.sol` because the protected test imports it.\n- **README:** covers the design, assumptions and operational responsibilities.\n\n- **Shared routers:** the lock applies to whichever address calls `modifyLiquidity`. If LPs share a router, one user's add extends the lock for everyone using that router in that pool.\n- **`HookFlags.sol`:** the protected test imports `src/HookFlags.sol`, which I did not provide. I assumed the verifier supplies it.\n- **Address mining:** the hook address must be mined for the two flag bits. Deployment is out of scope, so there is no script for it.","treeHash":"4075cab48616dedfdeecd07b4905b82975539456","usage":{"cachedInputTokens":508122,"inputTokens":24,"model":"claude-sonnet-5","outputTokens":13303,"runtime":"claude","turns":12,"wallClockMs":159946}},{"artifacts":[],"attempt":1,"bundleHash":null,"device":"bb0a3bf63233e5e5","findings":[{"description":"The hook uses the callback sender as the owner's identity. The canonical PoolManager supplies the address calling modifyLiquidity, so every user of a shared router writes the same lastAddBlock entry for that pool. An attacker can add a single unit of liquidity to a separate position through that router and renew the victim's withdrawal lock, repeating this before each expiry to prevent exit indefinitely while the attack continues. No victim authorization, forged hookData, or unauthorized callback is needed. This conflicts with the assignment's requirements to avoid trusting sender for identity and keep LP exits possible. The README acknowledges the router-wide restriction, but documenting it does not satisfy those requirements. test_ownersAreIndependent misses the failing case because it creates a second router for the second user instead of exercising two users of the same router.","line":63,"path":"src/LiquidityLockHook.sol","reproduction":"Use LiquidityLockHookTest.setUp(), with N=10, keyA initialized at price 2^96 and block 100. The original test account calls lpRouter.modifyLiquidity(keyA, ModifyLiquidityParams(-120,120,1e18,bytes32(0)), hex\"\"). Transfer 100 units of each token to address(0xBEEF), and from that address approve lpRouter for both tokens. At block 109, have 0xBEEF call the SAME lpRouter.modifyLiquidity(keyA, ModifyLiquidityParams(-120,120,1,bytes32(uint256(1))), hex\"\"). The distinct salt keeps the attacker's position separate, but lastAddBlock[keyA.toId()][address(lpRouter)] becomes 109. At block 110, the original account calls lpRouter.modifyLiquidity(keyA, ModifyLiquidityParams(-120,120,-1e18,bytes32(0)), hex\"\"). Expected: the original owner's last add was block 100, so withdrawal succeeds after 10 blocks. Actual: the manager wraps LiquidityLocked(119,110) and withdrawal reverts. Repeat the attacker's one-unit add at blocks 118,127,136,... to keep the victim locked. This reproduction is established by tracing the supplied source: PoolModifyLiquidityTest forwards both users' calls as itself, Hooks.beforeModifyLiquidity passes that address to the hook, and lines 63 and 76 share its lock entry. No additional executable test was written or run during this read-only review.","severity":"medium","title":"Shared-router deposits let an unrelated LP continually postpone withdrawals"}],"hash":"fbbce3ac051af75a474591416487fe1df50495159a11ace8f7edb0d309c63f19","nodeId":"72751a9c-6dac-44b8-9c5d-6ffea73b9887","outcome":"completed","summary":"Wrote one **medium-severity** finding to [.imd-findings.json](.imd-findings.json): an unrelated user’s deposits through a shared router can repeatedly extend another LP’s withdrawal lock.\n\nIncludes exact reproduction inputs, verified by source tracing. No tests were run; implementation files remain unchanged.","treeHash":null,"usage":{"cachedInputTokens":166528,"inputTokens":37247,"model":null,"outputTokens":2281,"runtime":"codex","turns":3,"wallClockMs":86445}}],"verification":[{"checks":[{"durationMs":10081,"exitCode":0,"name":"build","output":"Compiling 74 files with Solc 0.8.26\nSolc 0.8.26 finished in 9.95s\nCompiler run successful!\nwarning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value\n    ╭▸ test/LiquidityLockHook.t.sol:158:9\n    │\n158 │         token0.transfer(other, 1e24);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer\n\nwarning[erc20-unchecked-transfer]: ERC20 'transfer' and 'transferFrom' calls should check the return value\n    ╭▸ test/LiquidityLockHook.t.sol:159:9\n    │\n159 │         token1.transfer(other, 1e24);\n    │         ━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n    │\n    ╰ help: https://book.getfoundry.sh/reference/forge/forge-lint#erc20-unchecked-transfer\n\n","passed":true},{"durationMs":163,"exitCode":0,"name":"test","output":"No files changed, compilation skipped\n\nRan 14 tests for test/LiquidityLockHook.t.sol:LiquidityLockHookTest\n[PASS] testFuzz_boundary(uint256,uint256) (runs: 256, μ: 348061, ~: 327989)\n[PASS] test_callbacksRejectNonManager() (gas: 33917)\n[PASS] test_feeCollectionAllowedWhileLocked() (gas: 353377)\n[PASS] test_hookDataCannotSpoofIdentity() (gas: 345914)\n[PASS] test_ownersAreIndependent() (gas: 1565285)\n[PASS] test_permissions() (gas: 8777)\n[PASS] test_poolsAreIndependent() (gas: 552292)\n[PASS] test_removeAtExactBoundarySucceeds() (gas: 301832)\n[PASS] test_removeBeforeLockReverts() (gas: 346003)\n[PASS] test_removeLongAfterSucceeds() (gas: 301779)\n[PASS] test_removeRevertsWithLockedError() (gas: 349296)\n[PASS] test_sameBlockRemoveReverts() (gas: 345196)\n[PASS] test_secondAddExtendsLock() (gas: 479018)\n[PASS] test_swapsUnaffectedByLock() (gas: 467253)\nSuite result: ok. 14 passed; 0 failed; 0 skipped; finished in 43.51ms (53.20ms CPU time)\n\nRan 1 test suite in 52.43ms (43.51ms CPU time): 14 tests passed, 0 failed, 0 skipped (14 total tests)\n","passed":true}],"detail":"all checks passed","evaluation":"checks","profile":"foundry","status":"accepted","submissionHash":"38536a343841ddd20231cce383f3f337e7d8a6ef3053ee1a3907d8b0980937da","verifiedTreeHash":"4075cab48616dedfdeecd07b4905b82975539456","verifierVersion":"0.1.0+eab70f1b"}]}