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: 38/185
Findings: 4
Award: $119.47
🌟 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/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L136 https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L70
When depositAsset, the price of RsETH is related to the depositAmount. The larger the depositAmount is, the higher the price of RsETH is, and the less RsETH quantity can be obtained. this is unfair to users who deposit a lot of assets at once, and the price of RsETH is higher than the actual expected price because the assets in the pool are calculated in advance.
LRTDepositPool#depositAsset:
function depositAsset(address asset, uint256 depositAmount)
LRTDepositPool#depositAsset,transfer asset first, then mint RsETH:
function depositAsset( address asset, uint256 depositAmount ) external whenNotPaused nonReentrant onlySupportedAsset(asset) { ..... @> if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { revert TokenTransferFailed(); } @> uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount); emit AssetDeposit(asset, depositAmount, rsethAmountMinted); }
LRTOracle#getRSETHPrice, totalETHInPool
include the asset that is currently being deposited,the value of rsEthSupply
is before deposit.
function getRSETHPrice() external view returns (uint256 rsETHPrice) { address rsETHTokenAddress = lrtConfig.rsETH(); uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply(); ..... for (uint16 asset_idx; asset_idx < supportedAssetCount;) { address asset = supportedAssets[asset_idx]; uint256 assetER = getAssetPrice(asset); //@audit totalAssetDeposit include the asset that is currently being deposited @> uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset); totalETHInPool += totalAssetAmt * assetER; unchecked { ++asset_idx; } } @> return totalETHInPool / rsEthSupply; }
vscode manual
Get the price of RsETH before transferring the asset (calculate the number of Mint RsETH)
- if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { - revert TokenTransferFailed(); - } - uint256 rsethAmountMinted = _mintRsETH(asset, depositAmount); + (rsethAmountToMint) = getRsETHAmountToMint(_asset, _amount); + if (!IERC20(asset).transferFrom(msg.sender, address(this), depositAmount)) { + revert TokenTransferFailed(); + } + IRSETH(address(lrtConfig.rsETH())).mint(msg.sender, rsethAmountToMint);
Other
#0 - c4-pre-sort
2023-11-16T07:17:49Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T07:17:56Z
raymondfam marked the issue as duplicate of #62
#2 - c4-judge
2023-11-29T21:23:46Z
fatherGoose1 marked the issue as satisfactory
#3 - c4-judge
2023-12-04T15:31:41Z
fatherGoose1 changed the severity to 3 (High Risk)
🌟 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/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L119 https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L52
When RsETH totalSupply is 0, users deposit 1wei's assets, and then other users will only receive a small amount of rsETH when they redeposit (such as 1eth), protocol will not work.
LRTDepositPool#depositAsset calculates the number of RsETH tokens in mint
based on the price of deposited assets and RsETH.
rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
The price returned by lrtOracle.getRSETHPrice is determined by the total amount of RsETH and the total amount of assets(rETH+stETH+cbETH) deposited, as well as the price.
When the number of RsETH in the pool is only 1wei, if the user deposits 1eth, a large price will be returned.
function getRSETHPrice() external view returns (uint256 rsETHPrice) { address rsETHTokenAddress = lrtConfig.rsETH(); uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply(); if (rsEthSupply == 0) { return 1 ether; } return totalETHInPool / rsEthSupply;
When the number of RsETH in the pool is only 1wei, redeposit 1 eth, in lrtOracle.getRSETHPrice
function totalETHInPool
is 1 eth, rsEthSupply
is 1, totalETHInPool/rsEthSupply
value is very big, RsETH price will be high, the user will only get a few RsETH token.
The asset(rETH) is transferred to the pool before the price is calculated so the totalETHInPool is 1eth.
This can happen when the protocol is just started, or for some other reason RsETH totalSupply is 0.
Test code:
function test_DepositAsset2() external { vm.startPrank(alice); rETH.approve(address(lrtDepositPool), 2 ether); // first time deposit 1wei lrtDepositPool.depositAsset(rETHAddress, 1); // then deposit 1wei lrtDepositPool.depositAsset(rETHAddress, 1 ether); uint256 aliceBalanceAfter = rseth.balanceOf(address(alice)); vm.stopPrank(); //aliceBalanceAfter = 1 console.log(aliceBalanceAfter); }
Running test_DepositAsset1
yields the normal amount of RsETH, while test_DepositAsset2
deposits 1wei and then 1eth and only 1wei RsETH.
forge test --match-test test_DepositAsset1 -vv forge test --match-test test_DepositAsset2 -vv
Place the test file in the test directory:
pragma solidity 0.8.21; import {BaseTest} from "./BaseTest.t.sol"; import {LRTDepositPool} from "src/LRTDepositPool.sol"; import {RSETHTest, ILRTConfig, UtilLib, LRTConstants} from "./RSETHTest.t.sol"; import {ILRTDepositPool} from "src/interfaces/ILRTDepositPool.sol"; import {LRTOracle} from "src/LRTOracle.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {ProxyAdmin} from "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; import "forge-std/console.sol"; contract MockPriceOracle { function getAssetPrice(address) external pure returns (uint256) { return 1 ether; } } contract LRTDepositPoolTest is BaseTest, RSETHTest { LRTDepositPool public lrtDepositPool; LRTOracle public lrtOracle; function setUp() public virtual override(RSETHTest, BaseTest) { super.setUp(); // deploy LRTDepositPool ProxyAdmin proxyAdmin = new ProxyAdmin(); LRTDepositPool contractImpl = new LRTDepositPool(); TransparentUpgradeableProxy contractProxy = new TransparentUpgradeableProxy( address(contractImpl), address(proxyAdmin), "" ); lrtDepositPool = LRTDepositPool(address(contractProxy)); // initialize RSETH. LRTCOnfig is already initialized in RSETHTest rseth.initialize(address(admin), address(lrtConfig)); vm.startPrank(admin); // add rsETH to LRT config lrtConfig.setRSETH(address(rseth)); LRTOracle lrtOracleImpl = new LRTOracle(); MockPriceOracle priceOracle = new MockPriceOracle(); TransparentUpgradeableProxy lrtOracleProxy = new TransparentUpgradeableProxy( address(lrtOracleImpl), address(proxyAdmin), "" ); // add minter role for rseth to lrtDepositPool rseth.grantRole(rseth.MINTER_ROLE(), address(lrtDepositPool)); lrtOracle = LRTOracle(address(lrtOracleProxy)); lrtOracle.initialize(address(lrtConfig)); lrtConfig.grantRole(LRTConstants.MANAGER, admin); lrtConfig.setContract( LRTConstants.LRT_DEPOSIT_POOL, address(lrtDepositPool) ); lrtOracle.updatePriceOracleFor(address(rETH), address(priceOracle)); lrtOracle.updatePriceOracleFor(address(stETH), address(priceOracle)); lrtOracle.updatePriceOracleFor(address(cbETH), address(priceOracle)); //console.log("getAssetPrice:",lrtOracle.getAssetPrice(address(rETH))); // add oracle to LRT config lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(lrtOracle)); vm.stopPrank(); } } contract LRTDepositPoolDepositAsset is LRTDepositPoolTest { address public rETHAddress; function setUp() public override { super.setUp(); // initialize LRTDepositPool lrtDepositPool.initialize(address(lrtConfig)); rETHAddress = address(rETH); // add manager role within LRTConfig vm.startPrank(admin); lrtConfig.grantRole(LRTConstants.MANAGER, manager); vm.stopPrank(); } function test_DepositAsset1() external { vm.startPrank(alice); rETH.approve(address(lrtDepositPool), 1 ether); lrtDepositPool.depositAsset(rETHAddress, 1 ether); // alice balance of rsETH after deposit uint256 aliceBalanceAfter = rseth.balanceOf(address(alice)); vm.stopPrank(); console.log(aliceBalanceAfter); } function test_DepositAsset2() external { vm.startPrank(alice); rETH.approve(address(lrtDepositPool), 2 ether); lrtDepositPool.depositAsset(rETHAddress, 1); lrtDepositPool.depositAsset(rETHAddress, 1 ether); // alice balance of rsETH after deposit uint256 aliceBalanceAfter = rseth.balanceOf(address(alice)); vm.stopPrank(); console.log(aliceBalanceAfter); } }
vscode manual
function getRSETHPrice() external view returns (uint256 rsETHPrice) { ... - if (rsEthSupply == 0) { + if (rsEthSupply <= 1 ether) { return 1 ether; } ... }
Error
#0 - c4-pre-sort
2023-11-16T07:00:15Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T07:01:21Z
raymondfam marked the issue as duplicate of #42
#2 - c4-judge
2023-12-01T17:03:30Z
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
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTConfig.sol#L109 https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L84
LRTConfig#updateAssetStrategy will affect the price of RsETH, when the administrator calls updateAssetStrategy
the user may get more RsETH at a lower price.
The price of RsETH is affected by the total amount of deposited assets. When the total amount decreases, the price also decreases, and users can obtain more RsETH when they deposit.
The total amount of assets is calculated including the balance of assetStrategy
:
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; } } } 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)); uint256 ndcsCount = nodeDelegatorQueue.length; for (uint256 i; i < ndcsCount;) { assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]); @> assetStakedInEigenLayer += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset); unchecked { ++i; } } } function getAssetBalance(address asset) external view override returns (uint256) { address strategy = lrtConfig.assetStrategy(asset); @> return IStrategy(strategy).userUnderlyingView(address(this)); }
Therefore, when the administrator changes the assetStrategy
, if the balance of the new assetStrategy
is different from that of the old one, the total amount of assets will change.
If the new assetStrategy has less balance than the old one, the price of RsETH will go down.
Although the administrator can modify the balance of the new assetStrategy
to be consistent with the old one, but, if it is not in the same transaction, the user can use the frontrunning
to deposit assets before the administrator corrects the balance and obtain more RsETH at a lower price.
vscode manual
Check whether the balance changes before and after the update in LRTConfig#updateAssetStrategy
Other
#0 - c4-pre-sort
2023-11-16T07:37:33Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T07:37:45Z
raymondfam marked the issue as duplicate of #197
#2 - c4-judge
2023-12-01T17:24:56Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-12-08T17:25:53Z
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/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L56 https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTConfig.sol#L94
If the depositLimit exceeds the total amount of asset deposits, the deposit will always revert.
To deposit an asset in the pool, the depositAsset
function will check whether the depositAmount exceeds the maximum value.
function depositAsset( address asset, uint256 depositAmount ) external whenNotPaused nonReentrant onlySupportedAsset(asset) { .... if (depositAmount > getAssetCurrentLimit(asset)) { revert MaximumDepositLimitReached(); } .... } function getAssetCurrentLimit(address asset) public view override returns (uint256) { return lrtConfig.depositLimitByAsset(asset) - getTotalAssetDeposits(asset); }
The maximum value can be reset by the administrator, and if the reset value is smaller than getTotalAssetDeposits(asset),depositAsset will always fail to execute.
function updateAssetDepositLimit( address asset, uint256 depositLimit ) external onlyRole(LRTConstants.MANAGER) onlySupportedAsset(asset) { depositLimitByAsset[asset] = depositLimit; emit AssetDepositLimitUpdate(asset, depositLimit); }
vscode manual
When updateAssetDepositLimit, check whether the depositLimit will exceed the total amount
Other
#0 - c4-pre-sort
2023-11-16T07:12:34Z
raymondfam marked the issue as insufficient quality report
#1 - c4-pre-sort
2023-11-16T07:12:49Z
raymondfam marked the issue as duplicate of #69
#2 - c4-judge
2023-11-29T20:58:12Z
fatherGoose1 changed the severity to QA (Quality Assurance)
#3 - c4-judge
2023-11-29T21:02:15Z
fatherGoose1 marked the issue as grade-b