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: 16/185
Findings: 3
Award: $272.35
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: crack-the-kelp
Also found by: 0xDING99YA, 0xffchain, Aymen0909, Bauchibred, DanielArmstrong, Pechenite, Stormy, T1MOH, ZanyBonzy, ast3ros, bLnk, chaduke, crack-the-kelp, deepkin, deth, jasonxiale, jayfromthe13th, lsaudit, nmirchev8, osmanozdemir1, roleengineer, tallo, zhaojie
76.0163 USDC - $76.02
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTConfig.sol#L109-L122 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L71-L89
Each asset in Kelp DAO
has particular EigenLayer Strategy
. When LRT Manager
decide, he can transfer the deposited assets from NodeDelegator
to the particular EigenLayer Strategy
.
The LRTDepositPool
also enforce assets deposit limit tracked with depositLimitByAsset
mapping. The current deposit
for asset is sum of the LRTDepositPool
, NodeDelegator
and EigenLayer Strategy
contract balances of this asset. Users cannot deposit more than this limit.
The asset for particular EigenLayer Strategy
can be changed over time via LRTConfig.sol#updateAssetStrategy()
function.
The vulnerability is related to the way asset strategies are managed in the Kelp DAO protocol. When a EigenLayer Strategy
for an asset is changed, there hasn't a mechanism in place to migrate user deposits from the old strategy to the new one. This can result in user funds becoming permanently locked in the old strategy, effectively rendering them inaccessible.
Additionally, the getAssetDistributionData()
function, which is used to obtain information about the distribution of assets and used to enforce deposit limit during deposit, will not return the actual total current deposit of certain asset as it will have assets in the old strategies. The getAssetDistributionData()
function will get into the calculation the latest Strategy that is set for the particular asset, even if asset. This will lead to inaccuracies in the asset distribution.
This way users will also can deposit more asset than the set limit.
This issue in the Kelp DAO protocol has the following impact:
Risk of Permanent Loss of User Funds: After changing the strategy of a particular asset, user deposits made in the previous strategy can become permanently lost or locked within that strategy. This poses a significant risk to users, as they may be unable to recover their deposited assets for some period of time.
Exceeding the the assets deposit limit
The vulnerability can be observed in the following code snippets from the provided contracts:
Changing Strategy:
LRTConfig
updateAssetStrategy()
function. However, there is no mechanism to ensure that user deposits in the old strategy are migrated to the new one, potentially leading to permanent loss of user funds.function updateAssetStrategy( address asset, address strategy ) external onlyRole(DEFAULT_ADMIN_ROLE) onlySupportedAsset(asset) { UtilLib.checkNonZeroAddress(strategy); if (assetStrategy[asset] == strategy) { revert ValueAlreadyInUse(); } assetStrategy[asset] = strategy; }
Inaccurate Asset Distribution Information:
LRTDepositPool
getAssetDistributionData()
function retrieves the latest strategy that holds the asset balance. If strategies have changed, this function may return inaccurate information about the distribution of assets. Actually I think that the function flow is correct, the whole issue arise after the of asset for particular EigenLayer Strategy
.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? 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; } } }
Manual Review
There isn't an elegant way to fix this issue, but I would suggest the following mitigation steps:
First way: Implement a Migration Mechanism
or
Second way: Update the updateAssetStrategy()
function
updateAssetStrategy()
function to transfer user deposits from the old to the new Strategy after the new Strategy is set.I recommend the second way.
Error
#0 - c4-pre-sort
2023-11-16T21:04:00Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T21:04:13Z
raymondfam marked the issue as duplicate of #197
#2 - c4-judge
2023-12-01T17:24:53Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-12-08T17:26:27Z
fatherGoose1 marked the issue as satisfactory
🌟 Selected for report: max10afternoon
Also found by: 0xMilenov, 0xbrett8571, 0xhacksmithh, 0xmystery, Aymen0909, Bauer, Daniel526, PENGUN, Pechenite, Shaheen, adriro, anarcheuz, btk, ck, ge6a, glcanvas, hals, turvy_fuzz
140.2525 USDC - $140.25
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119-L144
This issue pertains to the depositAsset()
function within the LRTDepositPool.sol
contract, where certain critical checks are missing:
Minimum Minted Amount Check: Due to the highly volatile nature of the crypto and DeFi space, the function does not verify if the user is satisfied with the final minted rsETH
amount.
Expiration Timestamp Check: The LRTDepositPool.sol#depositAsset()
function lacks a check to ensure that the transaction has not been in the mempool for a very long time. This can result in the provided asset amount
from the user becoming invalid at the time of execution. Without this check, users could receive outdated data (minted rsETH amount
) without being satisfied with it.
In summary, the depositAsset()
function lacks a minimum minted amount
(a minimum amount specified by the user) and an expiration timestamp check
, making it susceptible to potential issues. These issues include improper minting of rsETH
tokens due to the high volatility of the crypto space.
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); }
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(); }
100 tokens
via the depositAmount()
function for asset X
. The price of ETH
at the moment of the transaction is $2000
, and let's assume the price of asset X
is $10000
.rsETH minted amount
for the user is 500
. | ((100 * 5) / 1) = 500
ETH
can change significantly during this time. So, the price of ETH
increases to $2500
.rsETH minted amount
for the user is 400
, but the user is not satisfied with this minted amount
as it represents a 20%
decrease from the expected value. | ((100 * 4) / 1) = 400
100 tokens
via the depositAmount()
function for asset X
. The price of ETH at the moment of the transaction is $2000
, and let's assume the price of asset X
is $10000
.rsETH minted amount
for the user is 500
. | ((100 * 5) / 1) = 500
asset X
can change significantly during this time. So, the price of asset X
decreases to $8000
.rsETH minted amount
for the user is 400
, but the user is not satisfied with this minted amount
as it represents a 20%
decrease from the expected value. | ((100 * 4) / 1) = 400
These two examples illustrate how changes in ETH price
or the price of assets deposited by the user
can affect the minted rsETH amount
. This highlights how the lack of a minimum minted amount
(a minimum amount specified by the user) and an expiration timestamp check
in the code could lead to inefficient minting calculations for users.
The vulnerability was identified through manual code inspection.
Add the following:
Add:
parameter for minimum amount of rsETH
that the user specify.depositAsset()
function if the transaction is in mempool for long period of time.Error
#0 - c4-pre-sort
2023-11-16T21:11:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T21:11:45Z
raymondfam marked the issue as duplicate of #102
#2 - c4-judge
2023-12-01T18:19:21Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-12-01T18:19:36Z
fatherGoose1 marked the issue as grade-b
#4 - radeveth
2023-12-02T22:44:55Z
Hey, @fatherGoose1.
I am convinced that my issue is distinct from #102 for the following reasons:
My report distinctly outlines a vulnerability due to the absence of a minimum minted amount check.
This issue pertains to the
depositAsset()
function within theLRTDepositPool.sol
contract, where certain critical checks are missing: Minimum Minted Amount Check: Due to the highly volatile nature of the crypto and DeFi space, the function does not verify if the user is satisfied with the final mintedrsETH
amount.. (also known as the min minted amount check)
Notably, issue #148, classified as a valid medium severity issue, addresses exactly this vulnerability.
Additionally, my report explains how the lack of expiration timestamp checks allows for the incorrect execution of pending transactions.
Expiration Timestamp Check: The
LRTDepositPool.sol#depositAsset()
function lacks a check to ensure that the transaction has not been in the mempool for a very long time. This can result in the providedasset amount
from the user becoming invalid at the time of execution. Without this check, users could receive outdated data (minted rsETH amount
) without being satisfied with it.
In previous Code4rena contests, `issues related to the missing of expiration timestamp checks were classified as valid medium severity issues. Example: https://solodit.xyz/issues/m-01-missing-deadline-checks-allow-pending-transactions-to-be-maliciously-executed-code4rena-caviar-caviar-contest-git (issue in code4rena report)
My report specifically states:
In summary, the
depositAsset()
function lacks aminimum minted amount
(a minimum amount specified by the user) and anexpiration timestamp check
, making it susceptible to potential issues. These issues include improper minting ofrsETH
tokens due to the high volatility of the crypto space.
The lack of these two checks in the Kelp DAO Protocol points to a singular issue: the Missing Slippage Protection issue.
I believe the issue has been misjudged and respectfully request a reassessment of this issue, given its considerable implications.
I propose that issues #102, #148, and their duplicates be re-evaluated as partially overlapping with this issue.
Have a good one!
#5 - liveactionllama
2023-12-04T17:45:12Z
Per request from the judge, removing the duplicate
label on their behalf.
#6 - c4-judge
2023-12-04T17:46:53Z
This previously downgraded issue has been upgraded by fatherGoose1
#7 - c4-judge
2023-12-04T17:46:53Z
This previously downgraded issue has been upgraded by fatherGoose1
#8 - c4-judge
2023-12-04T17:47:09Z
fatherGoose1 marked the issue as duplicate of #148
#9 - c4-judge
2023-12-04T17:47:26Z
fatherGoose1 marked the issue as satisfactory
#10 - liveactionllama
2023-12-04T17:54:44Z
Per request from judge, removing grade-b
label since this is no longer QA level severity.
🌟 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-
56.0791 USDC - $56.08
Project Name | Kelp DAO |
Repository | https://github.com/code-423n4/2023-11-kelp |
Website | Link |
Link | |
Methods | Manual Review |
Total SLOC | 504 over 9 contracts |
ID | Title | Severity |
---|---|---|
L‑1 | Do not allow the modification of the asset for particular EigenLayer Strategy after amount of the specific asset has already been deposited into the EigenLayer Strategy | Low |
L‑2 | A malicious LRT Manager can disrupt the deposit process for a particular asset by setting depositLimitByAsset[asset] to a value smaller than the actual total supply of this asset within contracts . Consequently, the depositing function will consistently revert due to underflow | Low |
L‑3 | Ownership Can Be Renounced | Low |
L‑4 | Possible DoS and Out of Gas errors | Low |
NC‑1 | Implement a check to determine if the NodeDelegator already exists within the LRTDepositPool.sol#addNodeDelegatorContractToQueue() function | Non Critical |
NC‑2 | A malicious user can disrupt the logic of the LRTDepositPool.sol contract by directly transferring assets to the LRTDepositPool , NodeDelegator or EigenLayer Strategy contracts | Non Critical |
NC‑3 | Grant the LRT Manager permissions during the initialization of the LRTDepositPool contract | Non Critical |
NC‑4 | Grant MINTER_ROLE and BURNER_ROLE permissions during the initialization of the RSETH contract | Non Critical |
NC‑5 | Implement a function for removing assets from supportedAssets only if the asset has not already been transferred to the NodeDelegator contract | Non Critical |
asset
for particular EigenLayer Strategy
after amount
of the specific asset
has already been deposited into the EigenLayer Strategy
The LRTConfig
contract has a vulnerability that allows the modification of the asset
for a particular EigenLayer Strategy
after an amount
of the specific asset
has already been deposited into the EigenLayer Strategy
. This can potentially disrupt the logic of the system, leading to unintended consequences and security risks like permanently locked users funds.
The vulnerability arises from the ability to change the asset
for a particular EigenLayer Strategy
even after an amount
of the specific asset
has already been deposited into the EigenLayer Strategy
. The sequence of events leading to this issue is as follows:
An EigenLayer Strategy
is configured with a specific asset
.
An amount
of that asset
is deposited into the EigenLayer Strategy
.
The LRTConfig
contract allows the asset
for the same EigenLayer Strategy
to be modified, potentially switching to a different asset.
This modification can disrupt the logic and expectations of the system, as it may not be designed to handle asset changes for an active EigenLayer Strategy
.
Changing the asset
for an active EigenLayer Strategy
could result in the permanently lock/loss of funds or assets being incorrectly handled.
Implement a restriction that prevents the modification of the asset
for a particular EigenLayer Strategy
after any amount
of the specific asset
has been deposited into that Strategy. This can be achieved by adding a check in the setAsset()
function to disallow changes when there is a non-zero balance of the asset
in the EigenLayer Strategy
.
LRT Manager
can disrupt the deposit process for a particular asset by setting depositLimitByAsset[asset]
to a value smaller than the actual total supply of this asset within contracts
. Consequently, the depositing function will consistently revert due to underflowThe LRTConfig
contract has a vulnerability that allows a malicious LRT Manager
to disrupt the deposit process for a particular asset by setting depositLimitByAsset[asset]
to a value smaller than the actual total supply of this asset within the contracts
(LRTDepositPool
, NodeDelegator
and EigenLayer Strategy
contracts). This vulnerability can cause the depositing function to consistently revert due to underflow, effectively preventing legitimate deposits.
The vulnerability occurs due to the lack of proper checks and balances in the LRTConfig
contract. The sequence leading to this issue is as follows:
The LRT Manager
has the authority to set the depositLimitByAsset
for a particular asset.
The LRT Manager
sets depositLimitByAsset[asset]
to a value smaller than the actual total supply of that asset within the contract.
Users attempt to deposit the asset into the contract.
Since the depositLimitByAsset
is smaller than the total supply, the depositing function consistently reverts due to an underflow error when calculating available deposit limits.
Legitimate deposits are prevented, causing a disruption in the deposit process for the affected asset.
The impact of this vulnerability is severe, as it can disrupt the deposit process for a particular asset. Users attempting to deposit the asset will encounter consistent reverts, making it impossible to deposit funds as intended. This can lead to a breakdown in the functionality of the ecosystem.
Likelihood: Low
To mitigate this vulnerability, it is crucial to implement proper checks when setting the depositLimitByAsset
to ensure that it cannot be set to a value smaller than the actual total supply of the asset within the contracts
(LRTDepositPool
, NodeDelegator
and EigenLayer Strategy
contracts).
If the owner renounces their ownership in LRTConfig.sol
, all ownable contracts will be left without an owner. As a result, any function guarded by the onlyOwner
modifier will no longer be executable. Additionally, the DEFAULT_ADMIN_ROLE
can be renounced or revoked.
Confirm whether this is the intended behavior. If not, override and disable the renounceOwnership()
and revokeRole()
functions in the affected contracts. For added security, consider implementing a two-step process when transferring ownership of the contract (e.g., using Ownable2Step from OpenZeppelin).
The LRTDepositPool
contract contains nodeDelegatorQueue
array, which can lead to uncontrolled growth of the array over time. This uncontrolled growth can result in increased gas costs and potential out-of-gas errors during function calls that read or modify the array. Additionally, it may expose the contract to a denial-of-service (DoS) by causing transactions to run out of gas.
Increased gas costs: As the nodeDelegatorQueue
array grows, the gas required to execute functions that modify the array (such as addNodeDelegatorContractToQueue
and transferAssetToNodeDelegator
) also increases, potentially leading to higher transaction fees for users.
Possible out-of-gas errors: If the nodeDelegatorQueue
array becomes too large, it may exceed the gas limit for Ethereum transactions, resulting in out-of-gas errors and failed transactions.
Denial-of-Service (DoS) potential: An attacker could exploit the uncontrolled growth of the nodeDelegatorQueue
to launch a DoS attack by repeatedly calling functions that modify the array, causing legitimate transactions to fail due to high gas costs.
The vulnerability arises from the fact that the nodeDelegatorQueue
array is never reduced in size by removing elements. The contract allows NodeDelegator contract addresses to be added to the queue via the addNodeDelegatorContractToQueue
function but does not provide a mechanism to remove addresses. As a result, the array can grow indefinitely.
function addNodeDelegatorContractToQueue( address[] calldata nodeDelegatorContracts ) external onlyLRTAdmin { uint256 length = nodeDelegatorContracts.length; if (nodeDelegatorQueue.length + length > maxNodeDelegatorCount) { revert MaximumNodeDelegatorCountReached(); } for (uint256 i; i < length; ) { UtilLib.checkNonZeroAddress(nodeDelegatorContracts[i]); nodeDelegatorQueue.push(nodeDelegatorContracts[i]); emit NodeDelegatorAddedinQueue(nodeDelegatorContracts[i]); unchecked { ++i; } } }
The above function allows NodeDelegator
contract addresses to be added to the nodeDelegatorQueue
, but there is no corresponding function to remove addresses from the queue when they are no longer needed or have fulfilled their purpose.
This vulnerability can be demonstrated by deploying the LRTDepositPool
contract and repeatedly adding NodeDelegator contract addresses to the nodeDelegatorQueue
using the addNodeDelegatorContractToQueue
function. As addresses are added, the array will grow without limit.
// Deploy LRTDepositPool contract // ... // Repeatedly add NodeDelegator contract addresses to the queue for (uint256 i = 0; i < 1000; i++) { lrtDepositPool.addNodeDelegatorContractToQueue([nodeDelegatorAddress]); }
As a result, the nodeDelegatorQueue
will continue to grow, potentially leading to increased gas costs and potential out-of-gas errors during the addition of addresses.
Manual code review and analysis.
To address this vulnerability, it is recommended to implement a mechanism to remove or "pop" elements from the nodeDelegatorQueue
when NodeDelegator
contract addresses are no longer needed or have fulfilled their purpose. This can be achieved by adding a function to remove addresses from the queue or pop
elements in the end of transferAssetToNodeDelegator()
function when the asset is already transferred (the second possible solution is just an example mitigation step).
NodeDelegator
already exists within the LRTDepositPool.sol#addNodeDelegatorContractToQueue()
functionIn the LRTDepositPool.sol
contract, specifically within the addNodeDelegatorContractToQueue()
function, there is currently no check to determine if a NodeDelegator
contract address already exists in the nodeDelegatorQueue
before adding it. This omission could lead to the unintended addition of duplicate addresses to the queue, potentially causing issues when processing deposits or transfers to these contracts.
It is advisable to implement a check within the addNodeDelegatorContractToQueue
function to ensure that duplicate NodeDelegator
addresses are not added to the nodeDelegatorQueue
.
Here is an example of how the function could be modified to include the check:
function addNodeDelegatorContractToQueue( address[] calldata nodeDelegatorContracts ) external onlyLRTAdmin { uint256 length = nodeDelegatorContracts.length; if (nodeDelegatorQueue.length + length > maxNodeDelegatorCount) { revert MaximumNodeDelegatorCountReached(); } for (uint256 i; i < length; ) { + if (nodeDelegatorQueue[nodeDelegatorContracts[i]] != address(0)) ( + revert(); + ) UtilLib.checkNonZeroAddress(nodeDelegatorContracts[i]); nodeDelegatorQueue.push(nodeDelegatorContracts[i]); emit NodeDelegatorAddedinQueue(nodeDelegatorContracts[i]); unchecked { ++i; } } }
By implementing this check, the addNodeDelegatorContractToQueue
function will prevent the addition of duplicate NodeDelegator
contract addresses to the nodeDelegatorQueue
, ensuring the integrity of the queue and avoiding potential issues that may arise from duplicates.
LRTDepositPool.sol
contract by directly transferring assets
to the LRTDepositPool
, NodeDelegator
or EigenLayer Strategy
contractsThis will disrupt the intended deposit()
functioning of the contract and the associated ecosystem, as the contract relies on controlled depositing procedures to ensure proper asset management.
LRT Manager
permissions during the initialization of the LRTDepositPool
contractMINTER_ROLE
and BURNER_ROLE
permissions during the initialization of the RSETH
contractsupportedAssets
only if the asset has not already been transferred to the NodeDelegator
contractThis will prevent potential out of gas errors.
#0 - c4-pre-sort
2023-11-18T00:02:22Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-12-01T16:42:55Z
fatherGoose1 marked the issue as grade-a