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: 48/185
Findings: 3
Award: $83.44
🌟 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
LRTOracle.getRSETHPrice
is calculated as spot price, and it can be manipulated.
LRTOracle.getRSETHPrice is defined as
function getRSETHPrice() external view returns (uint256 rsETHPrice) { address rsETHTokenAddress = lrtConfig.rsETH(); uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply(); if (rsEthSupply == 0) { return 1 ether; } 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; }
So the price is calculated as totalETHInPool / rsEthSupply
, both totalETHInPool and rsEthSupply can be manipulated.
totalETHInPool += totalAssetAmt * assetER
.
assetER
is queried by price feed, which can't be manipulated.
But for totalAssetAmt, it's calculated in LRTDepositPool.getTotalAssetDeposits, and LRTDepositPool.getTotalAssetDeposits
calls LRTDepositPool.getAssetDistributionData, in LRTDepositPool.getAssetDistributionData
, assetLyingInDepositPool and assetLyingInNDCs are calculated by balaceOf
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)); <<<--- Here using balaceOf uint256 ndcsCount = nodeDelegatorQueue.length; for (uint256 i; i < ndcsCount;) { assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]); <<<--- Here using balaceOf assetStakedInEigenLayer += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset); unchecked { ++i; } } }
So if we transfer asset to LRTDepositPool.sol or nodeDelegatorQueue
, we can increase totalETHInPool
One way to exploit this vulnerable, we can abuse the trick similar to ERC4626 inflation attacks:
LRTDepositPool.depositAsset
to deposit 1 wei assetLRTDepositPool
to increase assetLyingInDepositPool, or transfer a large amount of asset to nodeDelegatorQueue[i]
to increase assetLyingInNDCsVIM
Other
#0 - c4-pre-sort
2023-11-16T19:40:42Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T19:40:50Z
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:29Z
fatherGoose1 marked the issue as satisfactory
🌟 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
strategy
is used to deposit asset as shown NodeDelegator.depositAssetIntoStrategy.
When calculating the rsETH price in LRTOracle.getRSETHPrice, the assets deposited in strategies will be used to calculate totalETHInPool.
Thus if replacing a existing strategy containing deposited asset will cause the price decline
In LRTConfig.updateAssetStrategy, before replacing an existing strategy, the function doesn't check if the strategy has asset deposited in.
function updateAssetStrategy( address asset, address strategy ) external onlyRole(DEFAULT_ADMIN_ROLE) onlySupportedAsset(asset) <<<<------------- check if the protocol support the asset { UtilLib.checkNonZeroAddress(strategy); <<<------- check if the strategy's address is 0 if (assetStrategy[asset] == strategy) { <<<------- check if the new strategy is the same as current revert ValueAlreadyInUse(); } assetStrategy[asset] = strategy; }
When a user calls LRTDepositPool.depositAsset to deposit some stETH, within the function, LRTDepositPool._mintRsETH
is called. And the amount of rsETH to be minted for the user is calculated in LRTDepositPool.sol#L109 as rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
While calling LRTOracle.getRSETHPrice, the function will add up all ETH in every LRTDepositPool
So the code flow will go to LRTDepositPool.getTotalAssetDeposits, and next will go to LRTDepositPool.getAssetDistributionData.
Within LRTDepositPool.getAssetDistributionData
, the function will call NodeDelegator.getAssetBalance.
In NodeDelegator.getAssetBalance, the function will requery the amount of underlyingToken
function getAssetBalance(address asset) external view override returns (uint256) { address strategy = lrtConfig.assetStrategy(asset); return IStrategy(strategy).userUnderlyingView(address(this)); }
So the if the admin calls LRTConfig.updateAssetStrategy
without checking the underlying balance, the price might be incorrectly.
VIM
Other
#0 - c4-pre-sort
2023-11-16T19:36:54Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T19:37:06Z
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:04Z
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
LRTDepositPool.addNodeDelegatorContractToQueue lacks of the duplicated nodes existing in the LRTDepositPool.nodeDelegatorQueue
, if there are duplicated nodes, it will increase price when calling LRTOracle.getRSETHPrice
to calculating the price.
In LRTDepositPool.addNodeDelegatorContractToQueue, there is no check if there are duplicated nodes existing in LRTDepositPool.nodeDelegatorQueue
before push the new node into the queue.
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; } } }
When calling LRTOracle.getRSETHPrice
, ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits will be called, and in LRTDepositPool.getTotalAssetDeposits, LRTDepositPool.getAssetDistributionData will be called.
If duplicated nodes existing, the duplicated node will be calculated twice:
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; } }
The following test can prove that duplicated elements can be added, add the following code in LRTDepositPoolTest.t.sol, under LRTDepositPoolGetNodeDelegatorQueue contract, and run forge test --mc LRTDepositPoolGetNodeDelegatorQueue --mt test_GetNodeDelegatorQueueDup
function test_GetNodeDelegatorQueueDup() external { address nodeDelegatorContractOne = address(new MockNodeDelegator()); address nodeDelegatorContractTwo = address(new MockNodeDelegator()); address nodeDelegatorContractThree = address(new MockNodeDelegator()); address[] memory nodeDelegatorQueue = new address[](4); nodeDelegatorQueue[0] = nodeDelegatorContractOne; nodeDelegatorQueue[1] = nodeDelegatorContractTwo; nodeDelegatorQueue[2] = nodeDelegatorContractThree; nodeDelegatorQueue[3] = nodeDelegatorContractTwo; vm.startPrank(admin); lrtDepositPool.addNodeDelegatorContractToQueue(nodeDelegatorQueue); vm.stopPrank(); assertEq(lrtDepositPool.getNodeDelegatorQueue(), nodeDelegatorQueue, "Node delegator queue is not set"); }
VIM
Other
#0 - c4-pre-sort
2023-11-16T19:38:23Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-11-16T19:38:36Z
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:43:48Z
fatherGoose1 marked the issue as grade-b