Kelp DAO | rsETH - SBSecurity's results

A collective DAO designed to unlock liquidity, DeFi and higher rewards for restaked assets through liquid restaking.

General Information

Platform: Code4rena

Start Date: 10/11/2023

Pot Size: $28,000 USDC

Total HM: 5

Participants: 185

Period: 5 days

Judge: 0xDjango

Id: 305

League: ETH

Kelp DAO

Findings Distribution

Researcher Performance

Rank: 6/185

Findings: 5

Award: $1,044.54

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

Labels

3 (High Risk)
satisfactory
duplicate-584

Awards

902.5718 USDC - $902.57

External Links

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

[L-03] Consider use stETH/UDS oracle Issue Description: The sponsor has confirmed their choice of Chainlink as an oracle to fetch prices. Since all other LST price feeds are 18 decimal places, they will most likely use stETH/ETH price feeds. However, this feed has a long heartbeat and a 2% deviation threshold, which could lead to fund loss. The 24-hour heartbeat and 2% deviation threshold mean the price can move up to 2% or remain unchanged for 24 hours before triggering a price update. This could result in the on-chain price being significantly different from the true stETH price, leading to incorrectly calculated rsETH to mint.

Recommendation:

Consider using the stETH/USD oracle instead, as it offers a 1-hour heartbeat and a 1% deviation threshold. To accommodate this change, also need to adjust the decimal calculation in LRTOracle::getAssetPrice to multiply by 18 ** (token.decimal - pricefeed.decimal) or a similar factor. This adjustment ensures that the price of all tokens is returned with the same decimal precision.

#0 - c4-judge

2023-12-08T18:33:13Z

fatherGoose1 marked the issue as duplicate of #584

#1 - c4-judge

2023-12-08T18:33:18Z

fatherGoose1 marked the issue as satisfactory

#2 - d3e4

2023-12-08T21:16:33Z

@fatherGoose1 This provides no justification for why an incorrect price is an issue, unlike #584 which makes this justification by noting how this can be exploited when there are multiple underlying assets.

Awards

36.0335 USDC - $36.03

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
upgraded by judge
duplicate-62

External Links

Lines of code

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

The initial depositor stands to gain an unfair amount of RSETH tokens compared to later depositors, as a result of the fixed exchange rate of 1 ether when no RSETH supply exists (i.e., no minted tokens are available).

Consequently, the first deposit will receive ETH price of deposited token (rETH price in term of ETH at the moment: 1,090,300,000,000,000,000) in RSETH tokens.

Proof of Concept

As mentioned earlier, the current price of rETH in terms of ETH is 1,090,300,000,000,000,000.

Obtain this information from 0x536218f9E9Eb48863970252233c8F271f554C2d0

rETH-ETH-Oracle
rETH-ETH-Oracle-answer
  1. Alice deposits 1e18 tokens of any supported asset can be either rETH, cbETH, or stETH (will use rETH for the explanation).
  2. The code calculates the RSETH token amount using the formula:
src: src/LRTDeposit#L109
rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();

Considering it's the first deposit, lrtOracle::getRSETHPrice returns a hardcoded value of 1 ether (1e18):

uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply();
if (rsEthSupply == 0) {
    return 1 ether;
}

For a single rETH token, the contract will mint the value of rETH taken from the chainlink at the moment RSETH tokens for Alice.

  1. Bob deposit 1e18 token.
  2. In depositAsset(), it starts by adding the depositAmount and then calculates the amount of rsETH tokens to be minted.
  3. getRsETHAmountToMint() will be called.

In this line, the amount on the left stays the same as the initial deposit, but the value from lrtOracle.getRSETHPrice() on the right side will change.

rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
//                   1e18 * 1,090,300,000,000,000,000 / ? (calculated in the next steps)
  1. getRSETHPrice() will get the rsEthSupply which will be 1,090,300,000,000,000,000. It then calculates the totalETHInPool based on the two deposited 1e18 rETH tokens from Alice and Bob.
  2. It will get 2e18 and will multiple it by 1,090,300,000,000,000,000 (price of rETH at the moment)
