RebateFi Hook

First Flight #53
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: high
Valid

[H-3] Hook returns `ZERO_DELTA` and doesn't collect any fees, making the entire revenue model non-functional

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; // @audit calculated but never used
emit ReFiSold(sender, swapAmount, feeAmount);
}
return (
BaseHook.beforeSwap.selector,
@> BeforeSwapDeltaLibrary.ZERO_DELTA, // @audit hook claims nothing!
fee | LPFeeLibrary.OVERRIDE_FEE_FLAG // @audit this goes to LPs, not the hook
);
}

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;
// Record hook's ReFi balance before swap
uint256 hookBalanceBefore = IERC20(ReFi).balanceOf(address(hook));
// User sells ReFi (should pay 0.3% fee to hook)
SwapParams memory params = SwapParams({
zeroForOne: false, // Selling ReFi (assuming ReFi is currency1)
amountSpecified: -int256(swapAmount),
sqrtPriceLimitX96: MAX_PRICE_LIMIT
});
vm.prank(swapper);
swap(key, params, ZERO_BYTES);
// Check hook balance after swap
uint256 hookBalanceAfter = IERC20(ReFi).balanceOf(address(hook));
// Hook balance unchanged - no fees collected!
assertEq(hookBalanceAfter, hookBalanceBefore); // Passes, proving hook collects nothing
// Expected fee that should have been collected
uint256 expectedFee = (swapAmount * 3000) / 100000; // 3% due to wrong calculation
// Verify hook didn't receive expected fees
assertEq(hookBalanceAfter - hookBalanceBefore, 0); // Should be expectedFee, but is 0
// Owner tries to withdraw fees
vm.prank(owner);
vm.expectRevert(); // Will revert due to insufficient balance
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
);
}
Updates

Lead Judging Commences

chaossr Lead Judge 12 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Ovveride fee

still not sure about this

Support

FAQs

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

Give us feedback!