Skip to content

Liquidation Guide

Danil Shaymurzin ⚡️ edited this page Sep 29, 2025 · 1 revision

EVAA SDK Liquidation Guide

This guide provides comprehensive documentation on how to perform liquidations using the EVAA SDK with both Pyth and Classic Master implementations.

Table of Contents

  1. Overview
  2. Prerequisites
  3. Pyth Master Liquidation
  4. Classic Master Liquidation
  5. Health Factor Calculation
  6. Liquidation Amount Calculation
  7. Transaction Workflow
  8. Error Handling
  9. Best Practices

Overview

Liquidation is a mechanism to maintain protocol solvency by resolving undercollateralized positions. The EVAA SDK supports two types of liquidation:

  • Pyth Master Liquidation: Uses Pyth Network oracles for real-time price data with automatic fee calculation
  • Classic Master Liquidation: Uses classic oracle implementation that automatically switches to spot prices for liquidations

Key Differences Between Pyth and Classic

Feature Pyth Master Classic Master
Price Source Pyth Network feeds Multiple oracle sources with aggregation
Liquidation Pricing Real-time Pyth feeds Automatically uses spot prices via getPricesForLiquidate()
Fee Calculation Dynamic Pyth update fees Fixed liquidation fees
Transaction Routing TON: Pyth Oracle address
Jettons: EVAA Master address
TON/Jetton: EVAA Master address
Price Validation Pyth network validation Multi-oracle consensus
Time Parameters TON: maxPublishTime/minPublishTime
Jetton: publishGap/maxStaleness
Fixed oracle staleness validation

Critical Transaction Routing Rules

⚠️ IMPORTANT: Transaction routing differs between Pyth and Classic implementations:

Pyth Master:

  • TON Liquidations: Transaction MUST be sent to Pyth Oracle address
  • Jetton Liquidations: Transaction MUST be sent to EVAA Master address (via jetton transfer)

Classic Master:

  • All Liquidations: Transaction sent to EVAA Master address

Pyth Time Parameter Usage

Critical: Pyth oracle uses different time validation parameters depending on the liquidation type:

  • TON Liquidations: Uses maxPublishTime and minPublishTime from price collector
  • Jetton Liquidations: Uses publishGap and maxStaleness with constraints:
    • publishGap > 0 && publishGap < maxStaleness
    • maxStaleness <= max oracle TTL in master config

Best Practice: Include both parameter sets for maximum compatibility across liquidation types.

graph TB
    A[Monitor Position] --> B{Check Health Factor}
    B --> |Healthy| A
    B --> |Liquidatable| C[Calculate Liquidation Amounts]
    C --> D[Get Price Data]
    D --> E{Oracle Type}
    E --> |Pyth| F[Get Pyth Prices]
    E --> |Classic| G[Get Classic Prices]
    F --> H[Create Liquidation Message]
    G --> H
    H --> I[Execute Transaction]
    I --> J[Verify Success]
Loading

Prerequisites

Before performing liquidations, ensure you have:

  1. Environment Setup:
import 'dotenv/config';
import { mnemonicToWalletKey } from '@ton/crypto';
import { Address, beginCell, toNano, TonClient, WalletContractV4 } from '@ton/ton';
  1. Required Imports for Pyth:
import {
    calculateHealthParams,
    calculateLiquidationAmounts,
    EvaaMasterPyth,
    getUserJettonWallet,
    JettonWallet,
    JUSDC_MAINNET,
    JUSDT_MAINNET,
    MAINNET_POOL_CONFIG,
    PYTH_ORACLE_MAINNET,
    STTON_MAINNET,
    TSTON_MAINNET,
    USDE_MAINNET,
} from '../src';
  1. Required Imports for Pyth:
