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: 106/185
Findings: 2
Award: $7.42
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L119-L144 https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L109
LRTDepositPool.depositAsset
relies on getRsETHAmountToMint
to determine the amount of rsETH to mint. The first deposit can mint a minimal number of rsETH then donate LST tokens to the pool to grossly manipulate the rsETH price. When later depositor stakes into the pool they will lose their fund due to precision loss.
The root cause of the vulnerability is that the depositAsset
function allows users to mint a realy small amount of rsETH at the initial stage (when rsETH supply is zero), exposing the pool to the exchange-rate inflation attack. Let us walk through the issue with the following scenario:
getRSETHPrice
returns 1 ether. Alice initiates a LRTDepositPool.depositAsset
call with a minimal amount of 1 stETH (price of approx. 1e18 ETH/stETH in base units) to mint roughly (1 * 1e18 / 1e18 = 1) rsETH.// File: src/LRTDepositPool.sol function getRsETHAmountToMint( ... @>1 rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice(); }
LRTDepositPool
contract so that getTotalAssetDeposits
now reads as 1 + 1e18
. As a result, the getRSETHPrice
call is manipulated to return an extremely high value since totalETHInPool
was pumped up while rsEthSupply
remains small. Given assetER
stays unchanged at 1e18, getRSETHPrice
now reads as (1 + 1e18) * 1e18 / 1 ~= 1e36
.// File: src/LRTOracle.sol function getRSETHPrice() external view returns (uint256 rsETHPrice) { uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); totalETHInPool += totalAssetAmt * assetER; ... @>2 return totalETHInPool / rsEthSupply; }
@>1 becomes: rsethAmountToMint = 0.99e18 (stETH) * 1e18 (ETH/stETH) / 1e36 ~= 0 rsETH
rsEthSupply
stays the same while totalETHInPool
has doubled, ensuring innevitable loss for future depositors.Notice: Bob's deposit amount being higher than 0.99e18 will not save him from the loss since Alice can always sniff Bob transaction and frontruns him with the right donation amount described at step 2.
Manual Review
Consider requiring a minimal amount of rsETH tokens to be minted for the first minter, so that the rsETH price can be more resistant to manipulation.
Math
#0 - c4-pre-sort
2023-11-16T19:26:59Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T19:27:07Z
raymondfam marked the issue as duplicate of #42
#2 - c4-judge
2023-12-01T17:02:48Z
fatherGoose1 changed the severity to 3 (High Risk)
#3 - c4-judge
2023-12-01T17:05:07Z
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/tree/main/src/LRTDepositPool.sol#L170
The smart contract fails to check for duplicate entries in the nodeDelegatorQueue
, leading to a potentially serious issue. The duplicated entries cause inflated balance readings in getAssetDistributionData
, impacting the protocol's accouting functionality. This issue could cause potential losses of funds for users interacting with the protocol when the assets prices are maliciously inflated.
The proof of concept involves an admin adding the same nodeDelegatorContract
to the queue multiple times, resulting in inflated balance readings during functions like getAssetDistributionData
, getTotalAssetDeposits
and getRSETHPrice
.
nodeDelegatorContract
to the queue multiple times without properly checking.getAssetDistributionData
, getTotalAssetDeposits
and getRSETHPrice
are referenced. As can be seen from Line 83-84 below, assetLyingInNDCs
& assetStakedInEigenLayer
can be incorrectly acrued multiple times if nodeDelegatorQueue[i]
is not unique, resulting in nth-time larger read value compared with the actual figures.// File: src/LRTDepositPool.sol 71: function getAssetDistributionData(address asset) ... 82: for (uint256 i; i < ndcsCount;) { 83: assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]); // <= FOUND 84: assetStakedInEigenLayer += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset); // <= FOUND ... 89: }
// File: src/LRTOracle.sol 52: function getRSETHPrice() external view returns (uint256 rsETHPrice) { ... 70: uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); // <= FOUND: getTotalAssetDeposits reads values from the affected getAssetDistributionData() described above, resulting in inflated totalAssetAmt value, like-wise getRSETHPrice also jumps. 71: totalETHInPool += totalAssetAmt * assetER; ... 79: }
Manual Review
Utilizing OpenZeppelin's EnumerableSet or a similar data structure for better uniqueness management.
Invalid Validation
#0 - c4-pre-sort
2023-11-16T03:25:05Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-11-16T03:25:18Z
raymondfam marked the issue as duplicate of #36
#2 - c4-judge
2023-11-29T21:35:51Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-29T21:42:42Z
fatherGoose1 marked the issue as grade-b