Risks Control Model
Collateral is required to against default risks, it will be liquidated if it met the designed liquidation conditions
Parameters
  1. 1.
    _targetAccountAddr: Address type, is the account that a user trying to liquidate.
  2. 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 5% 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
UAAL: User asset at Liquidation: The maximum collateral can be liquidated or swapped.
ILTV: Initial LTV ratio of collateral token: 0.6 currently for most 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. 1.
    Deposits: 100 USDT
  2. 2.
    Loans: 90 DAI
  3. 3.
    LTV: 0.9, liquidatable
User2:
  1. 1.
    Deposits 100 DAI
  2. 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. 1.
    Deposits: 100 - 85.7 = 14.3 USDT
  2. 2.
    Loans: 90-85.7*0.95 = 8.6 DAI
  3. 3.
    LTV: 0.6, not liquidatable
User2:
  1. 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. 1.
    Deposits: 100 USDT
  2. 2.
    Loans: 90 DAI
  3. 3.
    LTV: 0.9, liquidatable
User2:
  1. 1.
    Deposits 50 DAI
  2. 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 UAAL. That way, user2 only pays 50 DAI and user1 will pay 50 / 0.95 = 52.6 USDC.
After liquidation
User1:
  1. 1.
    Deposits: 100 - 50/0.95 = 47.4 USDC
  2. 2.
    Borrows: 90-50 = 40DAI
  3. 3.
    LTV: 40 * 1 / 47.4 * 1 = 0.84, not liquidatable
User2:
  1. 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. 1.
    Deposits: 50 USDT, 50 USDC
  2. 2.
    Loans: 90 DAI
  3. 3.
    LTV: 0.9, liquidatable
User2:
  1. 1.
    Deposits 100 DAI
  2. 2.
    It calls liquidate(user1, DAIAddress)
In the liquidation process, we actually iterate through all the tokens that are supported by DeFiner. To simplify it, I omitted this in the previous two examples. The process is the following: When we iterate one token, we compute an UAAL, and if the target account's deposit in the current token that we are visiting has a higher value than UAAL, which means by selling the current kind of token is enough to bring its borrow value less than its borrow power, we sell the UAAL 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. 1.
    Deposits: 50 USDC
  2. 2.
    Loans: 90 - 50 * 0.95 = 42.5 DAI
User2:
  1. 1.
    Deposits: 50 USDT, 100 - 50 * 0.95 = 52.5 DAI
Then we see DAI, we skip it too since the user doesn't have any deposit in DAI either. 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. 1.
    Deposits: 14.3 USDC
  2. 2.
    Loans: 42.5 - 35.7 * 0.95 = 8.6 DAI
User2:
  1. 1.
    Deposits: 35.7 USDC, 50 USDC, 52.5 - 35.7 * 0.95 = 18.6 DAI

Pseudocode

1
targetTotalBorrow := target.CBB
2
targetBorrowPower := target.borrowPower
3
totalRepayAmt := 0
4
// Suppose tokens are sorted by their market liquidity.
5
for token in Tokens {
6
// If it doesn't have deposit in this token, skip this token.
7
if (!hasDeposit(target, token)) continue
8
9
UAAL := (targetTotalBorrow–targetBorrowPower) / (1LDR−token.ILTV)
10
targetTokenValue := getDepositValue(target, token)
11
userTokenValue := getDepositValue(user, userToken)
12
currentExchangeValue := min(UAAL, targetTokenValue, userTokenValue / (1 - LDR))
13
14
transfer(user, target, userToken, currentExchangeValue * (1 - LDR) / userToken.price)
15
transfer(target, user, token, currentExchangeValue / token.price)
16
17
targetTotalBorrow -= currentExchangeValue * (1 - LDR)
18
targetBorrowPower -= currentExchangeValue * token.ILTV
19
totalRepayAmt += currentExchangeValue * (1 - LDR) / userToken.price
20
21
// Stop liquidate if
22
// 1. The user/liquidator doesn't have any tokens
23
// 2. The target doesn't have any loans in this token
24
// 3. The borrow power of the target is already bigger than its borrowed value
25
if (getDeposit(user, userToken) == 0 ||
26
targetTotalBorrow == 0 ||
27
targetTotalBorrow <= targetBorrowPower) {
28
break;
29
}
30
}
31
// Target repay the all the tokens user has just transferred to it
32
repay(target, userToken, totalRepayAmt)
Copied!

Source Code

