xanoutas/solidity-security-findings huggingface dataset:
Decimal Precision Issue in Price Calculations
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

The  `getRSETHPrice()` function perform calculations like multiplying prices by amounts without considering the token decimals. This can cause errors to accumulate over multiple calculations.

Ignoring token decimals when performing price calculations can cause small errors to accumulate over many transactions. This undermines the accuracy of the reported exchange rates.

The Root Cause

Token decimals are not validated or used when multiplying/dividing quantities. Precision is lost from the keystone currency (typically 18 decimal ETH).


## Proof of Concept
The function `getRSETHPrice()` contain calculations like:

```solidity
            uint256 assetER = getAssetPrice(asset);

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

Without proper scaling, precision is lost when tokens have differing decimals.


- Deploy test tokens with non-standard decimals
- Calculate price without scaling
- Observe errors increase over many calls

## Tools Used
Manual Review

## Recommended Mitigation Steps

Use fixed point or decimal math libraries



## Assessed type

Decimal
Missing check on deposited amount
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
depositAsset() transfers tokens from the sender without checking that the amount was indeed received. If the protocol were to accept other tokens than stETH, rETH, and cbETH, there is a possibility that some of the newly accepted tokens have custom ERC20 logic that implement fees. Since the amount of RSETH minted depends on how much was exactly deposited (`uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount);`), this would pause a big accounting problem. In the worst case, protocol could lose funds as it mints more than what was deposited, and it will also fire a wrong event `AssetDeposit` since the `depositAmount` is not correct, throwing off off-chain logging services.

This is not a high-severity, as I am assuming the accepted ERC20-tokens are those present in the deployment script. This will become a problem if the protocol plan to expands to other ERC20-tokens.

## Proof of Concept
```
    function depositAsset(
        address asset,
        uint256 depositAmount
    )
        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);

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

## Tools Used
Manual review.

## Recommended Mitigation Steps
The amounts received should be validated.


## Assessed type

Invalid Validation
Saving balance of token in variable can lead to wrong amount transfer or failure
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
Saving contract balance into variable will result in wrong price being transferred using the ```depositIntoStrategy``` method which will cause the transfer to fail if balance is less than transferred and wrong amount transferred if amount increases.


## Proof of Concept
The balance of token is saved into ```balance``` variable which is then used to transfer using the ```depositIntoStrategy``` method.

