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: 51/185
Findings: 2
Award: $78.78
🌟 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/LRTOracle.sol#L52 https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L121 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L47 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L71 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTConfig.sol#L109
A strategy change will result in an incorrect price evaluation for RSETH due to it undervaluing the amount of underlying token deposited.
LRTOracle#getRSETHPrice
is called it gets the price of RSETH based on the calculated total amount of ETH based on total balance of underlying assets (stETH, rETH, cbETH) multiplied by the exchange rate to ETHfunction getRSETHPrice() external view returns (uint256 rsETHPrice) { address rsETHTokenAddress = lrtConfig.rsETH(); uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply(); //... uint256 totalETHInPool; address lrtDepositPoolAddr = lrtConfig.getContract(LRTConstants.LRT_DEPOSIT_POOL); address[] memory supportedAssets = lrtConfig.getSupportedAssetList(); uint256 supportedAssetCount = supportedAssets.length; for (uint16 asset_idx; asset_idx < supportedAssetCount;) { address asset = supportedAssets[asset_idx]; uint256 assetER = getAssetPrice(asset); @> uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); @> totalETHInPool += totalAssetAmt * assetER; unchecked { ++asset_idx; } } return totalETHInPool / rsEthSupply; }
LRTDepositPool#getTotalAssetDeposits
is called which sums up the amount of assets in the 3 possible locations returned by LRTDepositPool#getAssetDistributionData
function getTotalAssetDeposits(address asset) public view override returns (uint256 totalAssetDeposit) { (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) = getAssetDistributionData(asset); return (assetLyingInDepositPool + assetLyingInNDCs + assetStakedInEigenLayer); }
LRTDepositPool#getAssetDistributionData
sums up the total assets from 3 possible locations:
i. In the LRTDepositPool
ii. Inside the node delegator queue contracts
iii. Inside the asset eigenlayer strategyfunction getAssetDistributionData(address asset) public view override onlySupportedAsset(asset) returns (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) { 1@> assetLyingInDepositPool = IERC20(asset).balanceOf(address(this)); uint256 ndcsCount = nodeDelegatorQueue.length; for (uint256 i; i < ndcsCount;) { 2@> assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]); 3@> assetStakedInEigenLayer += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset); unchecked { ++i; } } }
NodeDelegator#getAssetBalance
function getAssetBalance(address asset) external view override returns (uint256) { address strategy = lrtConfig.assetStrategy(asset); return IStrategy(strategy).userUnderlyingView(address(this)); }
CRTConfig
admin were to call CRTConfig#updateAssetStrategy
to change the strategy, it would result in the funds inside the previous strategy not being included in the accumulated funds; resulting in an incorrect price evaluation for rsETH.LRTOracle#getRSETHPrice
The before calculations for the price of rsETH are as follows:
totalAssetAmt = (0+0+100) rETH = 100 rETH totalETHInPool = totalAssetAmt * assetER = 100 rETH * 1 ETH/rETH = 100 ETH rsETHPrice = totalETHInPool / rsEthSupply = 100 ETH/100 rsETH= 1 ETH/rsETH
totalAssetAmt = (0+0+0) rETH = 0 rETH totalETHInPool = totalAssetAmt * assetER = 0 rETH * 1 ETH/rETH = 0 ETH rsETHPrice = totalETHInPool / rsEthSupply = 0 ETH/100 rsETH = 0 ETH/rsETH
VIM
Add functionality inside NodeDelegator that properly migrates the funds from one eigenlayer strategy to another whenever LRTConfig#updateAssetStrategy
is called. This function should migrate on all the relevant NodeDelegators that are affected by the changed strategy.
Math
#0 - c4-pre-sort
2023-11-16T23:33:51Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T23:34:00Z
raymondfam marked the issue as duplicate of #197
#2 - c4-judge
2023-12-01T17:24:51Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-12-04T16:41:52Z
fatherGoose1 changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-12-08T17:26:46Z
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
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTConfig.sol#L80 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L52
LRTConfig
only has the ability to add supported assets and not remove them. This means that the Kelp protocol will be forever vulnerable if there is an issue with any of the underlying assets.
There are several main issues that can be problematic:
While the administrator can stop deposits by just setting the deposit limit of the compromised/unintended asset to 0, the assets price is still taken into account inside the LRTOracle#getRSETHPrice
function to determine the price of RSETH.
address[] memory supportedAssets = lrtConfig.getSupportedAssetList(); uint256 supportedAssetCount = supportedAssets.length; for (uint16 asset_idx; asset_idx < supportedAssetCount;) { address asset = supportedAssets[asset_idx]; //@audit here get the asset price @> uint256 assetER = getAssetPrice(asset); uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); totalETHInPool += totalAssetAmt * assetER; unchecked { ++asset_idx; } } return totalETHInPool / rsEthSupply;
This means that the price of RSETH will forever be reliant on the compromised/unintended asset which can greatly affect the integrity of the protocol should something go wrong.
VIM
Add the ability for the admin to remove support for an asset inside LRTConfig
Other
#0 - c4-pre-sort
2023-11-16T21:39:41Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-11-16T21:39:49Z
raymondfam marked the issue as duplicate of #36
#2 - c4-judge
2023-11-29T21:35:52Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-29T21:44:07Z
fatherGoose1 marked the issue as grade-b