RebateFi Hook

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

Incorrect buy/sell classification in _isReFiBuy (inverted direction logic)

Root + Impact

Description

  • The _isReFiBuy helper should return true when the swap results in the trader receiving ReFi (i.e., the user is buying ReFi), and false when the trader is sending/receiving the opposite direction (selling ReFi). The function needs to interpret zeroForOne consistently with token ordering: when ReFi is currency0, zeroForOne == false (moving token1→token0) should indicate buying token0 (ReFi); when ReFi is currency1, zeroForOne == true indicates buying ReFi.

  • The implementation inlines an inverted mapping:

    • if ReFi == currency0 it returns zeroForOne, otherwise it returns !zeroForOne.

    • This inverts the correct mapping — for ReFi == currency0 a zeroForOne swap actually removes token0 (i.e., sells ReFi), but the function currently treats that as a buy. As a result, _beforeSwap uses sellFee on actual buys (and vice versa), and emits ReFiSold for buy swaps (observed in PoC tests).

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:

  • the faulty code runs on every swap for pools containing ReFi

Impact:

  • the hook misclassifies swap direction, selecting the wrong fee branch and emitting incorrect events


Proof of Concept

  • Sell ReFi in a swap and observe the emission of RefiBought or vice versa

    contract TestReFiSwapRebateHook is Test, Deployers, ERC1155TokenReceiver {
    // Redeclare hook events so `vm.expectEmit` can reference them here
    event ReFiBought(address indexed buyer, uint256 amount);
    event ReFiSold(address indexed seller, uint256 amount, uint256 fee);
    /// @notice Confirms which event the hook emits for an ETH->ReFi buy
    /// This test documents the current misclassification: the hook emits
    /// `ReFiSold` (and applies the sell fee) even for what the test calls a buy.
    function test_BuyReFi_EmitsReFiSoldEvent() public {
    uint256 ethAmount = 0.01 ether;
    vm.deal(user1, 1 ether);
    vm.startPrank(user1);
    SwapParams memory params = SwapParams({
    zeroForOne: true, // ETH -> ReFi
    amountSpecified: -int256(ethAmount),
    sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
    });
    PoolSwapTest.TestSettings memory testSettings = PoolSwapTest.TestSettings({
    takeClaims: false,
    settleUsingBurn: false
    });
    (, uint24 currentSellFee) = rebateHook.getFeeConfig();
    uint256 expectedFeeAmount = (ethAmount * uint256(currentSellFee)) / 100000;
    vm.expectEmit(true, false, false, true);
    emit ReFiSold(address(swapRouter), ethAmount, expectedFeeAmount);
    swapRouter.swap{value: ethAmount}(key, params, testSettings, ZERO_BYTES);
    vm.stopPrank();
    }
    /// @notice Confirms the hook emits ReFiBought for a ReFi->ETH sell
    function test_SellReFi_EmitsReFiBoughtEvent() public {
    uint256 reFiAmount = 0.01 ether;
    vm.startPrank(user1);
    reFiToken.approve(address(swapRouter), type(uint256).max);
    // Expect the hook to emit ReFiBought with the router as the sender
    vm.expectEmit(true, false, false, true);
    emit ReFiBought(address(swapRouter), reFiAmount);
    SwapParams memory params = SwapParams({
    zeroForOne: false, // ReFi -> ETH
    amountSpecified: -int256(reFiAmount),
    sqrtPriceLimitX96: TickMath.MAX_SQRT_PRICE - 1
    });
    PoolSwapTest.TestSettings memory testSettings = PoolSwapTest.TestSettings({
    takeClaims: false,
    settleUsingBurn: false
    });
    swapRouter.swap(key, params, testSettings, ZERO_BYTES);
    vm.stopPrank();
    }

Recommended Mitigation

Replace IsReFiCurrency0 with !IsReFiCurrency0

function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
- if (IsReFiCurrency0) {
+ if (!IsReFiCurrency0) {
return zeroForOne;
} else {
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!