Root + Impact
Description
The hook is designed to collect fees from swaps and hold them in the contract for later withdrawal by the owner. The _beforeSwap() function calculates feeAmount when users sell ReFi tokens, and the owner can call withdrawTokens() to extract accumulated fees.
However, the hook returns BeforeSwapDeltaLibrary.ZERO_DELTA, which means the hook never actually claims any tokens from swaps. The fee value returned is an LP fee override that directs fees to liquidity providers, not to the hook contract. The calculated feeAmount is never used to actually take tokens.
function _beforeSwap(
address sender,
PoolKey calldata key,
SwapParams calldata params,
bytes calldata
) internal override returns (bytes4, BeforeSwapDelta, uint24) {
bool isReFiBuy = _isReFiBuy(key, params.zeroForOne);
uint256 swapAmount = params.amountSpecified < 0
? uint256(-params.amountSpecified)
: uint256(params.amountSpecified);
uint24 fee;
if (isReFiBuy) {
fee = buyFee;
emit ReFiBought(sender, swapAmount);
} else {
fee = sellFee;
@> uint256 feeAmount = (swapAmount * sellFee) / 100000;
emit ReFiSold(sender, swapAmount, feeAmount);
}
return (
BaseHook.beforeSwap.selector,
@> BeforeSwapDeltaLibrary.ZERO_DELTA,
fee | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}
Risk
Likelihood:
Every swap through the hook will result in zero fees collected by the hook contract
The ZERO_DELTA return value explicitly tells the PoolManager that the hook is not claiming any tokens
The LP fee override redirects all fees to liquidity providers instead of the protocol
Impact:
The hook never accumulates any tokens despite appearing to charge fees
The withdrawTokens() function will always fail with insufficient balance
Protocol generates zero revenue despite users paying fees
All fees go to liquidity providers instead of the protocol
The entire business model and value capture mechanism is broken
Owner cannot extract any value from the protocol
Events are emitted showing fee collection that never actually happened
Proof of Concept
a simple POC in solitiy to show how this can be implemented in the real world
function test_HookDoesntCollectFees() public {
uint256 swapAmount = 1000e18;
uint256 hookBalanceBefore = IERC20(ReFi).balanceOf(address(hook));
SwapParams memory params = SwapParams({
zeroForOne: false,
amountSpecified: -int256(swapAmount),
sqrtPriceLimitX96: MAX_PRICE_LIMIT
});
vm.prank(swapper);
swap(key, params, ZERO_BYTES);
uint256 hookBalanceAfter = IERC20(ReFi).balanceOf(address(hook));
assertEq(hookBalanceAfter, hookBalanceBefore);
uint256 expectedFee = (swapAmount * 3000) / 100000;
assertEq(hookBalanceAfter - hookBalanceBefore, 0);
vm.prank(owner);
vm.expectRevert();
hook.withdrawTokens(ReFi, owner, 1e18);
}
Recommended Mitigation
a rather simple mitigation to fix this vulnerability
function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return Hooks.Permissions({
beforeInitialize: true,
afterInitialize: true,
beforeAddLiquidity: false,
afterAddLiquidity: false,
beforeRemoveLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true,
afterSwap: false,
beforeDonate: false,
afterDonate: false,
- beforeSwapReturnDelta: false,
+ beforeSwapReturnDelta: true,
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
function _beforeSwap(
address sender,
PoolKey calldata key,
SwapParams calldata params,
bytes calldata
) internal override returns (bytes4, BeforeSwapDelta, uint24) {
bool isReFiBuy = _isReFiBuy(key, params.zeroForOne);
uint256 swapAmount = params.amountSpecified < 0
? uint256(-params.amountSpecified)
: uint256(params.amountSpecified);
+ BeforeSwapDelta hookDelta = BeforeSwapDeltaLibrary.ZERO_DELTA;
uint24 fee;
if (isReFiBuy) {
fee = buyFee;
emit ReFiBought(sender, swapAmount);
} else {
fee = sellFee;
- uint256 feeAmount = (swapAmount * sellFee) / 100000;
+ uint256 feeAmount = (swapAmount * sellFee) / 1000000;
+
+ // Determine which currency is ReFi
+ bool isReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
+ Currency feeCurrency = isReFiCurrency0 ? key.currency0 : key.currency1;
+
+ // Take fee from the PoolManager
+ poolManager.take(feeCurrency, address(this), feeAmount);
+
+ // Create delta indicating hook claimed tokens
+ int128 amount0Delta = isReFiCurrency0 ? int128(int256(feeAmount)) : int128(0);
+ int128 amount1Delta = isReFiCurrency0 ? int128(0) : int128(int256(feeAmount));
+ hookDelta = toBeforeSwapDelta(amount0Delta, amount1Delta);
+
emit ReFiSold(sender, swapAmount, feeAmount);
}
return (
BaseHook.beforeSwap.selector,
- BeforeSwapDeltaLibrary.ZERO_DELTA,
+ hookDelta,
- fee | LPFeeLibrary.OVERRIDE_FEE_FLAG
+ 0
);
}