return totalETHInPool / rsEthSupply; 
// (2e18 * 1,090,300,000,000,000,000) / 1,090,300,000,000,000,000
// = (2e18 * ~~1,090,300,000,000,000,000~~) / ~~1,090,300,000,000,000,000~~
// = 2e18
rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
//                   1e18 * 1,090,300,000,000,000,000 / 2e18
//                   = 545,150,000,000,000,000
  1. In the end, Bob will deposit the same amount but will receive half as many rsETH tokens (545,150,000,000,000,000) compared to the Alice's initial deposit.
  2. If a third person participates, they will also receive 545,150,000,000,000,000 rsETH tokens following the same pattern.

Coded POC

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

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

Place the PoC at the end of the file.

Can be run with:

forge test --match-contract LRTDepositPoolInitialDeposit --match-test test_InitialDepositorGetTwiceMoreRSETHMinted -vvv
contract LRTOracleMock_InitialDepositor {
    RSETH rseth;
    MockToken public rETH;
    LRTDepositPool public lrtDepositPool;

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

    function getAssetPrice(address) public pure returns (uint256) {
        return 1_090_300_000_000_000_000; // Price of rETH at the moment of write
    }

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

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

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

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

    function setUp() public override {
        super.setUp();

        // initialize LRTDepositPool
        lrtDepositPool.initialize(address(lrtConfig));

        rETHAddress = address(rETH);

        // add manager role within LRTConfig
        vm.startPrank(admin);
        lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(new LRTOracleMock_InitialDepositor(rseth, rETH, lrtDepositPool)));

        lrtConfig.grantRole(LRTConstants.MANAGER, manager);
        vm.stopPrank();
    }

    function test_InitialDepositorGetTwiceMoreRSETHMinted() external {
        uint256 depositAmount = 1 ether;

        vm.startPrank(alice);

        rETH.approve(address(lrtDepositPool), depositAmount);
        lrtDepositPool.depositAsset(address(rETH), depositAmount);
        
        uint256 aliceRSETHBalance = rseth.balanceOf(alice);
        console.log("Alice rsETH Balance: ", aliceRSETHBalance);
        vm.stopPrank();

        vm.startPrank(bob);

        rETH.approve(address(lrtDepositPool), depositAmount);
        lrtDepositPool.depositAsset(address(rETH), depositAmount);
        
        uint256 bobRSETHBalance = rseth.balanceOf(bob);
        console.log("Bob rsETH Balance:    ", bobRSETHBalance);
    }
}
Logs: Alice rsETH Balance: 1090300000000000000 Bob rsETH Balance: 545150000000000000

Tools Used

Manual Review, Foundry

An effective solution to address this issue would be to ensure that the initial deposit in the DepositPool is made by the admin or one of the managers. Alternatively, a minimum deposit requirement of, for instance, 1 token (rETH, cbETH, or stETH) could be implemented. This way, all users will receive an equal amount of rsETH tokens when depositing the same amount of assets.

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-11-17T00:00:34Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-11-17T00:00:43Z

raymondfam marked the issue as duplicate of #62

#2 - c4-judge

2023-11-29T21:26:26Z

fatherGoose1 marked the issue as satisfactory

#3 - c4-judge

2023-12-04T15:31:42Z

fatherGoose1 changed the severity to 3 (High Risk)

Lines of code

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

Vulnerability details

Impact

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

Proof of Concept

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

Note: Will use rETH as an asset and also consider its price in terms of ETH based on the Chainlink oracle

rETH-ETH-Oracle

Flow:

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

Coded POC

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

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

Place the PoC at the end of the file.

Can be run with:

