RebateFi Hook

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

Owner's Absolute Control over Dynamic Fees, Leads to Economic Value Destruction of the Token via Near-100%.

Owner's Absolute Control over Dynamic Fees, Leads to Economic Value Destruction of the Token via Near-100%.

Description

  • The vulnerability lies in the ReFiSwapRebateHook contract's implementation, granting the Owner absolute control to override the standard pool fee and set it for example 99 % via the ChangeFee function. This action does not result in a "rug pull" where the owner steals the collected funds (as the fees are absorbed into the pool reserves, benefiting Liquidity Providers). Instead, it causes Economic Value Destruction for traders. A user attempting to sell the token receives an output close to zero approx 0.0001% of the trade value, effectively making the asset economically illiquid and untradable

/// @notice Updates the buy and/or sell fee percentages
/// @param _isBuyFee Whether to update the buy fee
/// @param _buyFee New buy fee value (if _isBuyFee is true)
/// @param _isSellFee Whether to update the sell fee
/// @param _sellFee New sell fee value (if _isSellFee is true)
/// @dev Only callable by owner
function ChangeFee(
@> bool _isBuyFee,
uint24 _buyFee,
bool _isSellFee,
@> uint24 _sellFee
) external onlyOwner {
@> if(_isBuyFee) buyFee = _buyFee;
@> if(_isSellFee) sellFee = _sellFee;
}

Risk

Likelihood:

  • It might occur under specific conditions. For example, The owner is raising the fees on the sale of ReFi by a very large amount.


Impact:

  • There's a severe disruption of protocol functionality or availability.

Proof of Concept

<details>
<summary>POC</summary>
1. Add this test to `RebateFiHookTest.t.sol` test file.
Note that the `currentSellFee` variable is used on a sale transaction due to faulty contract logic.
```javascript
function test_PoC_SellFee_99Percent() public {
// We assume: currency0 = ETH (native currency), currency1 = ReFi (MockERC20)
// --- 1) Set FEE = 99% for the direction that ACQUIRES ETH (SELL ReFi) ---
uint24 extremeFee = 990000; // 99% represented as basis points (990000 / 1000000)
// FIX: Set the 'Buy Fee' (acquiring currency0/ETH) to 99%
// Assuming ChangeFee(isBuyFeeSet, newBuyFee, isSellFeeSet, newSellFee)
rebateHook.ChangeFee(true, extremeFee, false, 0);
// Validate the fee was set correctly
uint24 currentBuyFee;
uint24 currentSellFee;
(currentBuyFee, currentSellFee) = rebateHook.getFeeConfig();
assertEq(currentBuyFee, extremeFee, "Buy fee was not set to 99%");
// --- 2) Add large liquidity correctly (50 ETH & 50 ReFi) ---
uint256 ethAmount = 50 ether;
uint256 reFiAmount = 50 ether;
// FIX 1A: Fund the test contract for the FIRST liquidity add
vm.deal(address(this), ethAmount);
// Compute liquidity amounts based on current price (1:1)
uint160 sqrtPriceLower = TickMath.getSqrtPriceAtTick(-60);
uint160 sqrtPriceUpper = TickMath.getSqrtPriceAtTick(60);
uint128 liquidityETH = LiquidityAmounts.getLiquidityForAmount0(
SQRT_PRICE_1_1_s,
sqrtPriceUpper,
ethAmount
);
uint128 liquidityReFi = LiquidityAmounts.getLiquidityForAmount1(
sqrtPriceLower,
SQRT_PRICE_1_1_s,
reFiAmount
);
uint128 liquidity = liquidityETH < liquidityReFi ? liquidityETH : liquidityReFi;
// Add liquidity - PART 1 (Sends ETH via {value})
modifyLiquidityRouter.modifyLiquidity{value: ethAmount}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liquidity)),
salt: bytes32(0)
}),
ZERO_BYTES
);
// Approve ReFi token for the PoolManager (handled by the router)
reFiToken.approve(address(modifyLiquidityRouter), type(uint256).max);
// FIX 1B: Re-fund the test contract for the SECOND liquidity add
// The previous call consumed the 50 ETH. This is essential.
vm.deal(address(this), ethAmount);
// FIX 2: Explicitly pass {value: ethAmount} in the second call.
// The trace showed PoolManager::settle{value: 50e18}() failing during this call,
// indicating the ETH must be passed via {value} to the router.
modifyLiquidityRouter.modifyLiquidity{value: ethAmount}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liquidity)),
salt: bytes32(0)
}),
ZERO_BYTES
);
// --- 3) User sells exactly 1 ReFi ---
uint256 sellAmount = 1 ether; // 1 ReFi
vm.startPrank(user1);
// Fund user1 with ReFi to sell
reFiToken.mint(user1, sellAmount);
reFiToken.approve(address(swapRouter), type(uint256).max);
// Record ETH balance before swap
uint256 initialEth = user1.balance;
// Swap parameters: Selling ReFi (currency1) for ETH (currency0)
SwapParams memory params = SwapParams({
zeroForOne: false, // Selling ReFi (currency1)
amountSpecified: -int256(sellAmount), // Negative amount means output
sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings(false, false);
swapRouter.swap(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
// --- 4) Validate output ---
uint256 finalEth = user1.balance;
uint256 receivedEth = finalEth - initialEth;
console.log("ETH received after selling 1 ReFi (99% fee):", receivedEth);
// Expected received ETH is approx 1% of 1 ETH = 0.01 ETH (1e16 wei)
uint256 expectedEth = 1e16;
// Check that received ETH is within a tight range of 0.01 ETH.
// We allow a 2% deviation for potential dust/slippage in the test swap calculation.
assertLe(receivedEth, 0.02 ether, "Expected output < 2% (tolerance)");
assertGe(receivedEth, expectedEth * 98 / 100, "Expected output >= 0.98% (tolerance)");
}
```
</details>

Recommended Mitigation

1.Modify the `ChangeFee` function To apply a maximum fee limit .
```diff
+ uint24 public constant MAX_FEE = 10000; // for example
/// @notice Updates the buy and/or sell fee percentages
/// @param _isBuyFee Whether to update the buy fee
/// @param _buyFee New buy fee value (if _isBuyFee is true)
/// @param _isSellFee Whether to update the sell fee
/// @param _sellFee New sell fee value (if _isSellFee is true)
/// @dev Only callable by owner
function ChangeFee(
bool _isBuyFee,
uint24 _buyFee,
bool _isSellFee,
uint24 _sellFee
) external onlyOwner {
- if(_isBuyFee) buyFee = _buyFee;
+ if(_isBuyFee) {
+ require(_buyFee <= MAX_FEE, "Buy fee exceeds max limit (1%)");
+ buyFee = _buyFee;
+ }
- if(_isSellFee) sellFee = _sellFee;
+ if(_isSellFee) {
+ require(_sellFee <= MAX_FEE, "Sell fee exceeds max limit (1%)");
+ sellFee = _sellFee;
+ }
}
```
Updates

Lead Judging Commences

chaossr Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!