mango-v4/ts/client/src/scripts/mb-liqtest-make-candidates.ts

531 lines
13 KiB
TypeScript

import { AnchorProvider, BN, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import fs from 'fs';
import { Bank } from '../accounts/bank';
import { MangoAccount } from '../accounts/mangoAccount';
import { PerpMarket, PerpOrderSide, PerpOrderType } from '../accounts/perp';
import {
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '../accounts/serum3';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
//
// This script creates liquidation candidates
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 200);
// native prices
const PRICES = {
ETH: 1200.0,
SOL: 0.015,
USDC: 1,
MNGO: 0.02,
};
const TOKEN_SCENARIOS: [string, [string, number][], [string, number][]][] = [
[
'LIQTEST, FUNDING',
[
['USDC', 5000000],
['ETH', 100000],
['SOL', 150000000],
],
[],
],
['LIQTEST, LIQOR', [['USDC', 1000000]], []],
['LIQTEST, A: USDC, L: SOL', [['USDC', 1000 * PRICES.SOL]], [['SOL', 920]]],
['LIQTEST, A: SOL, L: USDC', [['SOL', 1000]], [['USDC', 990 * PRICES.SOL]]],
[
'LIQTEST, A: ETH, L: SOL',
[['ETH', 20]],
[['SOL', (18 * PRICES.ETH) / PRICES.SOL]],
],
];
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL!, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
),
),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
{
idsSource: 'get-program-accounts',
},
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
const MAINNET_MINTS = new Map([
['USDC', group.banksMapByName.get('USDC')![0].mint],
['ETH', group.banksMapByName.get('ETH')![0].mint],
['SOL', group.banksMapByName.get('SOL')![0].mint],
]);
const accounts = await client.getMangoAccountsForOwner(
group,
admin.publicKey,
);
let maxAccountNum = Math.max(0, ...accounts.map((a) => a.accountNum));
async function createMangoAccount(name: string): Promise<MangoAccount> {
const accountNum = maxAccountNum + 1;
maxAccountNum = maxAccountNum + 1;
await client.createMangoAccount(group, accountNum, name, 4, 4, 4, 4);
return (await client.getMangoAccountForOwner(
group,
admin.publicKey,
accountNum,
))!;
}
async function setBankPrice(bank: Bank, price: number): Promise<void> {
await client.stubOracleSet(group, bank.oracle, price);
// reset stable price
await client.tokenEdit(
group,
bank.mint,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
true,
null,
null,
);
}
async function setPerpPrice(
perpMarket: PerpMarket,
price: number,
): Promise<void> {
await client.stubOracleSet(group, perpMarket.oracle, price);
// reset stable price
await client.perpEditMarket(
group,
perpMarket.perpMarketIndex,
perpMarket.oracle,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
);
}
for (const scenario of TOKEN_SCENARIOS) {
const [name, assets, liabs] = scenario;
// create account
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
for (let [assetName, assetAmount] of assets) {
const assetMint = new PublicKey(MAINNET_MINTS.get(assetName)!);
await client.tokenDepositNative(
group,
mangoAccount,
assetMint,
new BN(assetAmount),
);
await mangoAccount.reload(client);
}
for (let [liabName, liabAmount] of liabs) {
const liabMint = new PublicKey(MAINNET_MINTS.get(liabName)!);
// temporarily drop the borrowed token value, so the borrow goes through
const bank = group.banksMapByName.get(liabName)![0];
try {
await setBankPrice(bank, PRICES[liabName] / 2);
await client.tokenWithdrawNative(
group,
mangoAccount,
liabMint,
new BN(liabAmount),
true,
);
} finally {
// restore the oracle
await setBankPrice(bank, PRICES[liabName]);
}
}
}
const accounts2 = await client.getMangoAccountsForOwner(
group,
admin.publicKey,
);
const fundingAccount = accounts2.find(
(account) => account.name == 'LIQTEST, FUNDING',
);
if (!fundingAccount) {
throw new Error('could not find funding account');
}
// Serum order scenario
{
const name = 'LIQTEST, serum orders';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const market = group.getSerum3MarketByName('SOL/USDC')!;
const sellMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const buyMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
await client.tokenDepositNative(
group,
mangoAccount,
sellMint,
new BN(100000),
);
await mangoAccount.reload(client);
// temporarily up the init asset weight of the bought token
await client.tokenEdit(
group,
buyMint,
group.getFirstBankByMint(buyMint).oracle,
null,
null,
null,
null,
null,
1.0,
1.0,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
);
try {
// At a price of $1/ui-SOL we can buy 0.1 ui-SOL for the 100k native-USDC we have.
// With maint weight of 0.9 we have 10x main-leverage. Buying 12x as much causes liquidation.
await client.serum3PlaceOrder(
group,
mangoAccount,
market.serumMarketExternal,
Serum3Side.bid,
1,
12 * 0.1,
Serum3SelfTradeBehavior.abortTransaction,
Serum3OrderType.limit,
0,
5,
);
} finally {
// restore the weights
await client.tokenEdit(
group,
buyMint,
group.getFirstBankByMint(buyMint).oracle,
null,
null,
null,
null,
null,
0.9,
0.8,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
);
}
}
// Perp orders bring health <0, liquidator force closes
{
const name = 'LIQTEST, perp orders';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const collateralBank = group.banksMapByName.get('SOL')![0];
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
new BN(300000),
); // valued as 0.0003 SOL, $0.0045 maint collateral
await mangoAccount.reload(client);
await setBankPrice(collateralBank, PRICES['SOL'] * 4);
try {
await client.perpPlaceOrder(
group,
mangoAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.bid,
0.001, // ui price that won't get hit
1.1, // ui base quantity, 11 base lots, 1.1 MNGO, $0.022
0.022, // ui quote quantity
4200,
PerpOrderType.limit,
false,
0,
5,
);
} finally {
await setBankPrice(collateralBank, PRICES['SOL']);
}
}
// Perp base pos brings health<0, liquidator takes most of it
{
const name = 'LIQTEST, perp base pos';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const collateralBank = group.banksMapByName.get('SOL')![0];
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
new BN(300000),
); // valued as 0.0003 SOL, $0.0045 maint collateral
await mangoAccount.reload(client);
await setBankPrice(collateralBank, PRICES['SOL'] * 10);
try {
await client.perpPlaceOrder(
group,
fundingAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.ask,
0.03,
1.1, // ui base quantity, 11 base lots, $0.022 value, gain $0.033
0.033, // ui quote quantity
4200,
PerpOrderType.limit,
false,
0,
5,
);
await client.perpPlaceOrder(
group,
mangoAccount,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
PerpOrderSide.bid,
0.03,
1.1, // ui base quantity, 11 base lots, $0.022 value, cost $0.033
0.033, // ui quote quantity
4200,
PerpOrderType.market,
false,
0,
5,
);
await client.perpConsumeAllEvents(
group,
group.perpMarketsMapByName.get('MNGO-PERP')?.perpMarketIndex!,
);
} finally {
await setBankPrice(collateralBank, PRICES['SOL']);
}
}
// borrows and positive perp pnl (but no position)
{
const name = 'LIQTEST, perp positive pnl';
console.log(`Creating mangoaccount...`);
let mangoAccount = await createMangoAccount(name);
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
const perpMarket = group.perpMarketsMapByName.get('MNGO-PERP')!;
const perpIndex = perpMarket.perpMarketIndex;
const liabMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const collateralMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const collateralBank = group.banksMapByName.get('SOL')![0];
await client.tokenDepositNative(
group,
mangoAccount,
collateralMint,
new BN(300000),
); // valued as $0.0045 maint collateral
await mangoAccount.reload(client);
try {
await setBankPrice(collateralBank, PRICES['SOL'] * 10);
// Spot-borrow more than the collateral is worth
await client.tokenWithdrawNative(
group,
mangoAccount,
liabMint,
new BN(-5000),
true,
);
await mangoAccount.reload(client);
// Execute two trades that leave the account with +$0.011 positive pnl
await setPerpPrice(perpMarket, PRICES['MNGO'] / 2);
await client.perpPlaceOrder(
group,
fundingAccount,
perpIndex,
PerpOrderSide.ask,
0.01,
1.1, // ui base quantity, 11 base lots, $0.011
0.011, // ui quote quantity
4200,
PerpOrderType.limit,
false,
0,
5,
);
await client.perpPlaceOrder(
group,
mangoAccount,
perpIndex,
PerpOrderSide.bid,
0.01,
1.1, // ui base quantity, 11 base lots, $0.011
0.011, // ui quote quantity
4200,
PerpOrderType.market,
false,
0,
5,
);
await client.perpConsumeAllEvents(group, perpIndex);
await setPerpPrice(perpMarket, PRICES['MNGO']);
await client.perpPlaceOrder(
group,
fundingAccount,
perpIndex,
PerpOrderSide.bid,
0.02,
1.1, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
4201,
PerpOrderType.limit,
false,
0,
5,
);
await client.perpPlaceOrder(
group,
mangoAccount,
perpIndex,
PerpOrderSide.ask,
0.02,
1.1, // ui base quantity, 11 base lots, $0.022
0.022, // ui quote quantity
4201,
PerpOrderType.market,
false,
0,
5,
);
await client.perpConsumeAllEvents(group, perpIndex);
} finally {
await setPerpPrice(perpMarket, PRICES['MNGO']);
await setBankPrice(collateralBank, PRICES['SOL']);
}
}
process.exit();
}
main();