forge test --match-contract LRTDepositPoolInitialDepositOf1Wei --match-test test_InitialDepositorOf1WeiWillResultInNoMoreRSERHToBeMinted -vvv
contract LRTOracleMock_InitialDepositor {
    RSETH rseth;
    MockToken public rETH;
    LRTDepositPool public lrtDepositPool;

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

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

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

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

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

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

    function setUp() public override {
        super.setUp();

        // initialize LRTDepositPool
        lrtDepositPool.initialize(address(lrtConfig));

        rETHAddress = address(rETH);

        // add manager role within LRTConfig
        vm.startPrank(admin);
        lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(new LRTOracleMock_InitialDepositor(rseth, rETH, lrtDepositPool)));

        lrtConfig.grantRole(LRTConstants.MANAGER, manager);
        vm.stopPrank();
    }

    function test_InitialDepositorOf1WeiWillResultInNoMoreRSERHToBeMinted() external {
        uint256 depositAmountUpToAssetLimit = 100_000 ether - 1;

        vm.startPrank(alice);

        // Depositing 1 wei
        rETH.approve(address(lrtDepositPool), 1);
        lrtDepositPool.depositAsset(address(rETH), 1);
        
        uint lrtDepositPoolRETHBalanceAfterAliceDeposit = rETH.balanceOf(address(lrtDepositPool));
        console.log("DepositPool rETH balance after Alice deposits 1 wei: ", lrtDepositPoolRETHBalanceAfterAliceDeposit);

        uint256 aliceRSETHBalance = rseth.balanceOf(alice);
        console.log("Alice rsETH Balance: ", aliceRSETHBalance);
        vm.stopPrank();

        vm.startPrank(bob);

        rETH.approve(address(lrtDepositPool), depositAmountUpToAssetLimit);
        lrtDepositPool.depositAsset(address(rETH), depositAmountUpToAssetLimit);

        uint lrtDepositPoolRETHBalanceAfterBobDepositRestOfTheDepositLimit = rETH.balanceOf(address(lrtDepositPool));
        console.log("DepositPool rETH balance after Bob deposits (100 000 ether - 1 wei) : ", lrtDepositPoolRETHBalanceAfterBobDepositRestOfTheDepositLimit);
        
        uint256 bobRSETHBalance = rseth.balanceOf(bob);
        console.log("Bob rsETH Balance:   ", bobRSETHBalance);
        vm.stopPrank();
    }
}

Tools Used

Manual Review, Foundry

A potential solution would be to ensure that the admin or one of the managers makes the initial deposit in the DepositPool, or alternatively, set a minimum deposit requirement, perhaps 1 token (rETH, cbETH, or stETH). This approach can help address the issue of users receiving 0 rsETH due to rounding.

Assessed type

Invalid Validation

#0 - c4-pre-sort

2023-11-16T23:55:42Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-11-16T23:55:49Z

raymondfam marked the issue as duplicate of #42

#2 - c4-judge

2023-12-01T17:06:47Z

fatherGoose1 marked the issue as satisfactory

Awards

2.7592 USDC - $2.76

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
edited-by-warden
Q-53

External Links

Low Risk

CountTitle
L-01No removeSupportedAsset function in the LRTConfig
L-02cbETH has blacklisting
L-03Consider use stETH/UDS oracle
Total Low Risk Issues3

Non-Critical

CountTitle
NC-01Missing events in sensitive functions
NC-02NodeDelegatorAddedinQueue event does not follow CamalCase
NC-03IStrategy is not up to date with EigenLayer::IStrategy
NC-04No withdraw functionality
Total Non-Critical Issues4

Low Risks

[L-01] No removeSupportedAsset function in the LRTConfig

Issue Description: The contract provides a way to add new supported assets through the addNewSupportedAsset() function, but there is no corresponding function to remove supported assets.

Without the ability to remove supported assets, the contract may face challenges in adapting to changing circumstances, such as changes in the project's strategy, token ecosystem, or regulatory requirements.

Recommendation: Consider adding a function like removeSupportedAsset that allows the contract owner or another authorized role to remove an asset from the list of supported assets

[L-02] cbETH has blacklisting

Issue Description: The blacklisting mechanism in the CBETH token introduces potential complications and risks across various stages of the transaction lifecycle, from initial deposits in the LRTDepositPool to activities within the NodeDelegator and Eigen Strategy. If the LRTDepositPool or any of the NodeDelegator is blacklisted, it implies that the associated tokens will become trapped in these contracts.

[L-03] Consider use stETH/UDS oracle

