The Standard

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

Attacker can make chaotic fof all holders by calling function `distributeAssets()`

Summary

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.

Vulnerability Details

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

Impact

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

Tools Used

Manual review

Recommendations

Only allow PoolLiquidationManager to call function distributeAssets()

Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

distributeAssets-issue

hrishibhat Lead Judge over 1 year 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.