import {
    calculateHealthParams,
    calculateLiquidationAmounts,
    EvaaMasterPyth,
    FEES,
    getUserJettonWallet,
    isTonAsset,
    JettonWallet,
    MAINNET_POOL_CONFIG,
    PYTH_ORACLE_MAINNET,
    PythOracle,
    PythPrices,
    STTON_MAINNET,
    TSTON_MAINNET,
} from '../src';
  1. Required Imports for Classic:
import {
    calculateHealthParams,
    calculateLiquidationAmounts,
    ClassicCollector,
    EvaaMasterClassic,
    FEES,
    getUserJettonWallet,
    isTonAsset,
    JettonWallet,
    MAINNET_LP_POOL_CONFIG,
    ORACLES_LP,
    TON_MAINNET,
    USDT_MAINNET,
} from '../src';
  1. Environment Variables:
RPC_API_KEY_MAINNET=your_api_key
MAINNET_WALLET_MNEMONIC=your_wallet_mnemonic

Pyth Master Liquidation

Step 1: Initialize Components

const TON_CLIENT = new TonClient({
    endpoint: 'https://toncenter.com/api/v2/jsonRPC',
    apiKey: process.env.RPC_API_KEY_MAINNET,
});

async function liquidateWithPyth() {
    // Initialize wallet
    const WALLET_KEY_PAIR = await mnemonicToWalletKey(
        process.env.MAINNET_WALLET_MNEMONIC!.split(' ')
    );

    const WALLET_CONTRACT = TON_CLIENT.open(
        WalletContractV4.create({
            workchain: 0,
            publicKey: WALLET_KEY_PAIR.publicKey,
        }),
    );

    // Initialize EVAA Master
    const EVAA_MAINNET = TON_CLIENT.open(
        new EvaaMasterPyth({
            poolConfig: MAINNET_POOL_CONFIG,
            debug: true
        })
    );

    const WALLET_SENDER = {
        address: WALLET_CONTRACT.address,
        send: WALLET_CONTRACT.sender(WALLET_KEY_PAIR.secretKey).send,
    };

Step 2: Sync and Get User Data

// Sync master contract
await EVAA_MAINNET.getSync();

// Open user contract
const borrowerAddress = Address.parse('borroweraddress');
const EVAA_USER_MAINNET = TON_CLIENT.open(EVAA_MAINNET.openUserContract(borrowerAddress, 0));

if (!EVAA_MAINNET.data?.assetsData || !EVAA_MAINNET.data?.assetsConfig) {
    throw new Error('Assets data or config is not available');
}

await EVAA_USER_MAINNET.getSyncLite(EVAA_MAINNET.data?.assetsData, EVAA_MAINNET.data?.assetsConfig);

Step 3: Get Price Data

// Get prices specifically for liquidation (uses real-time Pyth feeds)
if (!EVAA_USER_MAINNET.liteData) {
    throw new Error('User lite data is not available');
}

const pc = await MAINNET_POOL_CONFIG.collector.getPricesForLiquidate(EVAA_USER_MAINNET.liteData.realPrincipals);

// Validate Pyth prices
if (pc instanceof PythPrices == false) {
    throw new Error('Pyth prices are not available');
}

Step 4: Calculate Health Parameters

const health = calculateHealthParams({
    assetsData: EVAA_MAINNET.data.assetsData,
    assetsConfig: EVAA_MAINNET.data.assetsConfig,
    principals: EVAA_USER_MAINNET.liteData!.principals,
    prices: pc.dict,
    poolConfig: MAINNET_POOL_CONFIG,
});

// Check if liquidatable
if (!health.isLiquidatable) {
    console.log('Position is not liquidatable');
    return;
}

Step 5: Calculate Liquidation Amounts

const loanAsset = TSTON_MAINNET;
const collateralAsset = STTON_MAINNET;
const collateralAssetConfig = EVAA_MAINNET.data.assetsConfig.get(collateralAsset.assetId);

const { maxCollateralRewardAmount, maxLiquidationAmount } = calculateLiquidationAmounts(
    loanAsset,
    collateralAsset,
    health.totalSupply,
    health.totalDebt,
    EVAA_USER_MAINNET.liteData!.principals,
    pc.dict,
    EVAA_MAINNET.data.assetsData,
    EVAA_MAINNET.data.assetsConfig,
    MAINNET_POOL_CONFIG.masterConstants,
);

if (!collateralAssetConfig) {
    throw new Error('Collateral asset config is not available');
}

const minCollateralAmount = (maxCollateralRewardAmount * 97n) / 100n - collateralAssetConfig.dust;

Step 6: Create and Send Liquidation Message

    const liqMessage = EVAA_MAINNET.createLiquidationMessage({
        asset: loanAsset,
        borrowerAddress: borrowerAddress,
        collateralAsset: collateralAsset.assetId,
        queryID: 0n,
        includeUserCode: true,
        liquidationAmount: maxLiquidationAmount,
        liquidatorAddress: WALLET_CONTRACT.address,
        loanAsset: loanAsset.assetId,
        minCollateralAmount,
        payload: beginCell().endCell(),
        customPayloadSaturationFlag: false,
        customPayloadRecipient: WALLET_CONTRACT.address,
        subaccountId: 0,
        pyth: {
            targetFeeds: pc.targetFeeds(),
            pythAddress: PYTH_ORACLE_MAINNET,
            priceData: pc.dataCell,
            // For Jetton liquidations: publishGap and maxStaleness are used
            publishGap: 10,        // Must be: 0 < publishGap < maxStaleness
            maxStaleness: 180,     // Must be: maxStaleness <= max oracle TTL in master config
            // For TON liquidations: maxPublishTime and minPublishTime are used
            // You can pass both sets of parameters for compatibility
            refAssets: pc.refAssets(),
            maxPublishTime: pc.maxPublishTime,
            minPublishTime: pc.minPublishTime,
        },
    });

    // Calculate Pyth update fee dynamically
    const pythOracle = TON_CLIENT.open(PythOracle.createFromAddress(PYTH_ORACLE_MAINNET));
    const pythUpdateFee = await pythOracle.getUpdateFee(pc.binaryUpdate());
    const liquidationFee = FEES.LIQUIDATION + pythUpdateFee;

    // CRITICAL: Different transaction routing for Pyth
    if (isTonAsset(loanAsset)) {
        // TON liquidations: Send to Pyth Oracle address
        WALLET_SENDER.send({
            value: liquidationFee,
            to: PYTH_ORACLE_MAINNET,
            body: liqMessage,
        });
    } else {
        // Jetton liquidations: Send via jetton transfer to EVAA Master
        const jettonWallet = TON_CLIENT.open(
            JettonWallet.createFromAddress(
                getUserJettonWallet(WALLET_CONTRACT.address, loanAsset)
            )
        );
        await jettonWallet.sendTransfer(
            WALLET_SENDER,
            liquidationFee + FEES.JETTON_FWD,
            liqMessage
        );
    }

    console.log('Liquidation sent');
}

