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
Rank: 6/185
Findings: 5
Award: $1,044.54
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: m_Rassska
Also found by: 0xDING99YA, Aamir, Bauchibred, CatsSecurity, HChang26, PENGUN, SBSecurity, adriro, almurhasan, anarcheuz, circlelooper, d3e4, deth, jayfromthe13th
902.5718 USDC - $902.57
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.
🌟 Selected for report: T1MOH
Also found by: 0x1337, 0xNaN, 0xepley, 0xluckhu, 0xmystery, 7siech, Aamir, AlexCzm, Aymen0909, DanielArmstrong, GREY-HAWK-REACH, HChang26, Jiamin, Juntao, QiuhaoLi, Ruhum, SBSecurity, Varun_05, Weed0607, adam-idarrha, adriro, ast3ros, ayden, circlelooper, crack-the-kelp, crunch, cryptothemex, deepplus, mahdirostami, max10afternoon, osmanozdemir1, rouhsamad, rvierdiiev, trachev, xAriextz, zhaojie
36.0335 USDC - $36.03
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
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.
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
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.
depositAsset()
, it starts by adding the depositAmount and then calculates the amount of rsETH tokens to be minted.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)
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.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
rsETH
tokens (545,150,000,000,000,000) compared to the Alice's initial deposit.545,150,000,000,000,000
rsETH tokens following the same pattern.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
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.
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)
🌟 Selected for report: Krace
Also found by: 0xDING99YA, 0xrugpull_detector, Aamir, AlexCzm, Aymen0909, Banditx0x, Bauer, CatsSecurity, GREY-HAWK-REACH, Madalad, Phantasmagoria, QiuhaoLi, Ruhum, SBSecurity, SandNallani, SpicyMeatball, T1MOH, TheSchnilch, adam-idarrha, adriro, almurhasan, ast3ros, ayden, bronze_pickaxe, btk, chaduke, ck, crack-the-kelp, critical-or-high, deth, gumgumzum, jasonxiale, joaovwfreire, ke1caM, m_Rassska, mahdirostami, mahyar, max10afternoon, osmanozdemir1, peanuts, pep7siup, peter, ptsanev, qpzm, rouhsamad, rvierdiiev, spark, twcctop, ubl4nk, wisdomn_, zach, zhaojie
4.6614 USDC - $4.66
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.
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
at 0x536218f9E9Eb48863970252233c8F271f554C2d0 = 1,090,300,000,000,000,000 (at the moment of writing)Flow:
100,000 ether - 1 wei
.lrtDepositPool
, but he will not receive any rsETH tokens due to rounding issues.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(); } }
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.
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
🌟 Selected for report: m_Rassska
Also found by: 0x1337, 0xAadi, 0xHelium, 0xLeveler, 0xblackskull, 0xbrett8571, 0xepley, 0xffchain, 0xluckhu, 0xmystery, 0xrugpull_detector, 0xvj, ABAIKUNANBAEV, Aamir, AerialRaider, Amithuddar, Bauchibred, Bauer, CatsSecurity, Cryptor, Daniel526, Draiakoo, Eigenvectors, ElCid, GREY-HAWK-REACH, Inspecktor, Juntao, King_, LinKenji, Madalad, MaslarovK, Matin, MatricksDeCoder, McToady, Noro, PENGUN, Pechenite, Phantasmagoria, RaoulSchaffranek, SBSecurity, SandNallani, Shaheen, Soul22, Stormreckson, T1MOH, Tadev, TeamSS, TheSchnilch, Topmark, Tumelo_Crypto, Udsen, Yanchuan, ZanyBonzy, _thanos1, adeolu, adriro, alexfilippov314, almurhasan, amaechieth, anarcheuz, ayden, baice, bareli, boredpukar, bronze_pickaxe, btk, cartlex_, catellatech, chaduke, cheatc0d3, circlelooper, codynhat, crack-the-kelp, critical-or-high, debo, deepkin, desaperh, dipp, eeshenggoh, evmboi32, ge6a, gesha17, glcanvas, gumgumzum, hals, hihen, hunter_w3b, jasonxiale, joaovwfreire, ke1caM, leegh, lsaudit, marchev, merlinboii, niser93, osmanozdemir1, paritomarrr, passion, pep7siup, phoenixV110, pipidu83, poneta, ro1sharkm, rouhsamad, rvierdiiev, sakshamguruji, seerether, shealtielanz, soliditytaker, spark, squeaky_cactus, stackachu, supersizer0x, tallo, taner2344, turvy_fuzz, twcctop, ubl4nk, wisdomn_, xAriextz, zach, zhaojie, zhaojohnson, ziyou-
2.7592 USDC - $2.76
Count | Title |
---|---|
L-01 | No removeSupportedAsset function in the LRTConfig |
L-02 | cbETH has blacklisting |
L-03 | Consider use stETH/UDS oracle |
Total Low Risk Issues | 3 |
---|
Count | Title |
---|---|
NC-01 | Missing events in sensitive functions |
NC-02 | NodeDelegatorAddedinQueue event does not follow CamalCase |
NC-03 | IStrategy is not up to date with EigenLayer::IStrategy |
NC-04 | No withdraw functionality |
Total Non-Critical Issues | 4 |
---|
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
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.
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.
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: }
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: }
Recommendation:
Add the following event
transferAssetToNodeDelegator() →
event AssetsTransferredSuccessfullyToNodeDelegator(address nodeDelegator, uint256 amount)
maxApproveToEigenStrategyManager() → event MaxApprovedToEigenStrategyManager()
transferBackToLRTDepositPool() →
event AssetsTransferredSuccessfullyBackToLTRDepositPool(address lrtDepositPool, uint256 amount)
Issue Description: The event NodeDelegatorAddedinQueue
is not compliant with CamelCase conventions.
Recommendation: Rename to NodeDelegatorAddedInQueue
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
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!
🌟 Selected for report: CatsSecurity
Also found by: 0xSmartContract, 0xbrett8571, 0xepley, Bauchibred, KeyKiril, Myd, SAAJ, SBSecurity, Sathish9098, ZanyBonzy, albahaca, clara, digitizeworx, fouzantanveer, foxb868, glcanvas, hunter_w3b, invitedtea, sakshamguruji, unique
98.52 USDC - $98.52
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 - 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:
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:
NodeDelegator - Mediator contracts that are designed to receive funds from LRTDepositPool and delegate them to EigenLayer and vice-versa.
Possible operations:
LRTOracle - an upgradeable contract that is responsible for fetching the price of Liquid Staking Tokens tokens from oracle services.
Price queries available:
Privileged roles used across Kelp DAO:
modifier onlyLRTManager() { if (!IAccessControl(address(lrtConfig)).hasRole(LRTConstants.MANAGER, msg.sender)) { revert ILRTConfig.CallerNotLRTConfigManager(); } _; }
modifier onlyLRTAdmin() { bytes32 DEFAULT_ADMIN_ROLE = 0x00; if (!IAccessControl(address(lrtConfig)).hasRole(DEFAULT_ADMIN_ROLE, msg.sender)) { revert ILRTConfig.CallerNotLRTConfigAdmin(); } _; }
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
Number | Stage | Details | Information |
---|---|---|---|
1 | Compile and Run Test | Installation | Clearly written and easy to setup, with the additional script to generate test coverage |
2 | Architecture Review | Kelp DAO | Provides a basic LST and architectural overview without emphasizing on the technical side |
3 | Graphical Analysis | Graphical Analysis with Solidity-Metrics | A visual view has been made to dominate the general structure of the codes of the project. |
4 | Slither Analysis | Slither Report | The project does not currently have a slither result. We ran local slither and no major issue were identified. |
5 | Test Suits | Tests | In this section, the scope and content of the tests of the project are analyzed. |
6 | Manual Code Review | Scope | Reviewed all the contracts in scope |
7 | Infographic | Article | Reviewed medium article written about the contracts in scope |
8 | Special focus on Areas of Concern | Areas of Concern | Observing the known issues and bot report |
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, assetIndexnodeDelegatorQueue
is not appropriate and sounds more like a queue to us, consider simplifying it to nodeDelegators
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 Categories | Comments | Score (1-10) |
---|---|---|
Code Maintainability and Reliability | The 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 Comments | The 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 |
Documentation | There 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 Formatting | The 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 |
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.
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:
getRsETHAmountToMint
function.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.
Learned about:
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.
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