The following test demonstrates that the contract fails to clear user state after a phase transition. Even after currentPhase updates, user1 retains their previous volume.
function test_resetPerAddressTracking_bug() public {
MockERC20 weth = new MockERC20("TOKEN", "TKN", 18);
MockSwapRouter _swapRouter;
_swapRouter = new MockSwapRouter(address(antiBotHook));
weth.mint(address(_swapRouter), 100 ether);
token.mint(address(_swapRouter), 100 ether);
address[] memory path = new address[](2);
path[0] = address(token);
path[1] = address(weth);
token.mint(user1, 10 ether);
weth.mint(user1, 10 ether);
token.mint(user2, 10 ether);
weth.mint(user2, 10 ether);
vm.prank(user1);
token.approve(address(_swapRouter), type(uint256).max);
weth.approve(address(_swapRouter), type(uint256).max);
vm.prank(user2);
token.approve(address(_swapRouter), type(uint256).max);
weth.approve(address(_swapRouter), type(uint256).max);
vm.prank(user1);
_swapRouter.swapExactTokensForTokens(1 ether, 0, path, user1, block.timestamp);
uint256 swappedPhase1 = antiBotHook.addressSwappedAmount(user1);
assert(swappedPhase1 > 0);
vm.roll(block.number + phase1Duration + 1);
vm.prank(user2);
_swapRouter.swapExactTokensForTokens(1 ether, 0, path, user2, block.timestamp);
vm.prank(user1);
_swapRouter.swapExactTokensForTokens(0.1 ether, 0, path, user1, block.timestamp);
uint256 finalAmount = antiBotHook.addressSwappedAmount(user1);
console.log("User 1 Total after Phase 2 swap:", finalAmount);
assertEq(finalAmount, 0.1 ether, "Lazy reset failed to wipe Phase 1 volume");
}
}
import {BeforeSwapDelta} from "v4-core/types/BeforeSwapDelta.sol";
interface IAntiBotHookHacker {
function beforeSwap(address sender, PoolKey memory key, SwapParams memory params, bytes memory hookData)
external
returns (bytes4, BeforeSwapDelta, uint24);
}
interface Vm {
function prank(address) external;
}
contract MockSwapRouter {
TokenLaunchHook public antiBotHook;
Vm public constant vm = Vm(address(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D));
constructor(address _hook) {
antiBotHook = TokenLaunchHook(_hook);
}
function swapExactTokensForTokens(uint256 amountIn, uint256, address[] calldata path, address to, uint256)
external
returns (uint256[] memory amounts)
{
PoolKey memory key = PoolKey({
currency0: Currency.wrap(path[0]),
currency1: Currency.wrap(path[1]),
fee: 3000,
tickSpacing: 60,
hooks: antiBotHook
});
SwapParams memory params =
SwapParams({zeroForOne: true, amountSpecified: -int256(amountIn), sqrtPriceLimitX96: 0});
address manager = address(antiBotHook.poolManager());
vm.prank(manager);
IAntiBotHookHacker(address(antiBotHook)).beforeSwap(msg.sender, key, params, "");
MockERC20(path[0]).transferFrom(msg.sender, address(this), amountIn);
MockERC20(path[1]).transfer(to, amountIn);
amounts = new uint256[](2);
amounts[0] = amountIn;
amounts[1] = amountIn;
}
}
+ mapping(address => uint256) public addressLastPhase;
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
if (launchStartBlock == 0) revert PoolNotInitialized();
if (initialLiquidity == 0) {
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity);
}
uint256 blocksSinceLaunch = block.number - launchStartBlock;
uint256 newPhase;
if (blocksSinceLaunch <= phase1Duration) {
newPhase = 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) {
newPhase = 2;
} else {
newPhase = 3;
}
if (newPhase != currentPhase) {
- _resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
if (currentPhase == 3) {
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
}
+ if (addressLastPhase[sender] < currentPhase) {
+ addressSwappedAmount[sender] = 0;
+ addressLastPhase[sender] = currentPhase;
+ }
uint256 phaseLimitBps = currentPhase == 1 ? phase1LimitBps : phase2LimitBps;
uint256 phaseCooldown = currentPhase == 1 ? phase1Cooldown : phase2Cooldown;
uint256 phasePenaltyBps = currentPhase == 1 ? phase1PenaltyBps : phase2PenaltyBps;
uint256 swapAmount =
params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);
uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
bool applyPenalty = false;
if (addressLastSwapBlock[sender] > 0) {
uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
addressSwappedAmount[sender] += swapAmount;
addressLastSwapBlock[sender] = block.number;
uint24 feeOverride = 0;
if (applyPenalty) {
feeOverride = uint24((phasePenaltyBps * 100));
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
feeOverride | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}
- function _resetPerAddressTracking() internal {-
- addressSwappedAmount[address(0)] = 0;
- addressLastSwapBlock[address(0)] = 0;
- }