RebateFi Hook

First Flight #53
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Protocol Fails to Collect Revenue (Business Logic)

Description:
The protocol documentation states that the hook applies fees to "generate protocol revenue".
However, the hook only sets the LPFeeLibrary.OVERRIDE_FEE_FLAG and returns ZERO_DELTA.
This means the fees are collected by the pool and distributed to Liquidity Providers (LPs), not the protocol/hook owner.
The withdrawTokens function suggests the hook expects to accumulate tokens, but there is no mechanism (e.g., afterSwap taking a fee, or donate) to move funds to the hook.

Impact:
The protocol fails to capture the intended revenue stream. The "premium fees" for selling ReFi benefit LPs instead of the protocol treasury.

Proof of Concept:

pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {PoolManager} from "v4-core/PoolManager.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
contract M3_NoRevenueTest is Test, Deployers {
using CurrencyLibrary for Currency;
ReFiSwapRebateHook public rebateHook;
MockERC20 reFiToken;
MockERC20 token;
Currency ethCurrency = Currency.wrap(address(0));
Currency reFiCurrency;
function setUp() public {
deployFreshManagerAndRouters();
reFiToken = new MockERC20("ReFi Token", "ReFi", 18);
reFiCurrency = Currency.wrap(address(reFiToken));
token = new MockERC20("TOKEN", "TKN", 18);
reFiToken.mint(address(this), 1000 ether);
token.mint(address(this), 1000 ether);
bytes memory creationCode = type(ReFiSwapRebateHook).creationCode;
bytes memory constructorArgs = abi.encode(manager, address(reFiToken));
uint160 flags = uint160(
Hooks.BEFORE_INITIALIZE_FLAG |
Hooks.AFTER_INITIALIZE_FLAG |
Hooks.BEFORE_SWAP_FLAG
);
(address hookAddress, bytes32 salt) = HookMiner.find(
address(this),
flags,
creationCode,
constructorArgs
);
rebateHook = new ReFiSwapRebateHook{salt: salt}(manager, address(reFiToken));
reFiToken.approve(address(rebateHook), type(uint256).max);
reFiToken.approve(address(swapRouter), type(uint256).max);
reFiToken.approve(address(modifyLiquidityRouter), type(uint256).max);
(key, ) = initPool(
ethCurrency,
reFiCurrency,
rebateHook,
LPFeeLibrary.DYNAMIC_FEE_FLAG,
SQRT_PRICE_1_1
);
modifyLiquidityRouter.modifyLiquidity{value: 10 ether}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: 10 ether,
salt: bytes32(0)
}),
ZERO_BYTES
);
}
function test_M3_HookCollectsNoRevenue() public {
// Perform a swap that generates fees (Sell ReFi -> 0.3% fee)
// Due to S-1, we use Buy ReFi to trigger Sell logic.
uint256 amountToSwap = 1 ether;
vm.deal(address(this), 10 ether);
uint256 hookBalanceBefore = reFiToken.balanceOf(address(rebateHook));
uint256 hookEthBalanceBefore = address(rebateHook).balance;
swapRouter.swap{value: amountToSwap}(
key,
SwapParams({
zeroForOne: true,
amountSpecified: -int256(amountToSwap),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
ZERO_BYTES
);
uint256 hookBalanceAfter = reFiToken.balanceOf(address(rebateHook));
uint256 hookEthBalanceAfter = address(rebateHook).balance;
// Verify hook collected NOTHING
assertEq(hookBalanceAfter, hookBalanceBefore, "Hook should not receive ReFi");
assertEq(hookEthBalanceAfter, hookEthBalanceBefore, "Hook should not receive ETH");
// This confirms that despite "generating protocol revenue", the hook gets nothing.
}
}

Recommended Mitigation:
Implement a fee collection mechanism. For example, use Hooks.AFTER_SWAP_FLAG to take a portion of the output tokens, or use ProtocolFeeController if applicable.

Updates

Lead Judging Commences

chaossr Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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

Give us feedback!