Classic Master Liquidation

Step 1: Initialize Components

async function liquidateWithClassic() {
    // Initialize wallet (same as Pyth example)
    const WALLET_KEY_PAIR = await mnemonicToWalletKey(
        process.env.MAINNET_WALLET_MNEMONIC!.split(' ')
    );

    const WALLET_CONTRACT = TON_CLIENT.open(
        WalletContractV4.create({
            workchain: 0,
            publicKey: WALLET_KEY_PAIR.publicKey,
        }),
    );

    // Initialize EVAA Master Classic
    const EVAA_MAINNET = TON_CLIENT.open(
        new EvaaMasterClassic({
            poolConfig: MAINNET_LP_POOL_CONFIG,
            debug: true
        })
    );

    const WALLET_SENDER = {
        address: WALLET_CONTRACT.address,
        send: WALLET_CONTRACT.sender(WALLET_KEY_PAIR.secretKey).send,
    };

Step 2: Get Price Data with Classic Collector

await EVAA_MAINNET.getSync();

const borrowerAddress = Address.parse('borrowaddress');
const EVAA_USER_MAINNET = TON_CLIENT.open(EVAA_MAINNET.openUserContract(borrowerAddress, 0));

if (!EVAA_MAINNET.data?.assetsData || !EVAA_MAINNET.data?.assetsConfig) {
    throw new Error('Assets data or config is not available');
}

await EVAA_USER_MAINNET.getSyncLite(EVAA_MAINNET.data?.assetsData, EVAA_MAINNET.data?.assetsConfig);

if (!EVAA_USER_MAINNET.liteData) {
    throw new Error('The user lite data is not available');
}

// Create Classic Collector for price data
const collector = new ClassicCollector({
    poolAssetsConfig: MAINNET_LP_POOL_CONFIG.poolAssetsConfig,
    minimalOracles: 3,
    evaaOracles: ORACLES_LP,
});

// IMPORTANT: getPricesForLiquidate automatically uses SPOT prices for liquidations
// This ensures accurate, real-time pricing for liquidation calculations
const pc = await collector.getPricesForLiquidate(EVAA_USER_MAINNET.liteData.realPrincipals);

Step 3: Calculate Health and Liquidation Amounts

const health = calculateHealthParams({
    assetsData: EVAA_MAINNET.data.assetsData,
    assetsConfig: EVAA_MAINNET.data.assetsConfig,
    principals: EVAA_USER_MAINNET.liteData!.principals,
    prices: pc.dict, // Using spot prices from getPricesForLiquidate
    poolConfig: MAINNET_LP_POOL_CONFIG,
});

const loanAsset = USDT_MAINNET;
const collateralAsset = TON_MAINNET;
const collateralAssetConfig = EVAA_MAINNET.data.assetsConfig.get(collateralAsset.assetId);

const { maxCollateralRewardAmount, maxLiquidationAmount } = calculateLiquidationAmounts(
    loanAsset,
    collateralAsset,
    health.totalSupply,
    health.totalDebt,
    EVAA_USER_MAINNET.liteData!.principals,
    pc.dict, // Using spot prices from getPricesForLiquidate
    EVAA_MAINNET.data.assetsData,
    EVAA_MAINNET.data.assetsConfig,
    MAINNET_LP_POOL_CONFIG.masterConstants,
);

if (!collateralAssetConfig) {
    throw new Error('Collateral asset config is not available');
}

const minCollateralAmount = (maxCollateralRewardAmount * 97n) / 100n - collateralAssetConfig.dust;

Step 4: Create Classic Liquidation Message

    const liqMessage = EVAA_MAINNET.createLiquidationMessage({
        asset: loanAsset,
        borrowerAddress: borrowerAddress,
        collateralAsset: collateralAsset.assetId,
        queryID: 0n,
        includeUserCode: true,
        liquidationAmount: maxLiquidationAmount,
        liquidatorAddress: WALLET_CONTRACT.address,
        loanAsset: loanAsset.assetId,
        minCollateralAmount,
        payload: beginCell().endCell(),
        customPayloadSaturationFlag: false,
        customPayloadRecipient: WALLET_CONTRACT.address,
        subaccountId: 0,
        priceData: pc.dataCell, // Use spot price data from getPricesForLiquidate
    });

    // Classic Master: Both TON and Jetton transactions go to EVAA Master
    const liquidationFee = FEES.LIQUIDATION;

    if (isTonAsset(loanAsset)) {
        // TON liquidations: Send directly to EVAA Master
        WALLET_SENDER.send({
            value: liquidationFee,
            to: EVAA_MAINNET.address,
            body: liqMessage,
        });
    } else {
        // Jetton liquidations: Send via jetton transfer to EVAA Master
        const jettonWallet = TON_CLIENT.open(
            JettonWallet.createFromAddress(
                getUserJettonWallet(WALLET_CONTRACT.address, loanAsset)
            )
        );
        await jettonWallet.sendTransfer(
            WALLET_SENDER,
            liquidationFee + FEES.JETTON_FWD,
            liqMessage
        );
    }

    console.log('Liquidation sent');
}

