Liquidate

Taurus 1.0

Parameters

  1. _targetAccountAddr: Address type, is the account that a user trying to liquidate.

  2. _targetToken: Address type, the address of the ERC20 token that you are going to spend to liquidate the target account.

Design Overview

By calling this function, the caller tries to buy the collateral of a target account who has borrowed some target tokens from DeFiner and it's liquidatable. The caller can buy the collateral at a discount of 95% of the price from chainlink, and the paid target tokens are used to repay the target account's debt. The money the caller used to liquidate the target account should already be deposited to DeFiner instead of transferring into DeFiner in the liquidate function.

The target account may have multiple kinds of collateral tokens, we are trying to let the caller to buy these tokens by the order of each token's market liquidity. Let's assume that the market liquidity is ETH>USDT>DAI>USDC, and the target account has ETH, DAI, and USDC as collateral and borrows some USDT and it's liquidatable. The caller has deposited some USDT and tries to liquidate the target account. The function will first compute the maximum USDT that the caller can send to the target, which makes sure that the caller's account's borrow ETH won't exceed its borrow power. The caller will try to first buy ETH with USDT at a price that is 5% less than the price from chainlink. If selling all the ETH is enough to bring the target account's LTV less or equal to 60%, which makes sure that its borrow power is greater or equal to its borrow ETH, or the caller's maximum transferrable amount of USDT is not enough to buy all the ETH, we stop the liquidation process here. Otherwise, it will then try to buy DAI from the target, and then try to buy USDC. Please check the pseudocode description section for a cleaner description.

In order to be liquidatable, an account's LTV should be between 85%~95%. If the LTV is larger than 85% it means that this account is at high risk of not repaying its debt. But if the LTV is larger than 95%, the collateral of the account is not enough for other accounts to liquidate it since the other account will buy the collateral of this account at a discount ratio of 5%.

Example

Let's assume that there are only four kinds of tokens supported by DeFiner, and they're ETH, USDT, DAI and USDC. Their liquidity order is ETH>USDT>DAI>USDC. And there are no interests involved in this process for simplification.

Token Name

ETH

USDT

DAI

USDC

Liquidity Rank

1

2

3

4

Price

300

1

1

1

Terminology:

CBB: Current borrow balance = principle + accrued interest

MMRCV: Maintaining Minimum Required Collateral Value = current borrow balance / Maintaining LTV ratio

LDR: Liquidation Discount ratio: The discount ratio the liquidator will get when buying other's assets during the liquidation process.

CMPL: Collateral Market Price at Liquidation: The market price of underlying collateral when a liquidation event happens.

CCV: Current Collateral Value = CMPL * Collateral Amount

UUAL: User asset at Liquidation: The maximum collateral can be liquidated or swapped.

ILTV: Initial LTV ratio (of a token): 0.6 currently for all tokens

We have to make sure that:

(CBB(1LDR)UAAL)/(CCVUAAL)ILTV(CBB - (1-LDR)*UAAL)/(CCV-UAAL)\leq ILTV

Which can derive to:

UAAL=(CBBCCVILTV)/(1LDRILTV)UAAL= (CBB – CCV*ILTV)/(1-LDR-ILTV)

1.Target account has one kind of collateral, the caller tries to liquidate and can liquidate fully

Before liquidation:

User1:

  1. Deposits: 100 USDT

  2. Loans: 90 DAI

  3. LTV: 0.9, liquidatable

User2:

  1. Deposits 100 DAI

  2. It calls liquidate(user1, DAIAdress)

Explanation

The maximum amount of DAI that user2 can transfer to user1 is 100 since it doesn't have any borrows.

UAAL=(9011000.6)/(10.050.6)=85.7UAAL=(90-1*100*0.6)/(1-0.05-0.6)=85.7

Since DAI's price is 1, and 85.7 < 100, so user2 is able to pay the maximum value, which is 85.7 DAI. This is called fully liquidate.

After Liquidation

User1:

  1. Deposits: 100 - 85.7 = 14.3 USDT

  2. Loans: 90-85.7*0.95 = 8.6 DAI

  3. LTV: 0.6, not liquidatable

User2:

  1. Deposits: 18.6 DAI, 85.7 USDT

2.Target account has one kind of collateral, the caller tries to liquidate and can only liquidate partially:

Before Liquidation:

User1:

  1. Deposits: 100 USDT

  2. Loans: 90 DAI

  3. LTV: 0.9, liquidatable

User2:

  1. Deposits 50 DAI

  2. It calls liquidate(user1, DAIAdress)

Explanation:

Here, UAAL is computed the same way as the previous example.

UAAL=(9011000.6)/(10.050.6)=85.7UAAL=(90-1*100*0.6)/(1-0.05-0.6)=85.7

But here user2 only has 50DAI, which worth 50. 50 < 85.7 so user2 can't be swapped to the maximum value UUAL. That way, user2 only pays 50 DAI and user1 will pay 50 / 0.95 = 52.6 USDC.

