xanoutas/solidity-security-findings huggingface dataset:
Mint amount calculation in deposit is incorrect
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 calculation in the deposit function of the DepositPool contract is flawed as it factors the deposited amount into the RSETH price to calculate the amount to mint.

## Impact

When a user deposits in the DepositPool contract, the amount of RSETH to mint is determined by the price returned by the LRTOracle:

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

In summary, the implementation takes all assets held by the protocol and normalizes them into ETH. This value is then divided by the current supply of RSETH to get the price per token. For each asset, deposits are given by the `getTotalAssetDeposits()` function:

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

```solidity
47:     function getTotalAssetDeposits(address asset) public view override returns (uint256 totalAssetDeposit) {
48:         (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) =
49:             getAssetDistributionData(asset);
50:         return (assetLyingInDepositPool + assetLyingInNDCs + assetStakedInEigenLayer);
51:     }
```

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

```solidity
71:     function getAssetDistributionData(address asset)
72:         public
73:         view
74:         override
75:         onlySupportedAsset(asset)
76:         returns (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer)
77:     {
78:         // Question: is here the right place to have this? Could it be in LRTConfig?
7
Funds cannot be withdrawn from EigenLayer
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

`NodeDelegator` contracts handle depositing LSTs into EigenLayer to earn yield. However the contract lacks functions to withdraw those tokens afterwards, meaning they will become trapped in the EigenLayer protocol. This breaks the functionality of the protocol and causes a loss of funds for users.

## Proof of Concept

After users call `LRTDepositPool#depositAsset()` to deposit their LST tokens, the funds are transferred to the `LRTDepositPool` contract. An LRT manager can then transfer the funds to a `NodeDelegator` by calling `transferAssetToNodeDelegator()`, and then deposit them into EigenLayer by calling `NodeDelegator#depositAssetIntoStrategy()`.

```solidity
File: src\NodeDelegator.sol

48:     /// @notice Deposits an asset lying in this NDC into its strategy
49:     /// @dev only supported assets can be deposited and only called by the LRT manager
50:     /// @param asset the asset to deposit
51:     function depositAssetIntoStrategy(address asset)
52:         external
53:         override
54:         whenNotPaused
55:         nonReentrant
56:         onlySupportedAsset(asset)
57:         onlyLRTManager
58:     {
59:         address strategy = lrtConfig.assetStrategy(asset);
60:         IERC20 token = IERC20(asset);
61:         address eigenlayerStrategyManagerAddress = lrtConfig.getContract(LRTConstants.EIGEN_STRATEGY_MANAGER);
62: 
63:         uint256 balance = token.balanceOf(address(this));
64: 
65:         emit AssetDepositIntoStrategy(asset, strategy, balance);
66: 
67:         IEigenStrategyManager(eigenlayerStrategyManagerAddress).depositIntoStrategy(IStrategy(strategy), token, balance);
68:     }
```

However the contracts current implementation contains no code to withdraw those funds from EigenLayer in the future. This means that any funds deposited into EigenLayer become irretrievable, as the shares are credited to the `NodeDelegator` address. 

## Recommended Mitigation Steps

Implement a function in `NodeDelegator` callable by the LRT manager to withdraw funds from EigenLayer. See [EigenLayer docs](
https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/docs/core/StrategyManager.md) for more information on depositing and withdrawing.


## Assessed type

Other
Malicious first depositor can steal all funds from all future depositors
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Due to a miscalculation in `LRTOracle#getRSETHPrice()`, users who call `LRTDepositPool#depositAsset()` when `rsETH.totalSupply()` is non-zero will receive fewer `rsETH` tokens than they should due to a rounding error. This can be exploited by a malicious first depositor, who can deposit 1 wei (frontrunning if necessary), receive 1 share and therefore cause all subsequent deposits to mint 0 shares, despite the `asset` tokens being transferred to the contract. Because this results in the first depositor holding 100% of the outstanding shares (1 out of 1) no matter how many more deposits occur, the attacker is entitled to the funds of those future depositors and has essentially stolen their assets.

