xanoutas/solidity-security-findings huggingface dataset:
For extreme ratios getRatiosFromPriceSwap will return data for which is impossible to converge into a reserve
Audit: 2024-07-basin-findings
Severity: MEDIUM

# 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](https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/StableLUT/Stable2LUT1.sol#L740) 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**
- 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`
```
    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 pric
Stable2LUT1::getRatiosFromPriceLiquidity - In extreme cases, `updateReserve` will start breaking
Audit: 2024-07-basin-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
This is one of the edge cases in `getRatiosFromPriceLiquidity`:

```sol
if (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`.

```sol
if (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:
1. Because of the large range that the price can be in, thus using such a small `lowPrice` as `pd.currentPrice` makes the difference between `pd.targetPrice` so large that the corrections become very large and underflow before convergence can be achieved.
2. `lowPriceJ` is extremely large, in this case it's `2000e18`, 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 prob
Incorrect reserve updates for when `targetPrice` is exactly in the middle of high and low price
Audit: 2024-07-basin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L198-L211
https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L265-L279


# Vulnerability details

### Impact

The returned reserve will be incorrectly calculated in the `calcReserveAtRatioSwap` and `calcReserveAtRatioLiquidity` functions due to `pd.currentPrice` incorrectly set as high price when `targetPrice` lies directly in the middle of highPrice and Lowprice.

### Proof of Concept

1. Context

We look first at `calcReserveAtRatioLiquidity` function. The `pd.CurrentPrice` parameter is updated depending on if the `pd.targetPrice` is closer to `pd.lutData.highPrice` or `pd.lutData.lowPrice` . This is enforced by comparing the differences between them and the target price before settling `pd.currentPrice` to low/high price. This is also corroborated by the comments.

```solidity
        // update scaledReserve[j] such that calcRate(scaledReserves, i, j) = low/high Price,
        // depending on which is closer to targetPrice.
        if (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;
        } else {
            // targetPrice is closer to highPrice.
            scaledReserves[j] = scaledReserves[i] * pd.lutData.highPriceJ / pd.lutData.precision;

            // set current price to highPrice.
            pd.currentPrice = pd.lutData.highPrice;
        }
```

This can also be observed in the `calcReserveAtRatioSwap` function.
```solidity
        // update `scaledReserves` based on whether targetPrice is closer to low or high price:
        if (pd.lutData.highPrice - pd.targetPrice > pd.targetPrice - pd.lutData.lowPrice) {
            // targetPrice is closer to lowPrice.
            scaledReserves[i] = parityReserve * pd.lutData.lowPriceI / pd.lutData.precision;
            scaledReserves[j] = parityReserve * pd.lutData.lowPriceJ / pd.lutData.precision;
            // initialize currentPrice:
            pd.currentPrice = pd.lutData.lowPrice;
        } else {
            // targetPrice is closer to highPrice.
            scaledReserves[i] = parityReserve * pd.lutData.highPriceI / pd.lutData.precision;
            scaledReserves[j] = parityReserve * pd.lutData.highPriceJ / pd.lutData.precision;
            // initialize currentPrice:
            pd.currentPrice = pd.lutData.highPrice;
        }
