Implement the hook so it charges a fixed swap fee in basis points, taken from the input currency on every swap in both directions, and accrued to the hook rather than to the pool. The fee rate is set once at construction and cannot be changed afterwards. Nothing else about the pool changes: liquidity, donations and the zero-liquidity case must all behave exactly as they do with no fee.

Reuse the vendored BaseHook and the existing launch-pool harness rather than adding dependencies.

Work

  1. Posted19 minto the first attempt
  2. Impl1 file changed

    Done. src/Hook.sol is the only file changed.

    What it does

    The hook takes a fixed basis-point cut of whatever the swapper pays in, in both directions (currency0 when zero-for-one, currency1 when one-for-zero), and keeps it as ERC-6909 claims on itself — the underlying stays in the PoolManager, so nothing is moved out of the pool's own balances.

    Which callback charges it depends on the swap type, because v4 only lets a hook move the specified side from beforeSwap and the unspecified side from afterSwap:

    • Exact input (src/Hook.sol:116) — input is the specified currency, so the fee comes off the offered amount in beforeSwap; the swapper's debt is unchanged and the pool swaps the remainder.
    • Exact output (src/Hook.sol:139) — input is the unspecified currency and its size is only known afterwards, so afterSwap charges the fee on what the pool actually took, on top of the swap cost.

    Permissions are the four swap flags only, so initialization, liquidity and donations never reach the hook.

    Verification

    forge build is clean (no warnings from src/). forge test: 20 passed, 0 failed — the 8 existing tests in test/Hook.t.sol plus 12 I wrote in test/scratch/, covering fee-from-input in both directions for exact input and exact output, hook accrual vs. pool balances, add/remove liquidity and donate taking no fee, empty-pool swaps, and a control pool with no hook that returns an identical output for the post-fee amount — i.e. the curve itself is untouched.

    Two things worth your call

    The rate is a constant assigned to an immutable, not a constructor argument. BaseHookTest.deployHook passes abi.encode(manager), and the README says to keep constructor(IPoolManager); a second parameter would make every existing test revert at setUp, and the override that would fix it lives in test/, outside my allowed paths. So feeBips is set once at construction (100 bips = 1%) and is immutable after — but changing the rate means editing the constant. If you want it injected, BaseHookTest.hookConstructorArgs() needs the matching override.

    Exact-input swaps against a pool with no liquidity in that direction still pay the fee (exact output charges nothing there, since the pool took no input). This is structural, not an oversight: the specified side of a swap is fixed before the pool runs, so the only figure available to charge is the amount offered, and a hook has no way to credit the specified currency back afterwards. Charging the input on exact input requires beforeSwap; the alternative — charging in afterSwap — can only reach the output currency. It's documented in the contract header.

    I also left out any way to redeem the accrued claims: that needs an owner and an access-control design the task didn't ask for, and the vendored BaseHookFee treats accrual and payout as separate concerns too.

    ran onclaude · claude-opus-5 · 55 turns · 19m 10s · 92 in · 80.7K out · 5.5M cached
    submissionec41bbce4dd0587c048a4263662c5915bdf1f946d81535db9a024f78bbcdd0f6
    device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9
    started from6b238a0048e85ade820dc964f3e6e731dcc9abd6
    bundle8218b443e698686f042bafadee0b68b6b85a1bab13764366da2e94ea4e6cf679 · 3.4 KB
    verifiedrebuilt and matched · verifier 0.1.0 ·
    changed · 1 file
    src/Hook.sol
    may write
    src/Hook.sol
  3. Testsbudget exhausted3 attempts
    no seatClauderuntime errorAPI Error: Can't reach the API server — check your internet or DNS (ENOTFOUND)

    API Error: Can't reach the API server — check your internet or DNS (ENOTFOUND)

    ran onclaude · <synthetic> · 17 turns · 7m 13s · 34 in · 9.2K out · 784.3K cached
    submission06574f28ea8d3c76cd80d13d98236063c5c8f645ad75f409a759a5a609d05962
    device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9
    started from4f5ee6b9f716107fd8bc622adf7391b81c3733f6
    bundlenone
    applied on8218b443e698686f042bafadee0b68b6b85a1bab13764366da2e94ea4e6cf679
    no seatClaudebudget exhaustedwall-clock budget exhausted

    wall-clock budget exhausted

    ran onclaude · claude-opus-5 · 1h 7m
    submission2e9ffc6668415516c62e9268b9a0a9a351ddf54ae8c16e2f37c98ffc8ed4c696
    device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9
    started from4f5ee6b9f716107fd8bc622adf7391b81c3733f6
    bundlenone
    applied on8218b443e698686f042bafadee0b68b6b85a1bab13764366da2e94ea4e6cf679
    no seatClaudebudget exhaustedwall-clock budget exhausted

    wall-clock budget exhausted

    ran onclaude · claude-opus-5 · 56m 7s
    submissionb7dd304a29340745eee1d2d458abaec3c9c9a2a220e2f9ef9db21aeeafd6c74d
    device0edd2bbb66d2d014fbbda834d6ccbc278847c31414f601db126e7a1269baddd9
    started from4f5ee6b9f716107fd8bc622adf7391b81c3733f6
    bundlenone
    applied on8218b443e698686f042bafadee0b68b6b85a1bab13764366da2e94ea4e6cf679
    may write
    test/Hook.t.sol
  4. Reviewfailed
    waits onImpl, Tests
    may write
    src/Hook.soltest/Hook.t.sol
  5. Manifestfailed
    waits onImpl, Tests, Review
    may write
    launch.json
  6. Published
  7. Deployedto Sepolia