Health Factor Calculation

The health factor determines if a position is liquidatable:

flowchart TD
    A[Start Health Calculation] --> B[Get User Principals]
    B --> C[Loop Through Assets]
    C --> D{Has Principal?}
    D -->|No| E[Next Asset]
    D -->|Yes| F[Calculate Present Value]
    F --> G{Asset Type}
    G -->|Supply| H[Add to Total Supply & Limit]
    G -->|Borrow| I{Above Dust?}
    I -->|Yes| J[Add to Total Debt]
    I -->|No| E
    H --> E
    J --> E
    E --> K{More Assets?}
    K -->|Yes| C
    K -->|No| L[Calculate Health Ratio]
    L --> M{totalLimit < totalDebt?}
    M -->|Yes| N[Liquidatable]
    M -->|No| O[Healthy]
Loading

Key Components:

  • Total Supply: Sum of all supplied assets weighted by collateral factors
  • Total Debt: Sum of all borrowed assets above dust threshold
  • Total Limit: Maximum borrowing capacity based on collateral
  • Liquidatable: When totalLimit < totalDebt

Liquidation Amount Calculation

const { maxCollateralRewardAmount, maxLiquidationAmount } = calculateLiquidationAmounts(
    loanAsset, // Asset being repaid
    collateralAsset, // Asset being seized
    health.totalSupply, // Total collateral value
    health.totalDebt, // Total debt value
    userPrincipals, // User's asset balances
    priceDict, // Current asset prices
    assetsData, // Asset rate data
    assetsConfig, // Asset configuration
    masterConstants, // Protocol constants
);

