RebateFi Hook

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

inversion of buy/sell fee logic in _isReFiBuy()

Root + Impact

Description

  • The hook is intended to apply a 0% fee when users buy ReFi and a higher fee 0.30% when users sell ReFi.

  • Due to inverted logic in _isReFiBuy(), every swap does the exact opposite: buyers are charged 0.30% and sellers pay 0%, completely reversing the protocol’s intended economic model in all trades.

/// @notice Determines if a swap is buying or selling ReFi
/// @param key The pool key containing currency information
/// @param zeroForOne The swap direction
/// @return True if buying ReFi, false if selling
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:

  • Reason 1: _beforeInitialize function always intitializes the pool with ReFi token at key.currency1 , so IsReFiCurrency0 is always false inside _isReFibuy function, when the codition checked always the else block is checked (isReFicurrency0 = false). when zeroForOne = true (which means swappig key.currency0 to key.currency1), this means the user wants to to buy ReFi token, but the output of the function is !zerofForOne , which is false. when zeroForOne = false(which means swappig key.currency0 to key.currency1), this means the user wants to sell ReFi token, but the function returns !zeroForOne which is true. generally the isReFiBuy function returns true when user wants to sell and ` when the user wants to by , which opposite to what it supposed to do .



    Impact:

  • Impact 1: The hook taxes ReFi buyers 0.30% and gives sellers 0% fee on every single swap,which is completely inverting the protocol’s core economic model and breaking its entire revenue mechanism through .

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
contract IsReFiBuy_PoC is Deployers {
MockERC20 reFi;
MockERC20 other;
ReFiSwapRebateHook hook;
function setUp() public {
// Deploy manager from Deployers helper
deployFreshManager();
// Deploy ReFi token
reFi = new MockERC20("ReFi", "REFI", 18);
// Ensure ReFi is the lower address so it becomes currency0 in PoolKey
do { other = new MockERC20("Other", "OTHR", 18); } while (address(other) <= address(reFi));
// Deploy the hook via HookMiner/CREATE2 with the same flags as the implementation
(, bytes32 salt) = HookMiner.find(
address(this),
uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG),
type(ReFiSwapRebateHook).creationCode,
abi.encode(manager, address(reFi))
);
hook = new ReFiSwapRebateHook{salt: salt}(manager, address(reFi));
}
/// @notice PoC test: demonstrates `_isReFiBuy` returns inverted results when ReFi is `currency0`.
/// If the function were correct, for `currency0 == ReFi`:
/// - zeroForOne == true => SELL ReFi => isReFiBuy == false
/// - zeroForOne == false => BUY ReFi => isReFiBuy == true
/// Current (buggy) implementation returns the opposite; this test asserts the observed (buggy) values.
function test_isReFiBuy_InvertedWhenReFiIsCurrency0() public {
PoolKey memory key = PoolKey(
Currency.wrap(address(reFi)),
Currency.wrap(address(other)),
LPFeeLibrary.DYNAMIC_FEE_FLAG,
60,
hook
);
// Bug expectation: when ReFi is currency0, zeroForOne=true (token0->token1)
// should be SELL ReFi (isReFiBuy == false). The buggy function returns true.
bool observedBuyForZeroForOneTrue = hook.isReFiBuyPublic(key, true);
assertEq(observedBuyForZeroForOneTrue, true, "PoC: expected inverted result (true) for zeroForOne=true");
// Bug expectation: zeroForOne=false should be BUY ReFi (isReFiBuy == true).
// The buggy function returns false.
bool observedBuyForZeroForOneFalse = hook.isReFiBuyPublic(key, false);
assertEq(observedBuyForZeroForOneFalse, false, "PoC: expected inverted result (false) for zeroForOne=false");
}
}

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 11 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!