Inaccurate user limit calculation inside getUserRemainingLimit()
Description
During calculation to determine the user limit inside getUserRemainingLimit() on TokenLaunchHook.sol contract, the limit is calculated based on initial liquidity. The logic will be broken if there is additional liquidity is added after that, thus user limit should change also.
The function also will not be working if swap never happened.
function getUserRemainingLimit(address user) public view returns (uint256) {
uint256 phase = getCurrentPhase();
if (phase == 3) return type(uint256).max;
uint256 phaseLimitBps = phase == 1 ? phase1LimitBps : phase2LimitBps;
uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
if (addressSwappedAmount[user] >= maxSwapAmount) return 0;
return maxSwapAmount - addressSwappedAmount[user];
}
Risk
Minimal risk as it is only used for getter function
Inaccuracy information from getter function
Likelihood:
Impact:
Proof of Concept
Add bellow function into existing test suite TokenLaunchHook.t.sol
Run test suite with forge test --match-test test_GetCurrentLimit -vvv
function test_getCurrentLimit() public {
assertEq(antiBotHook.getCurrentPhase(), 1, "Should start in phase 1");
vm.deal(user1, 10 ether);
uint128 initialliquidity = StateLibrary.getLiquidity(manager, key.toId());
console.log("Initial liquidity: ", initialliquidity);
uint256 currentLimit = antiBotHook.getUserRemainingLimit(user1);
console.log("Current limit at start:", currentLimit);
console.log("Should be 0 as no swaps have been made yet");
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(1 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
vm.prank(user1);
swapRouter.swap{value: 1 ether}(key, params, testSettings, ZERO_BYTES);
console.log("Swap 1 ether");
uint256 ethToAdd = 5 ether;
uint160 sqrtPriceAtTickUpper = TickMath.getSqrtPriceAtTick(60);
uint128 liquidityDelta = LiquidityAmounts.getLiquidityForAmount1(
TickMath.MIN_SQRT_PRICE, sqrtPriceAtTickUpper, ethToAdd);
modifyLiquidityRouter.modifyLiquidity{value: ethToAdd}(
key,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liquidityDelta)),
salt: bytes32(0)
}),
ZERO_BYTES
);
console.log("Add liquidity: ", liquidityDelta);
uint128 liquidity = StateLibrary.getLiquidity(manager, key.toId());
uint256 limitphase1 = liquidity * 100 /10000;
console.log("Current liquidity: ", liquidity);
console.log("Phase 1 limit should be (liquidity * 100 / 10_000)");
uint256 remainingLimit1 = antiBotHook.getUserRemainingLimit(user1);
console.log("Remaining limit for phase 1:", remainingLimit1);
assertEq(remainingLimit1, (limitphase1 - 1e18));
}
Log output :
Ran 1 test for test/TokenLaunchHookUnit.t.sol:TestTokenLaunchHook
[FAIL: assertion failed: 33385024970969944913 != 32434875203222149374] test_getCurrentLimit() (gas: 320664)
Logs:
Initial liquidity: 3338502497096994491347
Current limit at start: 0
Should be 0 as no swaps have been made yet
Swap 1 ether
Add liquidity: 4985023225220446095
Current liquidity: 3343487520322214937442
Phase 1 limit should be (liquidity * 100 / 10_000)
Remaining limit for phase 1: 33385024970969944913
Recommended Mitigation
Use current liquidity on calculation instead of initial liquidity
function getUserRemainingLimit(address user) public view returns (uint256) {
// FIXED: Check actual current phase, not stored state
uint256 phase = getCurrentPhase();
if (phase == 3) return type(uint256).max;
uint256 phaseLimitBps = phase == 1 ? phase1LimitBps : phase2LimitBps;
+ uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
+ // state variable can be renamed to "liquidityAmt" to reflect the change
+ initialLiquidity = uint256(liquidity);
uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
if (addressSwappedAmount[user] >= maxSwapAmount) return 0;
return maxSwapAmount - addressSwappedAmount[user];
}