```
2. Bug Location

The functions however ignore an edgecase in which the value of `pd.lutData.highPrice - pd.targetPrice` is exactly equal to `pd.targetPrice - pd.lutData.lowPrice`, meaning there's no price deviations, and as such current price should remain the same as its target. This
Inaccurate LP Token Supply Calculation Due to Convergence Issue
Audit: 2024-07-basin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L74


# Vulnerability details

## Vulnerability Details
The `calcLpTokenSupply` function in the `Stable2` contract is responsible for calculating the supply of liquidity provider tokens based on the reserves of the two underlying tokens. The function uses an iterative approach to converge on the correct LP token supply value.

It uses an absolute difference of 1 as the convergence criteria, checking if the difference between the current and previous `lpTokenSupply` values is less than or equal to 1. This approach can lead to the function returning a slightly inaccurate LP token supply value, especially when the actual value is close to an integer.

## Code Snippet

```solidity
for (uint256 i = 0; i < 255; i++) {
    uint256 dP = lpTokenSupply;
    dP = dP * lpTokenSupply / (scaledReserves[0] * N);
    dP = dP * lpTokenSupply / (scaledReserves[1] * N);
    uint256 prevReserves = lpTokenSupply;
    lpTokenSupply = (Ann * sumReserves / A_PRECISION + (dP * N)) * lpTokenSupply
        / (((Ann - A_PRECISION) * lpTokenSupply / A_PRECISION) + ((N + 1) * dP));
    // Equality with the precision of 1
    if (lpTokenSupply > prevReserves) {
        if (lpTokenSupply - prevReserves <= 1) return lpTokenSupply;
    } else {
        if (prevReserves - lpTokenSupply <= 1) return lpTokenSupply;
    }
}
```

## Impact

The main impacts of the convergence issue in the `calcLpTokenSupply` function is on the accuracy of the calculated LP token supply and inaccurate accounting. When the actual LP token supply value is very close to an integer, the current convergence criteria of an absolute difference of 1 may not be strict enough, leading to the function returning a slightly inaccurate value.

The slightly inaccurate LP token supply could lead to suboptimal trading decisions or incorrect liquidity calculations, resulting in financial losses.

The inaccurate LP token supply could also lead to discrepancies in financial reporting, accounting, and other systems that rely on the accurate calculation of LP token balances.


## Poc

```solidity
function test_calcLpTokenSupply_convergence() public {
    _data = abi.encode(18, 18);
    uint256[] memory reserves = new uint256[](2);
    reserves[0] = 1e18;
    reserves[1] = 1e18;

    // Set the lpTokenSupply to a value very close to an integer
    uint256 expectedLpTokenSupply = 2_000_000_000_000_000_001;
    
    uint256 lpTokenSupply = _function.calcLpTokenSupply(reserves, _data);
    assertEq(lpTokenSupply, expectedLpTokenSupply, "Calculated LP token supply does not match expected value");
}
```

Run: forge test --match-test test_calcLpTokenSupply_convergence


## Tools Used
Manual

## Mitigation

Consider modifying the convergence criteria to use a smaller threshold, such as a relative difference of a small percentage instead of an absolute difference of 1. This would ensure that the function converges to a more accurate valu
Incorrect Rate Calculations Due to Insufficient Decimal Handling
Audit: 2024-07-basin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L153


# Vulnerability details

## Vulnerability Details

In the `calcRate` function, the `lpTokenSupply` is calculated using the `calcLpTokenSupply` function, which takes the `scaledReserves` as input. However, the `scaledReserves` are calculated using the `getScaledReserves` function, which scales the reserves based on the token decimals provided in the `data` parameter.

The problem is that the `calcLpTokenSupply` function expects the token decimals to be 18 for both tokens. This means that if the token decimals are not 18, the `lpTokenSupply` calculation will not be accurate, which can lead to incorrect rate calculations.

## Code

```solidity
function calcRate(
        uint256[] memory reserves,
        uint256 i,
        uint256 j,
        bytes memory data
    ) public view returns (uint256 rate) {
        uint256[] memory decimals = decodeWellData(data);
        uint256[] memory scaledReserves = getScaledReserves(reserves, decimals);

        // calc lp token supply (note: `scaledReserves` is scaled up, and does not require bytes).
        uint256 lpTokenSupply = calcLpTokenSupply(scaledReserves, abi.encode(18, 18));

        rate = _calcRate(scaledReserves, i, j, lpTokenSupply);
    }
```

## Impact

1. If the calcRate function is not accurately calculating the rates, the lookup table data will be flawed, leading to incorrect price estimates in the calcReserveAtRatioSwap function.

2. The calcRate function is crucial for providing accurate pricing information to the wider system. If the rates calculated by calcRate are inaccurate, it can lead to suboptimal pricing and trading decisions, potentially causing significant financial losses for users.


## Poc

```solidity
function test_calcRate_with_different_decimals() public {
    uint256[] memory reserves = new uint256[](2);
    reserves[0] = 10_000 * 1e6; // 10,000 with 6 decimals
    reserves[1] = 10 * 1e18; // 10 with 18 decimals
    _data = abi.encode(6, 18);

    uint256 rate = _function.calcRate(reserves, 0, 1, _data);
    assertEq(rate, 1e6); // Expected rate should be 1:1 (10,000:10)
}
```

## Mitigation

Modify the `calcRate` function to pass the correct token decimals to the `calcLpTokenSupply` function, instead of using the default 18 decimals. This ensures that the `lpTokenSupply` calculation will be accurate, and the `calcRate` function will provide the correct rate.

```solidity
function calcRate(
    uint256[] memory reserves,
    uint256 i,
    uint256 j,
    bytes memory data
) public view returns (uint256 rate) {
    uint256[] memory decimals = decodeWellData(data);
    uint256[] memory scaledReserves = getScaledReserves(reserves, decimals);

    // calc lp token supply with the correct token decimals
    uint256 lpTokenSupply = calcLpTokenSupply(scaledReserves, abi.encode(decimals[0], decimals[1]));

    rate = _calcRate(scaledReserves, i, j, lpTokenSupply);
}
```





## Ass
Stale `lp token supply` used causes wrong current price calculations
Audit: 2024-07-basin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L220-L236


# Vulnerability details

## Impact
Wrong calculation of `currentPrice` during swaps .

## Details
`Stable2.calcReserveAtRatioSwap` function performs the calculation of reserve using newtons method . Initially the function updates the lp token supply for the given reserves at : 
https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L193C9-L193C87
```solidity
uint256 lpTokenSupply = calcLpTokenSupply(scaledReserves, abi.encode(18, 18));
```
But further down, in the loop of the same function , when the reserves are updated , the corresponding `lp token supply` is not updated and the `internal _calcRate` function uses stale `lp token supply` for the calculation of current price as shown here : 
https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L221C2-L227C1
```solidity
           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); <@here

