Lucene search

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

AfEth deposits could use price data from an invalid Chainlink response

2023-09-2700:00:00
Code4rena
github.com
2
votiumstrategy
chainlink
price calculation
afeth deposits
invalid response

6.9 Medium

AI Score

Confidence

High

Lines of code

Vulnerability details

Summary

The current price implementation for the VotiumStrategy token uses a potentially invalid Chainlink response. This price is then used to calculate the price of AfEth and, subsequently, the amount of tokens to mint while depositing.

Impact

The price of VotiumStrategy tokens are determined by taking the amount of deposited CVX in the strategy, and multiplied by the current price of CVX in terms of ETH. This price is fetched using Chainlink in the ethPerCvx() function:

<https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategyCore.sol#L156-L186&gt;

156:     function ethPerCvx(bool _validate) public view returns (uint256) {
157:         ChainlinkResponse memory cl;
158:         try chainlinkCvxEthFeed.latestRoundData() returns (
159:             uint80 roundId,
160:             int256 answer,
161:             uint256 /* startedAt */,
162:             uint256 updatedAt,
163:             uint80 /* answeredInRound */
164:         ) {
165:             cl.success = true;
166:             cl.roundId = roundId;
167:             cl.answer = answer;
168:             cl.updatedAt = updatedAt;
169:         } catch {
170:             cl.success = false;
171:         }
172:         // verify chainlink response
173:         if (
174:             (!_validate ||
175:                 (cl.success == true &&
176:                     cl.roundId != 0 &&
177:                     cl.answer &gt;= 0 &&
178:                     cl.updatedAt != 0 &&
179:                     cl.updatedAt &lt;= block.timestamp &&
180:                     block.timestamp - cl.updatedAt &lt;= 25 hours))
181:         ) {
182:             return uint256(cl.answer);
183:         } else {
184:             revert ChainlinkFailed();
185:         }
186:     }

As we can see from the previous snippet of code, if the _validate flag is off, then no validation is done, it can even return an uninitialized response from a failed call given the usage of the try/catch structure. This means that it can invalid price, stale price, or even zero when the call fails.

The VotiumStrategy price() function calls ethPerCvx(false), which means it carries forward any invalid CVX/ETH price.

<https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/strategies/votium/VotiumStrategy.sol#L31-L33&gt;

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

The price of VotiumStrategy is then used in the AfEth contract to calculate its price and determine the amount of tokens to mint in deposit()

<https://github.com/code-423n4/2023-09-asymmetry/blob/main/contracts/AfEth.sol#L133-L169&gt;

133:     function price() public view returns (uint256) {
134:         if (totalSupply() == 0) return 1e18;
135:         AbstractStrategy vEthStrategy = AbstractStrategy(vEthAddress);
136:         uint256 safEthValueInEth = (ISafEth(SAF_ETH_ADDRESS).approxPrice(true) *
137:             safEthBalanceMinusPending()) / 1e18;
138:         uint256 vEthValueInEth = (vEthStrategy.price() *
139:             vEthStrategy.balanceOf(address(this))) / 1e18;
140:         return ((vEthValueInEth + safEthValueInEth) * 1e18) / totalSupply();
141:     }

148:     function deposit(uint256 _minout) external payable virtual {
149:         if (pauseDeposit) revert Paused();
150:         uint256 amount = msg.value;
151:         uint256 priceBeforeDeposit = price();
152:         uint256 totalValue;
153: 
154:         AbstractStrategy vStrategy = AbstractStrategy(vEthAddress);
155: 
156:         uint256 sValue = (amount * ratio) / 1e18;
157:         uint256 sMinted = sValue &gt; 0
158:             ? ISafEth(SAF_ETH_ADDRESS).stake{value: sValue}(0)
159:             : 0;
160:         uint256 vValue = (amount * (1e18 - ratio)) / 1e18;
161:         uint256 vMinted = vValue &gt; 0 ? vStrategy.deposit{value: vValue}() : 0;
162:         totalValue +=
163:             (sMinted * ISafEth(SAF_ETH_ADDRESS).approxPrice(true)) +
164:             (vMinted * vStrategy.price());
165:         if (totalValue == 0) revert FailedToDeposit();
166:         uint256 amountToMint = totalValue / priceBeforeDeposit;
167:         if (amountToMint &lt; _minout) revert BelowMinOut();
168:         _mint(msg.sender, amountToMint);
169:     }

The VotiumStrategy price is first used in line 138 to calculate its TVL (vEthValueInEth). Any invalid price here will also mean an invalid price for AfEth.

Then both the AfEth price (line 151) and again the VotiumStrategy price (line 164) are used in deposit() to calculate the number of minted tokens. Depending on the direction of the wrong price, this means that the user will be minted more or less tokens than it should.

Proof of Concept

Let’s suppose the Chainlink feed is stale and the current price of CVX/ETH has increased since then.

  1. A user calls deposit() to create a new position in AfEth.
  2. The function calculates the current price (priceBeforeDeposit) in order to know how many tokens should be minted.
  3. The price() implementation will calculate the Votium strategy TVL using ethPerCvx(false), which will successfully return the stale price.
  4. The price of AfEth will then be calculated using the old data, which will result in a lower value than the actual “real” price.
  5. The user is minted tokens based on the incorrectly calculated priceBeforeDeposit, since this price is lower than the expected “real” price the user will be minted more tokens than expected.

Recommendation

Change the ethPerCvx() argument to true to make sure prices coming from Chainlink are correctly validated.

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

Assessed type

Oracle


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

All reactions

6.9 Medium

AI Score

Confidence

High