Compound analysis of DeFi lending products: an overview

DeFi loan product Compound: Contract chapter

DeFi loan product Compound: Subgraph article


Clearing mechanism

Because the price of digital assets fluctuates, if the user’s borrowed assets rise or the mortgage assets fall, the value of the user’s debt exceeds the safe threshold of the mortgage asset, it can be liquidated. Let’s use a specific scenario to illustrate.

For example, the user stores 1 ETH with a value of USD 2200. If it is opened as collateral, the collateral factor (i.e., mortgage rate) of ETH is 75%, so the maximum value of 2200*0.75 = USD 1650 can be lent. Let’s say the user wants to borrow USDC, which is a stablecoin, and it’s always equal to 1 USD, so the user can borrow up to 1650 USDC, don’t borrow all of it, it’s easy to trigger the settlement at once, let’s borrow 1500 USDC, and then the debt is 1500 USD. There is still 1650-1500 = 150 USD available for borrowing.

After a period of time, suppose the PRICE of ETH drops to 2000 USD, then the loan amount of mortgage assets has been reduced to 2000*0.75 = 1500 USD, and the loan debt is also 1500 USD. At this time, it is at a critical point of liquidation. At this point, however, it will not be liquidated. However, as long as ETH falls below 2000 USD, the value of the loan debt will exceed the loanable value of the mortgage asset, and it can be liquidated.

However, because of the nature of smart contracts, there is no way to automatically perform clearing at the contract level, only to provide entry functions to perform clearing that can be called by external programs. These external programs that call the liquidation function are also called ** liquidators. ** There are costs for the liquidator to help complete the liquidation function, so the liquidator will be given some liquidation incentives, which will be borne by the liquidator (i.e., the borrower). Liquidation incentives are also deducted from users’ mortgage assets.

Compound uses a settlement system in which a liquidator makes payments on behalf of the borrower and gets access to the borrower’s mortgage assets. In addition, there is a closeFactor to limit the proportion of payments that the liquidator can make on behalf of the borrower, currently at 50%, meaning that the liquidator can only make on behalf of the borrower 50% of the time. For example, if the borrower borrowed 1500 USDC, it can only repay 1500*0.5=750 USDC. In this way, the borrower is protected from being liquidated all at once.

In addition, it is the liquidator who designates which mortgage assets are to be used for liquidation. In this way, there is more flexibility for the liquidator. For example, if the borrower has cETH and cUNI as collateral, and the liquidator may prefer cETH, the liquidator may designate cETH as collateral at the time of the liquidation. However, on the other hand, the liquidation service as the liquidator needs more procedural work.

So, when you liquidate, how much collateral does the liquidator get? This is calculated by the following formula:

seizeTokens = actualRepayAmount * liquidationIncentive * priceBorrowed / (priceCollateral * exchangeRate)
Copy the code
  • SeizeTokens means the number of secured assets, which is the number of Ctokens
  • ActualRepayAmount is the actual amount of repayment
  • LiquidationIncentive is the liquidationIncentive, currently 1.08, whereby the liquidator receives an additional 8% of the value of the loan
  • PriceBorrowed The current price of an asset that you are borrowing
  • PriceCollateral The underlying asset price of collateral
  • ExchangeRate exchange rate

Let’s do another example to better understand the logic of liquidation.

Again using the example above, the user borrows 1500 USDC and the mortgage asset is cETH. When the liquidator liquidates the loan, the payable amount is 1500*0.5=750 USDC, and the mortgage asset to be liquidated is designated as cETH. Assuming the ETH price is 1990 and the exchange rate is 0.02, then according to the formula, the quantity of mortgage assets available to the liquidator is 750 * 1.08 * 1 / (1990 * 0.02) = 810/39.8 = 20.3517… That is, the liquidator will get 20.3517… More cETH.

Now, let’s talk about how to design the liquidation service as a liquidator.

Clearing Services v1

Starting with the simplest version v1, the overall architecture is as follows:

The first step is to query all markets from the Subgraph, because the Compound Market is not many, one query can be all query. In addition, in case of the subsequent addition of new markets, you can start a scheduled task to query all markets again every other day or an hour and update the data.

In the second step, each market is separately processed with different coroutine/thread, and all accounts with outstanding debts in the market are queried periodically (every 1 minute), also from Subgraph. As long as storedBorrowBalance is greater than zero AccountCToken data is queried, it can be obtained. Query the GraphQL statement as follows:

query ($symbol: String! , $lastBlockNumber: BigInt) { accountCTokens(first:1000, orderBy: accrualBlockNumber, 
  where: {accrualBlockNumber_gt: $lastBlockNumber, storedBorrowBalance_gt: 0, symbol: $symbol}) {
    id
    symbol
    accrualBlockNumber
    storedBorrowBalance
    market {
      id
      underlyingAddress
      underlyingSymbol
    }
    account {
    	id
    }
  }
}
Copy the code

