With lending protocols like Compound V3 and its derivatives, such as Swaylend (a top lending protocol on Fuel Network), a liquidation happens when a borrower's collateral value drops below a certain threshold compared to their borrowed amount. In such cases, the user must be liquidated. Anyone can call the liquidation method; it is not limited to the governor. Liquidations are necessary to maintain a healthy market. They ensure that all debts are repaid appropriately and help prevent losses for both the protocol and its users.
With that purpose, instead of liquidating users manually, automated bots come in handy.
We roughly divide the liquidation process into three main steps:
Get and liquidate insolvent users
Buy off protocol's collateral
Sell collateral
The steps are explained in How to below.
Let’s take a look at Swaylend’s and Compound’s contracts, which are written in Sway and Solidity, respectively. One runs on FuelVM, while the other operates on the traditional EVM. The most relevant methods are listed below.
Absorbing users
// Sway
fn absorb(accounts: Vec<Identity>, price_data_update: PriceDataUpdate);
// Solidity
function absorb(address absorber, address[] calldata accounts) override external;
This method liquidates users passed as the accounts
parameter when called.
Getting collateral quotes
// Sway
fn collateral_value_to_sell(asset_id: AssetId, collateral_amount: u64) -> u64;
// Solidity
function absorb(address absorber, address[] calldata accounts) override external;
Calculates the amount of base asset to send along with the buy_collateral
call to buy the desired amount of the collateral asset.
Buying collateral
// Sway
fn buy_collateral(asset_id: AssetId, min_amount: u64, recipient: Identity);
// Solidity
function buyCollateral(address asset, uint minAmount, uint baseAmount, address recipient) override external nonReentrant;
Buys collateral for the amount of base asset passed along with the function and receives, at minimum, the passed min_amount
.
1. Getting and updating configurations
First up, we need to get the latest market configurations to ensure we are working with the latest data.
marketConfig := marketContract.getMarketConfiguration()
collateralConfig := marketContract.getCollateralConfiguration()
marketBasics := marketContract.getMarketBasics()
liquidator.updateConfigs(marketConfig, collateralConfig, marketBasics)
2. Finding liquidatable users
Next, we first need to get users with negative principals and calculate the insolvent ones, ready to go underwater.
usersWithNegativePrincipal := indexer.fetchUsersWithNegativePrincipals()
insolventUsers := []
for each user in usersWithNegativePrincipal
if isInsolvent(user)
append user to insolventUsers
Under the hood, we can use indexers like The Graph and Envio to index the market contract and to get users with negative principals — users currently borrowing from our lending protocol. All calculations are then made offchain by using an adapted version of market contract's function is_liquidatable
— be found here. Alternatively, instead of calculating users' insolvency off-chain, we can call the is_liquidatable
method for each user. However, this approach is less efficient.
3. Absorbing users
The users we get from step 3 are then passed into the absorb
call to liquidate underwater accounts.
marketContract.absorb(insolventUsers)
Note
The fees are not covered by the liquidator; they are paid by the protocol.
1. Updating collateral reserves
First, we need to update the market’s collateral reserves in the liquidator's internal storage.
collaterals := marketContract.getCollateralConfigurations()
for each collateral in collaterals
config := marketContract.getCollateralReservers(collateral.id)
updateCollateralReserves(collateral.id, config)
2. Updating collateral values
Next, we update the actual value of the collateral reserves by iterating through the previously updated reserves, fetching the price from oracle for each asset, and storing it in liquidator’s internal storage.
collaterals := getCollateralConfigurationsFromInternalStorage()
for each collateral in collaterals
price := oracle.getPrice(collateral.id)
updateCollateralPrices(collateral.id, price)
3. Building and executing buy transactions
With the collateral amount and value now stored in internal storage, we can begin buying the collateral. In this step, we can set any limitations we want, such as buying only specific collateral assets or avoiding buying if the value does not exceed a specific threshold. For simplicity, we’ll leave those configurations and algorithms for building transactions to optimize your return to you.
collaterals := getCollateralsToBuy()
for each collateral in collaterals
amountToBuy := calculateAmountToBuy(collateral)
marketContract.buyCollateral(collateral.id, amountToBuy)
Note
An important step in building transactions is calculating the price of each collateral amount we buy from the protocol. These collaterals are sold at a discount to incentivize users to repay the debts of liquidated users in base assets. The amount of collateral to sell to a user is based on the base asset the user sent with the transaction. This amount is either fetched from the contract or calculated off-chain. For example, you can find out how Swaylend performs this calculation here.
Now that we hold the collateral value in the Liquidator’s wallet, we can decide whether to sell or hold it. We leave this decision up to you. There are many options for how to approach this step. A simple example would be to check which DEX offers the best price, as shown in the pseudocode below.
collaterals := getCollateralConfigurationsFromInternalStorage()
baseAssetId := marketConfig.baseAssetId
for each collateral in collaterals
uniswapPrice := uniswapContract.getPrice(collateral.id)
balancerPrice := balancerContract.getPrice(collateral.id)
balance := wallet.getBalance(collateral.id)
dexContract := null
if uniswapPrice > balancerPrice
dexContract := uniswapContract
else
dexContract := balancerContract
poolId := dexContract.getPoolId(baseAssetId, collateral.id)
deadline := now() + 10000
dexContract.swap(
collateral.amountToBuy,
collateral.id,
poolId,
deadline
)
Future improvements could focus on optimizing transaction-building efficiency and the liquidator's performance. This includes optimizing the processes for building buy and sell transactions, storing transaction history, and the liquidator's overall profit realized. Additionally, implementing parallel execution of all main steps can significantly boost the bot's performance when other market makers arrive, and checking more DEXes can help identify the best platforms for selling the acquired collateral to maximize profit.
A liquidator automates identifying and liquidating insolvent users, purchasing discounted collateral waiting in the protocol, and immediately selling it for profit. The bot aims to run at optimal performance and profitability by utilizing advanced transaction-building strategies.
Website | Discord | LinkedIn | Twitter/X | GitHub | E-Mail
Written by Urban Vidovič, COO