RebateFi Hook

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

Change of the fee on sell can be backrun by unintended user, unauthorized discount

Fee changes via ChangeFee can be backrun, any user can sell ReFi at the new (potentially heavily discounted) sell fee immediately

Description

  • The owner uses ChangeFee to update the global sellFee, often to temporarily grant a very low or zero sell fee as a premium/discount for a specific user or campaign.

  • The function writes the new sellFee directly to storage with no delay or recipient restriction.

  • Because the fee update is effective in the same block (or even same transaction bundle), any searcher/MEV bots or regular users can immediately front-run or back-run the owner’s transaction and sell ReFi at the newly discounted rate before the owner can revert it.

// Root cause in the codebase with @> marks to highlight the relevant section
function ChangeFee(bool _isBuyFee, uint24 _buyFee, bool _isSellFee, uint24 _sellFee) external onlyOwner {
if (_isBuyFee) buyFee = _buyFee;
@> if (_isSellFee) sellFee = _sellFee;
}

Risk

Likelihood:

  • Every on-chain fee change is public in the mempool and can be observed in real time

  • Searchers routinely monitor privileged transactions (onlyOwner calls) on popular protocols and hooks

  • Back-running a fee change requires only a simple swap transaction — trivial to execute

Impact:

  • Intended premium discounts (e.g., 0–1 bp sell fee) are exploited by arbitragers instead of the target user/community

  • The project loses significant fee revenue it expected to collect after the promotion

  • Creates unfair economics and erodes trust in the rebate/discount mechanism

Proof of Concept

Add the following code snippet to the RebateFiHookTest.t.sol test file.

This snippet of code is to demonstrate that the user2 can sell ReFi with 100% discount by bypassing the standard fee.

function test_BuyReFi_MultipleUsers() public {
uint256 ethAmount = 0.001 ether;
// owner change sell fee to 1 in order to give maximum discount to user1 (PREMIUM FEE)
rebateHook.ChangeFee(true, 0, true, 1);
// user1 sell ReFi with 100% discount
vm.deal(user1, 1 ether);
vm.startPrank(user1);
uint256 user1InitialReFi = reFiToken.balanceOf(user1);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(ethAmount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: ethAmount}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
// User2 backrun user1 and owner and sell ReFi with PREMIUM FEE
vm.deal(user2, 1 ether);
vm.startPrank(user2);
uint256 user2InitialReFi = reFiToken.balanceOf(user2);
swapRouter.swap{value: ethAmount}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
// Both users should have more ReFi tokens
assertGt(reFiToken.balanceOf(user1), user1InitialReFi, "User1 should have more ReFi");
assertGt(reFiToken.balanceOf(user2), user2InitialReFi, "User2 should have more ReFi");
(, uint24 sellFee) = rebateHook.getFeeConfig();
assertEq(sellFee, 1, "Sell fee should be 1");
}

Recommended Mitigation

Possible mitigation is to add a premium fee registry for specific users. This can be done by adding a mapping of address to uint24 and setting the premium fee for the user.

mapping(address => uint24) public premiumFees;
function ChangeFee(bool _isBuyFee, uint24 _buyFee, bool _isSellFee, uint24 _sellFee) external onlyOwner {
if (_isBuyFee) buyFee = _buyFee;
- if (_isSellFee) sellFee = _sellFee;
+ if (_isSellFee) {
+ sellFee = _sellFee;
+ premiumFees[msg.sender] = _sellFee;
}
}
.
.
.
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 {
+ uint24 premiumFee = premiumFees[sender];
+ if (premiumFee > 0) {
+ fee = premiumFee;
+ } else {
+ fee = sellFee;
+ }
- fee = sellFee;
uint256 feeAmount = (swapAmount * sellFee) / 100000;
emit ReFiSold(sender, swapAmount, feeAmount);
}
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, fee | LPFeeLibrary.OVERRIDE_FEE_FLAG);
}
Updates

Lead Judging Commences

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