Root + Impact
Description
The _resetPerAddressTracking() function is intended to clear user trading history when transitioning between launch phases (e.g., from Phase 1 to Phase 2). However, the implementation contains a critical flaw: it only resets the entries for address(0) instead of iterating through and resetting actual user addresses. In Solidity, it is impossible to iterate over all keys in a mapping, and the current implementation effectively resets nothing. Consequently, all users retain their accumulated swap amounts from previous phases, preventing them from accessing fresh allowances in new phases and potentially causing permanent penalties.
function _resetPerAddressTracking() internal {
addressSwappedAmount[address(0)] = 0;
addressLastSwapBlock[address(0)] = 0;
}
Risk
Likelihood:
Impact:
Users entering a new phase still carry accumulated swap amounts from previous phases. This means:
Users cannot utilize their new phase allowance (e.g., 3% in Phase 2)
Users may be immediately penalized upon phase entry if they exceeded previous phase limits
The anti-bot protection mechanism becomes a punitive trap for legitimate users
Proof of Concept
The following PoC demonstrates that after a phase transition, user tracking data persists despite the _resetPerAddressTracking() call. We simulate a user trading in Phase 1, then advancing to Phase 2, and verify that their accumulated amount was NOT reset.
function test_BrokenPhaseResetPoC() public {
vm.deal(address(this), 10 ether);
swapRouter.swap{value: 0.001 ether}(
key,
SwapParams({
zeroForOne: true,
amountSpecified: -0.001 ether,
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
ZERO_BYTES
);
uint256 trappedAmount = hook.addressSwappedAmount(address(swapRouter));
assertGt(trappedAmount, 0, "Phase 1 amount not tracked");
vm.roll(block.number + phase1Duration + 1);
swapRouter.swap{value: 0.0001 ether}(
key,
SwapParams({
zeroForOne: true,
amountSpecified: -0.0001 ether,
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
ZERO_BYTES
);
assertEq(hook.currentPhase(), 2, "Phase did not transition");
uint256 amountInPhase2 = hook.addressSwappedAmount(address(swapRouter));
assertEq(amountInPhase2, trappedAmount + 0.0001 ether, "Tracking was correctly reset (WHICH IS UNEXPECTED)");
console.log("Bug Confirmed: Swap amounts carried over from Phase 1 to Phase 2");
console.log("Phase 1 Amount:", trappedAmount);
console.log("Current Amount in Phase 2:", amountInPhase2);
}
Recommended Mitigation
The fundamental issue is that Solidity mappings cannot be iterated to reset all entries. Instead of attempting to reset user data, track swap amounts per phase using a nested mapping structure. This allows each phase to have independent accounting without requiring any reset logic.
mapping(uint256 => mapping(address => uint256)) public phaseAddressSwappedAmount;
mapping(uint256 => mapping(address => uint256)) public phaseAddressLastSwapBlock;
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal override returns (bytes4, BeforeSwapDelta, uint24)
{
if (newPhase != currentPhase) {
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
uint256 userSwapped = phaseAddressSwappedAmount[currentPhase][sender];
uint256 userLastBlock = phaseAddressLastSwapBlock[currentPhase][sender];
if (userLastBlock > 0 && block.number - userLastBlock < phaseCooldown) {
applyPenalty = true;
}
if (!applyPenalty && userSwapped + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
phaseAddressSwappedAmount[currentPhase][sender] += swapAmount;
phaseAddressLastSwapBlock[currentPhase][sender] = block.number;
}