The Standard

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

Liquidated assets portion size should only consider the EUROs staked; Bad debt can be created while there is EUROs to pay for liquidated assets

Summary

Users can stake Standard Token (TST) and The Standard EURO (EUROs) to get a portion of liquidated assets, and their staked EUROs position is spent to buy their given portion of the liquidated assets.

The function distributeAssets, which distribute liquidated assets for the stakers, calculates the portion to be given to each staker based on the amount of TST or EUR staked, choosing the bigger amount of the two, effectively giving the same weight by unit of either token.

Allowing TST to be considered for the stake position size can result in positions which are given a big portion of the liquidated assets, without having the EUROs to pay for the full portion. In that case, they are given only a smaller portion, that they can pay for with staked EUROs, and the rest is transferred to the protocol wallet, without any EUR backing being burned. It will be a loss for the protocol and the onus of taking EUR out of circulation will be on the protocol admin.

The amount of distributed assets for a user should only consider the amount of EUROs staked, resulting in a better optimized distribution of liquidated assets, and a more resilient protocol with less creation of bad debt.

Vulnerability Details

At this moment for the standard protocol, there is in circulation 65 million TST and 400 thousand EUROs.
There is a possibility that the TST will be worth a lot less than the value of an EUR.

The portion to be sold for each user is calculated by checking TST and EUROs amounts staked and choosing the bigger of the two.

Take this simple example:

  • Bob has 0 TST and 1.000 EUROs staked.

  • Alice has 10.000 TST and 10 EUROs staked.

  1. A liquidation occurs with total liquidated collateral cost of 100 EUROs, already with the discount of the defined collateral rate.

  2. stakeTotal = 10.000 + 1.000 = 11.000

  • Bob will get 1000 / 11.000 = 9,1 EUROs worth of liquidated assets.

9,1 EUROs will be burned and the assets will be available for Bob to claim.

At the end Bob has 0 TST and 990,9 EUROs staked.

  • Alice will get 10.000 / 11.000 = 90,9 EUROs worth of liquidated assets

10 EUROs will be burned. 80,9 EUROs left will be sent by the LiquidationPoolManager to the protocol address (supposed to be a gnosis wallet belonging to the project admin).

At the end Alice has 10.000 TST and 0 EUROs staked. The protocol has 80,9 EUROs worth of liquidated assets, without any EUR backing being burned.

Impact

If EUR is not burned for liquidated assets, this will mean the EUR token will lose backing and in consequence lose their peg will the Euro.

  • Liquidations will not be optimized, and on liquidations create partial bad debt for the protocol. In this cases it is possible that there is EUROs in the LiquidationPool that will not be used.

  • Griefing can also be done, with economic expense to the attacker, by creating sizeable positions of TST without EUROs, removing often the earned EUROs from the fees, resulting in bad debt for the protocol.

  • In liquidation cascade events, the EUROs liquidity of the liquidation pool will start to dry up, first for the large TST holders, with smaller amount of EUROs, progressively for the rest of the stackers, creating increasingly bigger portions of bad debt for the protocol.

In all the situations mentioned above, stackers with sizeable amounts of EUROs can be given small amounts of debt to purchase, because their stake is diluted by TST stackers. The bad debt will occur while there is liquidity in the LiquidationPool, and it could have been avoided if the distribution of assets only considered the EUROs staked.

Tools Used

Manual Review

Recommendations

Consider only the amount of EUROs staked to determine the portion of liquidated assets to be distributed to each user. Keep the stacking of TST only to earn the protocol fees.

        @@ -41,17 +42,7 @@ contract LiquidationPool is ILiquidationPool {
                _;
            }
        
        -    function stake(Position memory _position) private pure returns (uint256) {
        -        return _position.TST > _position.EUROs ? _position.EUROs : _position.TST;
        -    }
        -
        -    function getStakeTotal() private view returns (uint256 _stakes) {
        -        for (uint256 i = 0; i < holders.length; i++) {
        -            Position memory _position = positions[holders[i]];
        -            _stakes += stake(_position);
        -        }
        -    }

        @@ -52,6 +53,14 @@ contract LiquidationPool is ILiquidationPool {
                }
            }
        
        +    function getEuroTotal() private view returns (uint256 _euro) {
        +        for (uint256 i = 0; i < holders.length; i++) {
        +            Position memory _position = positions[holders[i]];
        +            _euro += _position.EUROs;
        +        }
        +    }
        +

        @@ -205,24 +214,36 @@ contract LiquidationPool is ILiquidationPool {
            function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
                consolidatePendingStakes();
                (,int256 priceEurUsd,,,) = Chainlink.AggregatorV3Interface(eurUsd).latestRoundData();
        -        uint256 stakeTotal = getStakeTotal();
        +        uint256 stakeTotal = getEuroTotal();
                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) {
        +            if (_position.EUROs > 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 _portion = asset.amount * _position.EUROs / 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;
Updates

Lead Judging Commences

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

Bad-debt

hrishibhat Lead Judge
over 1 year ago
hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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