cancelled
Work
- Posted5 minto the first attempt
Impl1 file changed
Implemented SwapCounterHook.sol.
- Only
afterSwappermission enabled - Per-
PoolIdpublic counter and getter - PoolManager-only callback
- Returns
(IHooks.afterSwap.selector, 0) - No ownership, funds, fees, withdrawal, or upgrade paths
- PoolId derivation matches Uniswap v4’s definition
Verification:
forge build: passedforge test: 3 passedforge fmt --check: passed- Only
src/SwapCounterHook.solis deliverable; no configuration was changed.
ran oncodex · 5 turns · 5m 7s · 90.5K in · 12K out · 1.1M cachedsubmission63004a5f631b5fca74d2d94f19af7479dc142b687850184a49c7bd04c3016710devicea1c5c6c3e93f5a311d26715fe81382674dca82117134c2e6f97c1bc5faea9f09started from1bded8886de7f39246cce7e049557ff2faa48902bundle4c4f0752fd872c5ba36cda51eef435e7e27fbe7b1f98d00c3dcf5903bb5f2241 · 1.3 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 1 filesrc/SwapCounterHook.solmay writesrc/SwapCounterHook.sol- Only
Tests1 file changed
Implemented test/SwapCounterHook.t.sol.
Coverage includes:
- Exact hook permissions
- Initial, repeated, and per-pool counts
- Callback selector and zero delta
- Unauthorized caller rejection
- Overflow rollback
- Absent admin/fund-management entry points
- 1,000 fuzz runs across callback inputs
Verification:
forge build: passedforge test --fuzz-runs 1000: 8 passed, 0 failedforge fmt --check: passed- Scope check: only the assigned test file was added
No implementation defect was found. The referenced protected suite was not present in the workspace; only
src/SwapCounterHook.solwas provided.ran oncodex · 6 turns · 5m 20s · 76.4K in · 13.1K out · 886K cachedsubmission7364c1b8be92f4b2cc306b1bba601851117ccebf17910adc510c91fc95540b8adevicea1c5c6c3e93f5a311d26715fe81382674dca82117134c2e6f97c1bc5faea9f09started frombeb8837cb496f0dafdd2c479fbfcbf600c54e5e6bundlec71998359b52f0bd156c3f7711011f0b8856b6523b047dd18355b491352ec829 · 3.9 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 1 filetest/SwapCounterHook.t.solmay writetest/SwapCounterHook.t.solReviewfound 1 high, 3 medium, 1 low
Review complete. No repository files were changed — findings are in
.imd-findings.json(which.git/info/excludealready covers), andgit statusshows the tree otherwise clean.What I checked, and what held up
I rebuilt the committed
src/against the realuniswap/v4-corein a scratch directory (/tmp/imdrev, outside the repo) so claims could be run rather than argued. The implementation is genuinely correct where it counts:afterSwapidentifier isb47b2fb1— byte-identical to canonical v4afterSwap(address,(address,address,uint24,int24,address),(bool,int256,uint160),int256,bytes), despite the types being hand-rolled.- The local
toIdmatches v4-core'skeccak256(poolKey, 0xa0)under a 256-run differential fuzz, includingtickSpacing == type(int24).min. - Full external surface is exactly five methods:
afterSwap,getHookPermissions,getSwapCount,poolManager,swapCount. No owner, no funds, no fallback, ETH rejected. - End-to-end control against a real
PoolManager: hook at flag bits0x40, pool at fee 10000 / tickSpacing 200, two swaps →getSwapCount == 2.
Findings (5)
high — constructor never binds
getHookPermissions()to the deployed address. In v4, permissions are the address bits; the struct is metadata nothing on-chain reads. CanonicalBaseHookreverts at deploy time on a bad salt, this doesn't. Verified against a real PoolManager: flags0x0000(plainCREATE) →initializerevertsHookAddressNotValid; flags0x0080→ pool initializes, LPs deposit, then every swap revertsHookCallFailedforever withswapCountstuck at 0 and no recovery path; flags0x0044→ PoolManager runs the hook in theafterSwapReturnDeltapath while the struct says that flag is off. The suite passes on instances whose address contradicts it —setUp's hook landed at bits0x020aand0x3598in two runs, both rejected by v4, whiletestPermissionsEnableOnlyAfterSwappassed.medium —
_assertMissingconflates "reverted" with "doesn't exist". A hardcoded-operator backdoor withreceive(),withdraw(),collectFees(),pause()passes all 8 tests. The spec's central constraint is untested.medium — the selector assertions are tautological. Reordering two fields of
SwapParamspasses 8/8 while the real PoolManager reverts every swap; the test derives the expected selector from the mutated interface.medium — no build config committed. Four plausible unpinned Foundry defaults produce four distinct creation-code hashes, so the CREATE2 address a mined salt yields isn't determined by the repo — which feeds directly back into the high finding. Out of my write scope to fix.
low —
address(0)pool manager deploys fine and yields a permanently dead, immutable hook.The top finding is marked high deliberately: the fix is one
requirein the constructor, and the failure it prevents is a live pool with liquidity in it that can never swap again.ran onclaude · claude-opus-5 · 30 turns · 11m 9s · 54 in · 47.2K out · 1.9M cachedsubmission7e1ba8cd57aecdb3eb117b5017e5fa53f45c96299e8f70154790d4ec744da9a4device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started from455a54015bef1e70d7d7297fa2bc47959361e7e1bundlenonemay writesrc/SwapCounterHook.soltest/SwapCounterHook.t.solhighConstructor never binds getHookPermissions() to the deployed address, so a mis-mined hook bricks the pool instead of failing at deploy timesrc/SwapCounterHook.sol:67
medium_assertMissing treats 'reverted' as 'does not exist', so the no-owner/no-funds guarantee is not actually testedtest/SwapCounterHook.t.sol:190
mediumSelector and struct-layout assertions are tautological: the tests validate the hook against the hook's own type declarations, so drift from v4-core is undetectabletest/SwapCounterHook.t.sol:79
mediumNo build configuration is committed, so the creation code -- and therefore the mined CREATE2 hook address -- is not determined by the repositorysrc/SwapCounterHook.sol:2
Constructor accepts address(0) as the pool manager, producing a permanently dead hooksrc/SwapCounterHook.sol:67
poolManager is immutable and unchecked. Deployed with address(0) the contract is well-formed but can never be called by anything, since msg.sender is never the zero address, so onlyPoolManager rejects every callback. There is no setter (correctly so, per the spec), so recovery means redeploying and re-mining the salt.
A single
if (address(_poolManager) == address(0)) revert(...)makes it a deploy-time failure. Ranked low because it needs a deployment-script mistake to trigger and moves no funds; it is the same class of miss as finding #1 -- the constructor validates nothing.
Manifestfailed
waits onImpl, Tests, Reviewmay writelaunch.json- Published
- Deployedto Sepolia