Vanguard

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

Broken _resetPerAddressTracking fails to clear user limits during phase transitions

Author Revealed upon completion

Description

  • Normal Behavior: The protocol implements a phased launch where swap limits and cooldowns are intended to be reset when transitioning to a new phase (e.g., Phase 1 to Phase 2). This ensures users can trade against the new, relaxed limits of the incoming phase.

  • Specific Issue: The _resetPerAddressTracking() function, which is called during a phase update, only clears the storage for address(0). Since EVM mappings are not iterable, data for all actual users remains in storage, meaning their Phase 1 activity persists into Phase 2.

// src/TokenLaunchHook.sol
function _resetPerAddressTracking() internal {
// @> Root Cause: Only resets the zero address.
// @> Mappings like addressSwappedAmount cannot be cleared globally like this.
addressSwappedAmount[address(0)] = 0;
addressLastSwapBlock[address(0)] = 0;
}

Risk

Likelihood:

  • Logical Failure: The state management is fundamentally broken for any protocol attempting a multi-phased state transition while relying on non-iterable mappings.

  • Certainty: This occurs every time a launch moves from Phase 1 to Phase 2.

Impact:

  • Persistent Penalization: A user who traded up to their limit in Phase 1 will enter Phase 2 already "over the limit" because their Phase 1 usage was never cleared.

  • Broken Phase Logic: The "relaxed limits" (e.g., 5% in Phase 2 vs 1% in Phase 1) are undermined because the user starts Phase 2 with 1% already filled, rather than at zero.

Proof of Concept

This test confirms that the _resetPerAddressTracking function fails to clear state. We compare the user's recorded usage before and after a phase transition roll. The fact that usageP2 is greater than usageP1 after a "reset" proves the vulnerability.

function test_Vulnerability_NoStateReset() public {
// User swaps in Phase 1
vm.prank(user1);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
uint256 usageP1 = antiBotHook.addressSwappedAmount(address(swapRouter));
// Transition to Phase 2 (simulated by rolling blocks)
vm.roll(block.number + phase1Duration + 1);
// Trigger the phase update swap
vm.prank(user1);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
// FAILURE: Usage from P1 was NOT cleared and is added to P2 usage
uint256 usageP2 = antiBotHook.addressSwappedAmount(address(swapRouter));
assertGt(usageP2, usageP1);
}

Recommended Mitigation

Since it is impossible to iterate through and clear a mapping, the recommended pattern is to make the limit tracking phase-specific by adding the phase number to the mapping key.

- mapping(address => uint256) public addressSwappedAmount;
+ mapping(uint256 phase => mapping(address => uint256)) public addressSwappedAmount;
// In _beforeSwap
- addressSwappedAmount[sender] += swapAmount;
+ addressSwappedAmount[currentPhase][sender] += swapAmount;

Support

FAQs

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

Give us feedback!