Calculation Logic:

graph TD
    A[Prepare Asset Info] --> B{Valid Assets?}
    B -->|No| C[Return Zero]
    B -->|Yes| D[Calculate Allowed Collateral]
    D --> E{Bad Debt?}
    E -->|Yes| F[Use Full Collateral]
    E -->|No| G[Constrain to 1/3 or $100]
    F --> H[Calculate Base Value]
    G --> H
    H --> I[Apply Liquidation Bonus]
    H --> J[Apply Reserve Factor]
    I --> K[Convert to Collateral Amount]
    J --> L[Convert to Liquidation Amount]
    K --> M[Return Results]
    L --> M
Loading

Transaction Workflow

Complete Liquidation Flow

sequenceDiagram
    participant Bot as Liquidation Bot
    participant Master as EVAA Master
    participant User as User Contract
    participant Oracle as Price Oracle
    participant Wallet as Jetton Wallet

    Bot->>Master: getSync()
    Master-->>Bot: Pool data synced

    Bot->>User: getSyncLite()
    User-->>Bot: User data retrieved

    Bot->>Oracle: getPrices()
    Oracle-->>Bot: Current prices

    Bot->>Bot: calculateHealthParams()

    alt Position is liquidatable
        Bot->>Bot: calculateLiquidationAmounts()
        Bot->>Master: createLiquidationMessage()
        Master-->>Bot: Liquidation message
        Bot->>Wallet: sendTransfer()
        Wallet-->>Bot: Transaction sent
    else Position is healthy
        Bot->>Bot: Continue monitoring
    end
Loading

Error Handling

Common Error Scenarios

  1. Insufficient Balance:
const balance = await WALLET_CONTRACT.getBalance();
if (balance == 0n) {
    console.log(`Wallet ${WALLET_CONTRACT.address} balance is 0, nothing to do`);
    return;
}
  1. Missing Asset Data:
