Skip to main content
Liquidation is the enforcement mechanism that resolves an unhealthy account. When an account’s health factor drops below the 1.1x threshold, the owner can call AccountManager.liquidate() to clear all outstanding debt and recover any remaining collateral value. The protocol blocks this call when the account is healthy - it is an emergency exit only.

The Liquidation Guard

Liquidation is gated by the same is_account_healthy() function described in Health Factor: total collateral valuetotal debt value<1.1\frac{\text{total collateral value}}{\text{total debt value}} < 1.1 The liquidate() function calls the Risk Engine directly before taking any action:
if risk_engine_client.is_account_healthy(
    &risk_engine_client.get_current_total_balance(&smart_account),
    &risk_engine_client.get_current_total_borrows(&smart_account),
) {
    panic!("Cannot liquidate when account is healthy!!");
}
If the account is healthy, the call panics and reverts. Liquidation can only proceed when is_account_healthy() returns false.

Authorization

The liquidate() function requires the account owner’s authorization:
let trader_address = Self::get_trader_address(&env, &smart_account);
trader_address.require_auth();
require_auth() on the trader address means the transaction must include a valid authorization entry from the account owner. This is not a permissionless operation - it requires the trader’s direct participation or a pre-signed delegation. The protocol uses this model because the collateral is swept back to the owner, not to an external caller.

Debt Repayment

For every token the account has borrowed, the Account Manager:
  1. Reads the full outstanding balance from the Lending Pool:
    let liquidate_amount = xlm_client.get_borrow_balance(&smart_account);
    
    Unlike the health check (which uses raw borrow shares), liquidation uses get_borrow_balance() - the full outstanding amount including all accrued interest settled as of the current state.
  2. Clears the debt on the Lending Pool side:
    xlm_client.collect_from(&liquidate_amount, &smart_account);
    
  3. Transfers the corresponding tokens from the Smart Account to the pool and updates internal accounting:
    smart_account_client.remove_borrowed_token_balance(&XLM_SYMBOL, &amount_wad_u128);
    
  4. Removes the token from the account’s borrowed token list if the debt is fully cleared.
This loop runs for every token in the account’s borrowed token list: XLM and USDC. All debt positions are closed in a single transaction - partial liquidation is not supported.

Collateral Sweep

After all debt is cleared, the Account Manager calls:
smart_account_client.sweep_to(&trader_address);
sweep_to() iterates over every token in the Smart Account’s collateral list and transfers the full balance to the specified address - in this case, the account owner.

Tokens covered by sweep

sweep_to() calls remove_collateral_token_bal_internal() for each collateral token. This function contains explicit handlers for XLM and USDC, which transfer the actual token balances:
if token_symbol == XLM_SYMBOL {
    xlm_token.transfer(&this_account, &user_address, &amount_scaled);
} else if token_symbol == USDC_SYMBOL {
    usdc_token.transfer(&this_account, &user_address, &amount_scaled);
}
For Blend yield positions (BLEND_XLM, BLEND_USDC) and Aquarius LP tracking positions (AQ_XLM_USDC), the internal collateral balance is zeroed and the token is removed from the collateral list. However, no token transfer occurs for these positions - the underlying b-tokens held in Blend’s pool and LP positions in Aquarius are not unwound. Their tracking records are cleared from the Smart Account but the assets remain in the external protocols.

No Liquidation Fee

The current implementation contains no fee mechanism. The collect_from() call transfers exactly the amount needed to repay the debt. The sweep_to() call transfers the full remaining collateral balance to the trader. No percentage is deducted for any party.

Account State After Liquidation

The liquidate() function does not call deactivate_account() or registry.close_account(). After liquidation completes:
  • All borrowed token entries are removed from the Smart Account
  • has_debt is set to false when the last borrowed token is removed
  • All collateral balances are zeroed
  • The Smart Account remains in its current activation state in the registry
This differs from close_account(), which explicitly deactivates the Smart Account and updates the Registry. liquidate() is not an account closure - it is a position clear.

settle_account() - The Voluntary Alternative

settle_account() provides a voluntary path to repay all debt without sweeping collateral:
pub fn settle_account(env: Env, smart_account: Address) -> Result<bool, AccountManagerError> {
    let trader_address = Self::get_trader_address(&env, &smart_account);
    trader_address.require_auth();

    let borrowed_tokens = smart_account_contract_client.get_all_borrowed_tokens();
    for tokenx in borrowed_tokens.iter() {
        let token_debt = smart_account_contract_client.get_borrowed_token_debt(&tokenx.clone());
        Self::repay(env.clone(), token_debt, tokenx, smart_account.clone())...
    }
}
settle_account() calls repay() for each borrowed token. Unlike liquidate(), it does not check whether the account is healthy or unhealthy - it can be called at any time. Collateral remains in the Smart Account after settlement; the owner must then call close_account() to recover it.
liquidate()settle_account()
Health checkAccount must be unhealthyNo check
Debt clearedAll borrowed tokensAll borrowed tokens
Collateral returnedSwept to owner immediatelyStays in Smart Account
Account deactivatedNoNo

Full Execution Flow

Liquidation execution flow
  • Health Factor - the invariant that determines when liquidation becomes possible
  • Smart Accounts - the isolated contract that holds collateral and debt
  • Lending Pools - where borrow shares are stored and debt is cleared on repayment
  • Liquidation Guide - user-facing explanation of what liquidation means for a position