```
    function depositAssetIntoStrategy(address asset)
        external
        override
        whenNotPaused
        nonReentrant
        onlySupportedAsset(asset)
        onlyLRTManager
    {
        address strategy = lrtConfig.assetStrategy(asset);
        IERC20 token = IERC20(asset);
        address eigenlayerStrategyManagerAddress = lrtConfig.getContract(LRTConstants.EIGEN_STRATEGY_MANAGER);

        uint256 balance = token.balanceOf(address(this));

        emit AssetDepositIntoStrategy(asset, strategy, balance);

        IEigenStrategyManager(eigenlayerStrategyManagerAddress).depositIntoStrategy(IStrategy(strategy), token, balance);
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
Use token balance in the contract address directly to avoid wrong transfer.
```diff
    function depositAssetIntoStrategy(address asset)
        external
        override
        whenNotPaused
        nonReentrant
        onlySupportedAsset(asset)
        onlyLRTManager
    {
        address strategy = lrtConfig.assetStrategy(asset);
        IERC20 token = IERC20(asset);
        address eigenlayerStrategyManagerAddress = lrtConfig.getContract(LRTConstants.EIGEN_STRATEGY_MANAGER);

-        uint256 balance = token.balanceOf(address(this));

-        emit AssetDepositIntoStrategy(asset, strategy, balance);
+	emit AssetDepositIntoStrategy(asset, strategy, token.balanceOf(address(this));

- IEigenStrategyManager(eigenlayerStrategyManagerAddress).depositIntoStrategy(IStrategy(strategy), token, balance);
+ IEigenStrategyManager(eigenlayerStrategyManagerAddress).depositIntoStrategy(IStrategy(strategy), token, token.balanceOf(address(this));
    }
```



## Assessed type

Other
No slippage control on deposit of LRTDepositPool.sol
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact

There is no slippage control on `depositAsset()` of `LRTDepositPool`, which expose user to sandwich attack.

## Proof of Concept

Any deposit can be sandwiched in `LRTDepositPool`, especially when the pool is not balanced.

#### Exploit Scenario:

Bob, a normal user, calls `depositAsset()`. Since there is no `minAmountOut`, which means that the deposit can be executed at any price. As a result, when Eve sandwiches the deposit , Bob deposit the tokens without minting any, effectively giving away tokens for free.

[A Detailed Guide To Sandwich Attacks In DeFi](https://101blockchains.com/defi-sandwich-attack/#:~:text=Definition%20of%20Sandwich%20Attacks&text=Sandwich%20attacks%20are%20a%20variant,exchange%20of%20tokens%20from%20wallets).
## Tools Used

## Recommended Mitigation Steps

Add a `minAmountOut` in `depositAsset()`


## Assessed type

MEV
depositAsset() does not check for slippage
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Prices are fetched from Chainlink but it is possible that the prices are stale (due to to the fact that it is not checking for the freshness of the price) or chainlink could be experiencing a crash event where it would return prices between a minimum and maximum pre-defined. In these instances, an attacker could make use of this opportunity to delay a user transaction to force the user to incur high slippage.

## Proof of Concept
```
    function depositAsset(
        address asset,
        uint256 depositAmount
    )
        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);

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

## Tools Used
Manual review.

## Recommended Mitigation Steps
Add a slippage check.


## Assessed type

Other
latestRoundData recommendation does not have consideration for stale price
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


## Impact
The issue is highlighted in the bot [L-2](https://github.com/code-423n4/2023-11-kelp/blob/main/bot-report.md#l-02-avoid-the-use-of-deprecated-chainlink-functions) finding but fail to highlight the importance for checking stale price.
The ChainlinkPriceOracle when calls out to a Chainlink oracle receiving using the recommended latestRoundData() it can get stale price, if there is a problem with Chainlink starting a new round and finding consensus on the new value for the oracle (e.g. Chainlink nodes abandon the oracle, high volatility ) consumers of this contract may continue using outdated stale or incorrect data (if oracles are unable to submit no new round is started).


## Proof of Concept
The recommendation of using ```latestRoundData``` in the bot finding does not provide consideration for checking updated price feed which can result in loss of funds for user and protocol.

```
function getAssetPrice(address asset) external view onlySupportedAsset(asset) returns (uint256) {
        return AggregatorInterface(assetPriceFeed[asset]).latestRoundData();
    }
```

## Tools Used
Manual Review

## Recommended Mitigation Steps
It is recommended to check for price feed with an interval of 1 hour or more as desired by the protocol to have updated price for every asset.
```diff
function getAssetPrice(address asset) external view onlySupportedAsset(asset) returns (uint256) {
-        return AggregatorInterface(assetPriceFeed[asset]).latestAnswer();
   

+	(uint80 roundID, int256 answer, uint256 timestamp, uint256 updatedAt, ) = return AggregatorInterface(assetPriceFeed[asset]).latestRoundData
 ();
        
+       require(updatedAt >= roundID, "Stale price");
+      require(timestamp != 0,"Round not complete");
+        require(answer > 0,"Chainlink answer reporting 0");

+      if (updatedAt < block.timestamp – 1 hours)
+         revert PRICE_OUTDATED(assetPriceFeed[asset]);


}
```




## Assessed type

Oracle
Too small deposits will result in no rsEth mint for the 
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
User will get nothing if the deposit amount is too small . 

## Proof of Concept
The `getRsETHAmountToMint` is for getting the conversion rate of asset to rsEth . 

```solidity 

   /// @return rsethAmountToMint Amount of rseth to mint
    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
        rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
    }
```
However if the deposit is too small , the depositor will get nothing out of the deposit . Also , there's no mechanism to prevent solidity's rounding issue . 

 let's consider a scenario where amount = 1, assetPrice = 49, and rsEthPrice = 50. Without addressing the rounding issue, the calculation would be:

```solidity 
rsethAmountToMint = (1 * 49) / 50; // Resulting in 0 due to rounding

```
This would result in `rsethAmountToMint` being calculated as 0, which might not be the desired behavior. 
the user will get nothing out of this as `rsethAmountToMint ` is rounded down to zero . 

## Tools Used
Manual review ! 
## Recommended Mitigation Steps

To handle this rounding issue, the precision factor 1e18 should be introduced in the calculation:

```solidity 
rsethAmountToMint = (1 * 40 * 1e18) / 50 / 1e18; // Resulting in a non-zero value


```
This ensures that the intermediate calculations are done with higher precision before dividing by 1e18 to get the final result.



## Assessed type

Math
`getAssetPrice` in `ChainlinkPriceOracle.sol` can return stale price.
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details


### Summary

On chainlink oracle for every pair of tokens price updating time is different. After that particular time the price will be updated. `getAssetPrice` function is not checking when the last time the price was updated. So it may return stale price . So the price can be something other than the current price. To ensure that price is not stale, a check must be implemented when last time price was updated, using chainlink price feed.

### Vulnerability Details

Since Chainlink oracle returns price of any asset in the from of another asset. And price changes on every particular time for particular pair so chainlink work on heartbeat concept which means the price of an pair updated on every heartbeat(in seconds).

let's consider the ETH / USD oracles on different chains.
On Ethereum, the oracle will update the price data every ~1 hour.

In below function we are directly consuming price feeds from chainlink oracle without checking the price is stale or not.

[src/oracles/ChainlinkPriceOracle.sol#L37-L39](https://github.com/code-423n4/2023-11-kelp/blob/main/src/oracles/ChainlinkPriceOracle.sol#L37C5-L39C6)

```solidity
37:     function getAssetPrice(address asset) external view onlySupportedAsset(asset) returns (uint256) {
38:            return AggregatorInterface(assetPriceFeed[asset]).latestAnswer();//@audit check if the price is not stale
39:      }
```

### Impact

The incorrect prices can cause the protocol's functions to operate incorrectly, affecting the protocol's disruption.

### Tools Used

Manual Review

### Recommended Mitigation Steps

Use some TIME_OUT constant to ensure if the price is updated before this time than it will be considered stale. Since on the same chain, different collateral tokens can have different heartbeats (the period to update the price data on chain). So in this case mapping can be used having different TIME_OUT for different pair. And add one setter function to remove and add TIME_OUT for token pairs.


## Assessed type

Oracle
Missing pause check in `transferAssetToNodeDelegator()`
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary

The function `transferAssetToNodeDelegator()` present in the LRTDepositPool contact ignores the contract's pause state.

## Impact

The LRTDepositPool contract contains a functionality to _pause_ the contract in case of an emergency.

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

```solidity
208:     function pause() external onlyLRTManager {
209:         _pause();
210:     }
211: 
212:     /// @dev Returns to normal state. Contract must be paused
213:     function unpause() external onlyLRTAdmin {
214:         _unpause();
215:     }
```

The `transferAssetToNodeDelegator()` function, used to transfer funds to node delegators, is not affected by this pause, letting managers move funds even if the contract is paused.

Under an emergency scenario, this would allow a manager, intentionally or accidentally, to transfer assets into a compromised node delegator.

## Proof of Concept

The `whenNotPaused` modifier is absent in the implementation of `transferAssetToNodeDelegator()`.

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

```solidity
183:     function transferAssetToNodeDelegator(
184:         uint256 ndcIndex,
185:         address asset,
186:         uint256 amount
187:     )
188:         external
189:         nonReentrant
190:         onlyLRTManager
191:         onlySupportedAsset(asset)
192:     {
193:         address nodeDelegator = nodeDelegatorQueue[ndcIndex];
194:         if (!IERC20(asset).transfer(nodeDelegator, amount)) {
195:             revert TokenTransferFailed();
196:         }
197:     }
```

## Recommendation

Add the `whenNotPaused` modifier to the `transferAssetToNodeDelegator()` function.

```diff
    function transferAssetToNodeDelegator(
        uint256 ndcIndex,
        address asset,
        uint256 amount
    )
        external
        nonReentrant
        onlyLRTManager
        onlySupportedAsset(asset)
+       whenNotPaused
    {
```



## Assessed type

Other
Use contracts-upgradeable instead of contract variants of OpenZeppelin
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L13
https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L12
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L14


# Vulnerability details


## Impact
OpenZeppelin’s contracts variants when used with upgradeability will result in negative impact on the overall contract functionality.

Check this OpenZeppelin [warning]( https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable#use-upgradeable-libraries) about mixing contract variants with upgradeable-contract.



## Proof of Concept

Upgradeable versions of contract i.e., ``` LRTDepositPool```, ``` NodeDelegator``` and ``` LRTOracle``` are using contract variants instead of contract-upgradeable variants that affect the contract working due to dependence on initialize functionality.

```
File: LRTDepositPool.sol
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
```

```
File: NodeDelegator..sol
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
```

```
File: LRTOracle.sol
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";
```

When contracts are deployed with upgradeability they must consider the Upgradeable variant of OpenZeppelin rather than the simple contract variants as defined in OZ’s official [docs]( https://docs.openzeppelin.com/contracts/5.x/upgradeable).


## Tools Used
Manual Review

## Recommended Mitigation Steps
Import from contracts-upgradeable, not from contracts when any upgradeable OZ's contract is used.



## Assessed type

Library
ChainLink's latestAnswer is deprecated
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Impact
Chainlink's documentation indicates that the `latestAnswer` function is deprecated. This function lacks reversion if no answer is obtained, instead returning 0. Additionally, the reported `latestAnswer` varies with 18 decimals for some token quotes and 8 decimals for others. It is recommended to dynamically retrieve decimals from oracles rather than hard-coding them in the contract.

## Proof of Concept
https://github.com/code-423n4/2023-11-kelp/blob/main/src/oracles/ChainlinkPriceOracle.sol#L38
[Chainlink Price Feeds API Reference](https://docs.chain.link/docs/price-feeds-api-reference/)  
[Deprecated AggregatorInterface API Reference](https://docs.chain.link/docs/deprecated-aggregatorinterface-api-reference/)

## Tools Used
Manual Review

## Recommended Mitigation Steps
- Utilize the `latestRoundData` function to retrieve prices. Implement checks on the return data with revert messages if the price is stale. E.g:
  ```solidity
  (uint80 roundID, int256 price, , uint256 timeStamp, uint80 answeredInRound) = oracle.latestRoundData();
  require(answeredInRound >= roundID, "Stale price data");

- Add timestamp check to avoid staleness
- Add a buffer to avoid excessive price changes like for e.g:
```
require(price - lastPrice <= buffer, "");
```


## Assessed type

Oracle
Deposit into strategy could be blocked by limits in EigenLayer
Audit: 2023-11-kelp-findings
Severity: MEDIUM

# Lines of code

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


# Vulnerability details

## Summary

The implementation of `depositAssetIntoStrategy()` deposits the entire balance of the node delegator which could conflict with the max limits per deposit defined in EigenLayer.

## Impact

NodeDelegator are responsible for managing assets in EigenLayer. The DepositPool has a queue of node delegators, and each of these is in charge of depositing assets into EigenLayer. The core implementation can be seen in `depositAssetIntoStrategy()`:

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

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

As we can see in the previous snippet of code, the implementation simply grabs the entire balance of the given `asset` present in the contract and tries to deposit it into the corresponding EigenLayer strategy.

This may conflict with EigenLayer preconditions, which impose a max limit **per deposit action**:

https://github.com/Layr-Labs/eigenlayer-contracts/blob/0139d6213927c0a7812578899ddd3dda58051928/src/contracts/strategies/StrategyBaseTVLLimits.sol#L73-L76

```solidity
73:     function _beforeDeposit(IERC20 /*token*/, uint256 amount) internal virtual override {
74:         require(amount <= maxPerDeposit, "StrategyBaseTVLLimits: max per deposit exceeded");
75:         require(_tokenBalance() <= maxTotalDeposits, "StrategyBaseTVLLimits: max deposits exceeded");
76:     }
```

This means that if the current asset balance of the node delegator (`token.balanceOf(address(this))`) is greater than the defined `maxPerDeposit` in the corresponding EigenLayer strategy, the deposit action will fail since the manager doesn't have a way to control the volume of the deposit.

## Recommendation

Add an `amount` parameter to `depositAssetIntoStrategy()` so the manager can define the amount of the asset to be deposited in EigenLayer.

```diff
-   function depositAssetIntoStrategy(address asset)
+   function depositAssetIntoStrategy(address asset, uint256 amount)
        external
        override
        whenNotPaused
        nonReentrant
        onlySupportedAsset(asset)
        onlyLRTManager
    {