xanoutas/solidity-security-findings huggingface dataset:
ChainlinkPriceOracle.sol:: getAssetPrice function: Absence of Chainlink Heartbeat Check
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
In the getAssetPrice function we need to use a heartbeat check with the lastRoundData function. The absence of a Chainlink heartbeat check could potentially lead to several issues  to the overall protocol. 

The use of the lastRoundData function in this context is essential to ensure that the data being used is current and accurate. Without this Chainlink heartbeat check, the system is vulnerable to several risks. Firstly, there is the danger of relying on stale or outdated price data, which could lead to incorrect operational decisions. This is particularly critical in dynamic market environments where price accuracy is paramount.

Additionally, the absence of a heartbeat check opens up the possibility for malicious manipulation. An attacker could potentially freeze the price feed, providing data that benefits their operations while disadvantaging others. Furthermore, the system's heavy reliance on external data sources becomes a liability if these sources go offline or stop providing updates. In such scenarios, the contract might continue to operate on the last available price, potentially leading to significant financial losses. Therefore, implementing a heartbeat check is not just a technical enhancement but a fundamental requirement to safeguard the protocol against these issues.


## Proof of Concept
-Setup Scenario: In this hypothetical scenario, a user engages with the Kelp product by depositing Ethereum (ETH) and receiving RSETH tokens. The pricing mechanism for this transaction is managed by the LRTOracle contract, which relies on the ChainlinkPriceOracle contract's getAssetPrice function to determine the market price of Liquid Staking Tokens (LST).

-Issue with Chainlink Oracle: A critical situation arises when the Chainlink oracle, responsible for providing LST/ETH price updates, experiences a technical failure and goes offline. Consequently, the price feed halts, freezing the last recorded price at 1 LST = 0.01 ETH.

-Market Price Variation: In the market, the value of LST experiences a significant increase, reaching 1 LST = 0.02 ETH. However, due to the offline status of the Chainlink oracle, this price change is not reflected in the Kelp product's system.

-Redemption Process: A user, unaware of the oracle's failure, initiates a redemption of their RSETH tokens for LST. The LRTOracle contract, in response, calls upon the getAssetPrice function to ascertain the current LST price.

-Impact of Stale Data: The getAssetPrice function, lacking a mechanism to verify the freshness of data (a heartbeat check), erroneously returns the outdated price of 1 LST = 0.01 ETH.

-Resulting Under-Redemption: This incorrect price leads to the user receiving a lower quantity of LST tokens than the market value dictates, potentially causing a financial loss.

-Trust Implications: Should the user realize
First depositor can force loss of huge value for the next depositor
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The vulnerability is similar to how the well-known first deposit attack works, but in this case since there is no withdraw function, the result is huge loss of minted `rsETH` for the 2nd depositor.
## Proof of Concept
The entry-point for depositing into the protocol is `LRTDepositPool::depositAsset` which invokes `_mintRsETH`, which in turn invokes `getRsETHAmountToMint` to calculate how much rsETH to mint for the deposit amount. Taking a closer look at the [function](https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L95-L109) we can see that it is calculated by:

```javascript
// calculate rseth amount to mint based on asset amount and asset exchange rate
rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
```
Inspecting the `LRTOracle::getRSETHPrice` [function](https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L52-L79) we can see that if the `rsETHSupply == 0` it will return `1`:

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

        if (rsEthSupply == 0) {
            return 1 ether;
        }
```

Otherwise, the function loops through the `suportedAssetCount` storing their combined total value in the `totalETHInPool` variable, then divides it by the `rsEthSupply`:

```javascript
        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;
```
Attack order:
1. Bob monitors the mempool for the first coming deposit in the contract
2. Alice decides to deposit 10 stETH and mint the corresponding rsETH
3. Bob front-runs her and deposits 0.001 stETH, since `rsEthSupply == 0`, Bob receives 0.001 rsETH
4. Bob then sends another 0.1 stETH directly to the contract without depositing
5. Sinc
Users will receive less rsETH token than expected amount and the rsETH price will rise every minting.
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L136-L141
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L151-L157
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L95-L110
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L52-L79


# Vulnerability details

## Impact
Due to incorrect calculation of rsETH price, users will receive less rsETH token than expected amount and the rsETH price will rise every minting.

## Proof of Concept

Users can deposit asset into `LRTDepositPool` contract and mint rsETH token using `depositAsset` function.
```solidity
        if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) {
            revert TokenTransferFailed();
        }

        // interactions
        uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount);
