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:
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
We have a targetPrice of 212104 (0.21)
We end up with reserves for a price 44957 (0.04), which is ~ X5 less than the requested amount
As result we receive 3623929482258792273, instead of 2421185918213441156, which is a big difference
NOTE that we don't return the compromised data, but this is the calculated value on the last iteration
Proof of Concept
Place the following test inside /test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol and run it with forge test --mt "test_calcReserveAtRatioSwapSkipTarget" -vv
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
Fuzz Testing
Manual Review
Recommended Mitigation Steps
Recalculate the PriceData for the given case and consider narrowing down the price diff.
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 ofcalcReserveAtRatioSwap
andcalcReserveAtRatioLiquidity
functions. Stable2LUT1 has a functiongetRatiosFromPriceSwap
, which returnsPriceData
struct based on providedprice
. We can see that in case of super extreme price, the function will revert withLUT: 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 above0.001083e6
. Is such situation, function will return the following struct:If you can notice we have a large gap between
highPrice
andlowPrice
and more precisely.The following results in large jump in reserves calculation when
updateReserve
which leads to skipping the target price sequentially, which moves awaypd.currentPrice
until we exit thefor
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
targetPrice
of 212104 (0.21)44957
(0.04), which is ~ X5 less than the requested amount3623929482258792273
, instead of2421185918213441156
, which is a big differenceProof of Concept
Place the following test inside
/test/beanstalk/BeanstalkStable2.calcReserveAtRatioLiquidity.t.sol
and run it withforge test --mt "test_calcReserveAtRatioSwapSkipTarget" -vv
You can further add
console.log
inside the for loop ofcalcReserveAtRatioSwap
and logpd.currentPrice
on each update. You can notice that each time the current price is further away from the target. Here are logs:Tools Used
Recommended Mitigation Steps
Recalculate the
PriceData
for the given case and consider narrowing down the price diff.Assessed type
Invalid Validation