Issue Description: The sponsor has confirmed their choice of Chainlink as an oracle to fetch prices. Since all other LST price feeds are 18 decimal places, they will most likely use stETH/ETH price feeds. However, this feed has a long heartbeat and a 2% deviation threshold, which could lead to fund loss. The 24-hour heartbeat and 2% deviation threshold mean the price can move up to 2% or remain unchanged for 24 hours before triggering a price update. This could result in the on-chain price being significantly different from the true stETH price, leading to incorrectly calculated rsETH to mint.

Recommendation:

Consider using the stETH/USD oracle instead, as it offers a 1-hour heartbeat and a 1% deviation threshold. To accommodate this change, also need to adjust the decimal calculation in LRTOracle::getAssetPrice to multiply by 18 ** (token.decimal - pricefeed.decimal) or a similar factor. This adjustment ensures that the price of all tokens is returned with the same decimal precision.

Non-Critical

[N‑01] Missing events in sensitive functions

Issue Description: The following functions lack event emission:

File: src/LRTDepositPool.sol

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

          // @audit missing event
197: }

183

File: src/NodeDelegator.sol

38: function maxApproveToEigenStrategyManager(address asset)
39:     external
40:     override
41:     onlySupportedAsset(asset)
42:     onlyLRTManager
43: {
44:     address eigenlayerStrategyManagerAddress = lrtConfig.getContract(LRTConstants.EIGEN_STRATEGY_MANAGER);
45:     IERC20(asset).approve(eigenlayerStrategyManagerAddress, type(uint256).max);

        // @audit missing event
46: }

74: function transferBackToLRTDepositPool(
75:     address asset,
76:     uint256 amount
77: )
78:     external
79:     whenNotPaused
80:     nonReentrant
81:     onlySupportedAsset(asset)
82:     onlyLRTManager
83: {
84:     address lrtDepositPool = lrtConfig.getContract(LRTConstants.LRT_DEPOSIT_POOL);
85: 
86:     if (!IERC20(asset).transfer(lrtDepositPool, amount)) {
87:         revert TokenTransferFailed();
88:     }
        // @audit missing event
89: }

38, 74

Recommendation:

Add the following event

transferAssetToNodeDelegator() → event AssetsTransferredSuccessfullyToNodeDelegator(address nodeDelegator, uint256 amount)

maxApproveToEigenStrategyManager() → event MaxApprovedToEigenStrategyManager()

transferBackToLRTDepositPool() → event AssetsTransferredSuccessfullyBackToLTRDepositPool(address lrtDepositPool, uint256 amount)

[N‑02] NodeDelegatorAddedinQueue event does not follow CamalCase

Issue Description: The event NodeDelegatorAddedinQueue is not compliant with CamelCase conventions.

Recommendation: Rename to NodeDelegatorAddedInQueue

[N‑03] IStrategy is not up to date with EigenLayer::IStrategy

Issue Description: The protocol includes the IStrategy, which appears to be similar to the EigenLayer, but it uses an older version that lacks the newly added shares(address user) function and has some parameter renames.

Recommendation: Consider using the latest version from the EigenLayer repository:

https://github.com/Layr-Labs/eigenlayer-contracts/blob/master/src/contracts/interfaces/IStrategy.sol

[N‑04] No withdraw functionality

Issue Description: There is no withdraw functionality in the protocol all funds will be locked

Recommendation: Consider implement withdraw function

#0 - raymondfam

2023-11-18T00:34:06Z

Possible upgrade:

[L-03] --> #609

#1 - c4-pre-sort

2023-11-18T00:34:11Z

raymondfam marked the issue as sufficient quality report

#2 - c4-judge

2023-12-01T16:33:16Z

fatherGoose1 marked the issue as grade-b

#3 - AydoanB

2023-12-02T10:12:08Z

Hi @fatherGoose1,

We think our downgraded issue was not taken into consideration in the grading of our report, and we kindly request you to reevaluate the grade of the QA report with the mentioned finding:

the downgraded issue is: https://github.com/code-423n4/2023-11-kelp-findings/issues/537

Thanks!

#4 - fatherGoose1

2023-12-04T17:30:23Z

Hi @AydoanB, even with the consideration of #537, I do not view this as a Grade A QA report.

#5 - Slavchew

2023-12-08T18:09:22Z

