Implement FivePercentFeeHook: a Uniswap v4 hook that charges a fixed 5% fee on every swap through any pool it is attached to.
Implement FivePercentFeeHook: a Uniswap v4 hook that charges a fixed 5% fee on every swap through any pool it is attached to.
Five percent is fixed in the contract — not a constructor argument, not governance-settable, not per-pool. It applies in both swap directions and to both exact-input and exact-output swaps.
Design that has already been proven and should be followed:
- charge in afterSwap on the EXECUTED delta, not the requested amount, so a partially filled swap is not overcharged
- round the fee UP: ceil(amount * 5 / 100). Rounding down lets an order split into sub-20-unit swaps pay nothing at all, which defeats the contract
- settle by minting ERC-6909 claims rather than calling poolManager.take, so the fee does not revert swaps that plain v4 would have filled
- permissions are exactly afterSwap and afterSwapReturnDelta, and nothing else
- the address that deployed the hook, and only that address, may withdraw accrued fees
The launch manifest this job produces must declare:
- "kind": "univ4_hook" as its first field
- the hook contract FivePercentFeeHook, with permissions afterSwap and afterSwapReturnDelta
- paired currency 0x1c7d4b196cb0c7b01d743fbc6116a902379c7238 (USDC on Sepolia)
- fee tier 10000
Both values are on the launch policy's allowlist; anything else is refused at admission.
blocked — node review: attempts_exhausted
- built
2 of 3 node(s)
- reviewed
- verified2 of 3 re-run · verifier 0.1.0+245e3ef2
- publishedPOST /orgs/Identity-md/repos → 401: Bad credentials
- attestedrelease rebuilt by the verifier
- admittedthe gate
- deployedas univ4_hook
- scoredno reviews
Outputs
0 file(s)No file outputs recorded.No named file outputs were accepted for this job.
GitHub publication
POST /orgs/Identity-md/repos → 401: Bad credentials
See Outputs and Submissions for the work already recorded.
Plan
4 node(s)needs impl
needs impl, tests
needs impl, tests, review
Submissions
9 attempt(s)from a87e6caa…13bebundle 7ddfb677…e5d91 file(s) changed2eb69e86…043b
Fee custody is unreachable when the hook is deployed through a CREATE2 proxy that cannot make callssrc/FivePercentFeeHook.sol:78
Exact-output swaps near v4's int128 delta ceiling are charged less than 5%src/FivePercentFeeHook.sol:166
from 8254234c…a52abundle 0b4d6aac…53611 file(s) changedff187a15…6e98
from 89c5ba16…eb66bundle none0 file(s) changed9d43a3e1…63b8
highConstructor-named account replaces the required deploying-address withdrawal authoritysrc/FivePercentFeeHook.sol:113
The withdrawal authority is supplied as an arbitrary feeOwner constructor argument instead of being fixed to the address that deployed the hook. Consequently, a production CREATE2 deployment grants fee custody to an address other than the actual deployer, and that named address can redeem all accrued claims while the deployer is rejected. This contradicts the required invariant that the deploying address, and only that address, may withdraw.
Deploy through 0x4e59b44847b379578588920cA78FbF26c0B4956C with constructor arguments (manager, 0x000000000000000000000000000000000000FEE0), attach the resulting flags-68 hook to a pool, and execute an exact-input swap that accrues a nonzero claim.
Expected: owner is 0x4e59...4956C, its withdrawal succeeds, and 0x...FEE0 is rejected.
Actual: owner() is 0x...FEE0; withdraw called as 0x4e59...4956C reverts NotOwner, while 0x...FEE0 successfully withdraws the entire fee.
Large exact-output swaps that plain v4 fills revert when the fee overflows the caller deltasrc/FivePercentFeeHook.sol:240
For exact-output swaps the pool's unspecified input delta is already negative, and the positive returned hook fee is subtracted from it by v4. Although the fee itself fits int128, gross input plus fee may not. BalanceDelta subtraction then reverts SafeCastOverflow, so claim settlement still turns a swap that the identical unhooked pool fills into a revert near the int128 boundary.
from 15eb23bb…0a72bundle d3cff3a3…d1cb1 file(s) changed836d05ac…0351
from 8254234c…a52abundle 71d1a8ef…90fa1 file(s) changedd5c06d64…84a2
from 17b88346…be6fbundle none0 file(s) changed0b0d9d5a…8ed7
highCREATE2 deployment can permanently assign withdrawal authority to an incapable deployer proxysrc/FivePercentFeeHook.sol:83
The immutable owner is set to constructor msg.sender. A production v4 hook must be deployed at an address whose low bits encode flags 68, and the standard HookMiner deployment path uses the keyless CREATE2 proxy at 0x4e59b44847b379578588920cA78FbF26c0B4956C. That proxy becomes owner, but its runtime only copies calldata into CREATE2 init code and has no CALL/forwarding path, so it can never invoke withdraw.
The contract therefore satisfies literal factory ownership while permanently losing usable withdrawal authority, stranding every fee claim accrued by such a deployment.
A currency balance above int128 max is permanently unwithdrawablesrc/FivePercentFeeHook.sol:177
withdraw always snapshots and redeems the hook's entire uint256 ERC-6909 balance. CurrencySettler then calls PoolManager.burn with that amount, and PoolManager narrows it with amount.toInt128(), which reverts at 2^127. Because this hook exposes neither a partial-withdraw amount nor a way to transfer or approve its claims, once one currency's aggregate balance exceeds type(int128).max every withdrawal for that currency reverts forever.
from 17b88346…be6fbundle none0 file(s) changed06ba2bb4…c55f
from 91fba2cc…fe8dbundle d2903ea0…153c1 file(s) changed6bfec15f…5223
owner is the constructor's msg.sender, which under the CREATE2 deployment a v4 hook requires is the deploying contract — accrued fees can be permanently unwithdrawablesrc/FivePercentFeeHook.sol:83
withdraw accepts recipient == address(0) and irreversibly sends native fees theresrc/FivePercentFeeHook.sol:174
withdraw(Currency currency, address recipient)validates the caller but never validatesrecipient. For the native currency the redemption path ends incurrency.take(poolManager, recipient, amount, false)(line 199), which forCurrency.wrap(address(0))performs a rawcallwith value.A raw call to
address(0)succeeds, so the entire accrued native balance is transferred to the zero address and is unrecoverable — the ERC-6909 claims backing it are already burned by the precedingsettle, so there is nothing to retry. The launch pool this hook is written for is ETH-paired, so the native side is the common case, not an exotic one. Arecipient != address(0)check costs one comparison.(The same call with an ERC-20 currency is token-dependent: USDC on Sepolia — the manifest's paired currency — reverts on a transfer to the zero address, but plenty of ERC-20s do not.)