First: 1000 specifies the maximum number of records to be found, which is also the maximum number of GraphQL queries per time. How do you look up more than 1,000 entries? This is where accrualBlockNumber comes into play. When querying, sort accrualBlockNumber by adding orderBy and add accrualBlockNumber_gt:lastBlockNumber to the WHERE condition to start the query from the specified block. In the first query, the $lastBlockNumber parameter is 0. If the number of results reaches 1000, set the last record’s accrualBlockNumber to the lastBlockNumber of the next query, then query again, and so on. I can find all the data.

After querying all the AccountCToken records with outstanding obligations, you need to determine whether each record exceeds the clearing threshold individually. And if there is a value greater than 0 which might indicate that the clearing threshold is exceeded, there might be some shortfall to it which might suggest shortfall. You can then throw the AccountCToken record into the pending queue and wait for the clearing executor to proceed.

The clearing executor is a separate coroutine/thread that listens to the queue to be cleared and consumes the AccountCToken records in that queue in turn. Since the AccountCToken record has passed a waiting time in the queue, during which time the debt state of the user may have returned to a safe value, after the clearing actuator reads the AccountCToken from the queue, You should call GetAccountLiquidity(Address) again to determine if the clearing threshold is exceeded. If so, proceed to the next step of clearing. Clearing is not as simple as simply calling the contract’s clearing function. There are a few steps to take before this happens.

First, the clearing actuator needs to bind a wallet private key that has assets that can be used to pay Gas and mortgage payments. Secondly, the wallet needs to be authorized to operate, and the cToken contracts in each market need to be authorized to transfer the underlying assets. Next, you need to calculate the payment on behalf of the liquidation and select what kind of mortgage asset the liquidating user has, both of which are passed as entries to the liquidation function. However, it is not easy to determine the validity of these two parameters. Consider the following two scenarios:

  • Assuming that the borrower’s loan amount is 100 USDC, the maximum repayment in this settlement is 100*0.5=50 USDC, but the wallet balance is less than 50 USDC.
  • Let’s say the repayment amount is 50 USDC, but the selected mortgage asset is worth less than 50 USDC.

The first scenario is easy to solve. If the wallet balance is insufficient, the repayment amount will be the remaining balance of the wallet. Of course, if the repayment asset is ETH, some of it needs to be set aside as Gas fee.

The second scenario is more troublesome to deal with. It is necessary to reversely calculate how much the loan asset can actually be repaid by the user’s single mortgage asset, which can be calculated according to the following formula:

actualRepayAmount = seizeTokens * priceCollateral * exchangeRate / (liquidationIncentive * priceBorrowed)
Copy the code

To obtain the data needed for the calculations, three contracts were involved:

  • PriceOracle price predictor contracts to read the latest prices in both currencies
  • Comptroller contract to read the clearing incentive value
  • CToken contract, which reads the borrower’s mortgage asset balance and exchange rate

In order to benefit maximization of liquidation, the liquidation services as much as I can for the reimbursement, so, if the user’s first mortgage assets are insufficient to deduction the generation of reimbursement amount in the first scenario, then need to compute a mortgage assets under enough, if each kind of mortgage assets is not enough, you can only choose the highest value of mortgage assets liquidation to do, And the repayment amount is set as the actualRepayAmount calculated by the mortgage asset.

To sum up, the flow chart of the entire liquidation implementation is roughly as follows:

Clearing Services v2

The V1 version of the clearing service is functional and can handle small amounts of data easily. However, once the amount of data is up, performance becomes a bottleneck. There are two main places that will affect performance. One is to query all accounts with outstanding loans and in turn query whether they can be liquidated, and the other is to clear the actuator.

First of all, although we have done concurrent processing for different markets, there are differences in the amount of data due to the differences in heat in different markets. I have compared the data and found that there are only hundreds of markets with record of borrowing, while thousands of markets with record of borrowing. For markets with thousands of records, retrieving all the data from Subgraph requires several more calls in batches, and it takes longer time to query whether the data can be cleared in sequence because of the large amount of data.

Optimization scheme is not difficult, more than a few coroutines/threads separately processing is good. You can limit each coroutine/thread to query and process at most 500 pieces of data, after which another coroutine/thread will start to process, and the other coroutine/thread will also query and process only the next 500 pieces of data, and so on. In this case, markets that reach the thousands of loaning data are simply opening a few more coroutines/threads for batch concurrent processing. The flowchart is as follows:

Next, let’s look at how the liquidation actuator can be optimized. The performance bottleneck of liquidation execution is that there is only one queue to be cleared, so when there are many assets to be cleared, the queue is easy to build up, or even block, which can also affect the performance of the previous query processing. Then, the optimization idea is the same as the previous one: separate and concurrent processing is good, that is, the unliquidated assets in different markets can be distributed to different unliquidated queues, and each queue is processed by different clearing actuators. The diagram below:

However, with this separation, each clearing actuator needs to use its own separate wallet to perform the clearing operation. If you have 10 markets, you need 10 wallets for each.

Some partners may not understand why each liquidation actuator needs to use a separate wallet. Can’t all actuators use the same wallet?

