RebateFi Hook

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

Buy and sell logic is inverted

Root + Impact

Description

  • It is designed to charge asymmetric fees depending on the swap direction with 0% charged on buying ReFi tokens and 3% charged when selling ReFi tokens as a way of discouraging dumping

  • The logic is completely inversed leading to sells being detected as buys and buys detected as sells

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

Risk

Likelihood:

  • Every sell transaction bypasses the 3% sell fee

  • Every buy transaction incorrectly pays the 3% fee meant for sells

Impact:

  • Liquidity providers lose all sell fee revenue

  • Buyers are overcharged

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol";
import {SwapParams} from "v4-core/types/PoolOperation.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
contract FeeInversionTest is Test {
using CurrencyLibrary for Currency;
ReFiSwapRebateHook hook;
PoolKey poolKey;
address refiToken = address(0x123);
address otherToken = address(0x456);
function setUp() public {
IPoolManager poolManager = IPoolManager(address(0x789));
hook = new ReFiSwapRebateHook(poolManager, refiToken);
// ReFi must be currency1 due to validation bug
poolKey = PoolKey({
currency0: Currency.wrap(otherToken),
currency1: Currency.wrap(refiToken),
fee: 0x800000, // Dynamic fee flag
tickSpacing: 60,
hooks: hook
});
}
function testSellReFiChargesZeroFee() public {
SwapParams memory params = SwapParams({
zeroForOne: false,
amountSpecified: -1000e18, // Selling 1000 ReFi
sqrtPriceLimitX96: 0
});
(, , uint24 appliedFee) = hook.beforeSwap(
address(this),
poolKey,
params,
""
);
assertEq(appliedFee & 0xFFFFFF, 0, "Sell incorrectly charged 0% instead of 3%");
}
function testBuyReFiChargesThreePercent() public {
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -1000e18,
sqrtPriceLimitX96: 0
});
(, , uint24 appliedFee) = hook.beforeSwap(
address(this),
poolKey,
params,
""
);
assertEq(appliedFee & 0xFFFFFF, 3000, "Buy incorrectly charged 3% instead of 0%");
}
}

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!