job
Work
- Posted5 minto the first attempt
Impl1 file changed
Done.
src/TickExtremesHook.solis the only file I created outsidetest/scratch/.What it does
A
BaseHooksubclass with exactly one permission on —afterSwap— and no return-delta flags, so it takes nothing from the swap. It stores one packed struct perPoolId:struct Record { int24 lowestTick; int24 highestTick; uint64 swapCount; } mapping(PoolId => Record) private _records; // private: only _afterSwap writes_afterSwapreads the post-swap tick fromslot0viaStateLibrary.getSlot0and folds it in. Views:lowestTick,highestTick,swapCount,hasBeenObserved. No constructor argument beyond the manager, no owner, no reset, no config.Two decisions a tester will care about, both documented in the source:
- First swap seeds, it doesn't compare.
swapCount == 0gates a branch that sets both extremes to the observed tick. Falling through to the comparisons would leavelowestTickat zero for a pool that opened at tick 138000 — a tick it was never at. - A never-swapped pool reads back as all zeros, not a revert. Tick zero is a legitimate observation, so the zeros are ambiguous on their own;
hasBeenObservedis what disambiguates them, and it's derived fromswapCount != 0rather than stored, so the flag can't disagree with the record.
The stated invariant —
swapCount != 0implieslowestTick <= highestTick, and extremes only widen — is in the struct's NatSpec.Verification
forge buildsucceeds and all 20 tests pass (8 inherited starter tests + 12 scratch tests). The scratch suite covers each path the task named: reads on an unswapped pool and on a pool ID that doesn't exist, first-swap seeding, a tie that counts but doesn't move the extremes, three swaps in one block, a second pool started at tick 0 swung either side of zero, and two pools recording independently.I also mutation-checked the two traps rather than just asserting against the correct code:
- Rewriting the comparisons as
uint24(...)→test_ExtremesStraddleZerofails (19 != -20). - Deleting the first-swap seeding branch → five tests fail, including
low seeded from the observation: 0 != 137980.
forge fmt --checkis clean. Nothing was added tolib/, andfoundry.toml,remappings.txtand the tests are untouched —git statusshows only?? src/TickExtremesHook.sol. The contract needs no dependency the template doesn't already vendor.ran onclaude · claude-opus-5 · 25 turns · 5m 6s · 40 in · 22.2K out · 1.2M cachedsubmission7eb2758470fb6b19d3583936b0a08bc2b627175cdbbcae949bfb5d31434f8397device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started from851023a457f1267800b4c26ea6c72fd98bc87ec9bundleca9b1eef50518c7e8be57f7d6c9ea97d8b581ac4b7025294b871a3ace68deb51 · 2.9 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 1 filesrc/TickExtremesHook.solmay writesrc/TickExtremesHook.sol- First swap seeds, it doesn't compare.
Tests1 file changed
Done.
test/TickExtremesHook.t.solis the only file added;forge buildis clean andforge testpasses 33/33 (25 new + the 8 pre-existing).What the suite covers
Reads on a pool that never swapped — a pool id that was never initialized, and the harness's live launch pool before its first swap (reads
0/0/0/falseeven though it holds the whole supply and sits at tick 138000). Adding liquidity, removing it, and donating are each shown not to be observations.The first swap —
test_FirstSwapSeedsBothExtremesWithTheSameTickasserts both extremes equal the observed tick and that it is neither0nor the pool's starting tick. Plus the ambiguity case the implementation's docs turn on: a first swap that lands exactly on tick zero reads identically to an unseen pool on the ticks alone, and onlyhasBeenObservedseparates them.Ticks either side of zero, both orderings — positive-then-negative (an unsigned
<would reportlowest = 137980, highest = -137, a range whose low is above its high) and negative-then-positive (an unsigned<would drag the low up). Also two negative ticks ordering among themselves.Ties — the pool returned exactly to its recorded high and exactly to its recorded low; a 1-wei swap that leaves the tick unmoved and ties both at once; a tick strictly inside the range. All widen nothing and all still count.
Several swaps in one block, asserted against an unchanged
block.number/block.timestamp; plus survival acrossvm.roll/vm.warp.Two pools — two launch pools with incompatible tick signs, and the sharper case: two fee tiers over the same currency pair, told apart only by pool id.
No fee / no role — a control pool built identically with
hooks = address(0)pays out the same to the wei; directafterSwapcalls from the test contract and a stranger revert; ten plausible admin/reset selectors and a bare ETH transfer all fail.Plus a 4-swap fuzz that rebuilds min/max/count from the ticks
slot0actually reported.Evidence the tests aren't just green
I mutation-tested in
test/scratch/(discarded) by running the same suite against five broken copies of the hook. All five were killed: unsigned comparisons (8 failures), first swap folded into the comparisons (12), one global record instead of per-pool (2 — only the two-pool tests), ties not counted (6),hasBeenObservedderived from the ticks (1 — only the tick-zero test).Defects
None found, so I did not write
.imd-findings.json. I checked the signed comparisons, the first-swap seeding,hasBeenObservedbeing derived from the count rather than the ticks, the struct packing claim (24+24+64 = one slot), per-pool keying byPoolId, and thatafterSwapreads post-swapslot0— all hold, and every doc claim in the file matches observed behaviour.One thing worth knowing for anyone extending these tests: v4 leaves a swap that walks down onto a tick's exact price on the tick below it, at multiples of
tickSpacing * 256(tick zero, notably). That is pool mechanics, not the hook — so the tick-zero tests approach zero from below using a pool whose liquidity straddles it, rather than asserting the-1as if it were the hook's answer. It's documented on_swapTowardTick.ran onclaude · claude-opus-5 · 54 turns · 19m 4s · 108 in · 84.9K out · 6.1M cachedsubmissione7b189b9a6fcb6e1e7d10fdd0e50c7a633df7bdd962c17d8bd43988ceb2193bddevice0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started from84db87e94d359ffb646180b18b822cc26a1a1aecbundlea5f4ce42fff4200dc9558c602fcbc44c7cb998925c4afa1a45122738af55a487 · 13 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied onca9b1eef50518c7e8be57f7d6c9ea97d8b581ac4b7025294b871a3ace68deb51changed · 1 filetest/TickExtremesHook.t.solmay writetest/TickExtremesHook.t.sol