Due to no resitrction role to call distributeAssets()
function, attacker can abusing it to burn EUROs of all holders, mint more rewards for them but no additional token is deposited.
In the liquidation process, function LiquidationPool#distributeAssets()
is called to distribute tokens and eth in the vault:
LiquidationPool(pool).distributeAssets{value: ethBalance}(assets, manager.collateralRate(), manager.HUNDRED_PC());
It can be seen that native eth is sent to the pool through distributeAssets()
function call, and it will handle the distributing and return unpurchased eth token based on the input:
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();
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; // <--- if token is native token, it will only increase `nativePurchased` variable, because native token is already transfered to this contract
} 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); // <---- non-used native token is returned
}
returnUnpurchasedNative()
function:
function returnUnpurchasedNative(ILiquidationPoolManager.Asset[] memory _assets, uint256 _nativePurchased) private {
for (uint256 i = 0; i < _assets.length; i++) {
if (_assets[i].token.addr == address(0) && _assets[i].token.symbol != bytes32(0)) {
(bool _sent,) = manager.call{value: _assets[i].amount - _nativePurchased}(""); // <---- non-used native token is returned
require(_sent);
}
}
}
But there is no restriction to call distributeAssets()
function. Attacker can abusing this to make a chaotic for holders:
Calling distributeAssets()
function with token input is native eth. Attacker will calculate to make a input to make sure _assets[i].amount - _nativePurchased
is equal or smaller than token total eth in LiquidationPool
contract
After success call, all holders get more eth reward than they should, but no one can withdraw them because lack of eth avaiable
Another vault being liquidated with eth in the vault, all holders got reward one more time with these rewards. This will lead to race condition for all holders to claim eth reward, because there is not enough eth for all holders to claim
Race condition for all holders to claim eth reward. some holders will get more eth than they should, som are not able due to lack of eth to claim
Manual review
Only allow PoolLiquidationManager
to call function distributeAssets()
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.