The Standard

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

Lack of validation to check whether or not the price-retrieved via the Chainlink's AggregatorV3Interface#`latestRoundData()` would be a `stale` price

Summary

Within the LiquidationPool#distributeAssets(), there is no validation to check whether or not the price-retrieved via the Chainlink's AggregatorV3Interface#latestRoundData(), which lead to retrieving a stale price.

Vulnerability Details

When a liquidator would liquidate a SmartVault, the liquidator would call the LiquidationPoolManager#runLiquidation().

Within the LiquidationPoolManager#runLiquidation(), the LiquidationPool#distributeAssets() would be called.
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/LiquidationPoolManager.sol#L80

Within the LiquidationPool#distributeAssets(), the current $EUROs price in $USD (priceEurUsd) and the current asset price in $USD would be retrieved via the Chainlink's AggregatorV3Interface#latestRoundData() like this:
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/LiquidationPool.sol#L207
https://github.com/Cyfrin/2023-12-the-standard/blob/main/contracts/LiquidationPool.sol#L218

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
(,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData(); /// @audit - $EURO price via the Chainlink Oracle
uint256 stakeTotal = getStakeTotal();
uint256 burnEuros;
uint256 nativePurchased;
for (uint256 j = 0; j < holders.length; j++) {
Position memory _position = positions[holders[j]];
uint256 _positionStake = stake(_position);
if (_positionStake > 0) {
for (uint256 i = 0; i < _assets.length; i++) {
ILiquidationPoolManager.Asset memory asset = _assets[i];
if (asset.amount > 0) {
(,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData(); ///<--------- @audit
uint256 _portion = asset.amount * _positionStake / stakeTotal;
...

However, within the LiquidationPool#distributeAssets(), there is no validation to check whether or not the returned-price of both $EUROs and asset in $USD via the Chainlink's AggregatorV3Interface#latestRoundData() would be a stale price.

This lead to retrieving a stale price of both $EUROs and asset in $USD in the LiquidationPool#distributeAssets().

Impact

Within the LiquidationPool#distributeAssets(), there is no validation to check whether or not the price-retrieved via the Chainlink's AggregatorV3Interface#latestRoundData(), which lead to retrieving a stale price.

Tools Used

  • Manual review

Recommendations

Within the LiquidationPool#distributeAssets(), consider adding a validation to check whether or not the returned-price of both $EUROs and asset in $USD via the Chainlink's AggregatorV3Interface#latestRoundData() would be a stale price like this:

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
+ (uint80 roundID, int256 priceEurUsd, uint256 timestamp, uint256 updatedAt,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
- (,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
+ require(updatedAt >= roundID, "Stale price");
+ require(timestamp != 0,"Round not complete");
+ require(answer > 0,"Chainlink answer reporting 0");
+ require(updatedAt < block.timestamp - maxDelayTime, "Price is outdated");
...
for (uint256 j = 0; j < holders.length; j++) {
Position memory _position = positions[holders[j]];
uint256 _positionStake = stake(_position);
if (_positionStake > 0) {
for (uint256 i = 0; i < _assets.length; i++) {
ILiquidationPoolManager.Asset memory asset = _assets[i];
if (asset.amount > 0) {
+ (uint80 roundID, int256 assetPriceUsd, uint256 timestamp, uint256 updatedAt,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
- (,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
+ require(updatedAt >= roundID, "Stale price");
+ require(timestamp != 0,"Round not complete");
+ require(answer > 0,"Chainlink answer reporting 0");
+ require(updatedAt < block.timestamp - maxDelayTime, "Price is outdated");
...

(NOTE:Some acceptable time delay should be set to the maxDelayTime above in advance)

Updates

Lead Judging Commences

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

Chainlink-price

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

Chainlink-price

Support

FAQs

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