Kelp DAO | rsETH - tallo'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: 51/185

Findings: 2

Award: $78.78

QA:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Awards

76.0163 USDC - $76.02

Labels

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

External Links

Lines of code

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/NodeDelegator.sol#L121 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L47 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTDepositPool.sol#L71 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTConfig.sol#L109

Vulnerability details

Impact

A strategy change will result in an incorrect price evaluation for RSETH due to it undervaluing the amount of underlying token deposited.

Vulnerability Explanation

  1. When LRTOracle#getRSETHPrice is called it gets the price of RSETH based on the calculated total amount of ETH based on total balance of underlying assets (stETH, rETH, cbETH) multiplied by the exchange rate to ETH
    function getRSETHPrice() external view returns (uint256 rsETHPrice) {
        address rsETHTokenAddress = lrtConfig.rsETH();
        uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply();

        //...

        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;
    }
  1. To get the total deposits, LRTDepositPool#getTotalAssetDeposits is called which sums up the amount of assets in the 3 possible locations returned by LRTDepositPool#getAssetDistributionData
    function getTotalAssetDeposits(address asset) public view override returns (uint256 totalAssetDeposit) {
        (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer) =
            getAssetDistributionData(asset);
        return (assetLyingInDepositPool + assetLyingInNDCs + assetStakedInEigenLayer);
    }
  1. LRTDepositPool#getAssetDistributionData sums up the total assets from 3 possible locations: i. In the LRTDepositPool ii. Inside the node delegator queue contracts iii. Inside the asset eigenlayer strategy
    function getAssetDistributionData(address asset)
        public
        view
        override
        onlySupportedAsset(asset)
        returns (uint256 assetLyingInDepositPool, uint256 assetLyingInNDCs, uint256 assetStakedInEigenLayer)
    {
1@>        assetLyingInDepositPool = IERC20(asset).balanceOf(address(this));
        uint256 ndcsCount = nodeDelegatorQueue.length;
        for (uint256 i; i < ndcsCount;) {
2@>            assetLyingInNDCs += IERC20(asset).balanceOf(nodeDelegatorQueue[i]);
3@>            assetStakedInEigenLayer += INodeDelegator(nodeDelegatorQueue[i]).getAssetBalance(asset);
            unchecked {
                ++i;
            }
        }
    }
  1. The amount of assets stored inside the eigenlayer strategy is calculated from NodeDelegator#getAssetBalance
    function getAssetBalance(address asset) external view override returns (uint256) {
        address strategy = lrtConfig.assetStrategy(asset);
        return IStrategy(strategy).userUnderlyingView(address(this));
    }
  1. If the CRTConfig admin were to call CRTConfig#updateAssetStrategy to change the strategy, it would result in the funds inside the previous strategy not being included in the accumulated funds; resulting in an incorrect price evaluation for rsETH.

Proof of Concept

  1. Assume rsETH's initial price is 1 ETH/rsETH with 100 rETH of underyling token deposit in the strategy. The following calculation are done based on the formulas inside LRTOracle#getRSETHPrice

The before calculations for the price of rsETH are as follows:

totalAssetAmt = (0+0+100) rETH = 100 rETH totalETHInPool = totalAssetAmt * assetER = 100 rETH * 1 ETH/rETH = 100 ETH rsETHPrice = totalETHInPool / rsEthSupply = 100 ETH/100 rsETH= 1 ETH/rsETH
  1. Strategy gets changed resulting in the following price calculation for rsETH:
totalAssetAmt = (0+0+0) rETH = 0 rETH totalETHInPool = totalAssetAmt * assetER = 0 rETH * 1 ETH/rETH = 0 ETH rsETHPrice = totalETHInPool / rsEthSupply = 0 ETH/100 rsETH = 0 ETH/rsETH
  1. The price of rsETH is now 0 (or ideally just above 0 so it doesnt revert) after the strategy migration, allowing the user to effectively mint rsETH for free

Tools Used

VIM

Add functionality inside NodeDelegator that properly migrates the funds from one eigenlayer strategy to another whenever LRTConfig#updateAssetStrategy is called. This function should migrate on all the relevant NodeDelegators that are affected by the changed strategy.

Assessed type

Math

#0 - c4-pre-sort

2023-11-16T23:33:51Z

raymondfam marked the issue as sufficient quality report

#1 - c4-pre-sort

2023-11-16T23:34:00Z

raymondfam marked the issue as duplicate of #197

#2 - c4-judge

2023-12-01T17:24:51Z

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:26:46Z

fatherGoose1 marked the issue as satisfactory

Awards

2.7592 USDC - $2.76

Labels

bug
downgraded by judge
grade-b
insufficient quality report
QA (Quality Assurance)
edited-by-warden
duplicate-36
Q-28

External Links

Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTConfig.sol#L80 https://github.com/code-423n4/2023-11-kelp/blob/main/src/LRTOracle.sol#L52

Vulnerability details

Impact

LRTConfig only has the ability to add supported assets and not remove them. This means that the Kelp protocol will be forever vulnerable if there is an issue with any of the underlying assets.

Proof of Concept

There are several main issues that can be problematic:

  1. Admin accidentally adds an unintended asset to the supported list
  2. Existing underlying has a security incident with an attacker or malicious admin
  3. Eigenlayer removes support for an asset

While the administrator can stop deposits by just setting the deposit limit of the compromised/unintended asset to 0, the assets price is still taken into account inside the LRTOracle#getRSETHPrice function to determine the price of RSETH.

     address[] memory supportedAssets = lrtConfig.getSupportedAssetList();
     uint256 supportedAssetCount = supportedAssets.length;
     for (uint16 asset_idx; asset_idx < supportedAssetCount;) {
            address asset = supportedAssets[asset_idx];

            //@audit here get the asset price
@>            uint256 assetER = getAssetPrice(asset);

            uint256 totalAssetAmt = ILRTDepositPool(lrtDepositPoolAddr).getTotalAssetDeposits(asset);
            totalETHInPool += totalAssetAmt * assetER;

            unchecked {
                ++asset_idx;
            }
        }

        return totalETHInPool / rsEthSupply;

This means that the price of RSETH will forever be reliant on the compromised/unintended asset which can greatly affect the integrity of the protocol should something go wrong.

Tools Used

VIM

Add the ability for the admin to remove support for an asset inside LRTConfig

Assessed type

Other

#0 - c4-pre-sort

2023-11-16T21:39:41Z

raymondfam marked the issue as insufficient quality report

#1 - c4-pre-sort

2023-11-16T21:39:49Z

raymondfam marked the issue as duplicate of #36

#2 - c4-judge

2023-11-29T21:35:52Z

fatherGoose1 changed the severity to QA (Quality Assurance)

#3 - c4-judge

2023-11-29T21:44:07Z

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