Stable2LUT1::getRatiosFromPriceLiquidity - In extreme cases, updateReserve will start breaking
Lines of code
Vulnerability details
Impact
This is one of the edge cases in getRatiosFromPriceLiquidity:
solif (price < 0.001083e6) { revert("LUT: Invalid price"); } else { return PriceData( 0.27702e6, 0, 9.646293093274934449e18, 0.001083e6, 0, 2000e18, 1e18); }
The range where the price can be isvery large, between 0.27702e6 (highPrice) and 0.001083e6 (lowPrice).
If we are closer to the lowPrice, then pd.currentPrice is set to pd.lutData.lowPrice.
solif (pd.lutData.highPrice - pd.targetPrice > pd.targetPrice - pd.lutData.lowPrice) { // targetPrice is closer to lowPrice. scaledReserves[j] = scaledReserves[i] * pd.lutData.lowPriceJ / pd.lutData.precision; // set current price to lowPrice. pd.currentPrice = pd.lutData.lowPrice; }
In this case, the current price is much smaller than the target price and because of this, updateReserve will have to do a very large correction of the reserve in order to converge on the target price.
Because of these large corrections, the reserve will become so small, that the next time the reserve has to be updated the amount that it has to be reduced by will be larger than the reserve itself, resulting in a panic underflow, bricking the function.
This revert happens because of two reasons:
- Because of the large range that the price can be in, thus using such a small
lowPriceaspd.currentPricemakes the difference betweenpd.targetPriceso large that the corrections become very large and underflow before convergence can be achieved. lowPriceJis extremely large, in this case it's2000e18, which makes the step size very large and thus the code will attempt extreme corrections of the reserve, resulting in a revert.
It's important to note, that even if lowPriceJ is significantly reduced, the function won't revert, but it will never converge on a price and this can only be fixed by reducing the gap between highPrice and lowPrice effectively reducing the range for that specific price.
We wanted to showcase both issues and that even if the underflow (lowPriceJ issue) is resolved that the estimated ranges still are too large, thus making the price impossible to converge.
The below tests showcase both the original code (underflow) and how reducing lowPriceJ doesn't fix the real problem.
Proof of Concept
Test case for underflow:
Paste the following inside BeanstalkStable2LiquidityTest and run forge test --match-contract BeanstalkStable2LiquidityTest --mt test_calcReserveAtRatioLiquiditExtreme -vvvv
solfunction test_calcReserveAtRatioLiquiditExtreme() public view { uint256[] memory reserves = new uint256[](2); reserves[0] = 1e18; reserves[1] = 1e18; uint256[] memory ratios = new uint256[](2); ratios[0] = 8; ratios[1] = 1; uint256 reserve0 = _f.calcReserveAtRatioLiquidity(reserves, 0, ratios, data); }
Logs:
jsxTarget Price: 125000 Original Current Price: 1083 -------------------------------- Reserve before step: 2000000000000000000000 -------------------------------- Reserve: 2000000000000000000000 Amount to decrease by: 893822359084720968727 Current Price at iteration 0 1993 Reserve: 1106177640915279031273 Amount to decrease by: 887258462712414537152 Current Price at iteration 1 10817 Reserve: 218919178202864494121 Amount to decrease by: 823610307119851952292
As you can see the code attempts massive corrections to the reserve in order to achieve convergence, but the amount to decrease the reserve is too large and the function underflows.
Test case when lowering lowPriceJ.
Run the same test as before, but change lowPriceJ to 10e18 for example purposes:
solif (price < 0.001083e6) { revert("LUT: Invalid price"); } else { return PriceData( 0.27702e6, 0, 9.646293093274934449e18, 0.001083e6, 0, -> 10e18, 1e18); }
Logs:
solTarget Price: 125000 Original Current Price: 1083 -------------------------------- Reserve before step: 10000000000000000000 -------------------------------- Reserve: 10000000000000000000 Amount to decrease by: 158841687633952488 Current Price at iteration 0 272070 ... Reserve: 20736004517896372313 Amount to increase by: 8588323693661738 Current Price at iteration 254 131644 END OF NEWTON's METHOD
The logs are compacted as they are very large, but the idea is this:
- Target price is again much larger than current price.
- During the first correction, the price moves by a very large amount, making it much larger than target price.
- Now
updateReserveshas to do corrections in the other direction (increase the reserve), so that the prices can converge on the target price, but this never happens, as Newton's method ends after 255 iterations and leaves the loop. calcReserveAtRatioLiquiditywill return 0, asreserveis never initialized anywhere, effectively making the function useless.
Tools Used
Manual Review Foundry
Recommended Mitigation Steps
To completely fix both issues, lowPriceJ must be lowered and the estimated range must be narrowed down.
Assessed type
DoS
