The Standard

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

Protocol currently supports real world assets but doesn't implement the pricing logic to support this

Proof of Concept

From the current list of supported assets, i.e "ETH, WBTC, ARB, LINK and PAXG". PAXG is a real world asset as it's just a tokenized version of Gold, now in multiple instances the price of supported asset are queried from Chainlink.

To showcase one instance, take a look at LiquidationPool.sol#L205-L241

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
(,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
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) {
//@audit -ve price could be returned
(,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
uint256 _portion = asset.amount * _positionStake / stakeTotal;
//@audit protocol here casts `assetPriceUsd` to a uint value
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd)
* _hundredPC / _collateralRate;
if (costInEuros > _position.EUROs) {
_portion = _portion * _position.EUROs / costInEuros;
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);
}

Issue is that since PAXG is a real world asset and unlike normal crypto tokens, the prices of real world assets could go negative as this has happened with other real world assets (oil) before, which is why Chainlink returns it's price in an int256 and not uint256 to support these types of tokens, now protocol directly casts this negative value to a positive uint256 and it would cause protocol pricing logic to be broken since this leads to an underflow and having a massive difference in price

Impact

This breaks the pricing logic as protocol currently expects price to always be positive which is inaccurate as protocol currently supports real world assets too.

Now whenever this happens, the execution reverts in this line uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd) * _hundredPC / _collateralRate; due to the fact that the now casted assetPriceUsd value would be very large and the accompanied multiplications would be more than type(uint256).max which would cause the revert.

The above claim can be proven by this minimalistic POC on Remix

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract TestCast {
function castNegativeIntToInt256(int256 negativeValue) public pure returns (uint256) {
//@audit Passing `-10` as `negativeValue` value here would work and produce a massively large value
return uint256(negativeValue);
}
}
contract CastAndMultiply {
////@audit However passing the same `-10` here would lead to a revert due to the overflow
function castAndMultiply(int256 negativeValue) public pure returns (uint256) {
uint256 castedValue = uint256(negativeValue);
// Multiplying by 2 to trigger overflow
return castedValue * 2;
}
}

Recommended Mitigation Steps

Do not unsafely attempt to cast the price returned from an int to a uint, that's to say clearly document this to users and let them know that in a case where the price of any RWA being supported goes below 0, protocol is going to assume the price is zero and operate at that.
Then apply this change to code instances, i.e in the case of distributeAssets()

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
(,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
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();
+ if (assetPriceUsd < 0) {
+ assetPriceUsd = 0
+ }
uint256 _portion = asset.amount * _positionStake / stakeTotal;
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd)
* _hundredPC / _collateralRate;
if (costInEuros > _position.EUROs) {
_portion = _portion * _position.EUROs / costInEuros;
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);
}
Updates

Lead Judging Commences

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

Chainlink-price

bauchibred Submitter
almost 2 years ago
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!