_isRefiBuy() wrong logic leads to inverted fees applied
Description
-
The _isReFiBuy function is intended to identify whether a swap is a "buy" or a "sell" of the ReFi token, allowing the _beforeSwap hook to apply the appropriate buyFee or sellFee.
-
The function's logic is inverted, causing it to misidentify buys as sells and sells as buys. Consequently, when a user buys the ReFi token, the sellFee is applied and a ReFiSold event is emitted. Conversely, when a user sells the ReFi token, the buyFee is applied and a ReFiBought event is emitted.
unction _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:
Impact:
Proof of Concept
Running the existing test case test_BuyReFi_ZeroFee with verbose logging (forge test --mt test_BuyReFi_ZeroFee -vvvv) demonstrates the issue. The test executes a "buy" of ReFi by swapping ETH fo ReFi (zeroForOne: true). The trace clearly shows that instead of a ReFiBought event, the hook incorrectly emits a ReFiSold event, proving the logic is inverted.
function test_BuyReFi_ZeroFee() public {
uint256 ethAmount = 0.01 ether;
vm.deal(user1, 1 ether);
uint256 initialEthBalance = user1.balance;
uint256 initialReFiBalance = reFiToken.balanceOf(user1);
vm.startPrank(user1);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(ethAmount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: ethAmount}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
uint256 finalEthBalance = user1.balance;
uint256 finalReFiBalance = reFiToken.balanceOf(user1);
assertEq(finalEthBalance, initialEthBalance - ethAmount, "ETH balance should decrease by swap amount");
assertGt(finalReFiBalance, initialReFiBalance, "ReFi balance should increase after buy");
(uint24 currentBuyFee,) = rebateHook.getFeeConfig();
}
│ │ │ │ ├─ [7225] ReFiSwapRebateHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x212224D2F2d262cd093eE13240ca4873fcCBbA3C, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0xF398DeAF12Ba47830ECA5a881175a3690A90F080 }), SwapParams({ zeroForOne: true, amountSpecified: -10000000000000000 [-1e16], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ │ ├─ emit ReFiSold(seller: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount: 10000000000000000 [1e16], fee: 300000000000000 [3e14])
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4197304 [4.197e6]
Recommended Mitigation
function _isReFiBuy(PoolKey calldata key, bool zeroForOne) internal view returns (bool) {
bool IsReFiCurrency0 = Currency.unwrap(key.currency0) == ReFi;
// @audit applied logic is inverted
if (IsReFiCurrency0) {
- return zeroForOne;
+ return !zeroForOne;
} else {
- return !zeroForOne;
+ return zeroForOne;
}
}