```
```solidity
    function _mintRsETH(address _asset, uint256 _amount) private returns (uint256 rsethAmountToMint) {
        (rsethAmountToMint) = getRsETHAmountToMint(_asset, _amount);

        address rsethToken = lrtConfig.rsETH();
        // mint rseth for user
        IRSETH(rsethToken).mint(msg.sender, rsethAmountToMint);
    }
```
And the amount of rsETH to mint is calculated based on asset amount and asset exchange rate
```solidity
        // calculate rseth amount to mint based on asset amount and asset exchange rate
        rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
```
The exchange rate of rsETH is calcurated based on total deposited asset `totalETHInPool` and minted amount of rsETH `rsEthSuppy`. (especially returns `1 ether` when it's first mint )
```solidity
    function getRSETHPrice() external view returns (uint256 rsETHPrice) {
        address rsETHTokenAddress = lrtConfig.rsETH();
        uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply();

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

        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;
    }
```
But in `depositAsset` function, rsETH is minted after asset is already transferred to `LRTDepositPool`. As a r
Upgraded Q -> 2 from #677 [1702060237936]
Audit: 2023-11-kelp-findings
Severity: MEDIUM

Judge has assessed an item in Issue #677 as 2 risk. The relevant finding follows:

 [L-04] Deposited amounts in the EigenLayer strategy should be checked before updating the strategy for the asset
Users deposit in this protocol and the protocol deposits these funds to EigenLayer strategy contracts. There are 3 supported assets (rETH, cbETH, stETH) at the moment and every asset has one strategy contract.

The protocol also has a maximum deposit limit for each asset. An asset’s total deposits are calculated by summing up the asset amounts in the pool, in node delegators and in the EigenLayer strategy.

The admin can update the strategy contract address for these assets.
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTConfig.sol#L109C1-L122C6

    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;
    }
As we can see above, this update is performed and the new strategy address is assigned without checking if there are already deposited assets in the previous strategy.

The biggest problem with that is the total asset deposits are calculated using the current EigenLayer strategy contract.
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L84
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/NodeDelegator.sol#L121C1-L124C6

file: NodeDelegator.sol
//@audit Strategy contracts can be changed for assets. If the strategy is changed for an asset (before withdrawing the balance from previous strategy), 
//       this function will return only the balance in the current strategy and the total deposits will be calculated lower than the actual.
    function getAssetBalance(address asset) external view override returns (uint256) { 
        address strategy = lrtConfig.assetStrategy(asset);
        return IStrategy(strategy).userUnderlyingView(address(this));
    }
If the strategy address is updated without withdrawing deposits from the previous strategy, the totalAssetDeposits will be calculated lower than the actual deposit amount. This will lead to the getAssetCurrentLimit() function returning a bigger value than the actual limit and breaking the maximum deposit invariant.

Recommendation: I would recommend checking if there are already deposited assets in the EigenLayer strategy, and withdrawing them before updating the strategy (Or transferring deposits from the old strategy to the new strategy in EigenLayer, and updating the asset strategy in the same transaction with a multicall-like action).
Upgraded Q -> 2 from #776 [1701456793936]
Audit: 2023-11-kelp-findings
Severity: MEDIUM

Judge has assessed an item in Issue #776 as 2 risk. The relevant finding follows:

 [Low-01] No minimum Amount(rsETH) receive parameter absent in depositAsset()
Here we can see that User deposit asset via depositAsset() which take asset address and asset depositAmount as parameter Then rsethAmountMinted calculated via _mintRsETH() - where it fetch corresponding asset price from oracle - then use formula (amount * assetPrice)/rsETHPrice to calculate rsethAmountMinted

Problem here is that if something go wrong with Oracle, then fetched price is mismatched with actual value Then User may receive less amount than he intended, may be there a huge slippage.

    function depositAsset( 
        address asset,
        uint256 depositAmount,
+       uint256 minAmount
    )
        external
        whenNotPaused
        nonReentrant
        onlySupportedAsset(asset)
    {
        // checks
        if (depositAmount == 0) {
            revert InvalidAmount();
        }
        if (depositAmount > getAssetCurrentLimit(asset)) { 
            revert MaximumDepositLimitReached();
        }

        if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { 
            revert TokenTransferFailed();
        }

        // interactions
-       uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount); 
+       uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount, minAmount); 

        emit AssetDeposit(asset, depositAmount, rsethAmountMinted);
    }
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L151-L157
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119-L122
Mitigation
To Overcome that function should also have a another parameter like minAmountReceived which inputed by user where User clearly says how much (slippage he can bear) rsEth he wanted to mint back.

And it should be check against rsethAmountToMint in _mintRsETH() before minting

-   function _mintRsETH(address _asset, uint256 _amount) private returns (uint256 rsethAmountToMint) {
+   function _mintRsETH(address _asset, uint256 _amount, uint _minAmount) private returns (uint256 rsethAmountToMint) {
        (rsethAmountToMint) = getRsETHAmountToMint(_asset, _amount);

+       require(rsethAmountToMint >= _minAmount, "error");
        address rsethToken = lrtConfig.rsETH();
        // mint rseth for user
        IRSETH(rsethToken).mint(msg.sender, rsethAmountToMint);
    }
Upgraded Q -> 2 from #776 [1701452999417]
Audit: 2023-11-kelp-findings
Severity: MEDIUM

Judge has assessed an item in Issue #776 as 2 risk. The relevant finding follows:

 [Low-01] No minimum Amount(rsETH) receive parameter absent in depositAsset()
Here we can see that User deposit asset via depositAsset() which take asset address and asset depositAmount as parameter Then rsethAmountMinted calculated via _mintRsETH() - where it fetch corresponding asset price from oracle - then use formula (amount * assetPrice)/rsETHPrice to calculate rsethAmountMinted

Problem here is that if something go wrong with Oracle, then fetched price is mismatched with actual value Then User may receive less amount than he intended, may be there a huge slippage.

    function depositAsset( 
        address asset,
        uint256 depositAmount,
+       uint256 minAmount
    )
        external
        whenNotPaused
        nonReentrant
        onlySupportedAsset(asset)
    {
        // checks
        if (depositAmount == 0) {
            revert InvalidAmount();
        }
        if (depositAmount > getAssetCurrentLimit(asset)) { 
            revert MaximumDepositLimitReached();
        }

        if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { 
            revert TokenTransferFailed();
        }

        // interactions
-       uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount); 
+       uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount, minAmount); 

        emit AssetDeposit(asset, depositAmount, rsethAmountMinted);
    }
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L151-L157
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119-L122
Mitigation
To Overcome that function should also have a another parameter like minAmountReceived which inputed by user where User clearly says how much (slippage he can bear) rsEth he wanted to mint back.

And it should be check against rsethAmountToMint in _mintRsETH() before minting

-   function _mintRsETH(address _asset, uint256 _amount) private returns (uint256 rsethAmountToMint) {
+   function _mintRsETH(address _asset, uint256 _amount, uint _minAmount) private returns (uint256 rsethAmountToMint) {
        (rsethAmountToMint) = getRsETHAmountToMint(_asset, _amount);

+       require(rsethAmountToMint >= _minAmount, "error");
        address rsethToken = lrtConfig.rsETH();
        // mint rseth for user
        IRSETH(rsethToken).mint(msg.sender, rsethAmountToMint);
    }
Upgraded Q -> 2 from #776 [1701285150754]
Audit: 2023-11-kelp-findings
Severity: MEDIUM

Judge has assessed an item in Issue #776 as 2 risk. The relevant finding follows:

 [Low-01] No minimum Amount(rsETH) receive parameter absent in depositAsset()
Here we can see that User deposit asset via depositAsset() which take asset address and asset depositAmount as parameter Then rsethAmountMinted calculated via _mintRsETH() - where it fetch corresponding asset price from oracle - then use formula (amount * assetPrice)/rsETHPrice to calculate rsethAmountMinted

Problem here is that if something go wrong with Oracle, then fetched price is mismatched with actual value Then User may receive less amount than he intended, may be there a huge slippage.

    function depositAsset( 
        address asset,
        uint256 depositAmount,
+       uint256 minAmount
    )
        external
        whenNotPaused
        nonReentrant
        onlySupportedAsset(asset)
    {
        // checks
        if (depositAmount == 0) {
            revert InvalidAmount();
        }
        if (depositAmount > getAssetCurrentLimit(asset)) { 
            revert MaximumDepositLimitReached();
        }

        if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { 
            revert TokenTransferFailed();
        }

        // interactions
-       uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount); 
+       uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount, minAmount); 

        emit AssetDeposit(asset, depositAmount, rsethAmountMinted);
    }
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L151-L157
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119-L122
Mitigation
To Overcome that function should also have a another parameter like minAmountReceived which inputed by user where User clearly says how much (slippage he can bear) rsEth he wanted to mint back.

And it should be check against rsethAmountToMint in _mintRsETH() before minting

-   function _mintRsETH(address _asset, uint256 _amount) private returns (uint256 rsethAmountToMint) {
+   function _mintRsETH(address _asset, uint256 _amount, uint _minAmount) private returns (uint256 rsethAmountToMint) {
        (rsethAmountToMint) = getRsETHAmountToMint(_asset, _amount);

+       require(rsethAmountToMint >= _minAmount, "error");
        address rsethToken = lrtConfig.rsETH();
        // mint rseth for user
        IRSETH(rsethToken).mint(msg.sender, rsethAmountToMint);
    }
Failure to Initialize Default Price Feeds Can Cause Unexpected Reverts
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
The ChainlinkPriceOracle contract does not initialize default price feed mappings for supported assets. This means calling the getAssetPrice function before explicitly setting a feed will result in a revert instead of a defined failure response.


Details:

The contract relies on price feeds stored in a mapping but does not populate this mapping on deployment.
If getAssetPrice is called before any feeds are set using updatePriceFeedFor, it will throw an error.
A more user-friendly failure response should be returned instead of an unexpected revert in this case.

## Tools Used

## Recommended Mitigation Steps


## Assessed type

Oracle
Changing rsETH address breaks contract
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
manipulating rsETH price 

## Proof of Concept
admin can change rsETH token address, while total supply of rsETH is used to calculate its price changing rsETH address changes its price. 

## Tools Used
Manual Review 
## Recommended Mitigation Steps
prevent changing rsETH address after it's been configured 


## Assessed type

Other
No slippage and / or deadline protection in depositAsset()
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Proof of Concept

The function depositAsset() allows a user to deposit a specific amount of any supported token and, in return, receive a specific amount of rsETH. This amount of rsETH is calculated by dividing the total value of deposited assets in ETH by the price of rsETH in ETH. The price of rsETH in ETH is obtained by summing all protocol assets denominated in ETH and dividing by the total supply of rsETH. The price of the supported assets in ETH is sourced from Chainlink or another oracle. Due to the volatility of the supported assets, the price of rsETH can change, affecting the amount of rsETH a user would receive. The tokens used represent staked ethers and typically have a price close to that of ether.
I examined historical charts of the supported Asset/ETH pairs, showing changes of a few percents. For instance, on 8/14/2023, the price of stETH dropped from 1.002 ETH to 0.9245 ETH. For users depositing a significant capital, slippage of a few percents can lead to substantial losses.
The same function lacks a deadline. When a deposit transaction is sent, it may take a considerable amount of time before it is executed due to various circumstances or may be intentionally delayed by a malicious operator. When executed, it can result in financial losses for the user, or there may no longer be a need for its execution from the user's perspective.

## Impact
Substantial financial losses for users which deposit significant capital. 

## Tools Used

Manual review

## Recommended Mitigation Steps

Implement slippage and deadline protection so that a user can choose the minimum amount of rsETH they want to receive against their deposit and the time until which the transaction is valid.


## Assessed type

Timing