1
function liquidate(address _targetAccountAddr, address _targetToken) public onlySupportedToken(_targetToken) whenNotPaused nonReentrant {
2
3
require(globalConfig.accounts().isAccountLiquidatable(_targetAccountAddr), "The borrower is not liquidatable.");
4
LiquidationVars memory vars;
5
6
// It is required that the liquidator doesn't exceed it's borrow power.
7
vars.msgTotalBorrow = globalConfig.accounts().getBorrowETH(msg.sender);
8
require(
9
vars.msgTotalBorrow.mul(100) < globalConfig.accounts().getBorrowPower(msg.sender),
10
"No extra funds are used for liquidation."
11
);
12
13
// Get the available amount of debt token for liquidation. It equals to the amount of target token
14
// that the liquidator has, or the amount of target token that the borrower has borrowed, whichever
15
// is smaller.
16
vars.targetTokenBalance = globalConfig.accounts().getDepositBalanceCurrent(_targetToken, msg.sender);
17
require(vars.targetTokenBalance > 0, "The account amount must be greater than zero.");
18
19
vars.targetTokenBalanceBorrowed = globalConfig.accounts().getBorrowBalanceCurrent(_targetToken, _targetAccountAddr);
20
require(vars.targetTokenBalanceBorrowed > 0, "The borrower doesn't own any debt token specified by the liquidator.");
21
22
if (vars.targetTokenBalance > vars.targetTokenBalanceBorrowed)
23
vars.targetTokenBalance = vars.targetTokenBalanceBorrowed;
24
25
// The value of the maximum amount of debt token that could transfered from the liquidator to the borrower
26
uint divisor = _targetToken == ETH_ADDR ? INT_UNIT : 10 ** uint256(globalConfig.tokenInfoRegistry().getTokenDecimals(_targetToken));
27
vars.targetTokenPrice = globalConfig.tokenInfoRegistry().priceFromAddress(_targetToken);
28
vars.liquidationDiscountRatio = globalConfig.liquidationDiscountRatio();
29
vars.liquidationDebtValue = vars.targetTokenBalance.mul(vars.targetTokenPrice).mul(100).div(vars.liquidationDiscountRatio).div(divisor);
30
31
// The collaterals are liquidate in the order of their market liquidity. The liquidation would stop if one
32
// of the following conditions are true. 1) The maximum amount of debt token has transfered from the
33
// liquidator to the borrower, which we call a partial liquidation. 2) The mount of loan reaches the
34
// borrowPower, which we call a full liquidation.
35
// Here we assume that there are always enough collaterals to be purchased to finish the liquidation process,
36
// given the 15% margin set by the liquidation threshold.
37
vars.totalBorrow = globalConfig.accounts().getBorrowETH(_targetAccountAddr);
38
vars.borrowPower = globalConfig.accounts().getBorrowPower(_targetAccountAddr);
39
40
uint256 totalBorrowBeforeLiquidation = vars.totalBorrow;
41
for(uint i = 0; i < globalConfig.tokenInfoRegistry().getCoinLength(); i++) {
42
vars.token = globalConfig.tokenInfoRegistry().addressFromIndex(i);
43
if(globalConfig.accounts().isUserHasDeposits(_targetAccountAddr, uint8(i))) {
44
// Get the collateral token price and divisor
45
vars.tokenPrice = globalConfig.tokenInfoRegistry().priceFromIndex(i);
46
vars.tokenDivisor = vars.token == ETH_ADDR ? INT_UNIT : 10**uint256(globalConfig.tokenInfoRegistry().getTokenDecimals(vars.token));
47
48
// Get the collateral token value
49
vars.coinValue = globalConfig.accounts().getDepositBalanceCurrent(vars.token, _targetAccountAddr).mul(vars.tokenPrice).div(vars.tokenDivisor);
50
51
// Checkout if the coin value is enough to set the borrow amount back to borrow power
52
uint256 fullLiquidationValue = vars.totalBorrow.sub(vars.borrowPower).mul(100).div(
53
vars.liquidationDiscountRatio.sub(globalConfig.tokenInfoRegistry().getBorrowLTV(vars.token)));
54
55
// Derive the true liquidation value.
56
if (vars.coinValue > fullLiquidationValue)
57
vars.coinValue = fullLiquidationValue;
58
if(vars.coinValue > vars.liquidationDebtValue)
59
vars.coinValue = vars.liquidationDebtValue;
60
61
// Update the totalBorrow and borrowPower
62
vars.totalBorrow = vars.totalBorrow.sub(vars.coinValue.mul(vars.liquidationDiscountRatio).div(100));
63
vars.borrowPower = vars.borrowPower.sub(vars.coinValue.mul(globalConfig.tokenInfoRegistry().getBorrowLTV(vars.token)).div(100));
64
vars.liquidationDebtValue = vars.liquidationDebtValue.sub(vars.coinValue);
65
66
// Update the account balance after the collateral is transfered.
67
vars.tokenAmount = vars.coinValue.mul(vars.tokenDivisor).div(vars.tokenPrice);
68
uint256 amount = globalConfig.accounts().withdraw(_targetAccountAddr, vars.token, vars.tokenAmount);
69
globalConfig.accounts().deposit(msg.sender, vars.token, amount);
70
}
71
72
if(vars.totalBorrow <= vars.borrowPower || vars.liquidationDebtValue == 0) {
73
break;
74
}
75
}
76
77
// Trasfer the debt token from borrower to the liquidator
78
// We call the withdraw/repay functions in SavingAccount instead of in Accounts because the total amount
79
// of loan in SavingAccount should be updated though the reservation and compound parts will not changed.
80
uint256 targetTokenTransfer = totalBorrowBeforeLiquidation.sub(vars.totalBorrow).mul(divisor).div(vars.targetTokenPrice);
81
uint256 amount = globalConfig.bank().withdraw(msg.sender, _targetToken, targetTokenTransfer);
82
globalConfig.bank().repay(_targetAccountAddr, _targetToken, amount);
83
}
Copied!

Last modified 2mo ago
Copy link
Contents