This is because the nature of blockchain requires that the transactions of each account need to be serial, otherwise it is prone to double flower problems. It’s easy to understand this by giving an example. Suppose that the user originally had 10 ETH in his wallet, and now he needs to transfer one ETH to A and B respectively. If he can transfer one ETH at the same time, the following status will appear:

This, of course, is not true. The correct state of affairs should be this:

In fact, the same scenario also exists in traditional finance, which is mainly solved by means of locking. In the blockchain, it is mainly controlled by an increasing nonce value. For example, if the nonce value of the previous transaction is 10, if the nonce value of this transaction is set to 12, it will wait until the transaction with the nonce value of 11 is executed. Before the nonce value of 12 is executed.

Therefore, when it comes to asset transfers, strict sequentiality needs to be ensured. Therefore, queues are added by design and the clearing executor behind each queue has a separate wallet, so that transactions can be sequenced.

Clearing Services V3

As stated above, when there are more markets, there are more wallets to manage, and clearing v3 is a solution to this problem.

The elegant solution would be to manage only one main wallet manually, while each wallet used by the clearing actuator is automatically managed by the program, including the creation, distribution, transfer, etc.

The entry and exit of assets are operated only through the main wallet, while the child wallets used by the clearing actuator can be derived from the main wallet in the way of HD wallets (layered deterministic wallets). In this way, only one set of mnemonic words can be saved. For example, the path of the first child wallet is m/44’/60’/0’/0/0, and the second child wallet is m/44’/60’/0’/0/1. The last number can be increments in sequence. We can call the last number index. Clearing actuators in different markets need to use different subwallets, which requires different indexes to be configured, which can be configured in the configuration file.

Because of the need for repayment in liquidation, the wallet for each market needs to be reserved for the underlying asset balance of the corresponding market. Assets can be recharged to the main wallet, and then automatically transfer assets from the main wallet to the child wallet through the program itself. For example, the M /44’/60’/0’/0/1 subwallet is used to handle the UNI market, and the M /44’/60’/0’/0/2 subwallet is used to handle the DAI market. These two subwallets need to hold UNI and DAI assets, respectively. First, the main wallet is charged with a certain amount of UNI and DAI. Then the clearing process automatically transfers UNI from the main wallet to m/44’/60’/0’/0/1 and DAI from the main wallet to M /44’/60’/0’/0/2.

As each sub-wallet continues to be liquidated, the underlying asset is constantly consumed, and new assets need to be transferred to it, which would be cumbersome to transfer from the outside to the main wallet each time. Is there an elegant solution? Below, I offer three different scenarios.

In the first scheme, after each liquidation, the cToken is obtained, and the cToken can be redeemed directly into the corresponding underlying assets, and then access the DEX platform (such as Uniswap) to exchange the underlying assets into the repayment assets in the liquidation. The benefits of this are obvious: whatever assets are paid out in liquidation, they are still recovered, and more often than not. In this way, as long as there is the first investment of capital, the sub-wallet itself can be basically self-sufficient. The disadvantage of this scheme is that it adds two more steps, which makes the time of single clearing longer, which will affect the overall clearing performance.

In the second scheme, each sub-wallet redeens the cToken and swaps it into a replacement repayment asset, but this step is not done after the completion of each liquidation, but at the end of each liquidation cycle. We execute the full query every 1 minute, that is, each liquidation period is 1 minute, in which all the liquidation records will be executed. Then, we can redeem all ctokens and swap all repayment assets after executing all liquidation within the period. In this way, the original liquidation performance will not be affected, but also to ensure the self-sufficiency of assets. However, unlike the first option, the first investment of capital needs to be able to support the whole round of liquidation.

The third option is to change the way of thinking. Instead of swap ctokens, we transfer these Ctokens directly to other subwallets in need. For example, cUNI is transferred to the subwallet responsible for UNI market, and cDAI is transferred to the subwallet responsible for DAI market. In fact, the ctokens obtained from the liquidation of all subwallets are redistributed to the subwallets in the corresponding market. In this way, the subwallets do not need to do a lot of swap operations, and the Ctokens collected from other subwallets can be directly redeemed as the underlying assets. However, there are several markets that cannot use this scheme. The assets in these markets can only be borrowed but cannot be used as collateral, including USDT, TUSD and LINK. That is to say, all subwallets will not be cleared to get the cToken of these assets. So let’s deal with plan two alone. In short, the few markets that do not support mortgages use the second option; For other markets, scenario 3 can be used. In this way, the cost caused by large amounts of swap can be reduced.

Finally, when the liquidation gains have accumulated to a certain extent, it should also allow the transfer of assets from the sub-wallet to the main wallet so that the gains can be withdrawn.

So much for the core design of clearing Services VERSION V3.

conclusion

However, this is not to say that the clearing service is finished with v3. There is still room for more optimized iterations, such as splitting into multiple services and becoming clustered. For example, by adding operational background, you can adjust some of the liquidation strategies.

This will be the last article in Compound’s series, an extension, so stay tuned!


Scan the following QR code to follow the public account (public account name: Keegan Xiaosteel)