Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: high

Broken limit reset logic

Author Revealed upon completion

Description

  • When the launch phase changes, the hook should reset per‑address tracking (e.g., traded amount and cooldown block) so each phase’s limits/cooldowns start fresh for every user. The code attempts to do this by calling _resetPerAddressTracking() inside _beforeSwap whenever a new phase is detected.

  • _resetPerAddressTracking() does not reset per‑address state. It only zeroes the entries for address(0), leaving all real users’ addressSwappedAmount and addressLastSwapBlock unchanged across phases. As a result, users can carry phase‑1 usage and cooldown into phase‑2, which defeats the intended per‑phase limits and can unjustly trigger penalties.

// TokenLaunchHook.sol
function _beforeSwap(...) internal override returns (bytes4, BeforeSwapDelta, uint24) {
...
if (newPhase != currentPhase) {
_resetPerAddressTracking(); // @> intended to clear per-address tracking on phase change
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
...
}
function _resetPerAddressTracking() internal {
@> addressSwappedAmount[address(0)] = 0; // only resets address(0)
@> addressLastSwapBlock[address(0)] = 0; // only resets address(0)
}

Risk

Likelihood: High

  • Deterministic behavior during every phase transition: Whenever a phase changes, the code path calls _resetPerAddressTracking(). Since it doesn’t actually clear real users’ entries, stale usage data is always carried into the next phase.

  • Common user flow: Typical launches will see users swapping in phase 1 and then continuing in phase 2; they are directly affected by stale limits/cooldowns.

Impact: High

  • Incorrect enforcement of per‑phase limits: Users may get penalized or throttled in phase 2 based on their phase‑1 activity, contrary to the intent of “fresh” phase limits.

  • User‑visible unfairness / DoS‑like throttling: Legitimate participants can be blocked or charged penalty fees in later phases due to residual accounting, hurting UX and undermining launch fairness.

Proof of Concept

  • Create BrokenLimitReset.t.sol under test directory and copy code below.

  • Run command forge test --mt test_PhaseChange_DoesNotResetPerAddressState -vvvv.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.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 {Hooks} from "v4-core/libraries/Hooks.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {TokenLaunchHook} from "../src/TokenLaunchHook.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {ERC1155TokenReceiver} from "solmate/src/tokens/ERC1155.sol";
contract BrokenLimitResetTest is Test, Deployers, ERC1155TokenReceiver {
MockERC20 token;
TokenLaunchHook public hook;
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();
// tokens & balances
token = new MockERC20("TOKEN", "TKN", 18);
tokenCurrency = Currency.wrap(address(token));
token.mint(address(this), 1_000 ether);
token.mint(user1, 1_000 ether);
// mine address with AFTER_INITIALIZE + BEFORE_SWAP flags (correct for this hook)
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory args = abi.encode(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG);
(address mined, bytes32 salt) =
HookMiner.find(address(this), flags, creationCode, args);
hook = new TokenLaunchHook{salt: salt}(
IPoolManager(manager),
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
require(address(hook) == mined, "Hook address mismatch");
// approvals
token.approve(address(swapRouter), type(uint256).max);
token.approve(address(modifyLiquidityRouter), type(uint256).max);
vm.prank(user1);
token.approve(address(swapRouter), type(uint256).max);
// init pool with dynamic fee and add liquidity
(key,) = initPool(ethCurrency, tokenCurrency, hook, LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1);
uint160 sqrtPriceAtTickUpper = TickMath.getSqrtPriceAtTick(60);
uint256 ethToAdd = 10 ether;
uint128 liqDelta = LiquidityAmounts.getLiquidityForAmount0(
SQRT_PRICE_1_1, sqrtPriceAtTickUpper, ethToAdd
);
modifyLiquidityRouter.modifyLiquidity{value: ethToAdd}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liqDelta)),
salt: bytes32(0)
}),
ZERO_BYTES
);
}
function test_PhaseChange_DoesNotResetPerAddressState() public {
// 1) First swap in phase 1
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 settings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
// Perform first swap
swapRouter.swap{value: swapAmount}(key, params, settings, ZERO_BYTES);
vm.stopPrank();
// Record router's tracked amount BEFORE phase transition
uint256 trackedBefore = hook.addressSwappedAmount(address(swapRouter));
assertGt(trackedBefore, 0, "sanity: router tracked amount should be > 0");
// 2) Advance into phase 2 and trigger transition with another swap
vm.roll(block.number + phase1Duration + 1);
vm.startPrank(user1);
swapRouter.swap{value: swapAmount}(key, params, settings, ZERO_BYTES);
vm.stopPrank();
// Sanity: now in phase 2
assertEq(hook.currentPhase(), 2, "should be in phase 2");
// 3) The per-address counters SHOULD reset on phase change...
// ...but they DO NOT: mapping still contains data from phase 1 plus this swap.
uint256 trackedAfter = hook.addressSwappedAmount(address(swapRouter));
// If reset worked, 'trackedAfter' would equal just 'swapAmount'.
// Broken behavior: it is strictly greater than 'swapAmount'.
assertGt(trackedAfter, swapAmount, "per-address tracking did not reset across phases");
}
}

