Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: high

Hook does not collect fees

Author Revealed upon completion

Description

  • In Uniswap v4, the LP fee (static or dynamic/overridden) is the fee paid by swappers and accrued to LPs. The fee value is represented in pips (hundredths of a bip) and can be overridden per‑swap by returning fee | LPFeeLibrary.OVERRIDE_FEE_FLAG from beforeSwap. If the hook wants to collect its own fee, it must actively transfer tokens (e.g., via poolManager.take(...)) typically in afterSwap, or otherwise implement a hook‑specific fee mechanism; simply overriding the LP fee does not credit the hook contract.

  • TokenLaunchHook only sets an LP fee override (on penalties) and never transfers tokens to itself or calls a collection method. There are no afterSwap handlers or poolManager.take(...) calls; as a result, the protocol (hook) earns nothing. All fees apply as LP fees and go to LPs.

// TokenLaunchHook._beforeSwap(...) returns an LP fee override for penalty swaps
uint24 feeOverride = 0;
if (applyPenalty) {
feeOverride = uint24((phasePenaltyBps * 100));
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
@> feeOverride | LPFeeLibrary.OVERRIDE_FEE_FLAG // sets LP fee; does not collect to the hook
);
// NOTE: There is no afterSwap implementation and no poolManager.take(...)
// i.e., the hook never transfers/collects any tokens for itself. // @>

Risk

Likelihood: High

  • Always: With the current design, even when penalties trigger, the hook only sets the LP fee. There is no path for funds to accrue to the hook.

Impact: High

  • Protocol earns nothing: The hook cannot monetize or fund operations/guard rails; all “penalty” fees flow to LPs (by v4 design for LP fee overrides). This contradicts expectations some teams may have when they see “penalty fees.”

  • Misleading telemetry: The presence of totalPenaltyFeesCollected (never updated) may further suggest fee collection exists when it does not. (Covered in separate “Unused state variables” report.)

Proof of Concept

  • Create HookDoesNotCollectFees.t.sol under test directory and copy code below.

  • Run command forge test --mt test_HookDoesNotCollectFees_EvenWhenPenaltyApplies -vvvv.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {TokenLaunchHook} from "../src/TokenLaunchHook.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {ERC1155TokenReceiver} from "solmate/src/tokens/ERC1155.sol";
contract HookDoesNotCollectFeesTest is Test, Deployers, ERC1155TokenReceiver {
TokenLaunchHook hook;
MockERC20 token;
Currency ethCurrency = Currency.wrap(address(0));
Currency tokenCurrency;
uint256 phase1Duration = 100;
uint256 phase2Duration = 200;
uint256 phase1LimitBps = 100;
uint256 phase2LimitBps = 300;
uint256 phase1Cooldown = 5;
uint256 phase2Cooldown = 3;
uint256 phase1PenaltyBps = 500; // 5% penalty (applied as LP fee override)
uint256 phase2PenaltyBps = 200; // 2%
address user = address(0xA11CE);
function setUp() public {
deployFreshManagerAndRouters();
token = new MockERC20("TOKEN", "TKN", 18);
tokenCurrency = Currency.wrap(address(token));
token.mint(address(this), 1_000 ether);
token.mint(user, 10 ether);
token.approve(address(modifyLiquidityRouter), type(uint256).max);
token.approve(address(swapRouter), type(uint256).max);
vm.prank(user);
token.approve(address(swapRouter), type(uint256).max);
// Mine hook with AFTER_INITIALIZE + BEFORE_SWAP flags (matches getHookPermissions)
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory args = abi.encode(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG);
(address mined, bytes32 salt) = HookMiner.find(address(this), flags, creationCode, args);
hook = new TokenLaunchHook{salt: salt}(
IPoolManager(manager),
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
require(address(hook) == mined, "Hook address mismatch");
// Initialize dynamic-fee pool
(key,) = initPool(ethCurrency, tokenCurrency, hook, LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1);
// Add liquidity (token0-only leg with slight buffer for settle, as used in your suite)
uint160 sqrtPriceAtTickUpper = TickMath.getSqrtPriceAtTick(60);
uint256 ethToAdd = 10 ether;
vm.deal(address(this), ethToAdd + 1 ether);
uint128 liqDelta = LiquidityAmounts.getLiquidityForAmount0(
SQRT_PRICE_1_1, sqrtPriceAtTickUpper, ethToAdd
);
modifyLiquidityRouter.modifyLiquidity{value: ethToAdd + 0.5 ether}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liqDelta)),
salt: bytes32(0)
}),
ZERO_BYTES
);
}
function test_HookDoesNotCollectFees_EvenWhenPenaltyApplies() public {
// Record hook balances before swaps
uint256 hookEthBefore = address(hook).balance;
uint256 hookTokenBefore = token.balanceOf(address(hook));
// Two back-to-back swaps in the same block (cooldown violation) to trigger penalty path
uint256 swapAmount = 0.001 ether;
vm.deal(user, 1 ether);
vm.startPrank(user);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(swapAmount), // exact-input
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory settings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
// First swap
swapRouter.swap{value: swapAmount}(key, params, settings, ZERO_BYTES);
// Second swap in same block — cooldown not respected, so penalty branch sets LP fee override
swapRouter.swap{value: swapAmount}(key, params, settings, ZERO_BYTES);
vm.stopPrank();
// Hook should NOT have collected any fees; balances remain unchanged
assertEq(address(hook).balance, hookEthBefore, "hook ETH balance should remain unchanged");
assertEq(token.balanceOf(address(hook)), hookTokenBefore, "hook token balance should remain unchanged");
}
}

