Lucene search

K
code423n4Code4renaCODE423N4:2023-09-ASYMMETRY-FINDINGS-ISSUES-28
HistorySep 27, 2023 - 12:00 a.m.

Users can deposit() even when Chainlink's price feed for CVX is stale

2023-09-2700:00:00
Code4rena
github.com
2
chainlink oracle
stale price feed
cvx/eth
solidity vulnerability
price calculation

7 High

AI Score

Confidence

High

Lines of code

Vulnerability details

Bug Description

In VotiumStrategy.sol, the price of vAfEth is determined by the price() function:

VotiumStrategy.sol#L31-L33

    function price() external view override returns (uint256) {
        return (cvxPerVotium() * ethPerCvx(false)) / 1e18;
    }

As seen from above, it calls ethPerCVX() with false to determine the price of CVX in ETH. ethPerCVX() relies on a Chainlink oracle to fetch the CVX / ETH price:

VotiumStrategyCore.sol#L156-L183

    function ethPerCvx(bool _validate) public view returns (uint256) {
        ChainlinkResponse memory cl;
        try chainlinkCvxEthFeed.latestRoundData() returns (
            uint80 roundId,
            int256 answer,
            uint256 /* startedAt */,
            uint256 updatedAt,
            uint80 /* answeredInRound */
        ) {
            cl.success = true;
            cl.roundId = roundId;
            cl.answer = answer;
            cl.updatedAt = updatedAt;
        } catch {
            cl.success = false;
        }
        // verify chainlink response
        if (
            (!_validate ||
                (cl.success == true &&
                    cl.roundId != 0 &&
                    cl.answer >= 0 &&
                    cl.updatedAt != 0 &&
                    cl.updatedAt <= block.timestamp &&
                    block.timestamp - cl.updatedAt <= 25 hours))
        ) {
            return uint256(cl.answer);
        } else {

If ethPerCvx() is called with _validate = false, no validation will be performed on Chainlink’s price feed. This means that ethPerCVX() could return an invalid price if:

  1. The price feed is stale. This would cause ethPerCvx() to return an outdated price that might deviate far from the actual value of CVX.
  2. latestRoundData() reverts. Since latestRoundData() is wrapped in a try-catch, cl.answer will not be updated, thus ethPerCvx() would return 0.

Note that scenario 2 could occur if Chainlink’s multisigs decide to block access to the CVX / ETH price feed, as described here:

> While currently there’s no whitelisting mechanism to allow or disallow contracts from reading prices, powerful multisigs can tighten these access controls. In other words, the multisigs can immediately block access to price feeds at will. Therefore, to prevent denial of service scenarios, it is recommended to query ChainLink price feeds using a defensive approach with Solidity’s try/catch structure.

This becomes problematic as the price of vAfEth is used when calculating the price of AfEth in its price() function:

AfEth.sol#L138-L140

        uint256 vEthValueInEth = (vEthStrategy.price() *
            vEthStrategy.balanceOf(address(this))) / 1e18;
        return ((vEthValueInEth + safEthValueInEth) * 1e18) / totalSupply();

Where:

  • safEthValueInEth is the amount of safETH locked in ETH.
  • vEthValueInEth is the amount of vAfEth locked in ETH.

As seen from above, the price of AfETH is determined as the sum of safETH and vAfEth value divided by totalSupply(), which uses vAfEth’s price (vEthStrategy.price()) in its calculation.

AfEth’s price is used to determine how much AfEth is minted to the user when deposit() is called:

AfEth.sol#L162-L166

        totalValue +=
            (sMinted * ISafEth(SAF_ETH_ADDRESS).approxPrice(true)) +
            (vMinted * vStrategy.price());
        if (totalValue == 0) revert FailedToDeposit();
        uint256 amountToMint = totalValue / priceBeforeDeposit;

Where:

  • priceBeforeDeposit is AfEth’s price, determined by calling its price() function.

However, since no validation is performed for Chainlink’s price feed when vStrategy.price() is called, users can still call deposit() even when ethPerCvx() returns an incorrect price.

Should this occur, depositors will receive less AfEth for their deposit than expected, since both vStrategy.price() and priceBeforeDeposit will be smaller than usual. In extreme scenarios, if ethPerCvx() returns 0, vMinted * vStrategy.price() will also be 0, therefore the depositor will only receive AfEth for the amount used to buy safETH.

Impact

Since Chainlink’s CVX / ETH price feed is not validated when users call deposit(), users that call deposit() while the price feed is stale/blocked will receive less afEth than expected, resulting in a loss of funds.

Recommended Mitigation

In the price() function of VotiumStrategy.sol, consider adding a parameter that determines if the response from Chainlink’s price feed is validated:

VotiumStrategy.sol#L31-L33

-   function price() external view override returns (uint256) {
-       return (cvxPerVotium() * ethPerCvx(false)) / 1e18;
+   function price(bool _validate) external view override returns (uint256) {
+       return (cvxPerVotium() * ethPerCvx(_validate)) / 1e18;
    }

The price() function in AfEth.sol should then use the validated price of vAfEth in its calculations, and revert if it the price is incorrect:

AfEth.sol#L138-L140

-       uint256 vEthValueInEth = (vEthStrategy.price() *
+       uint256 vEthValueInEth = (vEthStrategy.price(true) *
            vEthStrategy.balanceOf(address(this))) / 1e18;
        return ((vEthValueInEth + safEthValueInEth) * 1e18) / totalSupply();

Assessed type

Oracle


The text was updated successfully, but these errors were encountered:

All reactions

7 High

AI Score

Confidence

High