Hi @fatherGoose1,

Since #584 is now considered valid, and all its duplicates state the same as our [L-03] we would ask for an upgrade, because we think it is a valid duplicate.

You can see that #300 and #516 show the same as us in [L-03].

Thanks!

Awards

98.52 USDC - $98.52

Labels

analysis-advanced
grade-a
sufficient quality report
A-01

External Links

🛠️ Analysis - KelpDAO - rsETH


Overview


KelpDAO aims to launch a new and advanced Liquid Restaked Token called rsETH. rsETH represents a consolidated, re-staked token that can be generated using LSTs accepted as collateral within the EigenLayer ecosystem. The idea is to make restaking and diving into DeFi super easy, playing well with other DeFi stuff. KelpDAO is here to fix tricky reward systems and those pesky high gas fees, making a big splash in the web3 scene. To break it down, the Kelp protocol takes in deposits, mainly in the form of LSTs recognized as EigenLayer collateral. In return, users receive $rsETH – KelpDAO's distinctive reward token, symbolizing each individual's deposited assets.

The key contracts of KelpDAO - $rsETH protocol for this Audit are:

  • LRTConfig: This upgradeable contract plays a pivotal role in storing the configuration settings for the Kelp product. Additionally, it serves as a repository for the addresses of other contracts within the Kelp product, establishing a centralized hub for managing various aspects of the system's functionality.
  • LRTDepositPool: Operating as an upgradeable contract, LRTDepositPool functions as a receiving point for funds deposited by users into the Kelp product. Subsequently, these funds are channeled to NodeDelegator contracts, facilitating the efficient movement of resources within the Kelp ecosystem.
  • NodeDelegator: These contracts serve as intermediaries, receiving funds from the LRTDepositPool and strategically delegating them to the EigenLayer strategy. The allocated funds are then utilized to provide liquidity within the EigenLayer protocol. Notably, NodeDelegator is designed as an upgradeable contract, allowing for adaptability and future enhancements.
  • LRTOracle: Functioning as another upgradeable contract, LRTOracle is tasked with the responsibility of fetching accurate price data for Liquid Staking Tokens from external oracle services. This ensures that the Kelp product has real-time and reliable information to make informed decisions within its operations.
  • RSETH: As an upgradeable ERC20 token contract, RSETH serves as a receipt token for deposits made into the Kelp product. This specialized token represents the user's deposit within the system and is designed with upgradability to align with any future modifications or improvements to the Kelp ecosystem.

System Overview


Scope

  • LRTConfig - A core upgradeable contract that contains the state variables used across the entire protocol. It is responsible for storing and modifying them.

    Can also change the following core variables:

    • Adding/removing assets (LST tokens, such as stETH, cbETH, and rETH)
    • Limits for depositing into EigenLayer
    • The Strategy used in the NodeDelegators
    • External contracts used across the Kelp DAO
  • LRTDepositPool - An upgradeable contract, that serves as an entry point for the user, it receives the funds deposited by a user and with the help of LRTManager sends them to the NodeDelegators.

    Additional operations are available:

    • Querying the deposits for the entire protocol (total assets deposited, deposit limits, assets deposited in EigenLayer and in all the NodeDelegators)
    • Depositing assets to the Kelp DAO products
    • Calculating the amount of RSETH tokens to be minted to the user
    • Minting RSETH tokens to depositors
    • Transferring funds to NodeDelegators
    • Pausing/Upausing the important functionalities
  • NodeDelegator - Mediator contracts that are designed to receive funds from LRTDepositPool and delegate them to EigenLayer and vice-versa.

    Possible operations:

    • Approvals to EigenLayer’s StrategyManager contract
    • Depositing into the desired EigenLayer strategy
    • Transferring tokens back to the LRTDepositPool
    • Fetches the balance of the assets deposited into EigenLayer through this NodeDelegator contract
    • Pausing/Unpausing
  • LRTOracle - an upgradeable contract that is responsible for fetching the price of Liquid Staking Tokens tokens from oracle services.

    Price queries available:

    • Fetching the exchange rates of LST tokens in ETH
    • Calculating the price of RSETH tokens based on all the available asset
    • Adding/removing price oracles used as PriceFetchers
    • Pausing/unpausing functions

