The Standard

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

Attacker can drain the Liquidation pool by distributing assets

Summary

An attacker can distribute assets of the Liquidation pool to the holders due to the missing access control in LiquidationPool.distributeAssets.

Vulnerability Details

  • LiquidationPool.distributeAssets is missing access control. It is supposed be called only by its maanger via LiquidationPoolManager.runLiquidation action, where burning of EUROs and assets are distributed after someone vault's liquidation.

  • so currently LiquidationPool holds a lot of EUROs and TST, so all this balance of assets staked by users can be again distributed to holders, where an attacker can deposit for > 90% of the holders stake and call this distributeAssets and get 90% of rewards to himself. And within a same block, he can exit the Liquidation pool stake.

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
----------------------
---------------------
}
  • But as an attacker, anyone can call with any parameters. An attacker can enter with any collateralRatio and assets array, leading to assets getting distributed to holders.

  • But In order to distribute ether ERC20 tokens and ether or burn EUROs, we need approval or transfer which is done by LiquidationPool.distributeAssets as shown below.

LiquidationPool(pool).distributeAssets{value: ethBalance}(assets, manager.collateralRate(), manager.HUNDRED_PC());

Impact

High.
Burn the entire EUROs of Liquidation pool, And distribute Ether to hlders not even when liquidation happens.
We should only distribute if liquidation of vault happens.

Tools Used

Manual review

Recommendations

  • Add onlyManager modifier to LiquidationPool.distributeAssets

- function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
+ function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable onlyManager {
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();
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:

distributeAssets-issue

Support

FAQs

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

Give us feedback!