Incorrect decimal handling in Newton estimation mechanism of `calcReserveAtRatioSwap` and `calcReserveAtRatioLiquidity` functions of `Stable2` contract #49
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.
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]));
}
}
}
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
andcalcReserveAtRatioLiquidity
functions of theStable2
contract, incorrect decimal handling ofpd.currentPrice
can cause various issues, such as not converging exactly to the limit value.Proof of Concept
The
calcReserveAtRatioSwap
andcalcReserveAtRatioLiquidity
functions of theStable2
contract get the approximate closing ratio from the target price and perform Newton's method to converge to the reserve.The root of the issue is that here
pd.currentPrice
is calculated as a decimal of 18, andpd.targetPrice
is calculated as 6.pd.targetPrice
is calculated as follows:Meanwhile,
pd.currentPrice
is calculated as follows by the_calcRate
function.As you can see,
pd.currentPrice
is calculated as a decimal of 18, andpd.targetPrice
is calculated as 6.As a result, the
calcReserveAtRatioSwap
andcalcReserveAtRatioLiquidity
functions comparepd.currentPrice
, which is calculated as 18 decimals, withpd.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
andcalcReserveAtRatioLiquidity
functions as follows:Assessed type
Decimal