Vanguard

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

Inaccurate user limit calculation inside getUserRemainingLimit()

Author Revealed upon completion

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) {
// FIXED: Check actual current phase, not stored state
uint256 phase = getCurrentPhase();
if (phase == 3) return type(uint256).max;
uint256 phaseLimitBps = phase == 1 ? phase1LimitBps : phase2LimitBps;
uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000; //<@ the limit will not be valid if liquidity has changed
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:

  • It will always occurs whenever protocol is using current code

Impact:

  • Inaccuracy information from getter function

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];
}

Support

FAQs

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

Give us feedback!