Liquidate a vault

Learn how to liquidating a vault programmatically on the Cozy protocol.

Users who borrow funds are required to maintain a minimal level of account liquidity. If their account falls below the minimum liquidity threshold, they risk liquidation and losing their collateral.

Users can avoid liquidation by adding collateral or repaying debts. If a user with an account at risk of liquidation fails to respond—by adding collateral or repaying outstanding debt—the account will eventually have negative account liquidity, also known as a shortfall. Any user account with negative account liquidity can be liquidated and taken over programmatically by someone else. By liquidating an account with a shortfall, you can seize its assets at a discount.

This guide illustrates how you can liquidate vaults programmatically using a script written in TypeScript.

All code snippets in this guide are from the liquidate.ts script in the Cozy developer guides repository. See that repository for more context, definitions of helper methods used, and related information.

Everything in this guide assumes that you have experience with JavaScript, ethers.js, and Solidity, as well familiarity with the Cozy protocol.

Check account liquidity

The collateral value of an account is computed by multiplying the supplied USD balance in a market by that market's collateral factor, and summing the result across all markets. The total USD borrow balances for a user are subtracted from that result to arrive at the account liquidity value.

Only accounts with negative liquidity, also known as a shortfall, can be liquidated. Therefore, before liquidating an account, you need to check whether the account has a shortfall. The following code snippet illustrates how to check if an account has a shortfall using an arbitrary address.

% hint style="info" %} The sample script uses an arbitrary address because there's no way to guarantee there is a mainnet account with negative liquidity that could actually be liquidated. The comments describe what to expect if the arbitrary account could be liquidated.

liquidate.ts
// STEP 1: CHECK ACCOUNT LIQUIDITY
// Get instance of Comptroller contract and connect signer for sending transactions
const comptrollerAddress = getContractAddress('Comptroller', chainId);
const comptroller = new Contract(comptrollerAddress, comptrollerAbi, signer);
// Define the address to liquidate and check their liquidity
const borrowerToLiquidate = '0x0000000000000000000000000000000000000001';
const [errorCode, liquidity, shortfall] = await comptroller.getAccountLiquidity(borrowerToLiquidate);
// Make sure there were no errors reading the data
if (errorCode.toString() !== '0') {
logFailure(`Could not read liquidity. Received error code ${errorCode}. Exiting script`);
return;
}
// There were no errors, so now we check if we have an excess or a shortfall.
// One and only one of `shortfall` and `liquidity` will be above zero. (Since
// our chosen account above has no shortfall, you'll need to comment out the
// return statements to move past this section)
if (shortfall.gt(Zero)) {
logSuccess(`Account is undercollateralized and can be liquidated! Shortfall amount: ${shortfall}`);
} else if (liquidity.gt(Zero)) {
logFailure(`Account has excess liquidity and is safe. Amount of liquidity: ${liquidity}. Exiting script`);
return;
} else {
logFailure('Account has no liquidity and no shortfall. Exiting script.');
return;
}

Liquidate an account

If the script had found that the arbitrary account had a shortfall, it would proceed with liquidating the account. In liquidating the account, you can configure the script to repay some or all of the outstanding debt on behalf of the borrower. As a result of the liquidation, you receive some of the borrower's collateral at a discount. Receiving a portion of a borrower's collateral at a discount is referred to as the liquidation incentive.

As an account liquidator, you first need to determine the maximum percentage of the outstanding debt for an account that can be closed by repayment.

The following code snippet illustrates how to check the maximum percentage—called the close factor—that can be closed by liquidation:

liquidate.ts
// STEP 2: PERFORM LIQUIDATION
// Check the max close factor that can be liquidated for Cozy
const closeFactor = await comptroller.closeFactorMantissa();
logSuccess(`Close factor: ${formatUnits(closeFactor, 18)}`);

As part of the liquidation process, you can choose one of the assets that were supplied as collateral by the borrower as the asset you'll seize and receive at a discount. If needed, you could get a list of available collateral using the getAssetsIn() method described in Viewing Positions guide. For simplicity, the following code snippet assumes the collateral is ETH and the borrow is a protected Yearn yUSDC vault, then checks how much this user has borrowed and computes the maximum repay amount.

liquidate.ts
// Get contract instances
const cozyEthAddress = getContractAddress('CozyETH', chainId);
const yearnProtectionMarketAddress = getContractAddress('YearnProtectionMarket', chainId);
const yearnProtectionMarket = new Contract(yearnProtectionMarketAddress, cozyTokenAbi, signer);
// Next we choose an amount of their debt to repay. The max amount we
// can repay is equal to the closeFactor multiplied by their borrow
// balance, so let's check their borrow balance
const usdcBorrowed = await yearnProtectionMarket.borrowBalanceStored(borrowerToLiquidate);
// If we wanted to liquidate the max amount, we can compute this
// below. Here we divide the amount by 1e18 because we multiply the
// USDC amount (6 decimals) by the closeFactor (18 decimals), giving
// us a 24 decimal number. The liquidation amount should be in units
// of the borrowed asset, which is USDC, so we divide by 1e18 to get
// there
const scale = '1000000000000000000'; // 1e18
const repayAmount = usdcBorrowed.mul(closeFactor).div(scale);
logSuccess(`Ready to repay ${repayAmount.toString()} USDC`);

After the collateral is seized, the seized amount is transferred as Cozy tokens. Those tokens can be redeemed as if you had supplied the collateral asset yourself. Remember to approve the Cozy token contract to spend your tokens before calling liquidateBorrow().

The following code snippet illustrates executing the liquidation. However, in this case, the account to liquidate does not have a shortfall. Therefore, the findLog() method reports that the LiquidateBorrow event was not found and prints an error code (3) this corresponds to the COMPTROLLER_REJECTION failure message. Looking at the Comptroller's liquidateBorrowAllowed hook, you can find that the Comptroller rejected the liquidation because there is no shortfall.

liquidate.ts
// Execute the liquidation
const tx = await yearnProtectionMarket.liquidateBorrow(borrowerToLiquidate, repayAmount, cozyEthAddress);
await findLog(tx, yearnProtectionMarket, 'LiquidateBorrow', provider);