xanoutas/solidity-security-findings huggingface dataset:
Max deposit limit on assets per deposit on strategy on EigenLayer
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

https://github.com/Layr-Labs/eigenlayer-contracts/blob/75e59432d079c6f90d48d4e950a68c15867c82b2/src/contracts/strategies/StrategyBaseTVLLimits.sol#L56-L65


# Vulnerability details


## Impact
Each strategy on EigenLayer has a `maxPerDeposit` it validates all individual deposits into the concerned strategy against.  This is inclusive of the `maxTotalDeposits` against a strategy. These two upper limits are validated against deposits to the strategy, but only the later `maxTotalDeposits` is implemented in LRTDepositPool contract. 

## Proof of Concept

Currently the values of `MaxTotalDeposits` and `maxPerDeposit` on the three strategies (cbETH, stETH, rETH) deployed on  EigenLayer on mainnet are 1e23. 



    function  _setTVLLimits(uint256  newMaxPerDeposit, uint256  newMaxTotalDeposits) internal {
    
    emit  MaxPerDepositUpdated(maxPerDeposit, newMaxPerDeposit);
    
    emit  MaxTotalDepositsUpdated(maxTotalDeposits, newMaxTotalDeposits);
    
    require(
    
    newMaxPerDeposit <= newMaxTotalDeposits,
    
    "StrategyBaseTVLLimits._setTVLLimits: maxPerDeposit exceeds maxTotalDeposits"
    
    );
    
    maxPerDeposit = newMaxPerDeposit;
    
    maxTotalDeposits = newMaxTotalDeposits;
    
    // @audit-issue [ref-2] Although in all the deployed contracts, the maxPerDeposit is same as maxtotal,
    
    // it should however be noted that the code allows for it to be both same and lesser and there is no gaurantees that
    
    // it will always be same in code or docs.
    
    }
In the codebase [StrategyBaseTVLLimits](https://github.com/Layr-Labs/eigenlayer-contracts/blob/75e59432d079c6f90d48d4e950a68c15867c82b2/src/contracts/strategies/StrategyBaseTVLLimits.sol#L56C1-L65C6) The contract allows `maxPerDeposit` to be less than or equal to `maxTotalDeposits`. Therefore making no guarantees that the current state of the contract will always be `newMaxPerDeposit == newMaxTotalDeposits`. 


## Tools Used
Manual review 

## Recommended Mitigation Steps
Implement maxPerDeposit as is applicable in EigenLayer.   



## Assessed type

Invalid Validation
rsETH can return incorrect price because of future EigenLayer strategies
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
How currently rsETH price is calculated? totalEthLocked is divided by rsETH supply.
How totalEthLocked is calculated? It sums 3 amounts per every asset: 1) balance of LRTDepositPool.sol, 2) balance of all node delegators, 3) already deposited amount of asset to EigenLayer's Strategy. We are interested in the 3rd point.

EigenLayer has 2 types of functions to fetch the user's deposited amount: 1) view, which doesn't update state, 2) function that potentially modifies state.
Here's problem, now only view function is used. Future version of Strategy contract can contain specific logic which affects user's balance calculation, like interest accruing for example.

As a result rsETH price can be incorrect in protocol. It affects deposits and most likely withdrawals.

## Proof of Concept
That's callback order:
```
LRTOracle.getRSETHPrice()
    LRTDepositPool.getTotalAssetDeposits()
        LRTDepositPool.getAssetDistributionData()
            NodeDelegator.getAssetBalance()
                IStrategy.userUnderlyingView()
```
NodeDelegator in its turn calls view version of function:
```solidity
    function getAssetBalance(address asset) external view override returns (uint256) {
        address strategy = lrtConfig.assetStrategy(asset);
        return IStrategy(strategy).userUnderlyingView(address(this));
    }
```