It is worth noting that even if the first depositor is not malicious, and they deposit a reasonable amount of `asset` tokens, they would still be unintentionally stealing value from future depositors as the rounding error would still be present. In other words, a loss of funds for every depositor (except whoever deposits first) is guaranteed.

## Proof of Concept

### Explanation

In `LRTDepositPool#depositAsset()`, the `asset` tokens are transferred to the contract, and then `_mintRsETH()` is called to mint shares to `msg.sender`, which uses `getRsETHAmountToMint` to determine how many shares to mint. To perform the calculation, the prices of `asset` and `rsETH` are used, which are fetched by `LRTOracle#getAssetPrice()` and `LRTOracle#getRSETHPrice()`.

The issue here is that the output of `getRSETHPrice()` depends on the balance of `rsETH` tokens in `LRTDepositPool` (as long as `rsEthSupply` is non-zero), and this calculation occurs **after** the users `asset` tokens for the current mint have been transferred. This means that the `rsETH` price will be overinflated, and since it is used as the denominator in the calculation of `rsethAmountToMint`, this results in fewer tokens minted to the depositor.

```solidity
File: src\LRTOracle.sol

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: 
                // @audit getTotalAss
User can not receive own assets back
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The user has the opportunity to deposit tokens, but does not have the opportunity to withdraw them back. Consequently, his tokens will be locked in the contract and he will not be able to take them back.

Yes, the contract is upgradable - but this does not change anything. Because we are checking the current state of the code and there is a bug at the moment. Otherwise, one could say this about all the bugs - that the contract is being updated and it will be fixed/completed

## Proof of Concept
There is not function for withdraw tokens in LRTDepositPool.sol file

## Tools Used
Manual review
## Recommended Mitigation Steps
Add function for opportunity of user to get back own assets


## Assessed type

Other
Deriving the price with balanceOf() is dangerous
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact

Deriving the price with `balanceOf()` is dangerous as it can easily be manipulated by direct transfers.

## Proof of Concept

In the getAssetDistributionData() function, the asset lying in the LRTDepositPool is retrieved using balanceOf(address(this)):

```solidity
        assetLyingInDepositPool = IERC20(asset).balanceOf(address(this));
```

Similarly, the assets in :NodeDelegator are obtained through balanceOf(nodeDelegator):

```solidity
assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]);
```

Deriving the price with balanceOf is dangerous as balanceOf may be gamed. Consider univ2 as an example;
Exploiters can send tokens into the pool and pump the price, successfully manipulating the price.

> Similar finding: https://solodit.xyz/issues/m-05-pair-price-may-be-manipulated-by-direct-transfers-code4rena-caviar-caviar-contest-git

## Tools Used

Manual review

## Recommended Mitigation Steps

Consider tracking balances, using state variables, similarly to how Uniswap V2.


## Assessed type

Oracle
The staker could silently lose all their previously deposited assets when revert TokenTransferFailed() is called
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
The staker could silently lose all their deposited assets in the NodeDelegator.sol function when they time to transfer their assets. This could happen when the staker owns assets in the Eigen but it is not enough to be transferred when the calculated amount is low. 

## Proof of Concept
The fail of transfer could be as a result of arithmetic that could cause an overflow leading to the revert TokenTransferFailed() or it could be as a result of the transferBackToLRTDepositPool function of the NodeDelegator contract does not hold enough tokens or assets for the staker to be able to carry out the calculated transfer amount. 


Proof of Concept: A staker could deposit new assets, but the value of the asset could be affected by arithmetic overflow leading to not enough asset or token to transfer. This will cause a revert of the transaction. If the execution continues, then the staker’s assets and token stake can eventually be set to zero.

Contract name: 
NodeDelegator.sol

Code link: 
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/NodeDelegator.sol#L74C4-L90C1

Code line:
  address lrtDepositPool = lrtConfig.getContract(LRTConstants.LRT_DEPOSIT_POOL);

        if (!IERC20(asset).transfer(lrtDepositPool, amount)) {
            revert TokenTransferFailed();
        } 

## Tools Used
Manual review

## Recommended Mitigation Steps

For recommendation, the system can revert transactions whenever part of the assets or token amounts deposited to be transferred are low and could not be transferred because of an internal revert. This will prevent situations where certain stakers get to transfer their assets and tokens while others do not. 

For example, adding the code below would help check the balance before transfer is conducted by the staker to know if the balance is enough to transfer without fail.

address lrtDepositPool = lrtConfig.getContract(LRTConstants.LRT_DEPOSIT_POOL);
	uint256 balance = token.balanceOf(address(this));
emit AssetDepositIntoStrategy(asset, strategy, balance);
        if (!IERC20(asset).transfer(lrtDepositPool, amount)) {
            revert TokenTransferFailed();
        } 




## Assessed type

Under/Overflow
`getRSETHPrice()` of `LRTOracle.sol` will return 0 `totalETHInPool` is less than `rsEthSupply`.
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details


### Summary

If `totalETHInPool` is less than `rsEthSupply` then `getRSETHPrice()` will return 0 as price of rsEth in Eth. It will result in reverting when `depositAsset()` of `LRTDepositPool.sol` is called. Because `getRsETHAmountToMint` will revert when rsEth price is 0 and it is called inside `depositAsset`.

### Vulnerability Details

As you can see in below `getRSETHPrice()` function in line 78 if `totalETHInPool` is less than `rsEthSupply` then it will return 0.
[src/LRTOracle.sol#L52-L79](https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L52C4-L79C6)

```solidity
52:  function getRSETHPrice() external view returns (uint256 rsETHPrice) {

        ...

78:     return totalETHInPool / rsEthSupply;
       }
