Summary
SmartVaultV3
is dependent on price calculator which uses chainlink as source for price feed. But do not check the validity of price, neither the uptime of arbitrum sequencer. Which could lead to affect users when chainlink feed is down or revert, Similarly Chainlink price will produce inaccurate value when Arbitrum sequencer is down.
Vulnerability Details
SmartVaultV3
following functions uses calculator to get important values
function euroCollateral() private view returns (uint256 euros) {
ITokenManager.Token[] memory acceptedTokens = getTokenManager().getAcceptedTokens();
for (uint256 i = 0; i < acceptedTokens.length; i++) {
ITokenManager.Token memory token = acceptedTokens[i];
@> euros += calculator.tokenToEurAvg(token, getAssetBalance(token.symbol, token.addr));
}
}
function getAssets() private view returns (Asset[] memory) {
ITokenManager.Token[] memory acceptedTokens = getTokenManager().getAcceptedTokens();
Asset[] memory assets = new Asset[](acceptedTokens.length);
for (uint256 i = 0; i < acceptedTokens.length; i++) {
ITokenManager.Token memory token = acceptedTokens[i];
uint256 assetBalance = getAssetBalance(token.symbol, token.addr);
@> assets[i] = Asset(token, assetBalance, calculator.tokenToEurAvg(token, assetBalance));
}
return assets;
}
function euroCollateral() private view returns (uint256 euros) {
ITokenManager.Token[] memory acceptedTokens = getTokenManager().getAcceptedTokens();
for (uint256 i = 0; i < acceptedTokens.length; i++) {
ITokenManager.Token memory token = acceptedTokens[i];
@> euros += calculator.tokenToEurAvg(token, getAssetBalance(token.symbol, token.addr));
}
}
The highlighted code uses tokenToEurAvg
function which is in the Price Calculator
contract. Here is snippet of the function from PriceCalculator
.
function tokenToEurAvg(ITokenManager.Token memory _token, uint256 _tokenValue) external view returns (uint256) {
Chainlink.AggregatorV3Interface tokenUsdClFeed = Chainlink.AggregatorV3Interface(_token.clAddr);
uint256 scaledCollateral = _tokenValue * 10 ** getTokenScaleDiff(_token.symbol, _token.addr);
@> uint256 collateralUsd = scaledCollateral * avgPrice(4, tokenUsdClFeed);
@> (, int256 eurUsdPrice,,,) = clEurUsd.latestRoundData();
return collateralUsd / uint256(eurUsdPrice);
}
it checks for eurUsdPrice
in the function without checking validity of data. Moreover it uses avgPrice
function to get token value in usd. The function is given below.
function avgPrice(uint8 _hours, Chainlink.AggregatorV3Interface _priceFeed) private view returns (uint256) {
uint256 startPeriod = block.timestamp - _hours * 1 hours;
uint256 roundTS;
uint80 roundId;
int256 answer;
(roundId, answer,, roundTS,) = _priceFeed.latestRoundData();
@> uint256 accummulatedRoundPrices = uint256(answer);
uint256 roundCount = 1;
while (roundTS > startPeriod && roundId > 1) {
roundId--;
try _priceFeed.getRoundData(roundId) {
(, answer,, roundTS,) = _priceFeed.getRoundData(roundId);
accummulatedRoundPrices += uint256(answer);
roundCount++;
} catch {
continue;
}
}
@> return accummulatedRoundPrices / roundCount;
}
When the sequencer is inactive, above code will produce wrong values. results in returning wrong avg of the price of the token. Users during this outage could get liquidated during sharp volatility in any accept tokens. / or can mint more euro than collateral if price were return inflated.
This is valid for all L2 which has chainlink price feeds, as protocol want to deploy on other evm chains in future as well.
Impact
User will keep operating on stale or wrong price during the outage.
Tools Used
Manual Review
Recommendations
Add a check for price feed update time. Also check sequencer status, if it's offline then revert the tx.
+ function isSequencerActive() internal view returns (bool) {
+ (, int256 answer, uint256 startedAt,,) = sequencer.latestRoundData();
+ if (block.timestamp - startedAt <= GRACE_PERIOD_TIME || answer == 1)
+ return false;
+ return true;
+ }
function tokenToEurAvg(ITokenManager.Token memory _token, uint256 _tokenValue) external view returns (uint256) {
+ require (isSequencerActive(), "sequencer is down");
Chainlink.AggregatorV3Interface tokenUsdClFeed = Chainlink.AggregatorV3Interface(_token.clAddr);
uint256 scaledCollateral = _tokenValue * 10 ** getTokenScaleDiff(_token.symbol, _token.addr);
uint256 collateralUsd = scaledCollateral * avgPrice(4, tokenUsdClFeed);
- (, int256 eurUsdPrice,,,) = clEurUsd.latestRoundData();
+ ( roundId, eurUsdPrice, , updateTime, answeredInRound ) = priceFeed.latestRoundData();
+ require(rawPrice > 0, "Chainlink price <= 0");
+ require(updateTime != 0, "Incomplete round");
+ require(answeredInRound >= roundId, "Stale price");
return collateralUsd / uint256(eurUsdPrice);
}
function avgPrice(uint8 _hours, Chainlink.AggregatorV3Interface _priceFeed) private view returns (uint256) {
uint256 startPeriod = block.timestamp - _hours * 1 hours;
uint256 roundTS;
uint80 roundId;
int256 answer;
(roundId, answer,, roundTS,) = _priceFeed.latestRoundData();
+ require(block.timestamp - roundTs <= 1 hours, "stale price");
uint256 accummulatedRoundPrices = uint256(answer);
uint256 roundCount = 1;
while (roundTS > startPeriod && roundId > 1) {
roundId--;
try _priceFeed.getRoundData(roundId) {
(, answer,, roundTS,) = _priceFeed.getRoundData(roundId);
accummulatedRoundPrices += uint256(answer);
roundCount++;
} catch {
continue;
}
}
return accummulatedRoundPrices / roundCount;
}