Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LST with different decimals for Chainlink Price Feeds will mint less Amount of RSETH to users. #479

Open
c4-submissions opened this issue Nov 15, 2023 · 22 comments
Labels
bug Something isn't working disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) downgraded by judge Judge downgraded the risk level of this issue edited-by-warden grade-b high quality report This report is of especially high quality primary issue Highest quality submission among a set of duplicates Q-34 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax satisfactory satisfies C4 submission criteria; eligible for awards sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")

Comments

@c4-submissions
Copy link
Contributor

c4-submissions commented Nov 15, 2023

Lines of code

https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTDepositPool.sol#L109
https://github.com/code-423n4/2023-11-kelp/blob/f751d7594051c0766c7ecd1e68daeb0661e43ee3/src/LRTOracle.sol#L71

Vulnerability details

Chainlink Price Feeds does not maintain a consistent pattern for the number of decimals in returned prices. While some feeds provide token prices with 8 decimals, others may have 18 decimals or more. However, the contracts assume that prices will always be provided with 18 decimals. This could lead to discrepancies in the amount of tokens minted to users, potentially resulting in losses for them. While the price feeds for the initially supported token rETH, cbETH and stETH does have 18 decimals price feeds, but there are chances that in future new tokens are added with different decimals price feeds.

Impact

Some Chainlink price feeds return prices in different decimal places, such as 8 or 18, which may not align with the token's decimal representation. For example, USDC follows 6 decimal places, but the price feeds for USDC/USD return prices in 8 decimal places. You can check the information using the following links:

So there are good chances that the price feed for the newly added LST follows different decimal representation than the decimal representation of the LST itself.

This inconsistency can result in the amount of tokens minted to the user being less than the expected amount. The incorrect result is calculated by the following functions:

File: 2023-11-kelp/src/LRTDepositPool.sol

    function getRsETHAmountToMint(
        address asset,
        uint256 amount
    )
        public
        view
        override
        returns (uint256 rsethAmountToMint)
    {
        // setup oracle contract
        address lrtOracleAddress = lrtConfig.getContract(LRTConstants.LRT_ORACLE);
        ILRTOracle lrtOracle = ILRTOracle(lrtOracleAddress);

        // calculate rseth amount to mint based on asset amount and asset exchange rate
@>        rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset)) / lrtOracle.getRSETHPrice();
    }

Github: 109

File: 2023-11-kelp/src/LRTOracle.sol

 function getRSETHPrice() external view returns (uint256 rsETHPrice) {
        address rsETHTokenAddress = lrtConfig.rsETH();
        uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply();

        if (rsEthSupply == 0) {
            return 1 ether;
        }

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

Github: 71

Proof of Concept

Here is a test for PoC

    function test_ProtocolWillNotWorkWithChainlinkPriceFeedsWithDifferentDecimalValues() public {
        // adding new LSToken
        addNewLST();

        uint256 amount = 500 ether;
        // updating decimals of the mock price aggregator.
        // this is not possible to do in original price aggregator. It's just a mock funciton to
        // simulate the condition. This stil represents 1 ETH : 1 LST
        mockPriceAggregator.updateDecimals(1e8);

        // checking if the decimals are updated
        assertEq(mockPriceAggregator.decimals(), 1e8, "decimals are not same");

        // getting the RSETH amount to be minted for `amount` of tokens
        // the actual amount minted will be less than `rsEthMintedForAmount` because there is
        // another bug in the code that will make the amount less than `rsEthMintedForAmount`
        uint256 rsEthMintedForAmount = lrtDepositPoolP.getRsETHAmountToMint(address(mLst), amount);

        console2.log("> Details for the Amount to be Mint for LST deposit");
        console2.log("\tAmount To Deposit: %s", amount);
        console2.log("\tAmount of RSETH Tokens to Recieve: %s\n", rsEthMintedForAmount);
    }

Link to the Original Test: Test

Outupt:

Running 1 test for test/Integration.t.sol:BaseIntegrationTest
[PASS] test_ProtocolWillNotWorkWithChainlinkPriceFeedsWithDifferentDecimalValues() (gas: 425074)
Logs:
  > Details for the Amount to be Mint for LST deposit
        Amount To Deposit: 500000000000000000000
        Amount of RSETH Tokens to Recieve: 50000000000


Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 5.15ms

Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)

Note
The actual amount minted will be different as there is another vulnerability in the function

As you can see in the output, for 500e18 LST tokens we are only receiving 0.00000005 RSETH.

Tools Used

  • Manual Review
  • Foundry

Recommended Mitigation Steps

To address the issue of inconsistent decimals in the RSETH minting calculation, it is recommended to do the following changes in the contracts:

  1. Add a mapping in the contracts to represent the price feeds decimals corresponding to each asset.
  2. Modify the LRTDepositPool::getRsETHAmountToMint(...) and LRTOracle::getRSETHPrice(...) functions to use the price feed decimals for the asset when calculating the amount of RSETH to be minted.

Here's an example of how the changes could be implemented:

File: 2023-11-kelp/src/LRTDepositPool.sol

+    mapping(address asset => uint256 priceFeedDecimals) public priceFeedDecimals

    function getRsETHAmountToMint(
        address asset,
        uint256 amount
    )
        public
        view
        override
        returns (uint256 rsethAmountToMint)
    {
        // setup oracle contract
        address lrtOracleAddress = lrtConfig.getContract(LRTConstants.LRT_ORACLE);
        ILRTOracle lrtOracle = ILRTOracle(lrtOracleAddress);

        // calculate rseth amount to mint based on asset amount and asset exchange rate
+       rsethAmountToMint = (amount * lrtOracle.getAssetPrice(asset) * (18 - priceFeedDecimals(asset))) / lrtOracle.getRSETHPrice();
    }
