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: 23/185
Findings: 3
Award: $179.04
π 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
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L136-L141 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L52-L79 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L108-L109
The identified issue in the LRTDepositPool
and LRTOracle
contracts presents a significant vulnerability in the calculation of the RSETH token minting rate. The core of the problem lies in the variable returns of the getRSETHPrice
function, which leads to inconsistent RSETH minting rates for different stakers based on the timing of their deposits. This inconsistency can result in an unfair advantage for an early first staker, who may receive a disproportionately larger amount of RSETH for their deposits compared to later stakers. Such a discrepancy could undermine the trust in the protocol's fairness, potentially leading to reputational damage and loss of user confidence. Additionally, it poses a risk of economic imbalance within the protocol, which could be exploited for financial gain, thereby affecting the overall stability of the system.
Here's the scenario:
stETH
, rETH
or cbETH
Deposit):lrtOracle.getAssetPrice(asset)
is equal to the price of ETH and initially there is no RSETH supply, the function getRSETHPrice
would return 1 ETH.https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L56-L58
if (rsEthSupply == 0) { return 1 ether; }
rsethAmountToMint
for the first staker depositing 100 ETH would be (100 ETH * 1 ETH) / 1 ETH = 100e18 RSETH tokens, assuming lrtOracle.getAssetPrice(asset)
returns a value in wei (1 ETH = 1e18 wei.)https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L108-L109
// calculate rseth amount to mint based on asset amount and asset exchange rate rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
getRSETHPrice
now will return a value based on the total ETH in the pool divided by the total RSETH supply.https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L136-L141
if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { revert TokenTransferFailed(); } // interactions uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount);
rsethAmountToMint
for the second staker would be (100 ETH * 1 ETH) / 2 ETH = 50e18 RSETH tokens.Hence, rsEthSupply
is now 150e18. If the first staker were to withdraw the asset, he/she would be entitled to redeem 100e18 RSETH tokens for (100e18 / 150e18) * 200e18 = 133.33e18 ETH, making an instant 33.33% gain at the loss of the second staker.
Manual
Reversing the order of asset transfer and RSETH minting is one possible solution, but this is not a good check-and-effect practice despite the nonReentrant
function visibility of depositAsset()
.
Alternatively, extending ERC-4626, the Ethereum token standard for yield-bearing vaults, to include a feature for previewing share amounts can be a beneficial enhancement to your DeFi protocol, particularly in the context of your scenario involving the minting of RSETH tokens in depositAsset()
. This will nonetheless require some extensive amount of code refactoring.
Math
#0 - c4-pre-sort
2023-11-15T22:33:40Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-15T22:34:04Z
raymondfam marked the issue as duplicate of #62
#2 - c4-judge
2023-11-29T21:19:09Z
fatherGoose1 marked the issue as satisfactory
#3 - c4-judge
2023-12-01T19:00:05Z
fatherGoose1 changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-12-04T15:31:42Z
fatherGoose1 changed the severity to 3 (High Risk)
π Selected for report: max10afternoon
Also found by: 0xMilenov, 0xbrett8571, 0xhacksmithh, 0xmystery, Aymen0909, Bauer, Daniel526, PENGUN, Pechenite, Shaheen, adriro, anarcheuz, btk, ck, ge6a, glcanvas, hals, turvy_fuzz
140.2525 USDC - $140.25
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L116-L144 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L108-L109
The current implementation of the depositAsset
function in the DeFi protocol assumes that the prices of assets like stETH, rETH, and cbETH are either stable or appreciating relative to ETH. This assumption overlooks the potential volatility and price drops of these assets against ETH. In the absence of slippage protection, users could be exposed to significant financial risk, especially in scenarios where the asset value declines sharply after initiating a deposit but before the transaction is executed. This gap can lead to unfavorable RSETH minting rates and potential economic losses for users. The lack of slippage protection could undermine user confidence in the protocolβs robustness and its ability to safeguard user interests under volatile market conditions.
This vulnerability is rated medium severity because, while it may not directly lead to immediate large-scale financial losses or exploits, it exposes users to significant risk in volatile market conditions and can impact the trust and reliability of the protocol.
Illustrative Scenario:
stETH
.https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L116-L144
/// @notice helps user stake LST to the protocol /// @param asset LST asset address to stake /// @param depositAmount LST asset amount to stake 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); }
LRTOracle.getRSETHPrice()
returns a comparatively higher rsETHPrice
than getAssetPrice(asset)
because the prices of rETH
and cbETH
remain unaffected:https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L60-L78
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;
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L108-L109
// calculate rseth amount to mint based on asset amount and asset exchange rate rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
Manual
Introduce a slippage tolerance mechanism in the depositAsset
function. For instance, users could specify their minimum share of RSETH minted, and transactions falling below this threshold would be reverted.
Timing
#0 - c4-pre-sort
2023-11-15T22:40:16Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-15T22:40:23Z
raymondfam marked the issue as duplicate of #39
#2 - c4-pre-sort
2023-11-17T06:43:05Z
raymondfam marked the issue as duplicate of #148
#3 - c4-judge
2023-11-29T19:09:38Z
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/LRTDepositPool.sol#L108-L109 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119-L144
The absence of a deadline or time limit in the depositAsset
function exposes users to risks associated with transaction delays on the Ethereum blockchain, such as heavy congestion or gas fee hikes. In volatile market conditions, the price obtained from lrtOracle.getAssetPrice(asset)
could significantly change during the delay, potentially leading to unfavorable exchange rates at the time of execution.
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L108-L109
// calculate rseth amount to mint based on asset amount and asset exchange rate rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
This scenario can result in economic losses for users who expect a certain amount of RSETH based on the asset price at the time of initiating the transaction. Implementing a deadline within which the transaction must be executed can mitigate this risk by providing users with control over the maximum allowable time for their transactions, enhancing the security and reliability of the protocol.
This issue is rated as medium severity due to its potential to cause financial losses for users in specific scenarios of network congestion and price volatility, impacting user trust and the protocol's perceived reliability.
Illustrative Scenario:
depositAsset()
.https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L119-L144
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); }
LRTOracle.getRSETHPrice()
returns a comparatively higher rsETHPrice
than getAssetPrice(asset)
because the prices of other supported LST's remain unaffected:https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L60-L78
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;
Manual
Introduce Transaction Deadline: Modify the depositAsset
function to include a deadline parameter. Transactions that are not executed before this deadline should be automatically reverted.
User-Defined Deadlines: Allow users to specify their own deadlines based on their risk tolerance and expectations regarding network conditions.
Timing
#0 - c4-pre-sort
2023-11-15T22:40:41Z
raymondfam marked the issue as primary issue
#1 - c4-pre-sort
2023-11-15T22:40:47Z
raymondfam marked the issue as sufficient quality report
#2 - raymondfam
2023-11-15T22:42:15Z
Similar but yet different issue than slippage issue.
#3 - c4-sponsor
2023-11-24T09:16:14Z
gus-staderlabs (sponsor) disputed
#4 - gus-stdr
2023-11-24T09:18:58Z
We appreciate this issue but this is not an issue of the protocol but the way the Ethereum blockchain works. This requires users to know about gas prices and time to have the user's transaction validated and added to a block.
#5 - c4-judge
2023-12-01T18:19:23Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#6 - c4-judge
2023-12-01T18:19:28Z
fatherGoose1 marked the issue as grade-b
#7 - fatherGoose1
2023-12-01T18:20:01Z
Useful design recommendation. QA