Liquidation

Intro

Liquidation

In this section, we simulate a borrow->liquidation scenario:

  • Step 1: supply

  • Step 2: borrow max

  • Step 3: wait few blocks and let borrowed_balance > supplied_balance * collateral_factor -> leads to liquidation

  • Step 4: liquidate

Code:

TestCompoundLiquidate.sol

Setup

Set Comptroller, borrowed token and borrowed cToken:

Comptroller public comptroller = Comptroller(0x3d9819210A31b4961b30EF54bE2aeD79B9c9Cd3B);

IERC20 public tokenBorrow;
CErc20 public cTokenBorrow;

event Log(string message, uint val);

constructor(address _tokenBorrow, address _cTokenBorrow) {
    tokenBorrow = IERC20(_tokenBorrow);
    cTokenBorrow = CErc20(_cTokenBorrow);
}

Close Factor

What is "close factor"? Quote from doc:

The percent, ranging from 0% to 100%, of a liquidatable account’s borrow that can be repaid in a single liquidate transaction. If a user has multiple borrowed assets, the closeFactor applies to any single borrowed asset, not the aggregated value of a user’s outstanding borrowing.

Close factor can be queried via comptroller.closeFactorMantissa():

// close factor
function getCloseFactor() external view returns (uint) {
    return comptroller.closeFactorMantissa();
}

Liquidation Incentive

What is "liquidation incentive"? Quote from doc:

The additional collateral given to liquidators as an incentive to perform liquidation of underwater accounts. A portion of this is given to the collateral cToken reserves as determined by the seize share. The seize share is assumed to be 0 if the cToken does not have a protocolSeizeShareMantissa constant. For example, if the liquidation incentive is 1.08, and the collateral’s seize share is 1.028, liquidators receive an extra 5.2% of the borrower’s collateral for every unit they close, and the remaining 2.8% is added to the cToken’s reserves.

Liquidation incentive can be queried via comptroller.liquidationIncentiveMantissa():

// liquidation incentive
function getLiquidationIncentive() external view returns (uint) {
    return comptroller.liquidationIncentiveMantissa();
}

Liquidate

To compute the amount of the collateral that we can liquidate, call comptroller .liquidateCalculateSeizeTokens():

// get amount of collateral to be liquidated
function getAmountToBeLiquidated(
    address _cTokenBorrowed,
    address _cTokenCollateral,
    uint _actualRepayAmount
) external view returns (uint) {
    /*
     * Get the exchange rate and calculate the number of collateral tokens to seize:
     *  seizeAmount = actualRepayAmount * liquidationIncentive * priceBorrowed / priceCollateral
     *  seizeTokens = seizeAmount / exchangeRate
     *   = actualRepayAmount * (liquidationIncentive * priceBorrowed) / (priceCollateral * exchangeRate)
     */
    (uint error, uint cTokenCollateralAmount) = comptroller
    .liquidateCalculateSeizeTokens(
        _cTokenBorrowed,
        _cTokenCollateral,
        _actualRepayAmount
    );

    require(error == 0, "error");

    return cTokenCollateralAmount;
}

Finally let's implement the high-level liquidate() function:

// liquidate
function liquidate(
    address _borrower,
    uint _repayAmount,
    address _cTokenCollateral
) external {
    // Transfer the fund from user's wallet to this contract
    tokenBorrow.transferFrom(msg.sender, address(this), _repayAmount);
    // Approve the cTokenBorrow contract to spend this fund
    tokenBorrow.approve(address(cTokenBorrow), _repayAmount);

    // Call cTokenBorrow.liquidateBorrow() to liquidate
    require(
        cTokenBorrow.liquidateBorrow(_borrower, _repayAmount, _cTokenCollateral) == 0,
        "liquidate failed"
    );
}

Test

test-compound-liquidate.js

We supply WBTC and borrow DAI.

Test case:

