🌊Liquidator
Secure the Zeta platform from over-bankrupt accounts
Last updated
Was this helpful?
Secure the Zeta platform from over-bankrupt accounts
Last updated
Was this helpful?
Github repository: https://github.com/zetamarkets/sdk/tree/main/examples/liquidator-example
How the liquidation process works on Zeta DEX:
An underwater account goes below maintenance margin, and is able to be liquidated.
Liquidators must cancel all open orders for that account before performing liquidations.
Liquidations occur at mark price +/- a liquidation reward for shorts/longs. This reward is for liquidators can be found under .
Liquidators can send in liquidation instructions and proceed to liquidate positions one at a time for a certain party. At each liquidation instruction the smart contract code checks that the user is still below their maintenance margin requirement (partial liquidations).
Unlike the other examples, there are a few separate files. They're pasted below, but it's probably easier to clone our Github repository instead.
require("dotenv").config();
import {
CrossClient,
Exchange,
Network,
utils,
types,
assets,
programTypes,
constants,
} from "@zetamarkets/sdk";
import { PublicKey, Connection, Keypair } from "@solana/web3.js";
import { Wallet } from "@zetamarkets/anchor";
import {
cancelAllActiveOrders,
findAccountsAtRisk,
findLiquidatableAccounts,
liquidateAccounts,
} from "./liquidator-utils";
import * as anchor from "@zetamarkets/anchor";
import { airdropUsdc } from "./utils";
let client: CrossClient;
let scanning: boolean = false;
// Function that will do a few things sequentially.
// 1. Get all margin accounts for the program.
// 2. Cancel all active orders for accounts at risk. (This is required to liquidate an account)
// 3. Naively liquidate all margin accounts at risk up to your own margin account available balance limit.
export async function scanMarginAccounts() {
// Just early exit if previous scan is still running.
if (scanning) {
return;
}
console.log(`Scanning margin accounts...`);
scanning = true;
let marginAccounts: anchor.ProgramAccount[] =
await Exchange.program.account.crossMarginAccount.all();
console.log(`${marginAccounts.length} margin accounts.`);
let accountsAtRisk = await findAccountsAtRisk(marginAccounts);
if (accountsAtRisk.length == 0) {
console.log("No accounts at risk.");
scanning = false;
return;
}
// We need to cancel all orders on accounts that are at risk
// before we are able to liquidate them.
await cancelAllActiveOrders(client, accountsAtRisk);
// Liquidate the accounts that are under water exclusive of initial
// margin as cancelling active orders reduces initial margin to 0.
let liquidatableAccounts: anchor.ProgramAccount[] =
await findLiquidatableAccounts(accountsAtRisk);
await liquidateAccounts(client, liquidatableAccounts);
// Display the latest client state.
await client.updateState();
let clientMarginAccountState =
Exchange.riskCalculator.getCrossMarginAccountState(client.account);
console.log(
`Client margin account state: ${JSON.stringify(clientMarginAccountState)}`
);
scanning = false;
}
async function main() {
let connection: Connection = new Connection(process.env.connection, {
commitment: "confirmed",
disableRetryOnRateLimit: true,
});
// Starting balance for USDC to airdrop.
const startingBalance = 10_000;
// Set the network for the SDK to use.
const network = Network.DEVNET;
// Load user wallet.
const keypair = Keypair.generate();
// You can load from your local keypair as well.
/*
const keypair = Keypair.fromSecretKey(
new Uint8Array(JSON.parse(Buffer.from(process.env.private_key!).toString()))
);
*/
const wallet = new Wallet(keypair);
// Add some solana and airdrop some fake USDC.
if (network == Network.DEVNET) {
// Airdrop 1 SOL.
await connection.requestAirdrop(wallet.publicKey, 1_000_000_000);
await airdropUsdc(wallet.publicKey, startingBalance);
}
const loadExchangeConfig = types.defaultLoadExchangeConfig(
network,
connection,
utils.defaultCommitment(),
0, // Increase if you are getting rate limited on startup.
true
);
// Load the SDK Exchange object.
await Exchange.load(
loadExchangeConfig,
new types.DummyWallet() // Normal clients don't need to use a real wallet for exchange loading.
);
client = await CrossClient.load(
connection,
wallet,
utils.defaultCommitment()
);
// The deposit function for client will initialize a margin account for you
// atomically in the same transaction if you haven't created one already.
await client.deposit(utils.convertDecimalToNativeInteger(startingBalance));
setInterval(
async () => {
try {
await scanMarginAccounts();
} catch (e) {
console.log(`Scan margin account error: ${e}`);
}
},
process.env.check_interval_ms
? parseInt(process.env.check_interval_ms)
: 5000
);
}
main().catch(console.error.bind(console));
import * as anchor from "@zetamarkets/anchor";
import {
constants,
CrossClient,
Exchange,
programTypes,
assets,
} from "@zetamarkets/sdk";
export async function findAccountsAtRisk(
accounts: anchor.ProgramAccount[]
): Promise<anchor.ProgramAccount[]> {
let accountsAtRisk: anchor.ProgramAccount[] = [];
await Promise.all(
accounts.map((account: anchor.ProgramAccount) => {
if (account.account == null) {
return;
}
let marginAccountState =
Exchange.riskCalculator.getCrossMarginAccountState(
account.account as programTypes.CrossMarginAccount
);
if (marginAccountState.availableBalanceInitial >= 0) {
return;
}
console.log(
`[ACCOUNT_AT_RISK] [ACCOUNT]: ${account.publicKey.toString()} [BALANCE]: ${
marginAccountState.balance
} [INITIAL] ${marginAccountState.initialMarginTotal} [MAINTENANCE]: ${
marginAccountState.maintenanceMarginTotal
} [UNREALIZED PNL] ${
marginAccountState.unrealizedPnlTotal
} [AVAILABLE BALANCE INITIAL] ${
marginAccountState.availableBalanceInitial
} [AVAILABLE BALANCE MAINTENANCE] ${
marginAccountState.availableBalanceMaintenance
}`
);
accountsAtRisk.push(account);
})
);
return accountsAtRisk;
}
export async function findLiquidatableAccounts(
accounts: anchor.ProgramAccount[]
): Promise<anchor.ProgramAccount[]> {
let liquidatableAccounts: anchor.ProgramAccount[] = [];
await Promise.all(
accounts.map((account: anchor.ProgramAccount) => {
if (account.account == null) {
return;
}
let marginAccountState =
Exchange.riskCalculator.getCrossMarginAccountState(
account.account as programTypes.CrossMarginAccount
);
// We assume the accounts passed in have had their open orders cancelled.
// Therefore can just use availableBalanceLiquidation which assumes 0 open orders.
if (marginAccountState.availableBalanceMaintenance >= 0) {
return;
}
console.log(
`[LIQUIDATABLE ACCOUNT] [ACCOUNT] ${account.publicKey.toString()} [BALANCE] ${
marginAccountState.balance
} [AVAILABLE BALANCE MAINTENANCE] ${
marginAccountState.availableBalanceMaintenance
}`
);
liquidatableAccounts.push(account);
})
);
return liquidatableAccounts;
}
export async function cancelAllActiveOrders(
client: CrossClient,
accountsAtRisk: anchor.ProgramAccount[]
) {
await Promise.all(
accountsAtRisk.map(async (programAccount) => {
let marginAccount =
programAccount.account as programTypes.CrossMarginAccount;
// If they have any active orders, we can cancel.
for (var asset of Exchange.assets) {
let position =
marginAccount.productLedgers[assets.assetToIndex(asset)].orderState;
if (
position.openingOrders[0].toNumber() != 0 ||
position.openingOrders[1].toNumber() != 0 ||
position.closingOrders.toNumber() != 0
) {
console.log("[FORCE_CANCEL] " + programAccount.publicKey.toString());
try {
await client.forceCancelOrders(asset, programAccount.publicKey);
} catch (e) {
console.log(e);
}
}
}
})
);
}
// Naively liquidates all accounts up to initial margin requirement limits.
export async function liquidateAccounts(
client: CrossClient,
accounts: anchor.ProgramAccount[]
) {
for (var i = 0; i < accounts.length; i++) {
const liquidateeMarginAccount = accounts[i]
.account as programTypes.CrossMarginAccount;
const liquidateeKey = accounts[i].publicKey;
// If an account is underwater then we can liquidate any position
for (var asset of Exchange.assets) {
const position =
liquidateeMarginAccount.productLedgers[
assets.assetToIndex(asset)
].position.size.toNumber();
if (position == 0) {
continue;
}
// Get latest state for your margin account.
await client.updateState();
let clientState = Exchange.riskCalculator.getCrossMarginAccountState(
client.account
);
let marginConstrainedSize = calculateMaxLiquidationNativeSize(
asset,
clientState.availableBalanceInitial,
position > 0
);
const size = Math.min(marginConstrainedSize, Math.abs(position));
const side = position > 0 ? "Bid" : "Ask";
console.log(
"[LIQUIDATE] " +
liquidateeKey.toString() +
" [SIDE] " +
side +
" [AMOUNT] " +
size +
" [MAX CAPACITY WITH MARGIN] " +
marginConstrainedSize +
" [AVAILABLE SIZE] " +
Math.abs(position)
);
try {
let txId = await client.liquidate(asset, liquidateeKey, size);
console.log(`TX ID: ${txId}`);
} catch (e) {
console.log(e);
}
}
}
}
/**
* @param availableBalance Available balance for the liquidator.
* @param marketIndex The market index of the position to be liquidated.
* @param long Whether the liquidatee is long or short.
* @returns native lot size given liquidator available balance.
*/
export function calculateMaxLiquidationNativeSize(
asset: constants.Asset,
availableMargin: number,
long: boolean
): number {
// Initial margin requirement per contract in decimals.
// We use this so you are not at margin requirement limits after liquidation.
let marginRequirements =
Exchange.riskCalculator.getPerpMarginRequirements(asset);
let initialMarginRequirement = long
? marginRequirements.initialLong
: marginRequirements.initialShort;
console.log(asset, availableMargin, initialMarginRequirement);
return parseInt(
(
(availableMargin / initialMarginRequirement) *
Math.pow(10, constants.POSITION_PRECISION)
).toFixed(0)
);
}
import { PublicKey } from "@solana/web3.js";
import fetch from "node-fetch";
export async function airdropUsdc(publicKey: PublicKey, amount: number) {
const body = {
key: publicKey.toString(),
amount,
};
await fetch(`${process.env.server_url}/faucet/USDC`, {
method: "post",
body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
});
}