Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Off-Chain Liquidation Process

Introduction

This document outlines the off-chain liquidation process for the protocol. It details the interaction between a liquidation bot and the smart contracts to manage under-collateralized loans, ensuring the solvency of the protocol by efficiently liquidating risky positions.

Core Principles and Assumptions

  • Immutable Process: Once a position is marked for liquidation, the process will complete, regardless of any subsequent price movements that might improve the user’s Loan-to-Value (LTV) ratio.
  • Account Lock: While a user’s position is being liquidated, they cannot perform any other actions like depositing, withdrawing, or borrowing.
  • Full Liquidation: The entire position, including all types of collateral, is liquidated.
  • Socialized Loss (v1): In the initial version, the protocol does not use an insurance fund. Any bad debt resulting from a liquidation is socialized among lenders by adjusting the supply index. This is v1 requirement arising from the PRD.

Data Structure Changes

To support the liquidation process, the following changes are made to the smart contract’s state.

The main contract state is updated with:

#![allow(unused)]
fn main() {
    /// Account designated to handle collateral swaps.
    pub liquidator_account: AccountId,
    /// State of any ongoing liquidation.
    pub liquidation: Option<LiquidationInfo>,
	/// Tracks pending liquidations
    pub pending_liquidations: Vector<UserId>
}

The InFlightOperation enum is extended to include liquidation states:

#![allow(unused)]
fn main() {
pub enum InFlightOperation {
    Borrowing,
    WithdrawingUsdc,
    WithdrawingCollateral,
    Liquidation
	LiquidationFinal(Usdc)
}
}

A new LiquidationInfo struct tracks the state of an ongoing liquidation:

#![allow(unused)]
fn main() {
#[derive(Debug, Clone, PartialEq, Default)]
pub struct LiquidationInfo {
    /// The user whose position is being liquidated.
    pub user: UserId,
    /// The current state of the liquidation process.
    pub liquidation_state: LiquidationState,
}
}

The LiquidationState enum tracks the sub-steps within the collateral transfer phase:

#![allow(unused)]
fn main() {
pub enum LiquidationState {
    #[default]
    MarkedForLiquidation,
    OngoingTransfer,
    TransferSuccess,
    TransferFailure(Vec<CollateralContract>),
}
}

Swap and Hold Account

The protocol uses a “Swap and Hold Account” during the liquidation process. This is a standard NEAR Account ID and does not require any special smart contract code.

This account is used to store the user’s collateral in an intents contract, which facilitates the swap from the collateral asset to USDC.

End-to-End Liquidation Flow

The liquidation process is a multi-stage operation orchestrated by an off-chain bot interacting with the main smart contract.

Stage 1: Position Flagging

  1. Monitor LTV: The bot continuously monitors each user’s LTV ratio off-chain.
  2. Initiate Liquidation: If a user’s LTV exceeds the liquidation threshold, the bot calls liquidate_position(user_id) on the smart contract. This function marks the user for liquidation, records their current borrow value, and locks their account from further operations.

Stage 2: Collateral Transfer

  1. Iterate and Transfer: The smart contract initiates all collateral transfers at once. The bot calls perform_transfer_for_liquidation(contracts: Option<Vec<CollateralContract>>) to initiate it..
  2. On-Chain Transfer: This contract call initiates an async transfer of the specified collateral from the protocol to the designated liquidator_account. The contract updates the state to OngoingTransfer.
  3. Handle Callbacks: The contract handles the async transfer’s callback:
    • On success, the state is updated to TransferSuccess.
    • On failure, the state is updated to TransferFailure with the collateral info, signaling the bot to retry the transfer for that specific asset.

Stage 3: Asset Swapping (Off-Chain)

  1. Swap to USDC: Once the collateral is in the liquidator_account, the bot uses an external service (e.g., 1Click API) to swap each collateral asset for USDC.
  2. Hold Funds: The resulting USDC is temporarily held in a “Swap & Hold” contract controlled by the liquidator.

Stage 4: USDC Repatriation

  1. Transfer Back: After all collateral has been swapped, the bot triggers a transfer of the total accumulated USDC from the “Swap & Hold” contract back to the main protocol contract.
  2. Record Value: The main contract recognizes the incoming transfer is from the liquidator_account and records the amount in the swapped_collateral_value field of the user’s LiquidationInfo.

Stage 5: Finalizing Liquidation

  1. Finalize: With the USDC returned, the bot calls finalize_liquidation().
  2. Settle Account: This function performs the final accounting: it repays the user’s debt, calculates and distributes liquidation fees, and handles any surplus or deficit. The user’s account is then unlocked.

Smart Contract Interface

liquidate_account(user_id)

Marks a user’s position for liquidation.

  • Authorization: Can only be called by a permissioned liquidator bot account.
  • Checks: Verifies that the user’s position is indeed eligible for liquidation based on their LTV.
  • Actions:
    • Creates a LiquidationInfo struct for the user.
    • Sets liquidation_state to MarkedForLiquidation.
    • Stores the user’s current borrow value.
    • Sets the user’s in_flight_operation to Liquidation, preventing other actions.

transfer_collateral_for_liquidation(ft_contract)

Transfers a specific collateral asset from the user’s position to the liquidator_account.

  • Authorization: Callable by any account, allowing for permissionless retries.
  • Pre-conditions:
    • The user must be in a liquidation state (MarkedForLiquidation or TransferFailure).
    • If the state is TransferFailure(failed_contract), the ft_contract parameter must match failed_contract.
    • The user must have a balance of the specified collateral (ft_contract).
  • Actions:
    • Initiates an async transfer of the user’s full balance of ft_contract to the liquidator_account.
    • Sets the liquidation_state to OngoingTransfer(ft_contract).
    • The callback updates the state to TransferSuccess or TransferFailure based on the outcome.

finalize_liquidation()

Settles the user’s account after collateral has been swapped and USDC has been returned to the contract.

  • Authorization: Callable by any account, allowing for permissionless retries.
  • Pre-conditions:
    • Asserts that all of the user’s collateral has been transferred out (collateral balances are zero).
    • Asserts that swapped_collateral_value is greater than zero.
  • Actions:
    • Repay Debt: The user’s borrowed shares are deducted from the protocol’s total.
    • Calculate Fees: A liquidation penalty is calculated from swapped_collatal_value and split into liquidator and protocol fees.
    • Calculate Final Balance: net_collateral = swapped_collateral_value - liquidator_fee - protocol_fee final_balance = net_collateral - user_borrow_value
  • Settlement:
    • Surplus (final_balance > 0): Fees are paid, and the remaining surplus is sent to the liquidated user.
    • Bad Debt (final_balance < 0): Fees are paid from the swapped collateral. The shortfall is socialized among lenders by reducing the supply index.
  • Cleanup: The user’s in_flight_operation is cleared and the LiquidationInfo is removed.

Security Considerations

  • A 1 yoctoNEAR deposit should be required for these calls to prevent call-based attacks (see NEAR documentation).