```
Similar calculation can be seen in `calcReserveAtRatioLiquidity` function , where it uses the `public calcRate` function instead of the internal one & thereby updating `lp token supply` internally before calculating `currentPrice` as seen here : 
https://github.com/code-423n4/2024-07-basin/blob/main/src/functions/Stable2.sol#L288C9-L291C82
```solidity
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));
```

Also the sponsor mentioned in the pinned discord message that 
`The logic used to calculate the ratios used can be found in StableswapCalcRatiosLiqSim.s.sol and StableswapCalcRatiosSwapSim.s.sol.`

And looking into `StableswapCalcRatiosSwapSim.s.sol.`, we see the logic used for `Stable2.calcReserveAtRatioSwap` is -
```
         // for n times (1...n) :
        // 1) increment x_n-1 by some amount to get x_n
        // 2) calc y_n using calcReserves(...)
        // 3) calc price_n using calcRate(...)

        // csv header
        console.log("Price (P),Reserve (x),Reserve (y)");

        for (uint256 i; i < 20; i++) {
            // update reserve x
            reserve_x = reserve_x * 92 / 100;
            reserves[0] = reserve_x;
            // get y_n --> corresponding reserve y for a given liquidity level
            uint256 reserve_y = stable2.calcReserve(reserves, 1, lpTokenSupply, data);
            // update reserve y
            reserves[1] = reserve_y;
            // mark price
            price = stable2.calcRate(reserves, 0, 1, data);      <@audit-here
            co
Inaccurate Reserve Initialization in `calcReserveAtRatioLiquidity` Function Leads to Suboptimal Swap Pricing
Audit: 2024-07-basin-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2024-07-basin/blob/7d5aacbb144d0ba0bc358dfde6e0cc913d25310e/src/functions/Stable2.sol#L267


# Vulnerability details

## Vulnerability Details

The `calcReserveAtRatioLiquidity` function tries to update `scaledReserves[j]` based on the `pd.lutData` (the data from the lookup table) and the `scaledReserves[i]` value. However, this approach is not ideal. If the target price (`pd.targetPrice`) is closer to the `pd.lutData.highPrice`, the function sets `scaledReserves[j]` based on the `pd.lutData.highPriceJ` value. Similarly, if the target price is closer to the `pd.lutData.lowPrice`, the function sets `scaledReserves[j]` based on the `pd.lutData.lowPriceJ` value.

## Code

```solidity
 if (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;
        } else {
            // targetPrice is closer to highPrice.
            scaledReserves[j] = scaledReserves[i] * pd.lutData.highPriceJ / pd.lutData.precision;

```

The problem with this logic is that these price ratios (`pd.lutData.highPriceJ` and `pd.lutData.lowPriceJ`) may not necessarily be accurate enough to correctly set the initial value of `scaledReserves[j]`. This can lead to the loop not converging to the desired result within the 255 iterations.

## Impact

The `calcReserveAtRatioLiquidity` function is used to calculate the reserves for swap operations. If the reserve values are not calculated accurately, it can lead to suboptimal swap pricing and potentially less efficient swap executions.

In the worst-case scenario, the inaccurate reserve calculations could lead to capital loss for users providing liquidity or executing swaps, as the pool may not be able to maintain the expected parity between the token reserves.

## Poc

```solidity
function test_calcReserveAtRatioLiquidity_initializeReserve() public {
    uint256[] memory reserves = new uint256[](2);
    reserves[0] = 1e18; 
    reserves[1] = 1e6; 
    uint256[] memory ratios = new uint256[](2);
    ratios[0] = 1e6; 
    ratios[1] = 1e6;
    _data = abi.encode(18, 6);
    
    uint256 reserve = _function.calcReserveAtRatioLiquidity(reserves, 1, ratios, _data);
    uint256 expectedReserve = reserves[0] * ratios[1] / ratios[0];
    
    assertApproxEqRel(reserve, expectedReserve, 1e-6);
}
```

In this test, I set up a scenario with different decimal places for the two tokens (18 and 6 decimals). We then call the calcReserveAtRatioLiquidity function and check that the returned reserve value is close to the expected value, which is calculated directly from the provided ratios and the other reserve.

## Mitigation

Consider using a more robust method to initialize the `scaledReserves[j]` value, such as calculating it directly from t