```

In below `getRsETHAmountToMint` function in line 109 `getRSETHPrice()` is called in denominator if it returns 0 then `getRsETHAmountToMint` will revert. This will cause `depositAsset` to revert when it will be called if `totalETHInPool` is less than `rsEthSupply` in `getRSETHPrice()` of `LRTOracle.sol`.
In line 141 `_mintRsETH` is called in which `getRsETHAmountToMint` is called.

[src/LRTDepositPool.sol#L95-L110](https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L95C3-L110C6)
[src/LRTDepositPool.sol#L119-L158](https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119C4-L158C1)

```solidity
95:     function getRsETHAmountToMint(
           address asset,
           uint256 amount
          )
            public
            view
            override
            returns (uint256 rsethAmountToMint)
        {
            // setup oracle contract
             address lrtOracleAddress = lrtConfig.getContract(LRTConstants.LRT_ORACLE);
             ILRTOracle lrtOracle = ILRTOracle(lrtOracleAddress);

             // calculate rseth amount to mint based on asset amount and asset exchange rate
109:         rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
         }


119:        function depositAsset(

             ...

141:        uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount);

            emit AssetDeposit(asset, depositAmount, rsethAmountMinted);
           }
              ...

151:     function _mintRsETH(address _asset, uint256 _amount) private returns (uint256 rsethAmountToMint) {
152:        (rsethAmountToMint) = getRsETHAmountToMint(_asset, _amount);

           address rsethToken = lrtConfig.rsETH();
            // mint rseth for user
            IRSETH(rsethToken).mint(msg.sender, rsethAmountToMint);
          }
```

### Impact

If `totalETHInPool` is less than `rsEthSupply` then `getRSETHPrice()` will return 0 as price of rsEth in Eth. It will result in reverting when `depositAsset()` of `LRTDepositPool.sol` is called. Because `getRsETHAmountToMint` will revert when rsEth price is 0 and i
The deposited amount is included in how rsEthAmountToMint is calculated and it should not. Second depositors get less rsETH shares than deserved.
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Impact
All deposits, starting with the second one, incur a loss in the received rsETH amount.

## Proof of Concept
`LRTDepositPool::depositAsset` helps users to stake LST in exchange for rsETH shares.
First the LST is transferedFrom user to depositPool and rsETH is minted to depositor. 

```solidity
...
        if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) {
            revert TokenTransferFailed();
        }
        // interactions
        uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount);
 ```

The amount of rsETH to mint is [calculated](https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L109) by `getRsETHAmountToMint` as following:

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

`lrtOracle.getRSETHPrice` calls `LRTDepositPool::getTotalAssetDeposits` -> `getAssetDistributionData` to get LST assets amounts distributed among depositPool, NDCs and eigenLayer to calculate the rsETH/ETH exchange rate. 
```solidity
    function getAssetDistributionData(address asset)
        public
        view
        override
        onlySupportedAsset(asset)
        returns (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer)
    {
        // Question: is here the right place to have this? Could it be in LRTConfig?

        // @audit includes the transferred amount from the depositor in the depositAsset() function 
        assetLyingInDepositPool = IERC20(asset).balanceOf(address(this));
        uint256 ndcsCount = nodeDelegatorQueue.length;
        for (uint256 i; i < ndcsCount;) {
            assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]);
            assetStakedInEigenLayer += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset);
            unchecked {
                ++i;
            }
        }
    }
