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

0 stars 0 forks source link

For extreme ratios getRatiosFromPriceSwap will return data for which is impossible to converge into a reserve #34

Open c4-bot-3 opened 3 months ago

c4-bot-3 commented 3 months ago

Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/StableLUT/Stable2LUT1.sol#L750-L758

Vulnerability details

Impact

Basin team has implemented look up table to fetch pre-calculated values, which are close to the targetPrice to decrease the complexity of calcReserveAtRatioSwap and calcReserveAtRatioLiquidity functions. Stable2LUT1 has a function getRatiosFromPriceSwap, which returns PriceData struct based on provided price. We can see that in case of super extreme price, the function will revert with LUT: Invalid price, but there is also support for extreme prices, which we assume should work correctly, when such situation occurs.

We will investigate an edge case, which is present, when we enter if (price < 0.213318e6) and price is above 0.001083e6. Is such situation, function will return the following struct:

PriceData(
0.213318e6, // highPrice
0.188693329162796575e18,
2.410556040105746423e18,
0.001083e6, // lowPrice                       
0.005263157894736842e18,                      
10.522774272309483479e18,
1e18
);

If you can notice we have a large gap between highPrice and lowPrice and more precisely.

The following results in large jump in reserves calculation when updateReserve which leads to skipping the target price sequentially, which moves away pd.currentPrice until we exit the for loop, which returns 0.

Here is an estimation of the impact based on the PoC which is below.: For a ratio ~ 1:4.6 of the price of the tokens

Proof of Concept

Place the following test inside /test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol and run it with forge test --mt "test_calcReserveAtRatioSwapSkipTarget" -vv

    function test_calcReserveAtRatioSwapSkipTarget() public view {
        uint256[] memory reserves = new uint256[](2);
        reserves[0] = 1e18;
        reserves[1] = 1e18;
        uint256[] memory ratios = new uint256[](2);
        ratios[0] = 4202;
        ratios[1] = 19811;

        // 4202 * 1e6 / 19811  = 212104 = 0.212 / 1  = 1 : 4.6
        //                                0.04  / 1  = 1 : 25    

        uint256 reserve1 = _f.calcReserveAtRatioSwap(reserves, 1, ratios, data);
        //console.log("Reserves 1 :", reserve1);
    }

You can further add console.log inside the for loop of calcReserveAtRatioSwap and log pd.currentPrice on each update. You can notice that each time the current price is further away from the target. Here are logs:

Logs:
  We want  212104
  Lud Data High:  213318
  Lud Data Low:  1083
  Max step size:  1858346112180059079
  Scaled reserves when closest J:  2421185918213441156
  Scaled reserves when closest I:  188693329162796575
  Rate at iteration  0  is  209986
  Rate at iteration  1  is  215834
  Rate at iteration  2  is  205646
  Rate at iteration  3  is  223618
  Rate at iteration  4  is  192647
  Rate at iteration  5  is  247954
  Rate at iteration  6  is  156345
  Rate at iteration  7  is  320536
  Rate at iteration  8  is  83844
  Rate at iteration  9  is  409344
  Rate at iteration  10  is  42030
  Rate at iteration  11  is  291810
  Rate at iteration  12  is  106911
  Rate at iteration  13  is  401234
  Rate at iteration  14  is  44576
  Rate at iteration  15  is  306730
  Rate at iteration  16  is  94144
  Rate at iteration  17  is  409601
  Rate at iteration  18  is  41952
  Rate at iteration  19  is  291337
  Rate at iteration  20  is  107346
  Rate at iteration  21  is  400812
...

Tools Used

Recommended Mitigation Steps

Recalculate the PriceData for the given case and consider narrowing down the price diff.

Assessed type

Invalid Validation

nevillehuang commented 3 months ago

Could be related to #119