Output:

[⠊] Compiling...
[⠢] Compiling 2 files with Solc 0.8.26
[⠆] Solc 0.8.26 finished in 57.70ms
No files changed, compilation skipped
Ran 1 test for test/BrokenLimitReset.t.sol:BrokenLimitResetTest
[PASS] test_PhaseChange_DoesNotResetPerAddressState() (gas: 268483)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 537.72ms (459.20µs CPU time)
Ran 1 test suite in 538.90ms (537.72ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
vladimir@BRAIN:~/web3/code-hawks/2026-01-vanguard$ forge test --mt test_PhaseChange_DoesNotResetPerAddressState -vvvv
[⠊] Compiling...
[⠢] Compiling 2 files with Solc 0.8.26
[⠆] Solc 0.8.26 finished in 89.04ms
No files changed, compilation skipped
Ran 1 test for test/BrokenLimitReset.t.sol:BrokenLimitResetTest
[PASS] test_PhaseChange_DoesNotResetPerAddressState() (gas: 268483)
Traces:
[268483] BrokenLimitResetTest::test_PhaseChange_DoesNotResetPerAddressState()
├─ [0] VM::deal(ECRecover: [0x0000000000000000000000000000000000000001], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::startPrank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [158624] PoolSwapTest::swap{value: 1000000000000000}(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), TestSettings({ takeClaims: false, settleUsingBurn: false }), 0x)
│ ├─ [152820] PoolManager::unlock(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ ├─ [150561] PoolSwapTest::unlockCallback(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [2552] MockERC20::balanceOf(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ │ │ │ └─ ← [Return] 1000000000000000000000 [1e21]
│ │ │ ├─ [2552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 10000000000000000000 [1e19]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [110236] PoolManager::swap(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ ├─ [76095] TokenLaunchHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ │ ├─ [2378] PoolManager::extsload(0x150354b2d6d2bc502e6f774e51ffab4638eb0768330b66d2c8d382c1598ebd1c) [staticcall]
│ │ │ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000b4fb077cf724ed8bd3
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4194304 [4.194e6]
│ │ │ │ ├─ emit Swap(id: 0x6109270a1f35ae82fb136621355ea4a2980baebc2e0a4c5b44084a1faf3b35f9, sender: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount0: -1000000000000000 [-1e15], amount1: 999999700464594 [9.999e14], sqrtPriceX96: 79228138782624522581392935400 [7.922e28], liquidity: 3338502497096994491347 [3.338e21], tick: -1, fee: 0)
│ │ │ │ └─ ← [Return] -340282366920938463463374607431768211455000000299535406 [-3.402e53]
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0xfffffffffffffffffffffffffffffffffffffffffffffffffffc72815b398000
│ │ │ ├─ [552] MockERC20::balanceOf(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ │ │ │ └─ ← [Return] 1000000000000000000000 [1e21]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 10000000000000000000 [1e19]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x00000000000000000000000000000000000000000000000000038d7e92ebf3d2
│ │ │ ├─ [1425] PoolManager::settle{value: 1000000000000000}()
│ │ │ │ └─ ← [Return] 1000000000000000 [1e15]
│ │ │ ├─ [10243] PoolManager::take(MockERC20: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], ECRecover: [0x0000000000000000000000000000000000000001], 999999700464594 [9.999e14])
│ │ │ │ ├─ [8511] MockERC20::transfer(ECRecover: [0x0000000000000000000000000000000000000001], 999999700464594 [9.999e14])
│ │ │ │ │ ├─ emit Transfer(from: PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: ECRecover: [0x0000000000000000000000000000000000000001], amount: 999999700464594 [9.999e14])
│ │ │ │ │ └─ ← [Return] true
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e92ebf3d2
│ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e92ebf3d2
│ └─ ← [Return] -340282366920938463463374607431768211455000000299535406 [-3.402e53]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [604] TokenLaunchHook::addressSwappedAmount(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ └─ ← [Return] 1000000000000000 [1e15]
├─ [0] VM::roll(102)
│ └─ ← [Return]
├─ [0] VM::startPrank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [66671] PoolSwapTest::swap{value: 1000000000000000}(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), TestSettings({ takeClaims: false, settleUsingBurn: false }), 0x)
│ ├─ [63367] PoolManager::unlock(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ ├─ [61108] PoolSwapTest::unlockCallback(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [552] MockERC20::balanceOf(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ │ │ │ └─ ← [Return] 1000000999999700464594 [1e21]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 9999000000299535406 [9.999e18]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [32883] PoolManager::swap(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ ├─ [15448] TokenLaunchHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4194304 [4.194e6]
│ │ │ │ ├─ emit Swap(id: 0x6109270a1f35ae82fb136621355ea4a2980baebc2e0a4c5b44084a1faf3b35f9, sender: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount0: -1000000000000000 [-1e15], amount1: 999999101394141 [9.999e14], sqrtPriceX96: 79228115050998924497711328286 [7.922e28], liquidity: 3338502497096994491347 [3.338e21], tick: -1, fee: 0)
│ │ │ │ └─ ← [Return] -340282366920938463463374607431768211455000000898605859 [-3.402e53]
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0xfffffffffffffffffffffffffffffffffffffffffffffffffffc72815b398000
│ │ │ ├─ [552] MockERC20::balanceOf(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ │ │ │ └─ ← [Return] 1000000999999700464594 [1e21]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 9999000000299535406 [9.999e18]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x00000000000000000000000000000000000000000000000000038d7e6f36dcdd
│ │ │ ├─ [1425] PoolManager::settle{value: 1000000000000000}()
│ │ │ │ └─ ← [Return] 1000000000000000 [1e15]
│ │ │ ├─ [4643] PoolManager::take(MockERC20: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], ECRecover: [0x0000000000000000000000000000000000000001], 999999101394141 [9.999e14])
│ │ │ │ ├─ [2911] MockERC20::transfer(ECRecover: [0x0000000000000000000000000000000000000001], 999999101394141 [9.999e14])
│ │ │ │ │ ├─ emit Transfer(from: PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: ECRecover: [0x0000000000000000000000000000000000000001], amount: 999999101394141 [9.999e14])
│ │ │ │ │ └─ ← [Return] true
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e6f36dcdd
│ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e6f36dcdd
│ └─ ← [Return] -340282366920938463463374607431768211455000000898605859 [-3.402e53]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [247] TokenLaunchHook::currentPhase() [staticcall]
│ └─ ← [Return] 2
├─ [604] TokenLaunchHook::addressSwappedAmount(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ └─ ← [Return] 2000000000000000 [2e15]
└─ ← [Return]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 628.47ms (709.50µs CPU time)
Ran 1 test suite in 631.27ms (628.47ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Use an epoch‑based reset (lazy clearing), so changing phases increments an epoch counter, and each user’s state is reset on first use in the new epoch.

  • Call _touchUser(sender) at the top of _beforeSwap after computing/possibly updating currentPhase.

- function _resetPerAddressTracking() internal {
- addressSwappedAmount[address(0)] = 0;
- addressLastSwapBlock[address(0)] = 0;
- }
+ // New state to support lazy per-user resets
+ uint256 internal trackingEpoch; // increments on phase change
+ mapping(address => uint256) internal userEpoch; // last epoch in which we saw the user
+
+ /// @dev Bump global epoch on phase change (cheap O(1) "reset")
+ function _resetPerAddressTracking() internal {
+ unchecked { trackingEpoch++; }
+ }
+
+ /// @dev Ensure user's counters are from the current epoch; otherwise reset them.
+ function _touchUser(address user) internal {
+ if (userEpoch[user] != trackingEpoch) {
+ addressSwappedAmount[user] = 0;
+ addressLastSwapBlock[user] = 0;
+ userEpoch[user] = trackingEpoch;
+ }
+ }
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
...
if (newPhase != currentPhase) {
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
+ // Ensure per-address state is fresh for the current epoch/phase
+ _touchUser(sender);
...
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!