That's EigenLayer documentation:
https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/src/contracts/strategies/StrategyBase.sol#L246-L259
https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/src/contracts/strategies/StrategyBase.sol#L193-L217
```solidity
    function underlyingToShares(uint256 amountUnderlying) external view virtual returns (uint256) {
        return underlyingToSharesView(amountUnderlying);
    }

    function userUnderlyingView(address user) external view virtual returns (uint256) {
        return sharesToUnderlyingView(shares(user));
    }

    /**
     * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy.
     * @notice In contrast to `sharesToUnderlying`, this function guarantees no state modifications
     * @param amountShares is the amount of shares to calculate its conversion into the underlying token
     * @return The amount of underlying tokens corresponding to the input `amountShares`
@>   * @dev Implementation for these functions in particular may vary significantly for different strategies
     */
    function sharesToUnderlyingView(uint256 amountShares) public view virtual override returns (uint256) {
        ...
    }

    /**
     * @notice Used to convert a number of shares to the equivalent amount of underlying tokens for this strategy.
     * @notice In contrast to `sharesToUnderlyingView`, this function **may** make state modifications
     * @param amountShares is
Missing pause checks in LRTOracle
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary

The LRTOracle oracle provides functionality to pause the contract but no restrictions are applied when the contract is in a paused state.

## Impact

Similar to the other contracts in the protocol, the LRTOracle contract offers pausing functionality:

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

```solidity
101:     /// @dev Triggers stopped state. Contract must not be paused.
102:     function pause() external onlyLRTManager {
103:         _pause();
104:     }
105: 
106:     /// @dev Returns to normal state. Contract must be paused
107:     function unpause() external onlyLRTAdmin {
108:         _unpause();
109:     }
```

However, none of the functions in the contract are affected by this pause. There's no difference at all between a paused or non-paused state.

Since this is the main oracle entrypoint, which provides both prices for individual assets and the RSETH token, the missing intention here is to likely block access to these functions when the contract is paused. In an emergency, and in case the contract is paused, consumers of this oracle, internally to the protocol or externally, should be blocked from fetching prices as it may imply there is something wrong with the current data.

## Recommendation

Add the `whenNotPaused` modifier to the functions that provide oracle prices.

```diff
-   function getAssetPrice(address asset) public view onlySupportedAsset(asset) returns (uint256) {
+   function getAssetPrice(address asset) public view onlySupportedAsset(asset) whenNotPaused returns (uint256) {
```

```diff
-   function getRSETHPrice() external view returns (uint256 rsETHPrice) {
+   function getRSETHPrice() external view whenNotPaused returns (uint256 rsETHPrice) {
```



## Assessed type

Other
Attacker can Grief The Call  to depositAsset Making It Impossible For The Victim To Deposit
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The attacker can grief the victim , making him unable to deposit asset into the pool.

## Proof of Concept

The attack flow is as follows -> 

* Alice decides to deposit assets into the LRTDepositPool.sol , she calls `depositPool()` here https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119

* At L152 the function makes the following comparison 

```solidity
        if (depositAmount > getAssetCurrentLimit(asset)) {
```
This is to ensure that total deposited amount of the asset being deposited does not cross the deposit limit of the asset. Notice how this comparison makes a call to the function `getAssetCurrentLimit(asset)`

* The attacker Chad notices Alice's tx to deposit in the mempool , Chad calls `lrtConfig.depositLimitByAsset(asset)` and `getTotalAssetDeposits(asset)` to find the values that would help him
in the attack

* The purpose of these values is that Chad wants to manipulate these values such that when Alice tx is processed ,i.e.  `depositToAsset()` which calls `getAssetCurrentLimit(asset)` would revert due to 

`L57 return lrtConfig.depositLimitByAsset(asset) - getTotalAssetDeposits(asset);`

* To make this happen , Chad sends assets directly to the LRTDepositPool contract , such that , when 

a.) `getTotalAssetDeposits()` is called at L57 , it calls `getAssetDistributionData()` at L71
b.) `assetLyingInDepositPool` would increase , i.e 
` assetLyingInDepositPool = IERC20(asset).balanceOf(address(this));` since chad made a direct transfer
c.) Chad sent sufficient amount of tokens such that `getTotalDeposits()` value increases and is more than the
`depositLimitByAsset()`

* This would make the call to this function revert due to underflow

```solidity
function getAssetCurrentLimit(address asset) public view override returns (uint256) {
        return lrtConfig.depositLimitByAsset(asset) - getTotalAssetDeposits(asset);
    }
```

* Alice tx is now mined and it revert due to underflow 

```solidity\function depositAsset(
        address asset,
        uint256 depositAmount
    )
        external
        whenNotPaused
        nonReentrant
        onlySupportedAsset(asset)
    {
        // checks
        if (depositAmount == 0) {
            revert InvalidAmount();
        }
        if (depositAmount > getAssetCurrentLimit(asset)) { // <---- This is where revert accurs
            revert MaximumDepositLimitReached();
        }
```

* This attack would be more meaningfull when the total Assets deposited were already close to the deposit limit,
in that case the attacker needs to transfer less assets.

## Tools Used

Manual analysis

## Recommended Mitigation Steps

Instead of accounting for balance through balanceOf() have an internal accounting system which only accounts for deposits through the depositAsset() function


## Assessed type

Math
Missing slippage check in deposit function
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary

Users depositing in the protocol have no control over the amount of RSETH minted in return for their deposit.

## Impact

The `depositAsset()` function present in the LRTDepositPool contract allows users to deposit any of the supported assets into the protocol in exchange for RSETH.

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

```solidity
119:     function depositAsset(
120:         address asset,
121:         uint256 depositAmount
122:     )
123:         external
124:         whenNotPaused
125:         nonReentrant
126:         onlySupportedAsset(asset)
127:     {
128:         // checks
129:         if (depositAmount == 0) {
130:             revert InvalidAmount();
131:         }
132:         if (depositAmount > getAssetCurrentLimit(asset)) {
133:             revert MaximumDepositLimitReached();
134:         }
135: 
136:         if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) {
137:             revert TokenTransferFailed();
138:         }
139: 
140:         // interactions
141:         uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount);
142: 
143:         emit AssetDeposit(asset, depositAmount, rsethAmountMinted);
144:     }
```

The RSETH mint amount depends on different factors, including the current price of the deposit asset, and the RSETH price, which also factors prices and amounts from all of the supported assets:

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

```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:     }
```

As the user is depositing a single asset, any change over any of the dependent factors may alter the minted amount the user receives for their deposit. Depositors don't have any slippage control over how much RSETH is minted for their deposit.

## Recommendation

Add a `minOutput` parameter to `depositAsset()` to allow depositors to control the minted amount.

```diff
    function depositAsset(
        address asset,
-       uint256 depositAmount
+       uint256 depositAmount,
+       uint256 minOutput
    )
        external
        whenNotPaused
        nonReentrant
        onlySupporte
Missing sanity checks in Chainlink response
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary

Chainlink responses from price feeds are being used without any sanity checks.

## Impact

The ChainlinkPriceOracle contract is used to interface with the Chainlink price feeds for the different LST assets in scope in the protocol.

The current implementation is returning the latest response available from the Chainlink feed without doing any sanity checks.

There is check for stale prices, the function doesn't validate if the last update timestamp is within a sane range, or if it's aligned with the feed's heartbeat. Note that different feeds may have different heartbeats, which means this should be checked using a per feed configuration.

There is also no check for prices being within the valid limits, or for big price deviations. During flash crashes, prices may fall abruptly for short periods of time, in which case the current implementation will simply return the current prices being reported by Chainlink.

## Proof of Concept

Prices from Chainlink are being returned as is, without any validation or sanity checks.

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

```solidity
34:     /// @notice Fetches Asset/ETH exchange rate
35:     /// @param asset the asset for which exchange rate is required
36:     /// @return assetPrice exchange rate of asset
37:     function getAssetPrice(address asset) external view onlySupportedAsset(asset) returns (uint256) {
38:         return AggregatorInterface(assetPriceFeed[asset]).latestAnswer();
39:     }
```

## Recommendation

The following [article](https://0xmacro.com/blog/how-to-consume-chainlink-price-feeds-safely/) contains a good summary of the recommended checks when consuming a Chainlink price feed.



## Assessed type

Oracle
Chainlink oracle uses deprecated function
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary

Prices from Chainlink are fetched using a deprecated function.

## Impact

The ChainlinkPriceOracle contract is used to interface with Chainlink price feeds to obtain the prices for the different LST assets used in the protocol.

The implementation is using the function `latestAnswer()` which [is deprecated](https://docs.chain.link/data-feeds/api-reference#latestanswer) and should not be used anymore.

> latestAnswer  
> ⚠️ THIS FUNCTION IS DEPRECATED. DO NOT USE THIS FUNCTION.

## Proof of Concept

The implementation of `getAssetPrice()` is using the deprecated function:

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

```solidity
37:     function getAssetPrice(address asset) external view onlySupportedAsset(asset) returns (uint256) {
38:         return AggregatorInterface(assetPriceFeed[asset]).latestAnswer();
39:     }
```

## Recommendation

Switch to [`latestRoundData()`](https://docs.chain.link/data-feeds/api-reference#latestrounddata).



## Assessed type

Oracle
Strategy migration can result in the price of rsETH losing substantial value
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L52
https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L121
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L47
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L71
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTConfig.sol#L109


# Vulnerability details

## Impact
A strategy change will result in an incorrect price evaluation for RSETH due to it undervaluing the amount of underlying token deposited.
## Vulnerability Explanation
1. When ```LRTOracle#getRSETHPrice``` is called it gets the price of RSETH based on the calculated total amount of ETH based on total balance of underlying assets (stETH, rETH, cbETH) multiplied by the exchange rate to ETH

```solidity
    function getRSETHPrice() external view returns (uint256 rsETHPrice) {
        address rsETHTokenAddress = lrtConfig.rsETH();
        uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply();

        //...

        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;
    }
```
2. To get the total deposits, ```LRTDepositPool#getTotalAssetDeposits``` is called which sums up the amount of assets in the 3 possible locations returned by ```LRTDepositPool#getAssetDistributionData```
```solidity
    function getTotalAssetDeposits(address asset) public view override returns (uint256 totalAssetDeposit) {
        (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) =
            getAssetDistributionData(asset);
        return (assetLyingInDepositPool + assetLyingInNDCs + assetStakedInEigenLayer);
    }
```
3. ```LRTDepositPool#getAssetDistributionData``` sums up the total assets from 3 possible locations:
   i. In the ```LRTDepositPool```
   ii. Inside the node delegator queue contracts
   iii. Inside the asset eigenlayer strategy

```solidity
    function getAssetDistributionData(address asset)
        public
        view
        override
        onlySupportedAsset(asset)
        returns (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer)
    {
1@>        assetLyingInDepositPool = IERC20(asset).balanceOf(address(this));
        uint256 ndcsCount = nodeDelegatorQueue.length;
        f
Users can loose assets deposited in strategy  
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

https://github.com/Layr-Labs/eigenlayer-contracts/blob/75e59432d079c6f90d48d4e950a68c15867c82b2/src/contracts/core/StrategyManager.sol#L237-L251
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTConfig.sol#L108-L122


# Vulnerability details


## Impact
A copy of each strategy for an asset is stored in the LRTConfig contract, This copy of the asset to strategy mapping allows for the updating of a strategy mapped to an asset in `updateAssetStrategy`. It is also of note that the contract does not allow an already supported asset to be unsupported. But a once supported strategy could be updated for another new strategy as specified in the code. 


    function  updateAssetStrategy(
    
    address  asset,
    
    address  strategy
    
    )
    
    external
    
    onlyRole(DEFAULT_ADMIN_ROLE)
    
    onlySupportedAsset(asset)
    
    {
    
    UtilLib.checkNonZeroAddress(strategy);
    
    if (assetStrategy[asset] == strategy) {
    
    revert  ValueAlreadyInUse();
    
    }
    
    assetStrategy[asset] = strategy;
    }


The challenge with this is that when a deposit is made to a strategy, and the strategy is swapped for another new one, it means that the deposits made to the previous strategy is now un-redeemable as it is no more in the system anywhere again. 

 
## Proof of Concept

1. Strategy mapped to CbETH has about 200 CbETH stored in it on the Eigen Layer and 50 CbEth on the NDC. 

2.  There is a vulnerability on CbEth strategy specifically or on CbEth protocol and EigenLayer removes the Strategy from `strategyIsWhitelistedForDeposit` from its [codebase](https://github.com/Layr-Labs/eigenlayer-contracts/blob/75e59432d079c6f90d48d4e950a68c15867c82b2/src/contracts/core/StrategyManager.sol#L237C1-L251C6)

3. The admin/manager on Kelp has two options, either to pause the entire contract cause of one strategy vulnerability or remove that strategy and update to a new one which the EigenLayer will deploy.  

4. If the admin goes for the second, it then means that deposits to the strategy is now trapped. This should not be the case as even EigenLayer only disables deposits and not deposits and withdrawal. 

5. Admin might also choose to both pause the contract and update the strategy, as the strategy update like other setters on config isn't pause protected.

## Tools Used
Manual 

## Recommended Mitigation Steps
Should not allow for updating of strategy explicitly rather create a mechanism to disable a strategy for deposit only. 


## Assessed type

Other