if (!EVAA_MAINNET.data?.assetsData || !EVAA_MAINNET.data?.assetsConfig) {
    throw new Error('Assets data or config is not available');
}
  1. Invalid User Data:
if (!EVAA_USER_MAINNET.liteData?.realPrincipals) {
    throw new Error('Real principals is not available');
}
  1. Price Data Issues:
try {
    const pc = await MAINNET_POOL_CONFIG.oracles.getPrices(MAINNET_POOL_CONFIG.poolAssetsConfig);
} catch (error) {
    console.error('Failed to fetch price data:', error);
    return;
}

Best Practices

1. Safety Margins

// Apply safety margin to minimum collateral
const minCollateralAmount = (maxCollateralRewardAmount * 97n) / 100n - collateralAssetConfig.dust;

2. Pyth Parameter Configuration

Important: Different Pyth parameters are used for different liquidation types:

// Pyth parameter usage by liquidation type:
pyth: {
    // JETTON LIQUIDATIONS use these parameters:
    publishGap: 10,        // Constraint: 0 < publishGap < maxStaleness
    maxStaleness: 180,     // Constraint: maxStaleness <= max oracle TTL in master config

    // TON LIQUIDATIONS use these parameters:
    maxPublishTime: pc.maxPublishTime,
    minPublishTime: pc.minPublishTime,

    // Recommended: Pass both for compatibility
    targetFeeds: pc.targetFeeds(),
    pythAddress: PYTH_ORACLE_MAINNET,
    priceData: pc.dataCell,
    refAssets: pc.refAssets(),
}

Parameter Constraints:

  • publishGap > 0 && publishGap < maxStaleness
  • maxStaleness <= max oracle TTL in master config

Safe Default Values:

publishGap: 10,
maxStaleness: 180,

5. Error Recovery

try {
    await jettonWallet.sendTransfer(WALLET_SENDER, gasAmount, liqMessage);
    console.log('Liquidation sent successfully');
} catch (error) {
    console.error('Liquidation failed:', error);
    // Implement retry logic or alerting
}

6. Understanding Price Sources

// Pyth: Uses real-time feeds from Pyth Network
const pythPrices = await MAINNET_POOL_CONFIG.collector.getPricesForLiquidate(userRealPrincipals);

// Classic: Automatically uses SPOT prices (not TWAP) for liquidations
const classicPrices = await collector.getPricesForLiquidate(userRealPrincipals);

7. Critical Transaction Routing Validation

// ALWAYS validate asset type and route correctly
if (isTonAsset(loanAsset)) {
    if (isPythMaster) {
        // Route to Pyth Oracle for TON liquidations
        WALLET_SENDER.send({
            value: liquidationFee,
            to: PYTH_ORACLE_MAINNET,
            body: liqMessage,
        });
    } else {
        // Route to EVAA Master for Classic TON liquidations
        WALLET_SENDER.send({
            value: liquidationFee,
            to: EVAA_MAINNET.address,
            body: liqMessage,
        });
    }
} else {
    // Both Pyth and Classic jetton liquidations use jetton transfer
    await jettonWallet.sendTransfer(WALLET_SENDER, totalFee, liqMessage);
}

Summary

This guide provides a comprehensive overview of liquidation implementation using both Pyth and Classic masters. The key differences include:

Price Data:

  • Pyth: Real-time feeds from Pyth Network with dynamic fee calculation
  • Classic: Automatic spot pricing via getPricesForLiquidate() (not TWAP)

Transaction Routing:

  • Pyth TON: Must route to Pyth Oracle address
  • Pyth Jetton: Routes to EVAA Master via jetton transfer
  • Classic TON/Jetton: Routes to EVAA Master address

Fee Structure:

  • Pyth: Dynamic fees based on Pyth update costs
  • Classic: Fixed liquidation fees

Always ensure proper price data collection, transaction routing, and fee calculation for successful liquidations.

If you have any questions, feel free to ask us at EVAA SDK & Liquidation Bot Community