After liquidation

User1:

  1. Deposits: 100 - 50/0.95 = 47.4 USDC

  2. Borrows: 90-50 = 40DAI

  3. LTV: 40 * 1 / 47.4 * 1 = 0.84, not liquidatable

User2:

  1. Deposits: 50/0.95 = 52.6 USDC

Notice here although user2 doesn't fully liquidate user1, user1 is not liquidatable after liquidation. This is because there is a gap between the initial borrow LTV and the LTV to become liquidatable.

3.Target account has multiple kinds of collateral, the caller tries to liquidate the user fully.

Before liquidation:

User1:

  1. Deposits: 50 USDT, 50 USDC

  2. Loans: 90 DAI

  3. LTV: 0.9, liquidatable

User2:

  1. Deposits 100 DAI

  2. It calls liquidate(user1, DAIAddress)

In the liquidation process, we actually iterate through all the tokens that are supported by DeFiner. To simplified it I omitted this in the previous two example simplification. The process is the following: When we iterate one token, we compute a UUAL, and if the target account's deposits in the current token we are visiting have a higher value than UUAL, which means by selling this the current kind of token is enough to bring its borrow value less than its borrow power, we sell the UUAL value of the token to the liquidator and end liquidation. Otherwise, it means that selling all current tokens is not enough, we sell all the tokens to the liquidator in the current token and continue this process to the next token. At anytime, if target.borrowETH < target.borrowPower or liquidator.targetTokenBalance = 0, we end this liquidate process.

Regarding the liquidity order, we first iterate ETH, but the target doesn't have any deposits in that token. We skip it. Then we meet USDT, and for USDT

UAAL=(9011000.6)/(10.050.6)=85.7UAAL=(90-1*100*0.6)/(1-0.05-0.6)=85.7

The value of deposited USDT is only 50 for user1, which is smaller than 85.7, so we sell as much as we can. And now:

After selling USDT

User1:

  1. Deposits: 50 USDC

  2. Loans: 90 - 50 * 0.95 = 42.5 DAI

User2:

  1. Deposits: 50 USDT, 100 - 50 * 0.95 = 52.5 DAI

Then we see DAI, we skip it too since the user also doesn't deposit anything in DAI. Finally, we use USDC.

UAAL=(42.51500.6)/(10.050.6)=35.7UAAL=(42.5-1*50*0.6)/(1-0.05-0.6)=35.7

User1 has enough deposits in USDC, so it sells 35.7 to User2, end liquidation process.

After selling USDC, end the liquidation

User1:

  1. Deposits: 14.3 USDC

  2. Loans: 42.5 - 35.7 * 0.95 = 8.6 DAI

User2:

  1. Deposits: 35.7 USDC, 50 USDC, 52.5 - 35.7 * 0.95 = 18.6 DAI

Pseudocode

targetTotalBorrow := target.CBB
targetBorrowPower := target.borrowPower
totalRepayAmt := 0
// Suppose tokens are sorted by their market liquidity.
for token in Tokens {
// If it doesn't have deposit in this token, skip this token.
if (!hasDeposit(target, token)) continue
UAAL := (targetTotalBorrow–targetBorrowPower) / (1LDR−token.ILTV)
targetTokenValue := getDepositValue(target, token)
userTokenValue := getDepositValue(user, userToken)
currentExchangeValue := min(UAAL, targetTokenValue, userTokenValue / (1 - LDR))
transfer(user, target, userToken, currentExchangeValue * (1 - LDR) / userToken.price)
transfer(target, user, token, currentExchangeValue / token.price)
targetTotalBorrow -= currentExchangeValue * (1 - LDR)
targetBorrowPower -= currentExchangeValue * token.ILTV
totalRepayAmt += currentExchangeValue * (1 - LDR) / userToken.price
// Stop liquidate if
// 1. The user/liquidator doesn't have any tokens
// 2. The target doesn't have any loans in this token
// 3. The borrow power of the target is already bigger than its borrowed value
if (getDeposit(user, userToken) == 0 ||
targetTotalBorrow == 0 ||
targetTotalBorrow <= targetBorrowPower) {
break;
}
}
// Target repay the all the tokens user has just transferred to it
repay(target, userToken, totalRepayAmt)

Source Code

