xanoutas/solidity-security-findings huggingface dataset:
Intrinsic arbitrage between assets due to price feed deviation threshold
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L66-L78


# Vulnerability details



## Impact
Withdrawals have not yet been implemented but I assume it will be implemented in the usual way such that the fraction of total supply of rsETH a user redeems gives him an equal fraction of total assets held, i.e. `received = sharesToRedeem * totalAssets / totalShares`. Thus one should be able to withdraw at most what one deposited.
The issue described here is that this invariant does not hold; the minted amount of rsETH may represent a greater value than just deposited. If we imagine that one could immediately withdraw, this means that one can deposit and immediately withdraw for profit.

## Proof of Concept
We have something like an ERC4626 vault, where the minted shares are calculated as `depositAmount * totalShares / totalAssets`. The complication here is that we have multiple assets so we cannot use a single balance, but must assign a single value to the held balances of all assets, i.e. `depositAmount * totalShares / totalETHValue`.
This is indeed how the number of shares to mint is calculated in our case:
```solidity
rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
```
where `lrtOracle.getAssetPrice(asset)` is the price given by an oracle (so far only Chainlink is implemented), and where `lrtOracle.getRSETHPrice()` is `totalETHInPool / rsEthSupply` where `totalETHInPool` is the sum of the value of all assets balances. Thus `rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) * rsEthSupply / totalETHInPool`.

This is correct in theory, but in practice the price feeds are slightly inaccurate. Chainlink has a deviation threshold of up to 2% or so, which means that it will not update the price unless the true price deviates at least 2%. Thus the price feeds may be off by up to 2%.

Note that for a normal single asset vault the price doesn't matter; it is merely a conversion factor, any discrepancy of which cancels out. It only matters that it be consistent. But when we have multiple assets the price does matter and must be carefully chosen such that one cannot profit by depositing.

Suppose there are two supported assets TIK and TOK.
The pool holds 100 TIK and 0 TOK and has minted 100 rsETH.
The true value of each is currently 1 ETH.
The oracle reports this exactly as 1 for TIK, but as 1.02 for TOK.
A user deposits 100 TOK, which he got for 100 ETH. He is thus minted 100 * 1.02 * 100 / (100 * 1 + 1.02 * 0) = 102 rsETH. However this is of course staked according to its true values.
He now owns 102 out of a total of 202 shares of the assets. If he were to withdraw he would get 102 rsETH * 200 ETH / 202 rsETH = 100.99 ETH, i.e. a profit of 0.99 ETH.

This is not simply because the price is wrong. Note what would have happened if the pool held 0 TIK but 100 TOK instead. Then he would get 100 * 1.02 * 100 / (0 * 1 + 100 * 1.02) =
First deposit of 1 Wei will block further rsETH minting
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L95-L110


# Vulnerability details

## Impact

If the initial deposit in the DepositPool is 1 wei of any supported token (rETH, cbETH, or stETH), 1 wei of rsETH will be minted for the first depositor. However, subsequent `rsETH` minting will be prevented because the `rsethAmountToMint` will always round down to 0, resulting in users not receiving any tokens for their deposits. 

## Proof of Concept

In the situation where the initial deposit in the pool is `1 wei`, the first depositor will receive 1 wei of rsETH. Even if users deposit the remaining amount up to the deposit limit `(100,000 ether - 1 wei)`, they will still receive `0 rsETH` due to rounding issues.

