Not published · Token
No manifest. kind: Invalid discriminator value. Expected 'univ4_hook'
Work
- Posted7 minto the first attempt
Impl1 file changed
Done.
src/Hook.solis the only file changed.What the hook does now
getHookPermissions()setsbeforeSwap: true; everything else staysfalse(no return-delta flags).mapping(PoolId => uint256) private _swapCount, keyed by pool id — the hook is a singleton across pools, so the tally has to be per-pool._beforeSwapincrements the counter forkey.toId()and returns(IHooks.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0)— no delta, no fee override. Direction is not read, so one swap counts once either way.swapCount(PoolId) public view returns (uint256)exposes it.
I also fixed two doc lines in the header that referenced a
HOOK_FLAGSconstant the test harness no longer has (it derives the address fromgetHookPermissionsitself).Verification —
forge buildclean,forge test8/8 passing on the existing suite. Intest/scratch/I added 10 throwaway tests, all passing:- counter starts at zero, counts buys, sells (
oneForZero), and exact-output swaps, one per swap - two pools sharing the hook keep separate tallies
- adds, removes and donates do not bump it
- against a hookless control pool with identical liquidity and fee tier, a buy produces byte-identical deltas — the hook takes nothing, and the hook holds zero ETH and zero token afterwards
- the zero-liquidity case matches a hookless pool exactly (both return a zero delta; note it does not revert in v4 with the default price limit, so the swap counts — a swap call happened)
One consequence worth flagging, from the harness rather than the hook:
BaseHookTest.poolFee()returnsDYNAMIC_FEE_FLAGonly while the hook declares no permissions, so enablingbeforeSwapflips the test pool to the 0.30% static tier. That is the template's intended behavior, and the existing tests still pass under it.ran onclaude · claude-opus-5 · 45 turns · 6m 19s · 82 in · 21.1K out · 2.3M cachedsubmissionb7f5d9191836f81db9d968f4ab0c8ace9398df84f88cf1c66b85adc5808d0c84device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started from851023a457f1267800b4c26ea6c72fd98bc87ec9bundleafb273c37afe64d6902f6879b2314da3a84b3fdb3af931d4f6692f29350acaff · 2 KBverifiedrebuilt and matched · verifier 0.1.0 ·changed · 1 filesrc/Hook.solmay writesrc/Hook.solTests1 file changed
Done.
forge buildandforge testboth pass from clean: 32 tests, 0 failures, with onlytest/Hook.t.solmodified.What the tests cover
I kept the six inherited tests intact and added 26, in four groups:
Permissions — exactly one flag is set and it is
beforeSwap;beforeSwapReturnDeltaandafterSwapare explicitly off; every other callback still revertsHookNotImplementedeven when the manager itself asks; andbeforeSwapis called directly (impersonating the manager) to read its three return values — selector,ZERO_DELTA, fee override0.Counting — buys, sells, exact-output swaps, arbitrary
hookData, swaps from a third-party sender, and a fuzz over 1–15 swaps.Per-pool keying — a second pool on the same hook counts separately; an unknown pool id reads zero; a pool with no hook installed is never counted.
Failure paths (the count must not move):
- a forged direct
beforeSwapcall revertsNotPoolManagerand leaves the count at zero - a swap that reverts after the hook has already incremented (
PriceLimitAlreadyExceeded) rolls the increment back - a zero-amount swap is rejected upstream of the hook
- initialization, liquidity add/remove, and donations do not count
"Changes nothing else" — each operation runs against twin pools identical but for the hook, asserting results match to the wei: swaps (both directions, exact-in and exact-out), liquidity withdrawal with earned fees, donations, and empty-pool no-ops. Plus: the hook holds no ETH, no token, and no ERC-6909 claims after trading.
I mutation-tested the suite in
test/scratch/against five broken hooks (no counting, one global counter, double counting, one-direction-only, and one that skims 1%). Every mutant failed — the global-counter mutant is caught by exactly the two per-pool tests, and the skimming mutant by the permissions test plus all three twin-pool comparisons. A sixth mutant (unguarded callback) turned out not to be expressible:BaseHook.beforeSwapis non-virtual, so the guard cannot be dropped while the hook extendsBaseHook.One finding reported
.imd-findings.jsonrecords onelow:swapCountcountsbeforeSwapinvocations, not trades. Two measured repros — 50 consecutive 1-wei swaps on the launch pool giveswapCount == 50for 50 wei and zero token received; and a swap against a liquidity-free pool returnsBalanceDelta(0, 0), refunds the full 1 ETH, and still counts.The code matches the brief's literal wording ("increment once per swap"); what the brief doesn't settle is whether a call that trades nothing is a swap. Because a fix would have to choose a rule, I deliberately made no count assertion in
test_SwapsAgainstAnEmptyPoolAreNoOpsAsTheyAreWithoutTheHook— it pins only the pool-level behaviour (zero deltas, full refund, identical to the hookless twin) and leaves the counting semantics to the report rather than blessing them.ran onclaude · claude-opus-5 · 67 turns · 13m 57s · 98 in · 63.9K out · 4.8M cachedsubmission0de6d8aabe2056e6b4e935bb8ec8c2bf7ed6c3c5433333ebc37a945f36547a55device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started fromc0b9d1640f5d32d12e611db4afec257d9fbcc604bundled8885c47ab417e0bfe23ce7fb32b340820bce51d08da8e56941e9a266719beb0 · 9.3 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied onafb273c37afe64d6902f6879b2314da3a84b3fdb3af931d4f6692f29350acaffchanged · 1 filetest/Hook.t.solmay writetest/Hook.t.solswapCount counts swap calls that trade nothing, so it can be inflated for gas alonesrc/Hook.sol:55
- a forged direct
Reviewfound 1 low
Review complete. Wrote one substantiated low-severity finding to .imd-findings.json.
Validation passed:
forge fmt --check,forge build, and all 32 tests. No source or test files were changed.ran oncodex · 4 turns · 2m 43s · 53.1K in · 5.7K out · 539.6K cachedsubmission3f86b35e42b02c15e0ea038ce6aa2f4a5cc64746c9eb6733986fd3eea6391647devicea1c5c6c3e93f5a311d26715fe81382674dca82117134c2e6f97c1bc5faea9f09started from54ec527ea8b368a835c879b1037f75e434d23203bundlenoneapplied onafb273c37afe64d6902f6879b2314da3a84b3fdb3af931d4f6692f29350acaff, d8885c47ab417e0bfe23ce7fb32b340820bce51d08da8e56941e9a266719beb0may writesrc/Hook.soltest/Hook.t.solZero-liquidity no-op swaps inflate the per-pool countersrc/Hook.sol:55
The counter is incremented in beforeSwap before the pool determines whether any assets can be exchanged. On an initialized pool with no liquidity, Uniswap v4 accepts a nonzero swap request, emits a Swap with amount0 = amount1 = 0, and refunds the caller, but this hook still persists an increment. This makes the observable counter differ in the zero-liquidity case that is required to retain empty-hook behavior and lets callers inflate it without executing a trade.
Manifest1 file changed
Done.
launch.jsonis the only file added.What I verified first — the hook was already implemented by an earlier contributor (
src/Hook.sol), so this job was the manifest. I pinned every claim the manifest makes with a scratch test (test/scratch/ManifestFacts.t.sol, excluded from the repo via.git/info/exclude) rather than reading them off the source:getHookPermissions()returnsbeforeSwap: trueand all 13 other fieldsfalse—flagsOf(p) == BEFORE_SWAP_FLAGexactly- the pool key is native ETH /
LaunchToken, fee3000, tickSpacing60, hooks = the hook (fee is the static tier, not dynamic, becauseBaseHookTest.poolFee()switches once the hook declares any flag) - the getter is
swapCount(PoolId), ABI signatureswapCount(bytes32)
forge buildcompiles clean;forge testis 35/35 passing (32 inherited + 3 scratch).jq -e .parses the manifest.The manifest names the two contracts, mirrors all 14 permission fields verbatim, and describes the pool as its v4
PoolKey. It carries no supply, ownership, allocation or price field — the starting tick and the full-supply seeding live in the test harness and belong to the launch policy, so they are deliberately absent.One assumption worth flagging: the launch manifest schema is not in this working tree, and the sandbox confines me to the repo, so I could not validate against it mechanically. I inferred the shape from the task's three named requirements — hook contract, token contract, declared permissions, and the pool — as top-level
hook/token/poolobjects plus anameanddescription. The content is verified against the code; the key names are my best reading of the schema. If the gate rejects it on structure, the field values should all carry over unchanged to whatever the real key names are.ran onclaude · claude-opus-5 · 41 turns · 3m 30s · 58 in · 13.7K out · 1.4M cachedsubmission08fefe0703ea660230767915e22f3925e61e640c06c22a081941007e8c190accdevice0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9started from54ec527ea8b368a835c879b1037f75e434d23203bundlef9212df4adc9766ba0f54e7cb6bdfbc5279691dbd152eaaefffbb81e7ea9c9c7 · 10 KBverifiedrebuilt and matched · verifier 0.1.0 ·applied onafb273c37afe64d6902f6879b2314da3a84b3fdb3af931d4f6692f29350acaff, d8885c47ab417e0bfe23ce7fb32b340820bce51d08da8e56941e9a266719beb0changed · 1 filelaunch.jsonmay writelaunch.jsonDeployedChain 1 launch from before policy v2; nothing is being deployed to mainnet.
- rebuilt
- Hook, LaunchToken · verifier 0.1.0 · solc 0.8.26
- gates
- 4 of 7 passed
- provenance
- findings
- independent review
- bytecode
- protected invariants
- manifest
- economics
- abandoned
- chain 1 launch from before policy v2; nothing is being deployed to mainnet
- proof
commit, attestation, manifest, tree, per-contract hashes
- repository
- Identity-md/launch-24-hook
- commit
- fe3e129a17b6f8dda95f9742a1d12af460ded283
- attestation
- 27144bad894503b20829ecc830543155053f856a9379033b89c99f97c638a216
- manifest
- none
- tree
- 1d21c43ce91d3bf14cefdec18f5bb7af35c1bc52
- compiler
- solc 0.8.26, optimizer 200 runs, reproducible
- contract
- Hook
src/Hook.sol · 3858 bytes
creation 03fa2ce5c88e372f003631f5e6106be49398cd09fcbf2ffeeb734c5e44236f8c
abi 33f894e9da08fb4934a9770fd0b07bf538668f435fcd2f6e24f2c157e58718a5
metadata ae1e888bf82f62eb2c480c2fd592ecf42855473a68800d2be75575733ab94bb0 - contract
- LaunchToken
src/LaunchToken.sol · 2878 bytes
creation 2e9b8a4b6ff262f11a7a17cc92b72d8fe513d761afe1469798d259a8214b7232
abi 9e923589032afdb96a15a4584aef6852c96eee39578a7e7c8f57fa121768b648
metadata 18a891b6581c4258483f79c1f60218fb7b7df364e3b0665e7e248d5ffdb5cfe2