RebateFi Hook

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

Broken Protocol Revenue Model: Fees Distributed to LPs Instead of Protocol

Root + Impact

Description

  • Normal Behavior: The protocol intends to collect fees from "Sell" transactions to "generate protocol revenue" and allow the owner to withdraw these accumulated tokens.

  • Specific Issue: The _beforeSwap hook returns the LPFeeLibrary.OVERRIDE_FEE_FLAG. In Uniswap V4, this flag dictates that the specified fee is collected by the Pool Manager and distributed to the pool's liquidity providers (added to feeGrowthGlobal). The Hook contract itself does not receive these tokens. There is no logic (e.g., HOOK_SWAP_FEE flag or beforeSwapReturnDelta transfers) to capture these fees for the protocol.

function _beforeSwap(...) internal override returns (bytes4, BeforeSwapDelta, uint24) {
// ... logic to calculate fee ...
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA, // No tokens taken/sent by hook
@> fee | LPFeeLibrary.OVERRIDE_FEE_FLAG // Fee overrides LP fee, goes to LPs
);
}

Risk

Likelihood:

  • 100%. The code implementation fundamentally routes fees to LPs, not the Hook/Protocol.

Impact:

  • Total Loss of Revenue: The protocol earns 0 fees.

  • Broken Admin Functionality: The withdrawTokens function is rendered useless as the contract never accumulates the fees it is supposed to withdraw.

Proof of Concept

I have developed a deterministic test case using Foundry that confirms the hook balance remains zero even after a high-fee swap is executed.

Step-by-Step Exploit:

  1. Deploy ReFi and Hook.

  2. Initialize Pool and add Liquidity.

  3. Set Sell Fee to 10%.

  4. Execute Swap (Sell ReFi).

  5. Check Hook Balance.

  6. Result: Hook Balance is 0.

POC Code:
(Paste this into test/RebateFiDeepExploit.t.sol)

function test_Exploit_NoProtocolRevenue() public {
// ... Setup Pool & Fee ...
uint24 sellFee = 100000; // 10%
hook.ChangeFee(true, 0, true, sellFee);
// ... Perform Swap triggering fee ...
uint256 hookBalanceBefore = reFiToken.balanceOf(address(hook));
swapRouter.swap(key, params, settings, ZERO_BYTES);
uint256 hookBalanceAfter = reFiToken.balanceOf(address(hook));
// Assertion: Balance never changed
assertEq(hookBalanceAfter, 0, "Hook collected no revenue");
}

Recommended Mitigation

To collect protocol revenue, the Hook should stop using the OVERRIDE_FEE_FLAG (which sends fees to LPs) and instead implement manual fee collection logic. This requires:

  1. Enabling the ACCESS_LOCK_FLAG (or enabling access to take tokens) if not already available, or using the delta return to modify the swap balance.

  2. Updating getHookPermissions to enable beforeSwapReturnDelta.

  3. Returning a BeforeSwapDelta that reflects the fee taken by the hook, and transferring the tokens from the PoolManager to the Hook.

Implementation Example:

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, // Enable delta return
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
function _beforeSwap(
address sender,
PoolKey calldata key,
SwapParams calldata params,
bytes calldata
) internal override returns (bytes4, BeforeSwapDelta, uint24) {
// ... fee calculation logic ...
uint256 feeAmount = 0;
if (!isReFiBuy) {
// Calculate fee amount (ensure scaling matches V4 pips)
feeAmount = (swapAmount * sellFee) / 1000000;
// Take the fee: The hook effectively "takes" tokens from the user by modifying the delta
// The user pays 'swapAmount', but the pool only receives 'swapAmount - feeAmount'
// The hook must take 'feeAmount' from the PoolManager (which takes it from the user)
// NOTE: Simplified logic for mitigation demonstration.
// Actual implementation needs to handle currency taking/settling depending on exact V4 version semantics.
// But strictly regarding the flag:
}
return (
BaseHook.beforeSwap.selector,
- BeforeSwapDeltaLibrary.ZERO_DELTA,
- fee | LPFeeLibrary.OVERRIDE_FEE_FLAG
+ toBeforeSwapDelta(int128(int256(feeAmount)), 0), // Example: Hook modifies delta to take fee
+ 0 // Remove Override Flag
);
}

Alternatively, if the intention was only to direct fees to LPs, update the documentation and remove the withdrawTokens function to avoid misleading users about "protocol revenue".


REPORT 4: MEDIUM SEVERITY - FEE SCALING MISMATCH

1. Title

Fee Scaling Mismatch: Event Logs Report 10x Higher Fees Than Actual Execution

2. Severity level

Medium

a. Impact

Medium - Creates a critical discrepancy between the "observed" fee (via events/dashboard) and the "actual" fee charged. If admins set fees based on the event logs (e.g., aiming for "3%"), they will inadvertently set the actual protocol fee to "0.3%", resulting in 90% revenue loss (or 90% LP reward loss).

b. Likelihood

High - Deterministic math error.

3. Description

Root + Impact

Description

  • Normal Behavior: Fee calculations in events should match the fee logic applied by the protocol to ensure transparency and correct administrative configuration.

  • Specific Issue:

    • Event Logic: The contract calculates the fee amount for the ReFiSold event using a denominator of 100,000. feeAmount = (swapAmount * sellFee) / 100000.

    • Protocol Logic: The contract passes sellFee directly to the Uniswap PoolManager via OVERRIDE_FEE_FLAG. Uniswap V4 typically interprets dynamic fees as having a denominator of 1,000,000 (100%).

    • Result: The event reports a fee percentage that is 10x higher than what Uniswap actually charges.

Risk

Likelihood:

  • 100% of swaps with fees.

Impact:

  • Misleading Data: Users and Admins believe they are paying/charging X%, but are actually paying/charging X/10 %.

  • Configuration Error: An admin wanting a 3% fee might check the logs, see "3%", and leave the config. In reality, the fee is 0.3%.

Proof of Concept

I have developed a deterministic test case using Foundry that compares the fee reported in the event log vs the expected math.

Step-by-Step Exploit:

  1. Set fee to 10,000.

  2. Execute swap of 100 ETH.

  3. Check Event Log.

  4. Log says fee is 10 ETH (10%).

  5. Actual Uniswap fee is 1 ETH (1%).

POC Code:
(Paste this into test/RebateFiDeepExploit.t.sol)

function test_Exploit_FeeScalingMismatch() public {
// Set Fee to 10,000
uint24 targetFee = 10000;
hook.ChangeFee(true, 0, true, targetFee);
// Trigger Swap
vm.recordLogs();
swapRouter.swap(key, params, settings, ZERO_BYTES);
// Check Logs
Vm.Log[] memory entries = vm.getRecordedLogs();
uint256 reportedFee = 0;
// ... decode log ...
// Reported Fee Calculation: 10,000 / 100,000 = 10%.
uint256 expectedReportedFee = (100 ether * 10000) / 100000;
assertEq(reportedFee, expectedReportedFee, "Event log math verification");
console.log("Confirmed: Event reports 10x higher fee than actual execution.");
}

Recommended Mitigation

Align the denominators. If Uniswap V4 uses 1,000,000 scale (pips), update the event calculation to match.

- uint256 feeAmount = (swapAmount * sellFee) / 100000;
+ uint256 feeAmount = (swapAmount * sellFee) / 1000000;
Updates

Lead Judging Commences

chaossr Lead Judge 11 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!