The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: low
Valid

costInEuros incorrectly scaled resulting in wrong distribution of assets

Summary

The LiquidationPool::distributeAssets function is designed to distribute assets to users who have staked in the liquidation pool. However, there is a vulnerability in the function related to the scaling of costInEuros .

Chainlink Pricefeeds:
WBTC
ARB
LINK
PAXG
EUR/USD

According to the test case and white paper:
_assets is the assets to be distributed.
_hundredPC is a constant to represent 100%, 1e5.
_collateralRateis the required collateral % in the smart vault. currently 110%, 1.1e5.

Vulnerability Details

priceEurUsd did not take into account if the decimals of asset tokens are not in 8 decimals place. It is assumed that all tokens were 8 decimals in place.
To check for decimals precision this is my PoC.
The following are the scenario exploits when LiquidationPool::distributeAssets is called:

First Scenario (Tokens that are in 18 Decimal Places)

// 1e18* 1e18 / 1e18
uint256 _portion = asset.amount * _positionStake / stakeTotal;
// 1e18 **1 * 1e18 / 1e8 * 1e5 / 1.1e5
// 1e18 * 1e18 / 1e8 * 1e5 / 1.1e5
// 1e36/ 1e13 / 1.1e5
// 9.090909090909090909090909090e27
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd) * _hundredPC / _collateralRate;
// 1e18* 1e18 / 9.090909090909090909090909090e27
// 1.10000000e8
_portion = _portion * _position.EUROs / costInEuros;
costInEuros = _position.EUROs;

Second Scenario (WBTC, 8 Decimal Places)

// 1e8* 1e18 / 1e18
uint256 _portion = asset.amount * _positionStake / stakeTotal;
// 1e8 * (1e18-8) * 1e8 / 1e8 * 1e5 / 1.1e5
// 1e8 * 1e10 * 1e8 / 1e8 * 1e5 / 1.1e5
// 1e18 * 1e5 / 1.1e5
// 9.09090909090909090e17
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd) * _hundredPC / _collateralRate;
// 1e18 * 1e18 / 9.09090909090909090e17
// 1.100000000000000001e18
// Portion should be in 1e8 in asset decimals.
// portion owned * euros owned, and then divide by cost in euros which was already converted before.
_portion = _portion * _position.EUROs / costInEuros;
costInEuros = _position.EUROs;

Full Code Snippet:

//@note Taking asset in USD and convert them to Euros then deduct balance
function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
consolidatePendingStakes();
// https://arbiscan.io/address/0xa14d53bc1f1c0f31b4aa3bd109344e5009051a84#readContract
// 8 decimals
// #audit unsafe conversion of int256 to uint
(,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];
//@note struct Asset { ITokenManager.Token token; uint256 amount; }
//@note struct Token { bytes32 symbol; address addr; uint8 dec; address clAddr; uint8 clDec; }
if (asset. Amount > 0) {
(,int256 assetPriceUsd,,,) = Chainlink.AggregatorV3Interface(asset.token.clAddr).latestRoundData();
// First Scenario (Other 18 Dec)
// 1e18* 1e18 / 1e18
// Second Scenario (WBTC)
// WBTC is 8 decimals, did not scale to 18
// 1e8* 1e18 / 1e18
uint256 _portion = asset.amount * _positionStake / stakeTotal;
// @audit priceEurUsd Scaled wrongly
// First Scenario (Other 18 Dec)
// 1e18 **1 * 1e18 / 1e8 * 1e5 / 1.1e5
// 1e18 * 1e18 / 1e8 * 1e5 / 1.1e5
// 1e36/ 1e13 / 1.1e5
// 9.090909090909090909090909090e27
// Second Scenario (WBTC)
// 1e8 * (1e18-8) * 1e8 / 1e8 * 1e5 / 1.1e5
// 1e8 * 1e10 * 1e8 / 1e8 * 1e5 / 1.1e5
// 1e18 * 1e5 / 1.1e5
// 9.09090909090909090e17
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd)
* _hundredPC / _collateralRate;
if (costInEuros > _position.EUROs) {
// First Scenario(Other 18 Dec)
// 1e18* 1e18 / 9.090909090909090909090909090e27
// 1.10000000e8
// Second Scenario(WBTC)
// 1e18 * 1e18 / 9.09090909090909090e17
// 1.100000000000000001e18
// Portion should be in 1e8 in asset decimals.
// portion owned * euros owned, and then divide by cost in euros which was already converted before.
_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);
}
}
}
}
// update position for passing if statement
positions[holders[j]] = _position;
}
if (burnEuros > 0) IEUROs(EUROs).burn(address(this), burnEuros);
returnUnpurchasedNative(_assets, nativePurchased);
}

Impact

If asset tokens are in 18 decimals place, the user gets a highly deflated reward portion, while if asset token is 8 decimals, the user gets a highly inflated reward portion.

Tools Used

Manual Review

Recommendations

Ensure that the priceEurUsd scales correctly for different asset decimals.

Updates

Lead Judging Commences

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

precision

thedoctor Submitter
almost 2 years ago

Support

FAQs

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

Give us feedback!