File: 2023-11-kelp/src/LRTOracle.sol
    
    function getRSETHPrice() external view returns (uint256 rsETHPrice) {
        address rsETHTokenAddress = lrtConfig.rsETH();
        uint256 rsEthSupply = IRSETH(rsETHTokenAddress).totalSupply();

        if (rsEthSupply == 0) {
            return 1 ether;
        }

        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;
+            totalETHInPool += totalAssetAmt * assetER * (18 - ILRTDepositPool(lrtDepositPoolAddr).priceFeedDecimals(asset));

            unchecked {
                ++asset_idx;
            }
        }
        // @audit can we manipulate the price and get more rsEth? or can we manipulate the price and make the rsEth more
        // expensive?
        return totalETHInPool / rsEthSupply;
    }

By adding the priceFeedDecimals mapping and modifying the calculation, we ensure that the RSETH amount is calculated correctly based on the decimals provided by the price feeds. This mitigates the potential issue of inconsistent decimals and ensures that users receive the correct amount of RSETH for their deposits.

The changes also needs to be done in other functions as well that are using this data.

Assessed type

Decimal

@c4-submissions c4-submissions added 3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working labels Nov 15, 2023
c4-submissions added a commit that referenced this issue Nov 15, 2023
@c4-pre-sort
Copy link

raymondfam marked the issue as sufficient quality report

@c4-pre-sort c4-pre-sort added the sufficient quality report This report is of sufficient quality label Nov 16, 2023
@c4-pre-sort
Copy link

raymondfam marked the issue as duplicate of #97

@c4-pre-sort
Copy link

raymondfam marked the issue as high quality report

@c4-pre-sort c4-pre-sort added high quality report This report is of especially high quality and removed sufficient quality report This report is of sufficient quality labels Nov 17, 2023
@c4-pre-sort
Copy link

raymondfam marked the issue as not a duplicate

@c4-pre-sort
Copy link

raymondfam marked the issue as primary issue

@fatherGoose1
Copy link

Confirming as Medium. This issue highlights the potential for miscalculation for LSTs added in the future.

@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Dec 1, 2023
@c4-judge
Copy link
Contributor

c4-judge commented Dec 1, 2023

fatherGoose1 marked the issue as satisfactory

@c4-judge
Copy link
Contributor

c4-judge commented Dec 1, 2023

fatherGoose1 marked the issue as selected for report

@thangtranth
Copy link

thangtranth commented Dec 2, 2023

@fatherGoose1 :
The price feeds with ETH as quote tokens (X/ETH) always have 18 decimals. It would be valid if the warden can show a not-yet-supported LST/ETH priceFeed with with a decimal count differing from 18.

image

The example in the issue is incorrect (USDC/USD pricefeed). The issue is purely speculative and has not yet been observed in practice, so it is appropriate to classify as QA.

@TradMod
Copy link
Member

TradMod commented Dec 2, 2023

I agree @thangtranth. Actually, I think this is not even a QA finding. USDC example is completely out of the box. If it will be added in the future then it should be in-scope of the future audits. This is clearly OOS. The fix will only gonna increase code complexity unnecessarily.

@AtanasDimulski
Copy link

AtanasDimulski commented Dec 2, 2023

I completely agree with @thangtranth and @DevABDee, I would also like to add that tokens that can interact with the Kelp DAO, are first whitelisted by the team. First, there is no guarantee that new tokens will be added at all, secondly, the possibility of Chainlink deciding to add a price feed for new LST/ETH with different decimals is highly unlikely. Most importantly issues of possible future upgrades have never been accepted, otherwise saying that the protocol may add a function that transfers all funds to my address should also be considered a valid finding. In my opinion, this is NC, Low at best.

@manoj9april
Copy link

Yes I agree with other wardens. We can assign it a low severity.

@imsrybr0 imsrybr0 mentioned this issue Dec 3, 2023
@PlamenTSV
Copy link

This is one of the reasons I didn't submit this - I couldn't find an LST with different than 18 decimals and it would be highly unlikely that one would arise and gain enough popularity. So I agree this is QA

@radeveth
Copy link

radeveth commented Dec 4, 2023

#122 (comment)

@fatherGoose1
Copy link

Thank you @thangtranth. This will be downgraded to QA despite sponsor's agreement with medium severity. After looking at Chainlink's available price feeds, I confirm that 18 decimals are the standard and it would not make sense for them to support an LST price feed with other than 18 decimals.

@c4-judge c4-judge added QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax and removed 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value labels Dec 4, 2023
@c4-judge
Copy link
Contributor

c4-judge commented Dec 4, 2023

fatherGoose1 changed the severity to QA (Quality Assurance)

@c4-judge
Copy link
Contributor

c4-judge commented Dec 8, 2023

fatherGoose1 marked the issue as grade-b

@c4-judge
Copy link
Contributor

c4-judge commented Dec 8, 2023

fatherGoose1 marked the issue as not selected for report

@c4-judge c4-judge removed the selected for report This submission will be included/highlighted in the audit report label Dec 8, 2023
@C4-Staff C4-Staff added the Q-34 label Dec 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working disagree with severity Sponsor confirms validity, but disagrees with warden’s risk assessment (sponsor explain in comments) downgraded by judge Judge downgraded the risk level of this issue edited-by-warden grade-b high quality report This report is of especially high quality primary issue Highest quality submission among a set of duplicates Q-34 QA (Quality Assurance) Assets are not at risk. State handling, function incorrect as to spec, issues with clarity, syntax satisfactory satisfies C4 submission criteria; eligible for awards sponsor confirmed Sponsor agrees this is a problem and intends to fix it (OK to use w/ "disagree with severity")
Projects
None yet
Development

No branches or pull requests