RebateFi Hook

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

Incorrect Buy/Sell Direction Logic

Root + Impact

Description

  • The _isReFiBuy function determines whether a swap is buying or selling the ReFi token by checking the pool's currency ordering and swap direction. In a Uniswap V4 pool, zeroForOne = true means swapping currency0 for currency1, and zeroForOne = false means swapping currency1 for currency0.

  • The function contains inverted logic: when ReFi is currency0 and zeroForOne is true, the user is selling ReFi (giving ReFi to get currency1), but the function incorrectly returns true indicating a buy. Similarly, when ReFi is currency1 and zeroForOne is false, the user is selling ReFi, but the function returns false (sell), which happens to be correct by coincidence of the double inversion.

function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
if (IsReFiCurrency0) {
@> return zeroForOne;
} else {
@> return !zeroForOne;
}
}

Risk

Likelihood:

  • Any swap involving the pool will trigger this incorrect logic.

Impact:

  • Buy transactions (which should have 0% fee) will incorrectly be charged the sell fee (default 3000 = 0.3%)

Sell transactions (which should have 0.3% fee) will incorrectly be charged the buy fee (0%)

  • Users buying ReFi tokens pay unexpected fees, reducing their received amount

  • Users selling ReFi tokens pay no fees when they should, causing protocol revenue loss

Proof of Concept

function test_InvertedSwapDirection() public {
// Check initial configuration: Buy fee = 0, Sell fee = 3000 (0.3%)
(uint24 buyFeeInitial, uint24 sellFeeInitial) = rebateHook.getFeeConfig();
assertEq(buyFeeInitial, 0, "Initial buy fee is 0");
assertEq(sellFeeInitial, 3000, "Initial sell fee is 3000");
uint256 ethAmount = 0.01 ether;
vm.deal(user1, 1 ether);
vm.startPrank(user1);
SwapParams memory buyParams = SwapParams({
zeroForOne: true, // ETH -> ReFi. This is a ReFi BUY.
amountSpecified: -int256(ethAmount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
uint256 initialReFiBalance = reFiToken.balanceOf(user1);
// Perform swap (ReFi BUY, should have 0 fee)
swapRouter.swap{value: ethAmount}(key, buyParams, testSettings, ZERO_BYTES);
uint256 finalReFiBalance = reFiToken.balanceOf(user1);
vm.stopPrank();
// Reset the sell fee to 0 and re-test the BUY
rebateHook.ChangeFee(false, 0, true, 0);
vm.startPrank(user1);
uint256 initialReFiBalance_2 = reFiToken.balanceOf(user1);
swapRouter.swap{value: ethAmount}(key, buyParams, testSettings, ZERO_BYTES);
uint256 finalReFiBalance_2 = reFiToken.balanceOf(user1);
vm.stopPrank();
// The second swap (with sell fee = 0) should yield more ReFi than the first swap
// (with sell fee = 3000), proving the BUY swap was incorrectly subject to the SELL fee.
assertGt(
finalReFiBalance_2 - initialReFiBalance_2,
finalReFiBalance - initialReFiBalance,
"Second swap (with 0 sell fee) should yield more tokens, confirming sell fee was applied to first BUY"
);
}

Recommended Mitigation

function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
if (IsReFiCurrency0) {
- return zeroForOne;
+ return !zeroForOne;
} else {
- return !zeroForOne;
+ return zeroForOne;
}
}
Updates

Lead Judging Commences

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

Inverted buy/sell logic when ReFi is currency0, leading to incorrect fee application.

Support

FAQs

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

Give us feedback!