334 lines
8.2 KiB
TypeScript
334 lines
8.2 KiB
TypeScript
// This tests the liquidator by creating new liqor & liqee account on devnet.3, opening a BTC long, then crashing the oracle price leading to bankruptcy.
|
|
// The test then runs the liquidator for 60s allowing you to observe the output.
|
|
// Running this test requires:
|
|
// - mango-client-v3 is cloned into the same directory as liquidator-v3 to run the keeper and crank
|
|
// - the shared devnet keypair (Cwg...) is present at ~/.config/solana/devnet.json'
|
|
// Note that the liquidator may fail to rebalance after running due to no liquidity on the orderbook.
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import {
|
|
Cluster,
|
|
Config,
|
|
MangoClient,
|
|
sleep,
|
|
QUOTE_INDEX,
|
|
IDS,
|
|
} from '@blockworks-foundation/mango-client';
|
|
import { Account, Commitment, Connection, Keypair } from '@solana/web3.js';
|
|
import { Market } from '@project-serum/serum';
|
|
import { Token, TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
|
import { spawn } from 'child_process';
|
|
import * as path from 'path';
|
|
|
|
async function testPerpLiquidationAndBankruptcy() {
|
|
const cluster = (process.env.CLUSTER || 'devnet') as Cluster;
|
|
const config = new Config(IDS);
|
|
const keypairPath = os.homedir() + '/.config/solana/devnet.json';
|
|
const payer = Keypair.fromSecretKey(
|
|
new Uint8Array(
|
|
JSON.parse(process.env.KEYPAIR || fs.readFileSync(keypairPath, 'utf-8')),
|
|
),
|
|
);
|
|
|
|
const connection = new Connection(
|
|
config.cluster_urls[cluster],
|
|
'processed' as Commitment,
|
|
);
|
|
|
|
const groupIds = config.getGroup(cluster, 'devnet.3');
|
|
if (!groupIds) {
|
|
throw new Error(`Group not found`);
|
|
}
|
|
|
|
const client = new MangoClient(connection, groupIds.mangoProgramId);
|
|
const mangoGroup = await client.getMangoGroup(groupIds.publicKey);
|
|
console.log(mangoGroup.admin.toBase58());
|
|
const perpMarkets = await Promise.all(
|
|
groupIds.perpMarkets.map((perpMarket) => {
|
|
return mangoGroup.loadPerpMarket(
|
|
connection,
|
|
perpMarket.marketIndex,
|
|
perpMarket.baseDecimals,
|
|
perpMarket.quoteDecimals,
|
|
);
|
|
}),
|
|
);
|
|
const spotMarkets = await Promise.all(
|
|
groupIds.spotMarkets.map((spotMarket) => {
|
|
return Market.load(
|
|
connection,
|
|
spotMarket.publicKey,
|
|
undefined,
|
|
groupIds.serumProgramId,
|
|
);
|
|
}),
|
|
);
|
|
|
|
// Run keeper
|
|
const keeper = spawn('yarn', ['keeper'], {
|
|
cwd: path.resolve(__dirname, '../../mango-client-v3/'),
|
|
env: {
|
|
CLUSTER: 'devnet',
|
|
GROUP: 'devnet.3',
|
|
PATH: process.env.PATH,
|
|
},
|
|
});
|
|
// keeper.stdout.on('data', (data) => {
|
|
// console.log(`keeper stdout: ${data}`);
|
|
// });
|
|
keeper.stderr.on('data', (data) => {
|
|
console.error(`keeper stderr: ${data}`);
|
|
});
|
|
|
|
keeper.on('close', (code) => {
|
|
console.log(`keeper exited with code ${code}`);
|
|
});
|
|
|
|
await sleep(10000);
|
|
|
|
// Run crank
|
|
const crank = spawn('yarn', ['crank'], {
|
|
cwd: path.resolve(__dirname, '../../mango-client-v3/'),
|
|
env: {
|
|
CLUSTER: 'devnet',
|
|
GROUP: 'devnet.3',
|
|
PATH: process.env.PATH,
|
|
},
|
|
});
|
|
|
|
crank.stderr.on('data', (data) => {
|
|
console.error(`crank stderr: ${data}`);
|
|
});
|
|
|
|
crank.on('close', (code) => {
|
|
console.log(`crank exited with code ${code}`);
|
|
});
|
|
|
|
await sleep(10000);
|
|
|
|
let cache = await mangoGroup.loadCache(connection);
|
|
const rootBanks = await mangoGroup.loadRootBanks(connection);
|
|
const quoteRootBank = rootBanks[QUOTE_INDEX];
|
|
if (!quoteRootBank) {
|
|
throw new Error('Quote Rootbank Not Found');
|
|
}
|
|
const quoteNodeBanks = await quoteRootBank.loadNodeBanks(connection);
|
|
const quoteTokenInfo = mangoGroup.tokens[QUOTE_INDEX];
|
|
const quoteToken = new Token(
|
|
connection,
|
|
quoteTokenInfo.mint,
|
|
TOKEN_PROGRAM_ID,
|
|
payer,
|
|
);
|
|
const quoteWallet = await quoteToken.getOrCreateAssociatedAccountInfo(
|
|
payer.publicKey,
|
|
);
|
|
|
|
const btcToken = new Token(
|
|
connection,
|
|
mangoGroup.tokens[1].mint,
|
|
TOKEN_PROGRAM_ID,
|
|
payer,
|
|
);
|
|
const btcWallet = await btcToken.getOrCreateAssociatedAccountInfo(
|
|
payer.publicKey,
|
|
);
|
|
|
|
const liqorPk = (await client.initMangoAccount(mangoGroup, payer))!;
|
|
const liqorAccount = await client.getMangoAccount(
|
|
liqorPk,
|
|
mangoGroup.dexProgramId,
|
|
);
|
|
console.log('Created Liqor:', liqorPk.toBase58());
|
|
|
|
const liqeePk = (await client.initMangoAccount(mangoGroup, payer))!;
|
|
const liqeeAccount = await client.getMangoAccount(
|
|
liqeePk,
|
|
mangoGroup.dexProgramId,
|
|
);
|
|
console.log('Created Liqee:', liqeePk.toBase58());
|
|
|
|
const makerPk = (await client.initMangoAccount(mangoGroup, payer))!;
|
|
const makerAccount = await client.getMangoAccount(
|
|
makerPk,
|
|
mangoGroup.dexProgramId,
|
|
);
|
|
console.log('Created Maker:', liqorPk.toBase58());
|
|
|
|
await client.setStubOracle(
|
|
mangoGroup.publicKey,
|
|
mangoGroup.oracles[1],
|
|
payer,
|
|
60000,
|
|
);
|
|
|
|
// await runKeeper();
|
|
console.log('Depositing for liqor');
|
|
await client.deposit(
|
|
mangoGroup,
|
|
liqorAccount,
|
|
payer,
|
|
quoteRootBank.publicKey,
|
|
quoteNodeBanks[0].publicKey,
|
|
quoteNodeBanks[0].vault,
|
|
quoteWallet.address,
|
|
100000,
|
|
);
|
|
console.log('Depositing for liqee');
|
|
await client.deposit(
|
|
mangoGroup,
|
|
liqeeAccount,
|
|
payer,
|
|
rootBanks[1]!.publicKey,
|
|
rootBanks[1]!.nodeBanks[0],
|
|
rootBanks[1]!.nodeBankAccounts[0].vault,
|
|
btcWallet.address,
|
|
1,
|
|
);
|
|
console.log('Depositing for maker');
|
|
await client.deposit(
|
|
mangoGroup,
|
|
makerAccount,
|
|
payer,
|
|
quoteRootBank.publicKey,
|
|
quoteNodeBanks[0].publicKey,
|
|
quoteNodeBanks[0].vault,
|
|
quoteWallet.address,
|
|
100000,
|
|
);
|
|
|
|
// await runKeeper();
|
|
|
|
console.log('Placing maker orders');
|
|
await client.placePerpOrder(
|
|
mangoGroup,
|
|
makerAccount,
|
|
mangoGroup.mangoCache,
|
|
perpMarkets[0],
|
|
payer,
|
|
'sell',
|
|
60000,
|
|
0.0111,
|
|
'limit',
|
|
);
|
|
|
|
await client.placePerpOrder(
|
|
mangoGroup,
|
|
makerAccount,
|
|
mangoGroup.mangoCache,
|
|
perpMarkets[0],
|
|
payer,
|
|
'buy',
|
|
1000,
|
|
10,
|
|
'limit',
|
|
);
|
|
|
|
await client.placeSpotOrder2(
|
|
mangoGroup,
|
|
makerAccount,
|
|
spotMarkets[1],
|
|
payer,
|
|
'buy',
|
|
100,
|
|
3,
|
|
'postOnly',
|
|
);
|
|
|
|
console.log('Placing taker order');
|
|
await client.placePerpOrder(
|
|
mangoGroup,
|
|
liqeeAccount,
|
|
mangoGroup.mangoCache,
|
|
perpMarkets[0],
|
|
payer,
|
|
'buy',
|
|
60000,
|
|
1,
|
|
'market',
|
|
);
|
|
|
|
// await runKeeper();
|
|
await liqeeAccount.reload(connection);
|
|
await liqorAccount.reload(connection);
|
|
|
|
console.log(
|
|
'Liqor base',
|
|
liqorAccount.perpAccounts[1].basePosition.toString(),
|
|
);
|
|
console.log(
|
|
'Liqor quote',
|
|
liqorAccount.perpAccounts[1].quotePosition.toString(),
|
|
);
|
|
console.log(
|
|
'Liqee base',
|
|
liqeeAccount.perpAccounts[1].basePosition.toString(),
|
|
);
|
|
console.log(
|
|
'Liqee quote',
|
|
liqeeAccount.perpAccounts[1].quotePosition.toString(),
|
|
);
|
|
|
|
await client.setStubOracle(
|
|
mangoGroup.publicKey,
|
|
mangoGroup.oracles[1],
|
|
payer,
|
|
100,
|
|
);
|
|
|
|
cache = await mangoGroup.loadCache(connection);
|
|
await liqeeAccount.reload(connection);
|
|
await liqorAccount.reload(connection);
|
|
|
|
console.log(
|
|
'Liqee Maint Health',
|
|
liqeeAccount.getHealthRatio(mangoGroup, cache, 'Maint').toString(),
|
|
);
|
|
console.log(
|
|
'Liqor Maint Health',
|
|
liqorAccount.getHealthRatio(mangoGroup, cache, 'Maint').toString(),
|
|
);
|
|
|
|
// Run the liquidator process for 60s
|
|
const liquidator = spawn('yarn', ['liquidator'], {
|
|
env: {
|
|
CLUSTER: 'devnet',
|
|
GROUP: 'devnet.3',
|
|
KEYPAIR: keypairPath,
|
|
LIQOR_PK: liqorAccount.publicKey.toBase58(),
|
|
ENDPOINT_URL: config.cluster_urls[cluster],
|
|
PATH: process.env.PATH,
|
|
},
|
|
});
|
|
|
|
liquidator.stdout.on('data', (data) => {
|
|
console.log(`Liquidator stdout: ${data}`);
|
|
});
|
|
|
|
liquidator.stderr.on('data', (data) => {
|
|
console.error(`Liquidator stderr: ${data}`);
|
|
});
|
|
|
|
liquidator.on('close', (code) => {
|
|
console.log(`Liquidator exited with code ${code}`);
|
|
});
|
|
|
|
await sleep(60000);
|
|
liquidator.kill();
|
|
|
|
await liqeeAccount.reload(connection);
|
|
await liqorAccount.reload(connection);
|
|
|
|
console.log(
|
|
'Liqee Maint Health',
|
|
liqeeAccount.getHealthRatio(mangoGroup, cache, 'Maint').toString(),
|
|
);
|
|
console.log('Liqee Bankrupt', liqeeAccount.isBankrupt);
|
|
console.log(
|
|
'Liqor Maint Health',
|
|
liqorAccount.getHealthRatio(mangoGroup, cache, 'Maint').toString(),
|
|
);
|
|
}
|
|
|
|
testPerpLiquidationAndBankruptcy();
|