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: 69/185
Findings: 2
Award: $38.79
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: T1MOH
Also found by: 0x1337, 0xNaN, 0xepley, 0xluckhu, 0xmystery, 7siech, Aamir, AlexCzm, Aymen0909, DanielArmstrong, GREY-HAWK-REACH, HChang26, Jiamin, Juntao, QiuhaoLi, Ruhum, SBSecurity, Varun_05, Weed0607, adam-idarrha, adriro, ast3ros, ayden, circlelooper, crack-the-kelp, crunch, cryptothemex, deepplus, mahdirostami, max10afternoon, osmanozdemir1, rouhsamad, rvierdiiev, trachev, xAriextz, zhaojie
36.0335 USDC - $36.03
Depositor may be grief attacked and no rsETH
will be minted.
Depositor can mint rsETH
by depositing asset through depositAsset(...) function.
Asset will be transferred from depositor to the protocol:
if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { revert TokenTransferFailed(); }
Then rsETH
will be minted through _mintRsETH(...) function:
uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount);
The amount of rsETH
to be minted is calculated is determined by rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice()
, here lrtOracle.getRSETHPrice() returns the rsETH
price:
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;
As we can see from above, the rsETH
price is determined by the asset owned by the protocol and the total supply of rsETH
. However, as the asset will be transferred to protocol before minting, the depositor may not receive a correct amount of rsETH
, and even worse, depositor can be grief attacked and receive no rsETH
at all.
Imagine the following scenario:
rsETH
by depositing 1 ether stETH
(price is 1e18);stETH
;rsETH
will be minted to Bob, so totalETHInPool
is 1 and rsEthSupply
is 1;totalETHInPool
is 1e18 + 1 and rsEthSupply
is 1, rsETH
price is 1e18 * (1e18 + 1);rsETH
.Please see below test case:
function test_AuditDepositAsset() external { ProxyAdmin proxyAdmin = new ProxyAdmin(); LRTOracle lrtOracleImpl = new LRTOracle(); TransparentUpgradeableProxy lrtOracleProxy = new TransparentUpgradeableProxy( address(lrtOracleImpl), address(proxyAdmin), "" ); LRTOracle lrtOracle = LRTOracle(address(lrtOracleProxy)); lrtOracle.initialize(address(lrtConfig)); vm.startPrank(manager); lrtOracle.updatePriceOracleFor(address(stETH), address(new MockPriceOracle())); lrtOracle.updatePriceOracleFor(address(cbETH), address(new MockPriceOracle())); lrtOracle.updatePriceOracleFor(address(rETH), address(new MockPriceOracle())); vm.stopPrank(); vm.startPrank(admin); lrtConfig.setContract(LRTConstants.LRT_DEPOSIT_POOL, address(lrtDepositPool)); lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(lrtOracle)); vm.stopPrank(); vm.startPrank(bob); rETH.approve(address(lrtDepositPool), 1); lrtDepositPool.depositAsset(rETHAddress, 1); uint256 bobBalance = rseth.balanceOf(address(bob)); console.log(bobBalance); vm.stopPrank(); vm.startPrank(alice); rETH.approve(address(lrtDepositPool), 1 ether); // lrtDepositPool.depositAsset(rETHAddress, 2 ether); lrtDepositPool.depositAsset(rETHAddress, 1 ether); uint256 aliceBalance = rseth.balanceOf(address(alice)); assertEq(aliceBalance, 0); vm.stopPrank(); }
Manual Review
When depositing, rsETH
should be minted before transferring asset.
+ // interactions + uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount); if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { revert TokenTransferFailed(); } - // interactions - uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount);
Math
#0 - c4-pre-sort
2023-11-16T03:04:29Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T03:04:36Z
raymondfam marked the issue as duplicate of #42
#2 - c4-judge
2023-12-01T17:01:26Z
fatherGoose1 marked the issue as not a duplicate
#3 - fatherGoose1
2023-12-01T17:02:07Z
Doesn't describe the standard donation attack. Instead highlights the issue with using the deposited funds in the share calculation, sharing impact with #62.
#4 - c4-judge
2023-12-01T17:02:16Z
fatherGoose1 marked the issue as duplicate of #62
#5 - c4-judge
2023-12-01T17:02:21Z
fatherGoose1 marked the issue as satisfactory
#6 - c4-judge
2023-12-01T19:00:05Z
fatherGoose1 changed the severity to 2 (Med Risk)
#7 - c4-judge
2023-12-04T15:31:42Z
fatherGoose1 changed the severity to 3 (High Risk)
🌟 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
Asset staked EigenLayer is not properly calculated, this could lead to incorrect total asset balance and incorrect rsETH
price.
To get the asset balance staked in EigenLayer, protocol calls userUserlyingView(...) function o get the asset balance staked in EigenLayer, this function is defined in StrategyBase
contract as below:
function userUnderlyingView(address user) external view virtual returns (uint256) { return sharesToUnderlyingView(shares(user)); }
Where [shares(...)]https://etherscan.io/address/0xdfda04f980be6a64e3607c95ca26012ab9aa46d3#code#F2#L267) function is called to get user's share:
function shares(address user) public view virtual returns (uint256) { return strategyManager.stakerStrategyShares(user, IStrategy(address(this))); }
stakerStrategyShares is a mapping define in StrategyManagerStorage
contract, when user deposits into strategy, share
will be updated accordingly:
_addShares(depositor, strategy, shares);
When user withdraws, it's reasonable to think user's share will be burned and asset will be returned immediately, however, this is not exactly true. To withdraw from EigenLayer, queueWithdrawal(...) function should be firstly called, user's share will be decreased:
if (_removeShares(msg.sender, strategyIndexes[strategyIndexIndex], strategies[i], shares[i])) {
Then completeQueuedWithdrawal(...) is required to complete the withdrawal and the asset will be returned to user. It is unlikely these 2 functions can be called in a single transaction, as said in the comment:
* @dev middlewareTimesIndex should be calculated off chain before calling this function by finding the first index that satisfies `slasher.canWithdraw`
So the problem is that the protocol does not take the queued withdrawal asset amount into consideration, incorrect total asset balance will be retrieved, if a user deposits before withdrawals are completed, incorrect rsETH
price will be used to calculated the rsETH
token to be minted, results in user receiving incorrect amount of rsETH
.
Withdrawal function is a must to the protocol even if it has not been implemented, incorrect calculation of asset balance should be fixed in the current release to prevent the issue mention above.
Manual Review
When calculating the asset balance staked into EigenLayer, the queued withdrawal balance should be took into consideration, protocol can save the queued balance in local and use it in the calculation.
Access Control
#0 - c4-pre-sort
2023-11-16T03:21:01Z
raymondfam marked the issue as sufficient quality report
#1 - raymondfam
2023-11-16T03:21:41Z
Inadequate integration.
#2 - c4-pre-sort
2023-11-16T03:35:09Z
raymondfam marked the issue as primary issue
#3 - c4-pre-sort
2023-11-17T08:26:56Z
raymondfam marked the issue as high quality report
#4 - c4-sponsor
2023-11-24T10:42:18Z
manoj9april (sponsor) disputed
#5 - manoj9april
2023-11-24T10:44:53Z
This is out of scope of audit. Withdrawal is not implemented yet. This bug doesn't exist in the current state of the codebase.
#6 - c4-judge
2023-12-01T17:41:37Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#7 - c4-judge
2023-12-06T18:19:18Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#8 - c4-judge
2023-12-08T18:52:28Z
fatherGoose1 marked the issue as grade-b