> Note: Will use rETH as an asset and also consider its price in terms of ETH based on the Chainlink oracle 
- `rETH / ETH` at [0x536218f9E9Eb48863970252233c8F271f554C2d0](https://etherscan.io/address/0x536218f9E9Eb48863970252233c8F271f554C2d0) = 1,090,300,000,000,000,000 (at the moment of writing)
> 

![rETH-ETH-Oracle](https://user-images.githubusercontent.com/84782275/283229965-ae2e52bc-b204-478e-9e4a-3c3e385a9b67.png)

Flow:

1. Alice deposit first in the DepositPool 1 wei of rETH.
2. 1 wei of rsETH will be minted for her.
3. Bob deposits the remaining rETH amount up to the deposit limit, which is `100,000 ether - 1 wei`.
4. Bob’s tokens will be deposited inside the `lrtDepositPool`, but he will not receive any rsETH tokens due to rounding issues.

### Coded POC

Import `console` ,`StdUtils`,`MockToken` and `RSETH` at the top of the `LRTDepositPoolTest.t.sol` file.

```solidity
import { console } from "forge-std/Test.sol";
import { StdUtils } from "forge-std/StdUtils.sol";
import { MockToken } from "./LRTConfigTest.t.sol";
import { RSETH } from "src/RSETH.sol";
```

Place the PoC at the end of the file.

Can be run with: 

```
forge test --match-contract LRTDepositPoolInitialDepositOf1Wei --match-test test_InitialDepositorOf1WeiWillResultInNoMoreRSERHToBeMinted -vvv
```

```solidity
contract LRTOracleMock_InitialDepositor {
    RSETH rseth;
    MockToken public rETH;
    LRTDepositPool public lrtDepositPool;

    constructor(RSETH rsEth, MockToken reth, LRTDepositPool LrtDepositPool) {
        rseth = rsEth;
        rETH = reth;
        lrtDepositPool = LrtDepositPool;
    }

    function getAssetPrice(address) public pure returns (uint256) {
        return 1_090_300_000_000_000_000;
    }

    function getRSETHPrice() external returns (uint256) {
        uint256 rsETHTotalSupply = rseth.totalSupply();

        if (rsETHTotalSupply == 0) {
            return 1 ether;
        }

        return (rETH.balanceOf(address(lrtDepositPool)) * getAssetPrice(address(0))) / rsETHTotalSupply;
    }
}

contract LRTDepositPoolInitialDepositOf1Wei is LRTDepositPoolTest {
    address public rETHAddress;
    LRTOracleMock_InitialDepositor public lrtOracle;

    function set
Biased `rsETH` price calculation in `depositAsset` results in lesser `rsETH` minted to user
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L119


# Vulnerability details

## Impact
The `LRTDepositPool` acts as a simplified vault allowing restakers to transfer their liquid staked tokens and receive `rsETH` tokens based on the current `rsETH` exchange rate. `rsETH` are minted to user by interacting with `depositAsset` function of `LRTDepositPool` contract and sending `amount` of liquid staked token (called `asset`) to it. First, `amount` of `asset` is transferred to `LRTDepositPool`, then `rsethAmountToMint` is computed and same `rsETH` are minted to the user. However, there is a flaw in calculation of `rsethAmountToMint` due to wrong price calculation of `rsETH` which results in lesser `rsETH` being minted to user. 

This happens due to the fact that while computing the price of `rsETH` i.e `price = totalETHInPool / rsEthSupply`, `asset` deposited by user for minting `rsETH` is also included in `totalETHInPool`, whereas, corresponding `rsEthSupply` has yet not been increased. This, results in biased price of `rsETH` and lesser `rsETH` are minted to the user for his deposited `asset`.

## Proof of Concept
[`getRsETHAmountToMint()` function of `LRTDepositPool` contract returns](https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L95-L110)
```
rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
```

[`getRSETHPrice()` function of `LRTOracle` contract returns `rsETHPrice` as](https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L52-L79)
```
return totalETHInPool / rsEthSupply; 
```
where `totalETHInPool` are computed as sum of all `assets` held in `LRTDepositPool`, `NDC contract` and eigen layer (after adjust price of each asset). Moreover, if `rsEthSupply == 0`, `getRSETHPrice()` returns price as `1 ether`.

Following explains the PoC working. For simplicity, assume that `totalETHInPool` is computed by all `asset` held in `LRTDepositPool` only (leave `NDC contract` and eigen layer for the moment).
1. At start, `totalETHInPool = 0` and `rsEthSupply = 0` as no `asset` has been deposited yet.
2. Alice calls `depositAsset` function and transfers 2 `rETH` to `LRTDepositPool`
3. Now, `rsethAmountToMint = 2 ether` as `totalETHInPool = 2` and `rsEthSupply = 0` (`getRSETHPrice()` returns price as `1 ether` when `rsEthSupply == 0`).
4. 2 `rsETH` are minted for Alice. Updated values are `totalETHInPool = 2` and `rsEthSupply = 2`.
5. Now, Bob calls `depositAsset` function and transfers 99 `rETH` to `LRTDepositPool`
6. Now, `rsethAmountToMint = 1.96 eth` as `totalETHInPool = 101` and `rsEthSupply = 2` (`getRSETHPrice()` returns price as `50.5 ether`).
7. 1.96 `rsETH` are minted to Bob. Updated values are `totalETHInPool = 101` and `rsEthSupply = 3.96`.

The above scenarios shows that a user buying later and buying with amo
Precision loss in getRSETHPrice
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L78


# Vulnerability details

## Impact
Precision loss and return zero price by Oracle 

## Proof of Concept
Since the value of staked ether increases, the price of RSETH goes above ETH price and leads to a precision loss in the getRSETHPrice function of the oracle, and RSETH price becomes zero.
Consider a scenario in which both rsEthSupply and totalETHInPool are 10; after sometime totalETHInPool increases and goes above rsEthSupply, which leads to precision loss in the following line 
```return totalETHInPool / rsEthSupply; ```


## Tools Used
Manual Review 
## Recommended Mitigation Steps
Use a scale factor to prevent precision loss 


## Assessed type

Other
The rsETH price can be manipulated easily when the deposit is low
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L52


# Vulnerability details

## Impact
The rsETH price can be manipulated easily when the deposit is low, since the price is relies on the contracts' balance.

## Proof of Concept
The rsETH's price is calculated by  $\frac{total\_asset\_value}{total\_supply}$, and when supply is zero, the price will be 1 ether.

However, when the first token is minted by Alice and Alice can transfer another 1 ether asset into the deposit pool, the current price will be 2 ether for 1 rsETH.

This will create a larger issue if the price will be used by any other protocol like a lending platform.
Also, there could be an inflationary attack since the `getRsETHAmountToMint` may return 0 when the user mints.

```
rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
```

```
    /// @notice Provides RSETH/ETH exchange rate
    /// @dev calculates based on stakedAsset value received from eigen layer
    /// @return rsETHPrice exchange rate of RSETH
    function getRSETHPrice() external view returns (uint256 rsETHPrice) {
        address rsETHTokenAddress = lrtConfig.rsETH();
        uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply();

        if (rsEthSupply == 0) {
            return 1 ether;
        } //@audit try reth/steth price

        uint256 totalETHInPool;
        address lrtDepositPoolAddr = lrtConfig.getContract(LRTConstants.LRT_DEPOSIT_POOL);

        address[] memory supportedAssets = lrtConfig.getSupportedAssetList();
        uint256 supportedAssetCount = supportedAssets.length;

        for (uint16 asset_idx; asset_idx < supportedAssetCount;) { 
            address asset = supportedAssets[asset_idx];
            uint256 assetER = getAssetPrice(asset); 

            uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset);
            totalETHInPool += totalAssetAmt * assetER;

            unchecked {
                ++asset_idx;
            }
        }

        return totalETHInPool / rsEthSupply;
    }
```


## Tools Used
Manual

## Recommended Mitigation Steps
Recommend tracking contract balance by creating a variable.


## Assessed type

Other
Users could game oracle price deviation
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/oracles/ChainlinkPriceOracle.sol#L17


# Vulnerability details

## Summary

Prices returned from Chainlink oracles have different conditions to update the reported values, which can be abused by

## Impact

Prices for the different LST assets supported in the Kelp protocol are obtained from a Chainlink oracle. The data feeds for each one can be found here:

- https://data.chain.link/ethereum/mainnet/crypto-eth/steth-eth
- https://data.chain.link/ethereum/mainnet/crypto-eth/cbeth-eth
- https://data.chain.link/ethereum/mainnet/crypto-eth/reth-eth

Chainlink price feeds follow different rules in order to update their prices. There are essentially two conditions that could trigger a price change, a heartbeat and a price deviation. A heartbeat updates the feed if the configured time since last update has elapsed, and the price deviation triggers the update when the price has moved above a configured percentage.

In all three cases, the heartbeat is the same (24 hours), however the price deviation target is not. stETH has a percentage of 0.5%, cbETH of 1% and rETH of 2%. This means that there are big differences between how fast an oracle is updated between the different assets.

As deposits in the protocol can be done using any assets, this means that a user could choose to deposit a particular asset knowing that the current on-chain price is deviated from the actual price, which can go as far as 2% for the rETH case. Note also that this can be used in a front-run fashion before the price of an oracle is updated on-chain, reducing the uncertainty to almost none in order to take advantage of a big price movement.

Similarly, since feeds have different triggers, a user could use this to their advantage to deposit the asset that is currently most deviated between all three. As participation in the protocol is measured by a share of RSETH, this means that the user's share will also include the other two assets, gaining an advantage in value due to the mispriced deposit asset.

Currently, there is no way to withdraw assets from the protocol, but this could eventually lead to a situation where a user deposits one asset before the price movement, and then withdraws after the price has impacted, or simply withdraws any other asset from the pool which has a more recent price update.

## Recommendation

As the protocol is currently relying on Chainlink oracles, it is difficult to provide a recommendation using solely these price feeds. A potential solution would be to factor different oracles, such as a TWAP or other on-chain price sources.



## Assessed type

Oracle
Staleness Vulnerability in chainlinkAdaptor's `getAssetPrice` function
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L45-L47


# Vulnerability details

## Impact
The current implementation of the `getAssetPrice` function in the `chainlinkAdaptor` contract lacks a crucial check for the heartbeat of the data feed, introducing a potential risk of consuming stale data. This issue is exacerbated by the fact that different assets like `stETh`, `rETH`, and `cbETH` have distinct heartbeat intervals. Failure to account for these differences may result in the use of outdated prices, leading to inaccurate calculations and potentially causing financial losses for users. Additionally, the use of a deprecated Chainlink function is another concern, although this aspect is covered in a separate analysis.

## Proof of Concept

```solidity
 /// @notice Fetches Asset/ETH exchange rate
    /// @param asset the asset for which exchange rate is required
    /// @return assetPrice exchange rate of asset

    function getAssetPrice(address asset) external view onlySupportedAsset(asset) returns (uint256) {
        return AggregatorInterface(assetPriceFeed[asset]).latestAnswer(); //@audit add heartbeat
    }
```

In the above implementation of the `getAssetPrice` function, the code uses Chainlink to fetch the price of an asset like `stETh`, `rETH`, and `cbETH`. While the bot race issue is covered in a separate analysis, the heartbeat issue is not addressed. The absence of heartbeat verification is problematic, given that all assets have different heartbeat intervals and deviation thresholds. This disparity in heartbeat intervals could lead to significant price deviations, potentially resulting in financial losses for users. Notably, this function is utilized in `getRsETHAmountToMint`.

see the details of `assets` on chainlink from here

https://data.chain.link/base/base/crypto-eth/steth-eth
https://data.chain.link/ethereum/mainnet/crypto-eth/cbeth-eth
https://data.chain.link/ethereum/mainnet/crypto-eth/reth-eth

### Tools Used
Manual Review

### Recommended Mitigation Steps

1. **Heartbeat Verification:**
   - Implement heartbeat verification in the `getAssetPrice` function to ensure that only fresh data is utilized.
   - Consider utilizing different heartbeat intervals for distinct assets to align with their respective data feed characteristics.


2. **Consider TWAP Oracle:**
   - Explore the use of Time-Weighted Average Price (TWAP) oracles as an alternative to mitigate potential issues associated with varying heartbeat intervals.


## Assessed type

Oracle
No way to withdraw from the strategy
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L51


# Vulnerability details

## Impact
NodeDelegator does not have any functions to withdraw the underlying from the strategy. It does have a function to transfer ERC-20 tokens to the LRTDepositPool, assuming the eigen layer strategy mints ERC20 tokens to represent the share of invested funds. However, LRTDepositPool does not have any functions to withdraw ERC20 tokens. Since the stake in the eigen layer strategy belongs to the NodeDelegator contract, there is no way to withdraw funds from the strategy. They will be stuck in the strategy.

## Proof of Concept


## Tools Used
Manual Review.

## Recommended Mitigation Steps
Add a function to withdraw the staked funds.


## Assessed type

Token-Transfer
DepositPool is susceptible to the inflation attack
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L119


# Vulnerability details

## Summary

The DepositPool contract is susceptible to the [Inflation Attack](https://mixbytes.io/blog/overview-of-the-inflation-attack), in which the first depositor can be front-runned by an attacker to steal their deposit.

## Impact

The DepositPool pool contract acts mainly as a vault: accounts deposit LST assets and get back RSETH tokens (a share) that represents their participation in the vault.

These types of contracts are potentially vulnerable to the inflation attack: an attacker can front-run the initial deposit to the vault to inflate the value of a share and render the front-runned deposit worthless.

The mint amount in the pool is given by the `getRsETHAmountToMint()` function: 

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L95-L111

```solidity
095:     function getRsETHAmountToMint(
096:         address asset,
097:         uint256 amount
098:     )
099:         public
100:         view
101:         override
102:         returns (uint256 rsethAmountToMint)
103:     {
104:         // setup oracle contract
105:         address lrtOracleAddress = lrtConfig.getContract(LRTConstants.LRT_ORACLE);
106:         ILRTOracle lrtOracle = ILRTOracle(lrtOracleAddress);
107: 
108:         // calculate rseth amount to mint based on asset amount and asset exchange rate
109:         rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
110:     }
```

While the `getRSETHPrice()` is determined by the asset balance in the contracts:

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L52-L79

```solidity
52:     function getRSETHPrice() external view returns (uint256 rsETHPrice) {
53:         address rsETHTokenAddress = lrtConfig.rsETH();
54:         uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply();
55: 
56:         if (rsEthSupply == 0) {
57:             return 1 ether;
58:         }
59: 
60:         uint256 totalETHInPool;
61:         address lrtDepositPoolAddr = lrtConfig.getContract(LRTConstants.LRT_DEPOSIT_POOL);
62: 
63:         address[] memory supportedAssets = lrtConfig.getSupportedAssetList();
64:         uint256 supportedAssetCount = supportedAssets.length;
65: 
66:         for (uint16 asset_idx; asset_idx < supportedAssetCount;) {
67:             address asset = supportedAssets[asset_idx];
68:             uint256 assetER = getAssetPrice(asset);
69: 
70:             uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset);
71:             totalETHInPool += totalAssetAmt * assetER;
72: 
73:             unchecked {
74:                 ++asset_idx;
75:             }
76:         }
77: 
78:         return totalETHInPool / rsEthSupply;
79:     }
```

An attacker can then mint a minimal