it("should liquidate", async () => {
    // used for debugging
    let tx
    let snap

    // supply
    await tokenSupply.approve(testCompound.address, SUPPLY_AMOUNT, {
        from: SUPPLY_WHALE
    })
    tx = await testCompound.supply(SUPPLY_AMOUNT, {
        from: SUPPLY_WHALE,
    })

    snap = await snapshot(testCompound, liquidator)
    console.log(`--- supplied ---`)
    console.log(`col factor: ${snap.colFactor} %`)
    console.log(`supplied: ${snap.supplied}`)

    // enter market
    tx = await testCompound.enterMarket({
        from: accounts[0]
    })

    // borrow
    const { liquidity } = await testCompound.getAccountLiquidity()
    const price = await testCompound.getPriceFeed(C_TOKEN_BORROW)
    const maxBorrow = liquidity.mul(pow(10, BORROW_DECIMALS)).div(price)
    // NOTE: tweak borrow amount if borrow fails
    const borrowAmount = maxBorrow.mul(new BN(9997)).div(new BN(10000))

    console.log(`--- entered market ---`)
    console.log(`liquidity: $ ${liquidity.div(pow(10, 18))}`)
    console.log(`price: $ ${price.div(pow(10, 18))}`)
    console.log(`max borrow: ${maxBorrow.div(pow(10, 18))}`)
    console.log(`borrow amount: ${borrowAmount.div(pow(10, 18))}`)

    tx = await testCompound.borrow(borrowAmount, {
        from: accounts[0]
    })

    snap = await snapshot(testCompound, liquidator)
    console.log(`--- borrowed ---`)
    console.log(`liquidity: $ ${snap.liquidity}`)
    console.log(`borrowed: ${snap.borrowed}`)

    // accrue interest on borrow
    const block = await web3.eth.getBlockNumber()
    // NOTE: tweak this to increase borrowed amount
    await time.advanceBlockTo(block + 10000)

    // send any tx to Compound to update liquidity and shortfall
    await testCompound.getBorrowBalance()

    snap = await snapshot(testCompound, liquidator)
    console.log(`--- after some blocks... ---`)
    console.log(`liquidity: $ ${snap.liquidity}`)
    console.log(`shortfall: $ ${snap.shortfall}`)
    console.log(`borrowed: ${snap.borrowed}`)

    // liquidate
    const closeFactor = await liquidator.getCloseFactor()
    const repayAmount = (await testCompound.getBorrowBalance.call()).mul(closeFactor).div(pow(10, 18))

    const liqBal = await tokenBorrow.balanceOf(LIQUIDATOR)
    console.log(`liquidator balance: ${liqBal.div(pow(10, BORROW_DECIMALS))}`)
    assert(liqBal.gte(repayAmount), "bal < repay")

    const amountToBeLiquidated = await liquidator.getAmountToBeLiquidated(C_TOKEN_BORROW, C_TOKEN_SUPPLY, repayAmount)
    console.log(
        `amount to be liquidated (cToken collateral):  ${amountToBeLiquidated.div(pow(10, SUPPLY_DECIMALS - 2)) / 100}`
    )

    await tokenBorrow.approve(liquidator.address, repayAmount, {
        from: LIQUIDATOR
    })
    tx = await liquidator.liquidate(testCompound.address, repayAmount, C_TOKEN_SUPPLY, {
        from: LIQUIDATOR,
    })

    snap = await snapshot(testCompound, liquidator)
    console.log(`--- liquidated ---`)
    console.log(`close factor: ${snap.closeFactor} %`)
    console.log(`liquidation incentive: ${snap.incentive}`)
    console.log(`supplied: ${snap.supplied}`)
    console.log(`liquidity: $ ${snap.liquidity}`)
    console.log(`shortfall: $ ${snap.shortfall}`)
    console.log(`borrowed: ${snap.borrowed}`)
    console.log(`liquidated: ${snap.liquidated}`)

    /* memo
    c = 31572
    r = c * 0.65 * 0.5
    b = 1
    i = 1.08
    r * i * b / c
    */
})

Last updated