Output:

[⠒] Compiling...
[⠢] Compiling 2 files with Solc 0.8.26
[⠆] Solc 0.8.26 finished in 79.24ms
No files changed, compilation skipped
Ran 1 test for test/HookDoesNotCollectFees.t.sol:HookDoesNotCollectFeesTest
[PASS] test_HookDoesNotCollectFees_EvenWhenPenaltyApplies() (gas: 275368)
Traces:
[275368] HookDoesNotCollectFeesTest::test_HookDoesNotCollectFees_EvenWhenPenaltyApplies()
├─ [2552] MockERC20::balanceOf(TokenLaunchHook: [0x7E4A8F76FEc89Ed2EbF193e4E5Cc1c1ab32E9080]) [staticcall]
│ └─ ← [Return] 0
├─ [0] VM::deal(0x00000000000000000000000000000000000A11cE, 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::startPrank(0x00000000000000000000000000000000000A11cE)
│ └─ ← [Return]
├─ [153624] PoolSwapTest::swap{value: 1000000000000000}(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x7E4A8F76FEc89Ed2EbF193e4E5Cc1c1ab32E9080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), TestSettings({ takeClaims: false, settleUsingBurn: false }), 0x)
│ ├─ [147820] PoolManager::unlock(0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000a11ce00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000007e4a8f76fec89ed2ebf193e4e5cc1c1ab32e90800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ ├─ [145561] PoolSwapTest::unlockCallback(0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000a11ce00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000007e4a8f76fec89ed2ebf193e4e5cc1c1ab32e90800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [2552] MockERC20::balanceOf(0x00000000000000000000000000000000000A11cE) [staticcall]
│ │ │ │ └─ ← [Return] 10000000000000000000 [1e19]
│ │ │ ├─ [2552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 10000000000000000000 [1e19]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [107736] PoolManager::swap(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x7E4A8F76FEc89Ed2EbF193e4E5Cc1c1ab32E9080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ ├─ [76095] TokenLaunchHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x7E4A8F76FEc89Ed2EbF193e4E5Cc1c1ab32E9080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ │ ├─ [2378] PoolManager::extsload(0xa88b2edf4fb72aaf361d8b4ec9d1adbe5291882f188b44e71deb27a0f6bb7388) [staticcall]
│ │ │ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000b4fb077cf724ed8bd3
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4194304 [4.194e6]
│ │ │ │ ├─ emit Swap(id: 0xc6eac5486de8d3ffee00cfbcd1e87bfd63f7fbfb9d06fcf16e570fe9fa93a4b5, sender: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount0: -1000000000000000 [-1e15], amount1: 999999700464594 [9.999e14], sqrtPriceX96: 79228138782624522581392935400 [7.922e28], liquidity: 3338502497096994491347 [3.338e21], tick: -1, fee: 0)
│ │ │ │ └─ ← [Return] -340282366920938463463374607431768211455000000299535406 [-3.402e53]
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0xfffffffffffffffffffffffffffffffffffffffffffffffffffc72815b398000
│ │ │ ├─ [552] MockERC20::balanceOf(0x00000000000000000000000000000000000A11cE) [staticcall]
│ │ │ │ └─ ← [Return] 10000000000000000000 [1e19]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 10000000000000000000 [1e19]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x00000000000000000000000000000000000000000000000000038d7e92ebf3d2
│ │ │ ├─ [1425] PoolManager::settle{value: 1000000000000000}()
│ │ │ │ └─ ← [Return] 1000000000000000 [1e15]
│ │ │ ├─ [10243] PoolManager::take(MockERC20: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], 0x00000000000000000000000000000000000A11cE, 999999700464594 [9.999e14])
│ │ │ │ ├─ [8511] MockERC20::transfer(0x00000000000000000000000000000000000A11cE, 999999700464594 [9.999e14])
│ │ │ │ │ ├─ emit Transfer(from: PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: 0x00000000000000000000000000000000000A11cE, amount: 999999700464594 [9.999e14])
│ │ │ │ │ └─ ← [Return] true
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e92ebf3d2
│ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e92ebf3d2
│ └─ ← [Return] -340282366920938463463374607431768211455000000299535406 [-3.402e53]
├─ [73934] PoolSwapTest::swap{value: 1000000000000000}(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x7E4A8F76FEc89Ed2EbF193e4E5Cc1c1ab32E9080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), TestSettings({ takeClaims: false, settleUsingBurn: false }), 0x)
│ ├─ [70630] PoolManager::unlock(0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000a11ce00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000007e4a8f76fec89ed2ebf193e4e5cc1c1ab32e90800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ ├─ [68371] PoolSwapTest::unlockCallback(0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000a11ce00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000007e4a8f76fec89ed2ebf193e4e5cc1c1ab32e90800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [552] MockERC20::balanceOf(0x00000000000000000000000000000000000A11cE) [staticcall]
│ │ │ │ └─ ← [Return] 10000999999700464594 [1e19]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 9999000000299535406 [9.999e18]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [40146] PoolManager::swap(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x7E4A8F76FEc89Ed2EbF193e4E5Cc1c1ab32E9080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ ├─ [2811] TokenLaunchHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x7E4A8F76FEc89Ed2EbF193e4E5Cc1c1ab32E9080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4244304 [4.244e6]
│ │ │ │ ├─ emit Swap(id: 0xc6eac5486de8d3ffee00cfbcd1e87bfd63f7fbfb9d06fcf16e570fe9fa93a4b5, sender: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount0: -1000000000000000 [-1e15], amount1: 949999160552349 [9.499e14], sqrtPriceX96: 79228116237579866750041480701 [7.922e28], liquidity: 3338502497096994491347 [3.338e21], tick: -1, fee: 50000 [5e4])
│ │ │ │ └─ ← [Return] -340282366920938463463374607431768211455050000839447651 [-3.402e53]
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0xfffffffffffffffffffffffffffffffffffffffffffffffffffc72815b398000
│ │ │ ├─ [552] MockERC20::balanceOf(0x00000000000000000000000000000000000A11cE) [staticcall]
│ │ │ │ └─ ← [Return] 10000999999700464594 [1e19]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 9999000000299535406 [9.999e18]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x00000000000000000000000000000000000000000000000000036004ea806b9d
│ │ │ ├─ [1425] PoolManager::settle{value: 1000000000000000}()
│ │ │ │ └─ ← [Return] 1000000000000000 [1e15]
│ │ │ ├─ [4643] PoolManager::take(MockERC20: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], 0x00000000000000000000000000000000000A11cE, 949999160552349 [9.499e14])
│ │ │ │ ├─ [2911] MockERC20::transfer(0x00000000000000000000000000000000000A11cE, 949999160552349 [9.499e14])
│ │ │ │ │ ├─ emit Transfer(from: PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: 0x00000000000000000000000000000000000A11cE, amount: 949999160552349 [9.499e14])
│ │ │ │ │ └─ ← [Return] true
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000036004ea806b9d
│ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000036004ea806b9d
│ └─ ← [Return] -340282366920938463463374607431768211455050000839447651 [-3.402e53]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [552] MockERC20::balanceOf(TokenLaunchHook: [0x7E4A8F76FEc89Ed2EbF193e4E5Cc1c1ab32E9080]) [staticcall]
│ └─ ← [Return] 0
└─ ← [Return]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 24.72ms (849.90µs CPU time)
Ran 1 test suite in 27.58ms (24.72ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Implement a hook fee in afterSwap
    Enable afterSwap + afterSwapReturnDelta permissions and collect a fee to the hook based on actual output/flow. A common pattern is to compute a fee and call poolManager.take(...) for the fee token, then return an appropriate delta. This approach is discussed in community guidance for implementing hook‑level fees in afterSwap.

  • Clarify documentation if no fee is intended

// Pseudocode sketch (requires enabling afterSwap & afterSwapReturnDelta permissions)
function afterSwap(
address sender,
PoolKey calldata key,
SwapParams calldata params,
BalanceDelta delta,
bytes calldata
) external override returns (bytes4, int128) {
// Determine output token and amount from delta
// Calculate hookFee = (outputAmount * hookFeePips) / 1_000_000
// poolManager.take(outputCurrency, address(this), hookFee); // collect to hook
// return (BaseHook.afterSwap.selector, int128(int256(hookFee)));
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!