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: 103/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/LRTOracle.sol#L52 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L109 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L79
An attacker could skew the RSETH price by donating LSTs (stETH, rETH, cbETH) to LTRDepositPool
. The getRSETHPrice()
calls ILRTDepositPool.getTotalAssetDeposits(asset)
in a loop for each supported asset as follows.
function getRSETHPrice() external view returns (uint256 rsETHPrice) { ... for (uint16 asset_idx; asset_idx < supportedAssetCount;) { ... uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); totalETHInPool += totalAssetAmt * assetER; unchecked { ++asset_idx; } } return totalETHInPool / rsEthSupply; }
Inside getTotalAssetDeposits()
, the call to getAssetDistributionData(asset)
returns the current balance of LST of LTRDepositPool
inside the assetLyingInDepositPool
variable.
function getTotalAssetDeposits(address asset) public view override returns (uint256 totalAssetDeposit) { (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) = getAssetDistributionData(asset); return (assetLyingInDepositPool + assetLyingInNDCs + assetStakedInEigenLayer); }
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)); ... }
Therefore, the RSETH price returned from getRSETHPrice()
could be dramatically increased by a donation attack.
And an attacker, as a first depositor, can block further deposits to LTRDepositPool
by forcing subsequent depositors to mint 0 RSETH tokens.
The attack scenario is the following:
depositAsset()
with 1 wei of stETH as depositAmount
and mints 1 wei of RSETH.LTRDepositPool
.getRSETHPrice()
will be 1 ether * 10^18
instead of 1 ether
.(amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice()
.Manual review.
Use ERC4626.sol as a base contract for the LTRDepositPool
which has built-in protection.
DoS
#0 - c4-pre-sort
2023-11-15T21:57:07Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-15T21:57:17Z
raymondfam marked the issue as duplicate of #42
#2 - c4-judge
2023-12-01T16:56:27Z
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/oracles/ChainlinkPriceOracle.sol#L37
The getAssetPrice()
function of ChainlinkPriceOracle
uses the latestAnswer()
call to get the price of an asset. LRTOracle
in turn may use price returned from ChainlinkPriceOracle
for LSTs (stETH, rETH, cbETH).
function getAssetPrice(address asset) external view onlySupportedAsset(asset) returns (uint256) { return AggregatorInterface(assetPriceFeed[asset]).latestAnswer(); }
Yet there are two problems with latestAnswer()
usage inside getAssetPrice()
:
ChainlinkPriceOracle
and other contracts of the protocol.latestAnswer()
is marked as deprecated and latestRoundData()
should be used instead.In case latestAnswer()
returns 0 value for one asset (for example, for only stETH), it leads to a situation when a user may mint 0 RSETH when calling depositAsset()
.
function getRsETHAmountToMint( address asset, uint256 amount ) public view override returns (uint256 rsethAmountToMint) { // setup oracle contract address lrtOracleAddress = lrtConfig.getContract(LRTConstants.LRT_ORACLE); ILRTOracle lrtOracle = ILRTOracle(lrtOracleAddress); // calculate rseth amount to mint based on asset amount and asset exchange rate rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice(); }
Where lrtOracle.getAssetPrice(asset)
returns 0 for stETH. But lrtOracle.getRSETHPrice()
is non-zero because 0 value is produced only for one asset - stETH.
function getRSETHPrice() external view returns (uint256 rsETHPrice) { ... 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; }
Manual review.
Use latestRoundData()
instead to retrieve the price. Perform checks that the returned price isn't stale and has non-zero value.
Oracle
#0 - c4-pre-sort
2023-11-15T22:20:36Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-11-15T22:20:55Z
raymondfam marked the issue as duplicate of #34
#2 - c4-pre-sort
2023-11-17T06:10:49Z
raymondfam marked the issue as sufficient quality report
#3 - c4-pre-sort
2023-11-17T06:10:54Z
raymondfam marked the issue as not a duplicate
#4 - c4-pre-sort
2023-11-17T06:11:09Z
raymondfam marked the issue as duplicate of #215
#5 - c4-judge
2023-11-29T19:26:07Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#6 - c4-judge
2023-12-04T17:45:53Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#7 - c4-judge
2023-12-08T18:51:09Z
fatherGoose1 marked the issue as grade-b