The Standard

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

`LiquidationPool::distributeAssets` does not check for stale data before trusting Chainlink's response

Summary

When LiquidationPool.sol::distributeAssets fetches the eurUsd price from Chainlink, it misses crucial checks that is recommended by Chainlink to ensure the returned data is fresh and accurate.

Vulnerability Details

According to Chainlink's documentation: https://docs.chain.link/data-feeds/price-feeds/historical-data, the following check is necessary to ensure that the returned price data is fresh and accurate.

The current distributeAssets() function does not check the timestamp the data was last updated, neither does it not implement a check on the round of the returned data: latestRoundData

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
@> (,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData(); @audit-issue no stale price check
...
if (asset.amount > 0) {
@> (,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData(); // @audit-issue no stale price check
...
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd) * _hundredPC / _collateralRate;
if (costInEuros > _position.EUROs) {
_portion = _portion * _position.EUROs / costInEuros; // usage
costInEuros = _position.EUROs;
}
_position.EUROs -= costInEuros;
rewards[abi.encodePacked(_position.holder, asset.token.symbol)] += _portion;
burnEuros += costInEuros;
if (asset.token.addr == address(0)) {
nativePurchased += _portion;
} else {
IERC20(asset.token.addr).safeTransferFrom(manager, address(this), _portion);
}
}
}
}
positions[holders[j]] = _position;
}
if (burnEuros > 0) IEUROs(EUROs).burn(address(this), burnEuros);
returnUnpurchasedNative(_assets, nativePurchased);
}

Impact

Chainlink may return stale data for both priceEurUsd & assetPriceUsd that becomes accepted by the protocol, either allowing _position.EUROs to deduct less EUROs or more than intended during distribution of assets.

Tools Used

Manual review

Recommendations

Let's add the following checks:

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
- (,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
+ (uint80 roundId, int256 priceEurUsd, ,uint256 updatedAt,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
+ if (updatedAt < block.timestamp - maxDelayTime) revert PRICE_OUTDATED();
...
if (asset.amount > 0) {
- (,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
+ (uint80 roundIdOne, int256 priceEurUsd, ,uint256 updatedAtOne,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
+ if (updatedAtOne < block.timestamp - maxDelayTime) revert PRICE_OUTDATED();
...
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd) * _hundredPC / _collateralRate;
if (costInEuros > _position.EUROs) {
_portion = _portion * _position.EUROs / costInEuros; // usage
costInEuros = _position.EUROs;
}
_position.EUROs -= costInEuros;
rewards[abi.encodePacked(_position.holder, asset.token.symbol)] += _portion;
burnEuros += costInEuros;
if (asset.token.addr == address(0)) {
nativePurchased += _portion;
} else {
IERC20(asset.token.addr).safeTransferFrom(manager, address(this), _portion);
}
}
}
}
positions[holders[j]] = _position;
}
if (burnEuros > 0) IEUROs(EUROs).burn(address(this), burnEuros);
returnUnpurchasedNative(_assets, nativePurchased);
}
  • You can also check that the roundId is after the LatestRoundId which shows that it uses the latest update of data aggregation by the node.

Updates

Lead Judging Commences

hrishibhat Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

Chainlink-price

hrishibhat Lead Judge almost 2 years 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.

Give us feedback!