code-423n4 / 2024-07-basin-validation

0 stars 0 forks source link

Incorrect decimal handling in Newton estimation mechanism of `calcReserveAtRatioSwap` and `calcReserveAtRatioLiquidity` functions of `Stable2` contract #49

Open c4-bot-2 opened 3 months ago

c4-bot-2 commented 3 months ago

Lines of code

https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L173-L239 https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L246-L304 https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L338-L351

Vulnerability details

Impact

In the Newton estimation mechanism of the calcReserveAtRatioSwap and calcReserveAtRatioLiquidity functions of the Stable2 contract, incorrect decimal handling of pd.currentPrice can cause various issues, such as not converging exactly to the limit value.

Proof of Concept

The calcReserveAtRatioSwap and calcReserveAtRatioLiquidity functions of the Stable2 contract get the approximate closing ratio from the target price and perform Newton's method to converge to the reserve.

    for (uint256 k; k < 255; k++) {
        scaledReserves[j] = updateReserve(pd, scaledReserves[j]);

        // calculate scaledReserve[i]:
        scaledReserves[i] = calcReserve(scaledReserves, i, lpTokenSupply, abi.encode(18, 18));
        // calc currentPrice:
-->     pd.currentPrice = _calcRate(scaledReserves, i, j, lpTokenSupply);

        // check if new price is within 1 of target price:
-->     if (pd.currentPrice > pd.targetPrice) {
            if (pd.currentPrice - pd.targetPrice <= PRICE_THRESHOLD) {
                return scaledReserves[j] / (10 ** (18 - decimals[j]));
            }
        } else {
            if (pd.targetPrice - pd.currentPrice <= PRICE_THRESHOLD) {
                return scaledReserves[j] / (10 ** (18 - decimals[j]));
            }
        }
    }

The root of the issue is that here pd.currentPrice is calculated as a decimal of 18, and pd.targetPrice is calculated as 6.

pd.targetPrice is calculated as follows:

    // calc target price with 6 decimal precision:
    pd.targetPrice = scaledRatios[i] * PRICE_PRECISION / scaledRatios[j];  // 6 decimal

Meanwhile, pd.currentPrice is calculated as follows by the _calcRate function.

    function _calcRate(
        uint256[] memory reserves,
        uint256 i,
        uint256 j,
        uint256 lpTokenSupply
    ) internal view returns (uint256 rate) {
        // add 1e6 to reserves:
        uint256[] memory _reserves = new uint256[](2);
        _reserves[i] = reserves[i];
        _reserves[j] = reserves[j] + PRICE_PRECISION;

        // calculate rate:
-->     rate = _reserves[i] - calcReserve(_reserves, i, lpTokenSupply, abi.encode(18, 18));  // 18 decmal
    }

As you can see, pd.currentPrice is calculated as a decimal of 18, and pd.targetPrice is calculated as 6.

As a result, the calcReserveAtRatioSwap and calcReserveAtRatioLiquidity functions compare pd.currentPrice, which is calculated as 18 decimals, with pd.targetPrice, which is calculated as 6 decimals, which does not work as intended.

Tools Used

Manual Review

Recommended Mitigation Steps

It is recommended to modify the Newton Estimate Mechanism of the calcReserveAtRatioSwap and calcReserveAtRatioLiquidity functions as follows:

    for (uint256 k; k < 255; k++) {
        scaledReserves[j] = updateReserve(pd, scaledReserves[j]);
        // calculate new price from reserves:
        pd.currentPrice = calcRate(scaledReserves, i, j, abi.encode(18, 18));

+++     pd.currentPrice /= 10 ** 12;

        // check if new price is within PRICE_THRESHOLD:
        if (pd.currentPrice > pd.targetPrice) {
            if (pd.currentPrice - pd.targetPrice <= PRICE_THRESHOLD) {
                return scaledReserves[j] / (10 ** (18 - decimals[j]));
            }
        } else {
            if (pd.targetPrice - pd.currentPrice <= PRICE_THRESHOLD) {
                return scaledReserves[j] / (10 ** (18 - decimals[j]));
            }
        }
    }

Assessed type

Decimal