Vanguard

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

Broken Phase Reset Logic Causes Persistent Swap Limits

Author Revealed upon completion

ROOT + IMPACT

## Description

The `TokenLaunchHook` is designed to reset user swap limits when the protocol transitions from Phase 1 to Phase 2. This is intended to give users a "fresh start" with the new phase's limits.

However, the `_resetPerAddressTracking` function is implemented incorrectly. In Solidity, deleting a key in a mapping (like `addressSwappedAmount[address(0)] = 0`) does **not** clear the entire mapping. It only clears that specific key. Consequently, all user swap history from Phase 1 persists into Phase 2.

**Code at Fault:**

```solidity

function _resetPerAddressTracking() internal {

// @audit Only clears the zero address. All actual user data remains.

addressSwappedAmount[address(0)] = 0;

addressLastSwapBlock[address(0)] = 0;

}

```

## Risk

**Likelihood**: High (Certainty)

* This logic runs on every phase transition. The bug is intrinsic to how Solidity mappings work.

**Impact**: High

* **Denial of Service:** Users active in Phase 1 will enter Phase 2 with their limits already partially or fully consumed.

* **Broken Core Logic:** The concept of "Phased Limits" is fundamentally broken as usage accumulates globally rather than per-phase.

## Proof of Concept

Add this function to your `TestTokenLaunchHook` contract. It uses the exact same variable naming and struct initialization as your working tests.

```solidity

function test_PoC_BrokenLimitReset_UserUsageCarriesOver() public {

// 1. SETUP: Give user1 funds

vm.deal(user1, 10 ether);

// Define swap params (Selling 0.05 ETH)

// Phase 1 Limit is 1% of 10 ETH = 0.1 ETH. We use 50% of it.

uint256 swapAmountPhase1 = 0.05 ether;

SwapParams memory params = SwapParams({

zeroForOne: true,

amountSpecified: -int256(swapAmountPhase1),

sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1

});

PoolSwapTest.TestSettings memory testSettings =

PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});

// 2. ACTION: User1 swaps in Phase 1

vm.startPrank(user1);

swapRouter.swap{value: swapAmountPhase1}(key, params, testSettings, ZERO_BYTES);

vm.stopPrank();

// Check Phase 1 usage is recorded

// Note: Checking 'swapRouter' address due to the known Router DoS bug

uint256 usageAfterPhase1 = antiBotHook.addressSwappedAmount(address(swapRouter));

assertEq(usageAfterPhase1, swapAmountPhase1, "Phase 1 usage should be recorded");

// 3. TRANSITION: Move to Phase 2

// Phase 1 duration is 100 blocks. We roll past it.

vm.roll(block.number + phase1Duration + 1);

// 4. ACTION: User1 swaps in Phase 2

// This triggers _beforeSwap -> newPhase detected -> _resetPerAddressTracking called

uint256 swapAmountPhase2 = 0.01 ether;

// Update params for the new amount

params.amountSpecified = -int256(swapAmountPhase2);

vm.startPrank(user1);

swapRouter.swap{value: swapAmountPhase2}(key, params, testSettings, ZERO_BYTES);

vm.stopPrank();

// 5. ASSERTION: Check if usage was reset

uint256 usageTotal = antiBotHook.addressSwappedAmount(address(swapRouter));

// EXPECTED (If Reset worked): usageTotal == swapAmountPhase2 (0.01)

// ACTUAL (Bug): usageTotal == swapAmountPhase1 + swapAmountPhase2 (0.06)

console.log("Expected Usage if Reset worked:", swapAmountPhase2);

console.log("Actual Usage (Accumulated): ", usageTotal);

assertGt(usageTotal, swapAmountPhase2, "FAIL: Phase 1 usage was not cleared!");

assertEq(usageTotal, swapAmountPhase1 + swapAmountPhase2, "FAIL: Limits are cumulative across phases");

}

```

## Recommended Mitigation

Since mappings cannot be cleared in O(1) time in Solidity, you must include the **Phase ID** in the mapping key. This effectively creates a new, empty limits bucket for every phase.

**1. Update the Mapping:**

```solidity

// Change mapping(address => uint256) to:

mapping(uint256 => mapping(address => uint256)) public phaseAddressSwappedAmount;

```

**2. Update `_beforeSwap`:**

```solidity

// Remove _resetPerAddressTracking() call entirely.

// access data using currentPhase

if (!applyPenalty && phaseAddressSwappedAmount[currentPhase][sender] + swapAmount > maxSwapAmount) {

// ...

}

phaseAddressSwappedAmount[currentPhase][sender] += swapAmount;

```

Support

FAQs

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

Give us feedback!