function liquidate(address _targetAccountAddr, address _targetToken) public onlySupportedToken(_targetToken) whenNotPaused nonReentrant {
require(globalConfig.accounts().isAccountLiquidatable(_targetAccountAddr), "The borrower is not liquidatable.");
LiquidationVars memory vars;
// It is required that the liquidator doesn't exceed it's borrow power.
vars.msgTotalBorrow = globalConfig.accounts().getBorrowETH(msg.sender);
require(
vars.msgTotalBorrow.mul(100) < globalConfig.accounts().getBorrowPower(msg.sender),
"No extra funds are used for liquidation."
);
// Get the available amount of debt token for liquidation. It equals to the amount of target token
// that the liquidator has, or the amount of target token that the borrower has borrowed, whichever
// is smaller.
vars.targetTokenBalance = globalConfig.accounts().getDepositBalanceCurrent(_targetToken, msg.sender);
require(vars.targetTokenBalance > 0, "The account amount must be greater than zero.");
vars.targetTokenBalanceBorrowed = globalConfig.accounts().getBorrowBalanceCurrent(_targetToken, _targetAccountAddr);
require(vars.targetTokenBalanceBorrowed > 0, "The borrower doesn't own any debt token specified by the liquidator.");
if (vars.targetTokenBalance > vars.targetTokenBalanceBorrowed)
vars.targetTokenBalance = vars.targetTokenBalanceBorrowed;
// The value of the maximum amount of debt token that could transfered from the liquidator to the borrower
uint divisor = _targetToken == ETH_ADDR ? INT_UNIT : 10 ** uint256(globalConfig.tokenInfoRegistry().getTokenDecimals(_targetToken));
vars.targetTokenPrice = globalConfig.tokenInfoRegistry().priceFromAddress(_targetToken);
vars.liquidationDiscountRatio = globalConfig.liquidationDiscountRatio();
vars.liquidationDebtValue = vars.targetTokenBalance.mul(vars.targetTokenPrice).mul(100).div(vars.liquidationDiscountRatio).div(divisor);
// The collaterals are liquidate in the order of their market liquidity. The liquidation would stop if one
// of the following conditions are true. 1) The maximum amount of debt token has transfered from the
// liquidator to the borrower, which we call a partial liquidation. 2) The mount of loan reaches the
// borrowPower, which we call a full liquidation.
// Here we assume that there are always enough collaterals to be purchased to finish the liquidation process,
// given the 15% margin set by the liquidation threshold.
vars.totalBorrow = globalConfig.accounts().getBorrowETH(_targetAccountAddr);
vars.borrowPower = globalConfig.accounts().getBorrowPower(_targetAccountAddr);
uint256 totalBorrowBeforeLiquidation = vars.totalBorrow;
for(uint i = 0; i < globalConfig.tokenInfoRegistry().getCoinLength(); i++) {
vars.token = globalConfig.tokenInfoRegistry().addressFromIndex(i);
if(globalConfig.accounts().isUserHasDeposits(_targetAccountAddr, uint8(i))) {
// Get the collateral token price and divisor
vars.tokenPrice = globalConfig.tokenInfoRegistry().priceFromIndex(i);
vars.tokenDivisor = vars.token == ETH_ADDR ? INT_UNIT : 10**uint256(globalConfig.tokenInfoRegistry().getTokenDecimals(vars.token));
// Get the collateral token value
vars.coinValue = globalConfig.accounts().getDepositBalanceCurrent(vars.token, _targetAccountAddr).mul(vars.tokenPrice).div(vars.tokenDivisor);
// Checkout if the coin value is enough to set the borrow amount back to borrow power
uint256 fullLiquidationValue = vars.totalBorrow.sub(vars.borrowPower).mul(100).div(
vars.liquidationDiscountRatio.sub(globalConfig.tokenInfoRegistry().getBorrowLTV(vars.token)));
// Derive the true liquidation value.
if (vars.coinValue > fullLiquidationValue)
vars.coinValue = fullLiquidationValue;
if(vars.coinValue > vars.liquidationDebtValue)
vars.coinValue = vars.liquidationDebtValue;
// Update the totalBorrow and borrowPower
vars.totalBorrow = vars.totalBorrow.sub(vars.coinValue.mul(vars.liquidationDiscountRatio).div(100));
vars.borrowPower = vars.borrowPower.sub(vars.coinValue.mul(globalConfig.tokenInfoRegistry().getBorrowLTV(vars.token)).div(100));
vars.liquidationDebtValue = vars.liquidationDebtValue.sub(vars.coinValue);
// Update the account balance after the collateral is transfered.
vars.tokenAmount = vars.coinValue.mul(vars.tokenDivisor).div(vars.tokenPrice);
uint256 amount = globalConfig.accounts().withdraw(_targetAccountAddr, vars.token, vars.tokenAmount);
globalConfig.accounts().deposit(msg.sender, vars.token, amount);
}
if(vars.totalBorrow <= vars.borrowPower || vars.liquidationDebtValue == 0) {
break;
}
}
// Trasfer the debt token from borrower to the liquidator
// We call the withdraw/repay functions in SavingAccount instead of in Accounts because the total amount
// of loan in SavingAccount should be updated though the reservation and compound parts will not changed.
uint256 targetTokenTransfer = totalBorrowBeforeLiquidation.sub(vars.totalBorrow).mul(divisor).div(vars.targetTokenPrice);
uint256 amount = globalConfig.bank().withdraw(msg.sender, _targetToken, targetTokenTransfer);
globalConfig.bank().repay(_targetAccountAddr, _targetToken, amount);
}