pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import "forge-std/console.sol";
import {TokenLaunchHook} from "../src/TokenLaunchHook.sol";
import {TokenLaunchHookHarness} from "./TokenLaunchHookHarness.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 {PoolId} from "v4-core/types/PoolId.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";
import {BaseHook} from "v4-periphery/src/utils/BaseHook.sol";
contract ZeroFeeOverrideBugTest is Test, Deployers, ERC1155TokenReceiver {
MockERC20 token;
TokenLaunchHookHarness public antiBotHook;
Currency ethCurrency = Currency.wrap(address(0));
Currency tokenCurrency;
address user1 = address(0x1);
uint256 phase1Duration = 100;
uint256 phase2Duration = 100;
uint256 phase1LimitBps = 100;
uint256 phase2LimitBps = 500;
uint256 phase1Cooldown = 5;
uint256 phase2Cooldown = 2;
uint256 phase1PenaltyBps = 1000;
uint256 phase2PenaltyBps = 500;
function setUp() public {
deployFreshManagerAndRouters();
token = new MockERC20("TOKEN", "TKN", 18);
tokenCurrency = Currency.wrap(address(token));
token.mint(address(this), 1000 ether);
token.mint(user1, 1000 ether);
bytes memory creationCode = type(TokenLaunchHookHarness).creationCode;
bytes memory constructorArgs = abi.encode(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
uint160 flags = uint160(Hooks.AFTER_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG);
(address hookAddress, bytes32 salt) = HookMiner.find(address(this), flags, creationCode, constructorArgs);
antiBotHook = new TokenLaunchHookHarness{salt: salt}(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
require(address(antiBotHook) == hookAddress, "Hook address mismatch");
token.approve(address(swapRouter), type(uint256).max);
token.approve(address(modifyLiquidityRouter), type(uint256).max);
vm.prank(user1);
token.approve(address(swapRouter), type(uint256).max);
(key,) = initPool(ethCurrency, tokenCurrency, antiBotHook, LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1);
uint160 sqrtPriceAtTickUpper = TickMath.getSqrtPriceAtTick(60);
uint256 ethToAdd = 10 ether;
uint128 liquidityDelta = LiquidityAmounts.getLiquidityForAmount0(SQRT_PRICE_1_1, sqrtPriceAtTickUpper, ethToAdd);
modifyLiquidityRouter.modifyLiquidity{value: ethToAdd}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liquidityDelta)),
salt: bytes32(0)
}),
ZERO_BYTES
);
}
ZERO FEE OVERRIDE BUG PoCs
══════════════════════════════════════════════════════════════════ */
function test_ZeroFeeOverride_Phase3_AllSwapsFree() public {
console.log("\n=== Testing Phase 3 Zero Fee Override Bug ===");
vm.roll(block.number + phase1Duration + phase2Duration + 1);
vm.deal(user1, 1 ether);
vm.startPrank(user1);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(0.001 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
assertEq(antiBotHook.getCurrentPhase(), 3, "Should be in phase 3");
assertEq(antiBotHook.currentPhase(), 3, "Stored currentPhase should be 3");
console.log("Current phase:", antiBotHook.getCurrentPhase());
(bytes4 sel, , uint24 feeOverride) =
antiBotHook.exposed_beforeSwap(address(swapRouter), key, params, ZERO_BYTES);
assertEq(sel, BaseHook.beforeSwap.selector, "Unexpected selector");
console.log("Fee override returned:", feeOverride);
console.log("OVERRIDE_FEE_FLAG:", LPFeeLibrary.OVERRIDE_FEE_FLAG);
assertEq(
feeOverride,
LPFeeLibrary.OVERRIDE_FEE_FLAG,
"Phase 3 should NOT be overriding to 0% fee"
);
uint24 encodedFee = feeOverride & 0x000FFFFF;
console.log("Encoded fee (bits 0-19):", encodedFee);
assertEq(encodedFee, 0, "Encoded LP fee in phase 3 is 0%, not standard pool fee");
console.log("\n[VULNERABILITY CONFIRMED]");
console.log("Phase 3 swaps pay 0% LP fees instead of standard pool fees");
console.log("LPs lose 100% of fee revenue on all post-launch swaps");
}
function test_ZeroFeeOverride_Phase1_CompliantSwap() public {
console.log("\n=== Testing Phase 1 Compliant Swap Zero Fee Override Bug ===");
assertEq(antiBotHook.getCurrentPhase(), 1, "Should start in phase 1");
console.log("Current phase:", antiBotHook.getCurrentPhase());
vm.deal(user1, 1 ether);
vm.startPrank(user1);
uint256 swapAmount = 0.001 ether;
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(swapAmount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: swapAmount}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
assertGt(
antiBotHook.addressSwappedAmount(address(swapRouter)),
0,
"SwapRouter should have recorded swapped amount"
);
console.log("Swapped amount recorded:", antiBotHook.addressSwappedAmount(address(swapRouter)));
vm.roll(block.number + phase1Cooldown + 1);
(bytes4 sel, , uint24 feeOverride) =
antiBotHook.exposed_beforeSwap(address(swapRouter), key, params, ZERO_BYTES);
assertEq(sel, BaseHook.beforeSwap.selector, "Unexpected selector");
console.log("Fee override returned:", feeOverride);
console.log("OVERRIDE_FEE_FLAG:", LPFeeLibrary.OVERRIDE_FEE_FLAG);
assertEq(
feeOverride,
LPFeeLibrary.OVERRIDE_FEE_FLAG,
"Compliant phase 1 swap should not be forced to 0% LP fee"
);
uint24 encodedFee = feeOverride & 0x000FFFFF;
console.log("Encoded fee (bits 0-19):", encodedFee);
assertEq(encodedFee, 0, "Encoded LP fee for compliant phase 1 swap is 0%, not pool fee");
console.log("\n[VULNERABILITY CONFIRMED]");
console.log("Phase 1 compliant swaps pay 0% LP fees instead of pool fees");
console.log("LPs lose 100% of fee revenue on all non-penalized swaps");
}
function test_ZeroFeeOverride_Phase2_CompliantSwap() public {
console.log("\n=== Testing Phase 2 Compliant Swap Zero Fee Override Bug ===");
vm.roll(block.number + phase1Duration + 1);
assertEq(antiBotHook.getCurrentPhase(), 2, "Should be in phase 2");
console.log("Current phase:", antiBotHook.getCurrentPhase());
vm.deal(user1, 1 ether);
vm.startPrank(user1);
uint256 swapAmount = 0.001 ether;
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(swapAmount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: swapAmount}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
vm.roll(block.number + phase2Cooldown + 1);
(bytes4 sel, , uint24 feeOverride) =
antiBotHook.exposed_beforeSwap(address(swapRouter), key, params, ZERO_BYTES);
assertEq(sel, BaseHook.beforeSwap.selector, "Unexpected selector");
console.log("Fee override returned:", feeOverride);
console.log("OVERRIDE_FEE_FLAG:", LPFeeLibrary.OVERRIDE_FEE_FLAG);
assertEq(
feeOverride,
LPFeeLibrary.OVERRIDE_FEE_FLAG,
"Compliant phase 2 swap should not be forced to 0% LP fee"
);
uint24 encodedFee = feeOverride & 0x000FFFFF;
console.log("Encoded fee (bits 0-19):", encodedFee);
assertEq(encodedFee, 0, "Encoded LP fee for compliant phase 2 swap is 0%, not pool fee");
console.log("\n[VULNERABILITY CONFIRMED]");
console.log("Phase 2 compliant swaps pay 0% LP fees instead of pool fees");
console.log("LPs lose 100% of fee revenue on all non-penalized swaps");
}
}