The Standard

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

Requirement of TST Deposit for Participation in Liquidations May Prevent Full Utilisation of Liquidation Pool Funds

Summary

The requirement to deposit TST for participating in liquidations may prevent the full utilisation of liquidation pool funds during liquidation, potentially leading to the appearance of bad debt.

Vulnerability Details

Bad debt note

The main invariant of collateralised stablecoins: Issued stable coin value <= collateral value * collateral factor (or euro value < collateral value / collateralRate in terms of The Standard protocol), where 0 <= collateral factor <= 1. This invariant should be valid both for each position and for the whole protocol.

There are two cases of invariant break for a position:

  • collateral value * collateral factor < Issued stable coin value <= collateral value - in such cases, the position is liquidated, collateral is sold, and its value is enough to repay (burn) all issued stablecoins.

  • Issued stable coin value > collateral value - in such cases, the position is also liquidated, collateral is sold, but its value is not enough to repay (burn) all issued stablecoins:

    • X - issued stablecoins, Y - repaid and burnt during liquidation stablecoins where Y < X. X-Y is the protocol's bad debt. It means if all users close their positions, X-Y stablecoins are still issued. But this amount is backed by nothing.

When the protocol has bad debt, the fair price of a stablecoin = (Issued stablecoins - Bad debt) / Issued stablecoins and it is < 1. It means that the stablecoin tends to be unpegged.

To return the peg, the protocol must buy stablecoins from the market and burn them. It means that Bad debt is the direct protocol loss.

In LiquidationPool::distributeAssets(), collaterals are distributed to holders, and EUROs from holders are repaid and burnt pro rata their stake (see 1@> in the details below). Stake is calculated depending on EUROs and TST amount in the holder's position (see 2@>). There could be cases when the total amount of EUROs is enough to liquidate a vault, but because of insufficient TST amount, not all holder EUROs would be used for repayment and burning (see 3@> and 4@>).

function distributeAssets(ILiquidationPoolManager.Asset[] memory _assets, uint256 _collateralRate, uint256 _hundredPC) external payable {
// ... [code omitted for brevity]
for (uint256 j = 0; j < holders.length; j++) {
// ... [code omitted for brevity]
for (uint256 i = 0; i < _assets.length; i++) {
// ... [code omitted for brevity]
1 @> uint256 _portion = asset.amount * _positionStake / stakeTotal;
uint256 costInEuros = _portion * 10 ** (18 - asset.token.dec) * uint256(assetPriceUsd) / uint256(priceEurUsd)
* _hundredPC / _collateralRate;
3 @> if (costInEuros > _position.EUROs) {
_portion = _portion * _position.EUROs / costInEuros;
costInEuros = _position.EUROs;
}
_position.EUROs -= costInEuros;
rewards[abi.encodePacked(_position.holder, asset.token.symbol)] += _portion;
4 @> burnEuros += costInEuros;
// ... [code omitted for brevity]
}
// ... [code omitted for brevity]
}
// ... [code omitted for brevity]
}
function stake(Position memory _position) private pure returns (uint256) {
2 @> return _position.TST > _position.EUROs ? _position.EUROs : _position.TST;
}

Exaggerated example:

  • Preconditions:

    • Fees=0 to simplify calculations.

    • _collateralRate = 1.1 (as in production).

    • Eur/BTC price is 10000 EUR/BTC.

    • User1 deposited 110 BTC.

    • User1 minted 1_000_000 EUROs.

  • Liquidation pool's positions are 1_000_000 EUROs.

    • User2's position is 800_000 EUROs and 300_000 TST (user may not fully understand how the protocol works).

    • User3's position is 300_000 EUROs and 800_000 TST (prior liquidations used part of User3's initial position).

  • Falling market, Eur/BTC price is 9500 EUR/BTC.

  • User1's vault becomes undercollateralised, liquidation is triggered.

    • Total EUROs amount is 1_100_000 EUROs, which is enough to liquidate User1's vault, but

      • User2's stake is 300_000 (see 2@> above).

      • User3's stake is 300_000 (see 2@> above).

    • _portion of User2 and User3 is 55 BTC (half of User1's collateral, see 1@>).

    • 475_000 EUROs from User2's position are used for liquidations, and 55 BTC are available for claiming by User2.

    • Only 300_000 EUROs from User3's position are used for liquidations.

      • Approximately 35 BTC (300000/475000*55) are available for claiming by User3.

      • 20 BTC goes to LiquidationPoolManager.protocol (EOA/multisig of the team as explained in the Discord channel).

  • It will take time for The Protocol team members to sell received collateral, buy and burn EURO to avoid bad debt.

    • This could be critical on a falling market, as the amount of bought EUROs might not cover all remaining debt.

This situation wouldn't occur if all of User2's funds participated in the liquidations.

Impact

Bad debt could appear.

Tools Used

Manual review

Recommended Mitigation

In cases where:

  • Liquidation pool EURO positions are enough to liquidate an undercollateralised vault.

  • Due to insufficient TST position, not all these EUROs are utilised (and collateral is sent to the LiquidationPoolManager.protocol address).

EUROs should be utilised regardless of TST positions in the liquidation pool.

Updates

Lead Judging Commences

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

Bad-debt

Support

FAQs

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