Privileged Roles

Privileged roles used across Kelp DAO:

  • LRTManager
modifier onlyLRTManager() {
        if (!IAccessControl(address(lrtConfig)).hasRole(LRTConstants.MANAGER, msg.sender)) {
            revert ILRTConfig.CallerNotLRTConfigManager();
        }
        _;
    }
  • LRTAdmin

    modifier onlyLRTAdmin() {
        bytes32 DEFAULT_ADMIN_ROLE = 0x00;
        if (!IAccessControl(address(lrtConfig)).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) {
            revert ILRTConfig.CallerNotLRTConfigAdmin();
        }
        _;
    }

Trusted Roles

MINTER_ROLE - given to the LRTDepositPool at deployment
BURNER_ROLE - TBC, will be given to the contract which will be used to withdraw from EigenLayer
MANAGER(LRTManager): 
	- can pause all the contracts that implement PausableUpgradeable from OpenZeppelin
	- can approve assets that have to be deposited into EigenLayer
	- can deposit assets into the EigenLayer strategy 
	- can transfer back assets to the LRTDepositPool contract
ADMIN(LRTAdmin):
	- can add new NodeDelegatorContracts to LRTDepositPool mapping
	- can update the max NodeDelegators count allowed in the LRTDepositPool
	- can unpause all the contracts that implement PausableUpgradeable from OpenZeppelin
User - Can deposit LST tokens into the protocol and see the assets available 

Approach Taken-in Evaluating The Kelp DAO Protocol


NumberStageDetailsInformation
1Compile and Run TestInstallationClearly written and easy to setup, with the additional script to generate test coverage
2Architecture ReviewKelp DAOProvides a basic LST and architectural overview without emphasizing on the technical side
3Graphical AnalysisGraphical Analysis with Solidity-MetricsA visual view has been made to dominate the general structure of the codes of the project.
4Slither AnalysisSlither ReportThe project does not currently have a slither result. We ran local slither and no major issue were identified.
5Test SuitsTestsIn this section, the scope and content of the tests of the project are analyzed.
6Manual Code ReviewScopeReviewed all the contracts in scope
7InfographicArticleReviewed medium article written about the contracts in scope
8Special focus on Areas of ConcernAreas of ConcernObserving the known issues and bot report

Architecture Recommendations


Comments: While the codebase contains comments, improving the quality of comments and documentation can enhance code readability. Consider using more explanatory comments to clarify the purpose and functionality of each section of the code. This will make it easier for other developers and auditor to understand and maintain the code.

Documentation: In overall, protocol team relies more on the high-level overview of the codebase, instead of describing the technical details and make the auditability of the protocol easier.

For example this is the only more technical article by the team: https://blog.kelpdao.xyz/decoding-the-tech-behind-rseth-409ecddcb421

Which explains only a part of the components of which the project is build, such as what is rsETH and his usages. Additional documentation regarding the whole flow will be beneficial for all the parties involved in the protocol. ****

Formal Verification: Consider a professional formal verification of the contract to identify and mitigate any security risks.

