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: 109/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/main/src/LRTDepositPool.sol#L119-L144
A user can use LRTDepositPool.depositAsset
to deposit any of the supported assets into the LRTDepositPool
contract:
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); }
The first user to deposit the funds(rsETH supply will be 0 because no one has minted rsETH before) will get rsETH minted in a 1:1 rate:
if (rsEthSupply == 0) { return 1 ether; }
The problem lays in LRTDepositPool.getAssetDistributionData()
. This function calculates the asset amount distribution data among depositPool, NDCs and eigenLayer
by using balanceOf()
. How can this impact a depositor? Consider the following.
1 wei
of stETH
(this can be any arbitrary amount depending on how many decimals the getAssetPrice()
function returns).rsETH
, Alice decides to directly transfer 100 wei
of stETH
(or any other supported coin) into the LRTDepositPool
contract.10 stETH
.rsETH
that Bob should receive is calculated here:rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
(amount * lrtOracle.getAssetPrice(asset))
will work as it is supposed to, but this time, since Alice has sent extra funds to the LRTDepositPool
contract,
(amount * lrtOracle.getAssetPrice(asset))
will be divided by a much bigger number, leading to having way less rsETH
minted:
uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); totalETHInPool += totalAssetAmt * assetER;
return totalETHInPool / rsEthSupply;
What happens here it the following:
totalAssetAmt
will be 101 wei
because of the 1 wei + 100 wei
of Alice.totalAssetAmt
will be multiplied by the price of the asset, this value will be assigned to totalETHInPool
totalETHInPool
will be divided by rsEthSupply
, which is 1 wei
not 101 wei
! This means this number lrtOracle.getRSETHPrice();
that gets used to divide, will be disproportionally bigger than the previous and next mints, leading to less rsETH
minted to Bob.Manual review
Do not use address(this).balanceOf()
, use another way of accounting the deposits.
Invalid Validation
#0 - c4-pre-sort
2023-11-16T20:22:41Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T20:22:51Z
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:57Z
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
There are a few problems with the usage of maxNodeDelegatorCount
in LRTDepositPool.sol
.
First problem, consider the following:
maxNodeDelegatorCount = 10
NodeDelegators
have been added using LRTDepositPool.addNodeDelegatorContractToQueue
maxNodeDelegatorCount
gets updated to 8
LRTDepositPool.getAssetDistributionData
will still use all 10 NodeDelegators
, despite the max being set to 8
:uint256 ndcsCount = nodeDelegatorQueue.length; for (uint256 i; i < ndcsCount;) { assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]); assetStakedInEigenLayer += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset); unchecked { ++i;
Second problem, consider the following: First problem, consider the following:
maxNodeDelegatorCount = 10
NodeDelegators
have been added using LRTDepositPool.addNodeDelegatorContractToQueue
maxNodeDelegatorCount
gets updated to 8
LRTDepositPool
to any NodeDelegator
that has ever been added using LRTDepositPool.transferAssetToNodeDelegator
address nodeDelegator = nodeDelegatorQueue[ndcIndex]; if (!IERC20(asset).transfer(nodeDelegator, amount)) { revert TokenTransferFailed();
These two scenarios should not be possible.
Manual Review
Either remove NodeDelegators
from the nodeDelegatorQueue
when setting a maxNodeDelegatorCount < currentmaxNodeDelegatorCount
, or, remove the functionality of being able to set a maxNodeDelegatorCount
that is less than the current maxNodeDelegatorCount
.
/// @notice add new node delegator contract addresses /// @dev only callable by LRT manager /// @param nodeDelegatorContracts Array of NodeDelegator contract addresses // @audit should be only callable by LRT Manager but is onlyLRTAdmin 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; } } }
As you can see from the comments of the devs comment it should be the LRT manager that should be able to call this, but currently its the LRTAdmin that can call it only because of the onlyLRTAdmin
modifier.
Use onlyLRTManager
instead of onlyLRTAdmin
stETH
from Lido is a rebase token. The Lido docs state:
The mechanism which updates the stETH balances every day is called a βrebaseβ. Every day at 12PM UTC the amount of stETH in your address will increase with the current APR.
This means that any user can just wait until stETH
almost rebases and mint rsETH
to receive more rsETH
than he would have after the rebase
#0 - raymondfam
2023-11-18T00:19:01Z
Possible upgrade:
#1 - c4-pre-sort
2023-11-18T00:19:07Z
raymondfam marked the issue as sufficient quality report
#2 - c4-judge
2023-12-01T18:49:01Z
fatherGoose1 marked the issue as grade-b