The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: medium
Invalid

`SmartVaultV3` is dependent on `PriceCalculator` which lacks of chainlink price validity and L2 Sequencer uptime status

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(); /// @audit lacks of price staleness
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); // @audit no check for price staleness
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; //@audit if price is stale, say 3 hours old, it will return the same
}

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

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

Arbitrum-sequncer

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Known issue
Assigned finding tags:

Arbitrum-sequncer

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.