External dependencies: Consider implementing reliable oracle, instead of [ChainlinkPriceOracle](https://github.com/code-423n4/2023-11-kelp/blob/main/src/oracles/ChainlinkPriceOracle.sol) which contains stale function latestAnswer and doesn’t comply with any of the suggested security measures.

Variable naming: Some of the variable are not well named, which can cause the confusion for the person who is reading them, there are some examples:

  • asset_idx is not self explanatory, can be replaced with index, assetIndex
  • nodeDelegatorQueue is not appropriate and sounds more like a queue to us, consider simplifying it to nodeDelegators
image

Codebase Quality Analysis


We consider the quality of the Kelp DAO as simple and robust, but incomplete, since there are functions which have to be added, such as withdraw both from EigenLayer and the protocol itself, because now there is no way for user to withdraw his deposited assets and the only way to unstake from EigenLayer is to rely to the DelegationManager of the Strategy to call withdrawSharesAsTokens function located in their StrategyManager contract.

Codebase Quality CategoriesCommentsScore (1-10)
Code Maintainability and ReliabilityThe codebase demonstrates moderate maintainability with well-structured functions but lacks proper function ordering based on their visibility. The approach that the development team has taken is different from the preferred one in the Solidity Style Guide. In Kelp DAO, functions are grouped by functionality instead of visibility. While this grouping may be better for readability, the pros of grouping by visibility are better from a development point of view. Adherence to standard conventions and practices contributes to overall code quality. Some variable names are not quite understandable and can lead to confusion for the developer and auditor. The usage of the UtilLib library to abstract the zero address check significantly reduces lines of code and improves readability. Exposing functions to directly check balances in all contracts improves UX and provides an easy way for potential depositors to verify required values.7
Code CommentsThe contracts have comments explaining the purpose and functionality of different parts, making it easier for developers to understand and maintain the code. Comments describe methods, variables, and the overall structure of the contract. For example, the code comments declare imported interfaces and contracts and provide descriptions of the contract's title and purpose.9
DocumentationThere is no extensive documentation to explain the architecture and functionality of the protocol, only a brief explanation of the contracts in scope. Documentation for the integration with EigenLayer is lacking, and there is no information on what will happen when the user deposits his funds. Diagrams must be drawn to represent how end-to-end flow is executed, given the fact this protocol will be used as a fund aggregator and deposit them into another protocol. Consider using flows from the Architecture Overview point.3
Code Structure and FormattingThe codebase contracts are well-structured and formatted, utilizing many of the OpenZeppelin contracts which are well-tested and documented. This lowers the attack surface and makes future development and auditability easier.10

Test analysis


The audit scope of the contracts to be audited is ~97% and it should be aimed to be 100%. A good amount of testing should be applied to the script files, as they contain critical parameters that have to be verified.

Systemic & Centralization Risks

The analysis provided highlights several significant systemic and centralization risks present in the Kelp DAO protocol. These risks encompass concentration risk in all the contracts in scope and centralization risks arising from the existence of an LRTManager and LRTOwner roles in all the contracts. However, the documentation lacks clarity on whether this address represents an externally owned account (EOA) or a contract, warranting the need for clarification. Additionally, the absence of deployment scripts tests could also pose risks to the protocol’s security since they have to be manually verified now. Regarding the tokens used, they are not quite similar and errors can occur due to rebasing of some of them.

Here's an analysis of potential systemic and centralization risks in the provided contracts:

Systemic Risks:

  1. No deployment scripts tests, which can introduce irreversible damage for the protocol caused by wrong addresses.
  2. A compromise of the admin, gatekeeper or minter roles would allow unauthorized minting/redeeming, threatening stability.
  3. stETH is a rebase token and can cause DOS in certain times when volatility of the token is high and due to rebase deposit limit can be hit, which will disallow everyone to deposit their funds in the EigenLayer.
  4. First depositor can brick the entire protocol if deposits 1 wei causing the other depositors to just donate tokens into the protocol, due to rounding issue in the getRsETHAmountToMint function.
  5. No way to remove supported asset after they are deprecated by EigenLayer, because of lacking remove function in the LRTConfig.

Centralization Risks:

  1. High centralization as most of the core functions are restricted only to a LRTManager and LRTAdmin contracts.

Properly managing these risks and implementing best practices in security and decentralization will contribute to the sustainability and long-term success of the Kelp DAO protocol.

New insights and learning from this audit


Learned about:

Conclusion


In general, the Kelp DAO project exhibits an standard and well-developed architecture we believe the team has done a good job regarding the code, but the identified risks need to be addressed, and measures should be implemented to protect the protocol from potential malicious use cases. Additionally, it is recommended to improve the documentation and comments in the code to enhance understanding and collaboration among developers. It is also highly recommended that the team continues to invest in security measures such as mitigation reviews, audits, and bug bounty programs to maintain the security and reliability of the project.

Time spent:

30 hours

#0 - c4-pre-sort

2023-11-17T03:23:06Z

raymondfam marked the issue as sufficient quality report

#1 - c4-judge

2023-11-29T19:04:31Z

fatherGoose1 marked the issue as grade-a

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter