Assessment reports>GTE>Critical findings>Purchase token with zero USDC repeat
Category: Coding Mistakes

Purchase token with zero USDC repeat

Critical Severity
High Impact
High Likelihood

Description

Function _getAverageCostInY performs the actual trade on the curve for an amount of tokens, returning the cost in USDC scaled.

First, remainingDistance, isForward, and stepRemaining are defined as below:

uint256 remainingDistance = fMath.dist(x_0, x_1) // x_1 - x_0. influenced by the buy amount.
bool isForward = x_1 >= x_0; // In buy process, most case follow: x_1 >= x_0
uint256 finalX = lastStep.x_rel;
stepRemaining = finalX > 0 ? (isForward ? currentStep.dx - finalX : finalX) : currentStep.dx;

When the buy amount is sufficiently small, remainingDistance becomes less than stepRemaining. In this case, the execution will pass through the conditional statement below,

if (remainingDistance <= stepRemaining) {
    // Final partial step
    uint256 y = _calculateY(currentStep, remainingDistance);
    totalY += y;
    // [...]
    if (remainingDistance == stepRemaining) {
        finalIdx = isForward ? uint16(finalIdx + 1) : finalIdx > 0 ? uint16(finalIdx - 1) : 0;
    }
    // [...]
    break;
}

and the _calculateY return value will be totalY, the average value Y.

This value will be divided with PRECISION_MULTIPLIER and QUOTE_SCALING. Consequently, if the value of Y at this point is less than PRECISION_MULTIPLIER * QUOTE_SCALING, it will be rounded down to 0. This enables the purchase of tokens for zero USDC.

During this process, finalIdx is only updated when remainingDistance equals stepRemaining. Consequently, if these values are inconsistent, finalIdx remains unchanged.

Subsequently, newLastStep is returned as shown below. This return value will subsequently alter currentStep.

newLastStep = LastStepData({
    i: finalIdx,
    x_rel: remainingDistance < stepRemaining
        ? (isForward ? remainingDistance : stepRemaining - remainingDistance)
        : 0,
    x_abs: uint256(int256(lastStep.x_abs) + totalStepDeltaX)
});

When remainingDistance is less than stepRemaining, during a buy operation, since isForward is true, x_rel becomes stepRemaining.

If the same amount is subsequently bought again, the value of remainingDistance remains unchanged, while stepRemaining becomes currentStep.dx - lastStep.x_rel(=remainingDistance).

In this case as well, if remainingDistance is less than stepRemaining, the above process repeats, potentially allowing for infinite repetition of this cycle.

Impact

To execute this attack, the quantity of LaunchToken that can be purchased at once is approximately 1e17, which is significantly small compared to the total supply of 1e27. However, this method enables unlimited initial purchases at zero USDC. This vulnerability has substantial potential impact if the token price increases due to factors such as listing on Uniswap pools or similar events.

Recommendations

Either enhance the division policy to prevent losses or implement precise step updates. While blocking purchases at zero USDC is another potential solution.

Remediation

This issue has been acknowledged by Liquid Labs, Inc., and a fix was implemented in commit df320180.

Liquid Labs, Inc. provided the following response:

The commit goes with Zellic's second recommendation of cleaning the amountIn using (amountIn / 1e18) * 1e18. This prevents buys that are small enough from costing 0.

However, we decided for the next audit, which covers SimpleLaunchpad and SimpleBondingCurve (less precision loss prone implementations of this curve) to go with Zellic's first recommendation, which is to revert if the cost is 0

Zellic © 2025Back to top ↑