```

So rsETHPrice is calculated as: `rsETHPrice = totalAssetDeposits * assetPrice / rsEthSupply

The issue rely in including the current deposit amount in the rsETH/ETH price calculation and then in rsETH amounts to mint formula.

Let's take the following example:
Note: to simplify I approximate the LST/ETH ratio to 1e18.
- Alice (first depositor) deposit 1 ether LST. rsETH totalSupply is 0 => `getRSETHPrice` returns 1 et
The first depositor can get double the amount of RSETH or DOS other users' deposit funds.
Audit: 2023-11-kelp-findings
Severity: HIGH

# Lines of code

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


# Vulnerability details

## Vulnerability Details
When a user deposits in the `LRTDepositPool.sol` contract before any other user and before any RSETH has ever been minted the price of RSETH is set to 1 ether.
```
if (rsEthSupply == 0) {
    return 1 ether;
}
```
After a deposit has been made, the price of RSETH is calculated in the following way:
`totalETHInPool / rsEthSupply`
For simplicity of the explanation, the RETH token will be used with a price of 1 ether as used in the `LRTOracleMock.sol` mock contract. In that case, if the first depositor deposits 10 ether of RETH he will always receive 10 ether of RSETH as the price is 1 ether.
After that, the second depositor who, let's say, also deposits 10 ether of RSETH will in contrast receive only 5 ether of RSETH, due to the following calculation in the `getRsETHAmountToMint` function:
```
rsethAmountToMint =
   (amount * lrtOracle.getAssetPrice(asset)) /
   lrtOracle.getRSETHPrice();
```
Essentially the amount to be minted will be equal to:
   `(10e18 * 1e18) / (20e18 * 1e18 / 10e18)`
In other words, firstly the deposited 10 ether of RSETH is multiplied by the price. The result of that is divided by the total amount of RETH in the contract (20e18) multiplied by its price (1e18) divided by the current supply of RSETH (10e18).
The total amount of RETH is 20e18 and not 10e18, because RETH is transferred from the depositor before the minting of RSETH, which is problematic, causing the amount of RSETH to be minted to be only 5 ether. Every other user who also deposits 10 ether of RETH will also get only 5 ether of RSETH, if they deposit 5 ether RETH, they will get 2.5 ether RSETH, and if they deposit 20 ether of RETH - 7.5 ether of RSETH.
That said, the amount, deposited by the first depositor, determines how much every other user will consequently receive. Therefore, if the first depositor only deposits 1 WEI every other deposit following that will not receive any RSETH despite their funds being transferred to the pool, due to the following calculations:
1/ the first depositor deposits 1 wei of RETH and receives 1 wei of RSETH: 1 * 1e18 / 1e18
2/ the second depositor deposits 10 ether of RETH and receives 0 wei: (10e18 * 1e18) / ((10e18+1) * 1e18 / 1)

## Impact
The first depositor decreases every other user's received RSETH or completely prevents them from receiving RSETH.

## Proof of Concept
The following coded POC demonstrates the issue. As deposit tests, written by the protocol team, use a mock LRTOracle contract (which does not act the way the real contract would) I have implemented a LRTOracle contract identical to the one meant to be used in production, except that it returns 1e18 for an asset's price instead of calling an external oracle.
Create a `ExploitFirstDepositor.t.sol` contract in the test folder of the project and paste the following code in i