From 63280fa992114029f990163ff2231a1d4aa8bd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Sat, 19 Aug 2023 13:45:21 +0100 Subject: [PATCH 01/24] feat: use jup v6 api for liquidating --- apps/alpha-liquidator/package.json | 5 +- apps/alpha-liquidator/src/config.ts | 11 +- apps/alpha-liquidator/src/liquidator.ts | 69 ++++---- apps/alpha-liquidator/src/rpcFetcher.ts | 141 ---------------- apps/alpha-liquidator/src/runLiquidator.ts | 158 ------------------ ...datorOnlyJup.ts => runLiquidatorJupApi.ts} | 11 -- yarn.lock | 14 +- 7 files changed, 58 insertions(+), 351 deletions(-) delete mode 100644 apps/alpha-liquidator/src/rpcFetcher.ts delete mode 100644 apps/alpha-liquidator/src/runLiquidator.ts rename apps/alpha-liquidator/src/{runLiquidatorOnlyJup.ts => runLiquidatorJupApi.ts} (78%) diff --git a/apps/alpha-liquidator/package.json b/apps/alpha-liquidator/package.json index 5b4d1a40f3..b06dc69b43 100644 --- a/apps/alpha-liquidator/package.json +++ b/apps/alpha-liquidator/package.json @@ -10,8 +10,7 @@ "watch": "tsc-watch -p tsconfig.json --onCompilationComplete 'yarn build' --onSuccess 'yarn serve'", "setup": "ts-node src/setup.ts", "inspect": "ts-node src/inspect.ts", - "serve": "pm2-runtime scripts/pm2.config.js", - "start": "ts-node src/runLiquidatorOnlyJup.ts" + "start": "ts-node src/runLiquidatorJupApi.ts" }, "license": "MIT", "dependencies": { @@ -33,7 +32,7 @@ "bn.js": "5.2.1", "cookie": "^0.4.1", "cron": "~2.0.0", - "cross-fetch": "3.1.5", + "cross-fetch": "^4.0.0", "decimal.js": "10.4.2", "dotenv": "^16.0.3", "fetch-retry": "~5.0.3", diff --git a/apps/alpha-liquidator/src/config.ts b/apps/alpha-liquidator/src/config.ts index 0d37adc881..5d1231e9c6 100644 --- a/apps/alpha-liquidator/src/config.ts +++ b/apps/alpha-liquidator/src/config.ts @@ -60,16 +60,21 @@ let envSchema = z.object({ .default("false") .transform((s) => s === "true" || s === "1"), SENTRY_DSN: z.string().optional(), - SLEEP_INTERVAL: z.string().default("10000").transform((s) => parseInt(s, 10)), + SLEEP_INTERVAL: z + .string() + .default("10000") + .transform((s) => parseInt(s, 10)), WALLET_KEYPAIR: z.string().transform((keypairStr) => { if (fs.existsSync(resolveHome(keypairStr))) { return loadKeypair(keypairStr); } else { - console.log(keypairStr); return Keypair.fromSecretKey(new Uint8Array(JSON.parse(keypairStr))); } }), - MIN_LIQUIDATION_AMOUNT_USD_UI: z.string().default("0.1").transform((s) => new BigNumber(s)), + MIN_LIQUIDATION_AMOUNT_USD_UI: z + .string() + .default("0.1") + .transform((s) => new BigNumber(s)), }); type EnvSchema = z.infer; diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 2c7d5aa017..ae27e2cfc4 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -1,4 +1,4 @@ -import { Connection, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { Connection, LAMPORTS_PER_SOL, PublicKey, VersionedTransaction } from "@solana/web3.js"; import { Bank, MarginfiAccount, @@ -11,9 +11,7 @@ import { nativeToUi, NodeWallet, shortenAddress, sleep, uiToNative } from "@mrgn import BigNumber from "bignumber.js"; import { associatedAddress } from "@project-serum/anchor/dist/cjs/utils/token"; import { NATIVE_MINT } from "@solana/spl-token"; -import { Jupiter } from "@jup-ag/core"; import { captureException, captureMessage, env_config } from "./config"; -import JSBI from "jsbi"; import BN from "bn.js"; import { BankMetadataMap, loadBankMetadatas } from "./utils/bankMetadata"; @@ -40,7 +38,6 @@ class Liquidator { readonly account: MarginfiAccount, readonly client: MarginfiClient, readonly wallet: NodeWallet, - readonly jupiter: Jupiter, readonly account_whitelist: PublicKey[] | undefined, readonly account_blacklist: PublicKey[] | undefined ) { @@ -112,30 +109,46 @@ class Liquidator { debug("Swapping %s %s to %s", amountIn, mintIn.toBase58(), mintOut.toBase58()); - const { routesInfos } = await this.jupiter.computeRoutes({ - inputMint: mintIn, - outputMint: mintOut, - amount: JSBI.BigInt(amountIn.toString()), - slippageBps: SLIPPAGE_BPS, - forceFetch: true, - }); + const { data } = await ( + await fetch(`https://quote-api.jup.ag/v6/quote?inputMint=${mintIn.toBase58()}&outputMint=${mintOut.toBase58()}&amount=${amountIn.toString()}&slippageBps=${SLIPPAGE_BPS}`) + ).json(); + + const transactionResponse = await ( + await fetch('https://quote-api.jup.ag/v6/swap', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + // quoteResponse from /quote api + quoteResponse: data, + // user public key to be used for the swap + userPublicKey: this.wallet.publicKey.toString(), + // auto wrap and unwrap SOL. default is true + wrapUnwrapSOL: true, + // feeAccount is optional. Use if you want to charge a fee. feeBps must have been passed in /quote API. + // feeAccount: "fee_account_public_key" + }) + }) + ).json(); - const route = routesInfos[0]; + const { swapTransaction } = transactionResponse; - const { execute } = await this.jupiter.exchange({ routeInfo: route }); + const swapTransactionBuf = Buffer.from(swapTransaction, 'base64'); - const result = await execute(); + const transaction = VersionedTransaction.deserialize(swapTransactionBuf); - // @ts-ignore - if (result.error && false) { - // @ts-ignore - debug("Error: %s", result.error); - // @ts-ignore - throw new Error(result.error); - } + transaction.sign([this.wallet.payer]); + + const rawTransaction = transaction.serialize() + const txid = await this.connection.sendRawTransaction(rawTransaction, { + skipPreflight: true, + maxRetries: 2 + }); + + debug("Swap transaction sent: %s", txid); - // @ts-ignore - debug("Trade successful %s", result.txid); + await this.connection.confirmTransaction(txid); } /** @@ -295,10 +308,10 @@ class Liquidator { const nativeAmount = nativeToUi( mint.equals(NATIVE_MINT) ? Math.max( - (await this.connection.getBalance(this.wallet.publicKey)) - - (ignoreNativeMint ? MIN_SOL_BALANCE / 2 : MIN_SOL_BALANCE), - 0 - ) + (await this.connection.getBalance(this.wallet.publicKey)) - + (ignoreNativeMint ? MIN_SOL_BALANCE / 2 : MIN_SOL_BALANCE), + 0 + ) : 0, 9 ); @@ -384,7 +397,7 @@ class Liquidator { if (lendingAccountToRebalanceExists) { debug("Lending accounts to rebalance:"); lendingAccountToRebalance.forEach(({ bank, assets, liabilities }) => { - debug(`Bank: ${this.getTokenSymbol(bank)}, Assets: ${assets}, Liabilities: ${liabilities}`); + debug(`Bank: ${this.getTokenSymbol(bank)}, Assets: ${assets}, Liabilities: ${liabilities} `); }); } diff --git a/apps/alpha-liquidator/src/rpcFetcher.ts b/apps/alpha-liquidator/src/rpcFetcher.ts deleted file mode 100644 index b5f569d40d..0000000000 --- a/apps/alpha-liquidator/src/rpcFetcher.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Jupiter } from "@jup-ag/core"; -import { connection } from "./utils/connection"; -import { chunkedGetRawMultipleAccountInfos } from "./utils/chunks"; -import { AccountInfo, PublicKey } from "@solana/web3.js"; -import { redis } from "./utils/redis"; -import { deserializeAccountInfosMap } from "./utils/accountInfos"; -import { wait } from "./utils/wait"; -import { ammsToExclude } from "./ammsToExclude"; - -/** - * Fetch the accounts from the RPC server and publish them through redis. - */ -async function main() { - let lastUpdatedData = { - value: process.uptime(), - }; - - async function loadJupiter() { - const jupiter = await Jupiter.load({ - connection, - cluster: "mainnet-beta", - ammsToExclude, - }); - const accountToAmmIdsMap = jupiter.getAccountToAmmIdsMap(); - const ammIdToAmmMap = jupiter.getAmmIdToAmmMap(); - const addressesToFetchSet = new Set(accountToAmmIdsMap.keys()); - - const addressesToFetch = { - set: addressesToFetchSet, - // we have this to prevent doing Array.from in the loop and waste computation - array: Array.from(addressesToFetchSet), - }; - - const [contextSlot, accountInfosMap] = await chunkedGetRawMultipleAccountInfos(connection, addressesToFetch.array); - - const deserializedAccountInfosMap = await deserializeAccountInfosMap(accountInfosMap); - - await redis.set("allAccounts", JSON.stringify(Array.from(accountInfosMap.entries()))); - await redis.set("contextSlot", contextSlot); - - deserializedAccountInfosMap.forEach((value) => { - value.data = Buffer.from(value.data); - value.owner = new PublicKey(value.owner); - }); - - process.send?.("ready"); - - return { - jupiter, - ammIdToAmmMap, - accountToAmmIdsMap, - addressesToFetch, - accountInfosMap, - deserializedAccountInfosMap, - }; - } - - let store = await loadJupiter(); - - setInterval(() => { - if (process.uptime() - lastUpdatedData.value > 30) { - console.error(new Error("Data is not being updated")); - // kill itself and pm2 will restart - process.exit(1); - } - }, 30000); - - while (true) { - try { - const { addressesToFetch, ammIdToAmmMap, accountInfosMap, accountToAmmIdsMap, deserializedAccountInfosMap } = - store; - const updatedAccountInfosMap = new Map>(); - const [contextSlot, newAccountInfosMap] = await chunkedGetRawMultipleAccountInfos( - connection, - addressesToFetch.array - ); - - newAccountInfosMap.forEach((value, key) => { - if (accountInfosMap.get(key)?.data[0] !== value.data[0]) { - // set updatedAccountInfosMap to be sent to main process - updatedAccountInfosMap.set(key, value); - // update accountInfosMap to be used in next iteration - accountInfosMap.set(key, value); - } - }); - - lastUpdatedData.value = process.uptime(); - - if (updatedAccountInfosMap.size > 0) { - let ammIdsToUpdate = new Set(); - - redis.set("allAccounts", JSON.stringify(Array.from(accountInfosMap.entries()))); - redis.set("contextSlot", contextSlot); - - (await deserializeAccountInfosMap(updatedAccountInfosMap)).forEach((value, key) => { - let ammIds = accountToAmmIdsMap.get(key); - value.owner = new PublicKey(value.owner); - - if (ammIds) { - ammIds.forEach((ammId) => { - ammIdsToUpdate.add(ammId); - }); - } - deserializedAccountInfosMap.set(key, value); - }); - - ammIdsToUpdate.forEach((ammId) => { - const amm = ammIdToAmmMap.get(ammId); - if (amm) { - try { - amm.update(deserializedAccountInfosMap); - } catch (e) { - console.error(e); - } - if (amm.hasDynamicAccounts) { - amm.getAccountsForUpdate().forEach((pk) => { - const account = pk.toString(); - const ammsFromMap = accountToAmmIdsMap.get(account) || new Set(); - ammsFromMap.add(amm.id); - accountToAmmIdsMap.set(account, ammsFromMap); - if (!addressesToFetch.set.has(account)) { - addressesToFetch.set.add(account); - addressesToFetch.array.push(account); - } - }); - } - } - }); - } - await wait(2_000); - } catch (e) { - console.error(e); - process.exit(1); - } - } -} - -main().catch((e) => { - console.error(e); - process.exit(1); -}); diff --git a/apps/alpha-liquidator/src/runLiquidator.ts b/apps/alpha-liquidator/src/runLiquidator.ts deleted file mode 100644 index 237435a6f3..0000000000 --- a/apps/alpha-liquidator/src/runLiquidator.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Jupiter } from "@jup-ag/core"; -import { AccountInfo, PublicKey } from "@solana/web3.js"; -import { isMainThread, Worker } from "worker_threads"; -import { runGetAccountInfosProcess } from "./getAccountInfosProcess"; -import { ammsToExclude } from "./ammsToExclude"; -import { connection } from "./utils/connection"; -import { NodeWallet } from "@mrgnlabs/mrgn-common"; -import { getConfig, MarginfiAccount, MarginfiClient } from "@mrgnlabs/marginfi-client-v2"; -import { env_config } from "./config"; -import { Liquidator } from "./liquidator"; - -const debug = require("debug")("mfi:liq-scheduler"); - -async function start() { - debug("Jupiter initializing"); - - const wallet = new NodeWallet(env_config.WALLET_KEYPAIR); - - const jupiter = await Jupiter.load({ - connection: connection, - cluster: "mainnet-beta", - routeCacheDuration: env_config.IS_DEV ? 5_000 : -1, - restrictIntermediateTokens: true, - ammsToExclude, - usePreloadedAddressLookupTableCache: true, - user: wallet.payer, - }); - - const accountToAmmIdsMap = jupiter.getAccountToAmmIdsMap(); - const ammIdToAmmMap = jupiter.getAmmIdToAmmMap(); - - debug("Fetching initial blockhash"); - let blockhashWithExpiryBlockHeight = await connection.getLatestBlockhash("confirmed"); - - // each blockhash can last about 1 minute, we refresh every second - setInterval(async () => { - blockhashWithExpiryBlockHeight = await connection.getLatestBlockhash("confirmed"); - }, 1000); - - const store = { - contextSlot: 0, - accountInfos: new Map>(), - }; - - debug("Starting worker"); - const worker = new Worker(__filename); - - worker.on("error", (err) => { - console.error(err); - process.exit(1); - }); - - worker.on("exit", () => { - debug("worker exited"); - process.exit(1); - }); - - // wait until the worker is ready` - let resolved = false; - await new Promise((resolve) => { - if (env_config.IS_DEV) resolve(); // The fetcher does not run in local dev mode - worker.on( - "message", - ({ - type, - contextSlot, - accountInfosMap, - }: { - type: string; - contextSlot: number; - accountInfosMap: Map>; - }) => { - store.contextSlot = contextSlot; - - // We are only updating the contextSlot. - if (type === "contextSlot") { - return; - } - - const ammsIdsToUpdate = new Set(); - - accountInfosMap.forEach((value, key) => { - const ammIds = accountToAmmIdsMap.get(key); - ammIds?.forEach((ammId) => { - ammsIdsToUpdate.add(ammId); - }); - - const accountInfo = store.accountInfos.get(key); - - // Hack to turn back the Uint8Array into a buffer so nothing unexpected occurs downstream - const newData = Buffer.from(value.data); - - if (accountInfo) { - accountInfo.data = newData; - store.accountInfos.set(key, accountInfo); - } else { - value.data = newData; - value.owner = new PublicKey(value.owner); - store.accountInfos.set(key, value); - } - }); - - // For most amms we would receive multiple accounts at once, we should update only once - ammsIdsToUpdate.forEach((ammId) => { - const amm = ammIdToAmmMap.get(ammId); - - if (amm) { - try { - amm.update(store.accountInfos); - } catch (e) { - console.error(`Failed to update amm ${amm.id}, reason ${e}`); - } - if (amm.hasDynamicAccounts) { - amm.getAccountsForUpdate().forEach((pk) => { - const account = pk.toString(); - const ammIds = accountToAmmIdsMap.get(account) || new Set(); - ammIds.add(amm.id); - accountToAmmIdsMap.set(account, ammIds); - }); - } - } - }); - - if (!resolved) { - resolve(); - } - } - ); - }); - - const config = getConfig(env_config.MRGN_ENV); - const client = await MarginfiClient.fetch(config, wallet, connection); - - const liquidatorAccount = await MarginfiAccount.fetch(env_config.LIQUIDATOR_PK, client); - const liquidator = new Liquidator( - connection, - liquidatorAccount, - client, - wallet, - jupiter, - env_config.MARGINFI_ACCOUNT_WHITELIST, - env_config.MARGINFI_ACCOUNT_BLACKLIST - ); - await liquidator.start(); -} - -if (isMainThread) { - console.log("Starting liquidator main thread"); - start().catch((e) => { - console.log(e); - process.exit(1); - }); -} else { - runGetAccountInfosProcess().catch((e) => { - console.log(e); - process.exit(1); - }); -} diff --git a/apps/alpha-liquidator/src/runLiquidatorOnlyJup.ts b/apps/alpha-liquidator/src/runLiquidatorJupApi.ts similarity index 78% rename from apps/alpha-liquidator/src/runLiquidatorOnlyJup.ts rename to apps/alpha-liquidator/src/runLiquidatorJupApi.ts index aeace3b864..146f6d1fbd 100644 --- a/apps/alpha-liquidator/src/runLiquidatorOnlyJup.ts +++ b/apps/alpha-liquidator/src/runLiquidatorJupApi.ts @@ -10,16 +10,6 @@ async function start() { console.log("Initializing"); const wallet = new NodeWallet(env_config.WALLET_KEYPAIR); - const jupiter = await Jupiter.load({ - connection: connection, - cluster: "mainnet-beta", - routeCacheDuration: 5_000, - restrictIntermediateTokens: true, - ammsToExclude, - usePreloadedAddressLookupTableCache: true, - user: wallet.payer, - }); - const config = getConfig(env_config.MRGN_ENV); const client = await MarginfiClient.fetch(config, wallet, connection); @@ -29,7 +19,6 @@ async function start() { liquidatorAccount, client, wallet, - jupiter, env_config.MARGINFI_ACCOUNT_WHITELIST, env_config.MARGINFI_ACCOUNT_BLACKLIST ); diff --git a/yarn.lock b/yarn.lock index c9bd0a64c3..9e501e91c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9721,13 +9721,6 @@ cross-fetch@3.0.6: dependencies: node-fetch "2.6.1" -cross-fetch@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - cross-fetch@3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" @@ -9742,6 +9735,13 @@ cross-fetch@^3.1.4, cross-fetch@^3.1.5: dependencies: node-fetch "^2.6.12" +cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" From f33bcdecdc37c247b25fd23657ef20516147589b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Mon, 21 Aug 2023 14:27:18 +0200 Subject: [PATCH 02/24] fix: correctly use api --- apps/alpha-liquidator/src/liquidator.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index ae27e2cfc4..a1e3bbd965 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -109,9 +109,9 @@ class Liquidator { debug("Swapping %s %s to %s", amountIn, mintIn.toBase58(), mintOut.toBase58()); - const { data } = await ( - await fetch(`https://quote-api.jup.ag/v6/quote?inputMint=${mintIn.toBase58()}&outputMint=${mintOut.toBase58()}&amount=${amountIn.toString()}&slippageBps=${SLIPPAGE_BPS}`) - ).json(); + const swapUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${mintIn.toBase58()}&outputMint=${mintOut.toBase58()}&amount=${amountIn.toString()}&slippageBps=${SLIPPAGE_BPS}`; + const quoteApiResponse = await fetch(swapUrl); + const data = await quoteApiResponse.json(); const transactionResponse = await ( await fetch('https://quote-api.jup.ag/v6/swap', { @@ -135,7 +135,6 @@ class Liquidator { const { swapTransaction } = transactionResponse; const swapTransactionBuf = Buffer.from(swapTransaction, 'base64'); - const transaction = VersionedTransaction.deserialize(swapTransactionBuf); transaction.sign([this.wallet.payer]); From 7e8279faf3492a2bbaceb0f1f7a5675ab59c8c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Mon, 11 Sep 2023 03:44:27 +0900 Subject: [PATCH 03/24] fix: use 1.4M cus --- packages/marginfi-client-v2/src/account.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/marginfi-client-v2/src/account.ts b/packages/marginfi-client-v2/src/account.ts index f01228f27c..8a0a826138 100644 --- a/packages/marginfi-client-v2/src/account.ts +++ b/packages/marginfi-client-v2/src/account.ts @@ -806,7 +806,7 @@ export class MarginfiAccount { */ public getMaxWithdrawForBank(bankPk: PublicKey, volatilityFactor: number = 1): BigNumber { const bank = this._group.getBankByPk(bankPk); - if (!bank) throw Error(`Bank ${bankPk.toBase58()} not found`) + if (!bank) throw Error(`Bank ${bankPk.toBase58()} not found`); const assetWeight = bank.getAssetWeight(MarginRequirementType.Init); const balance = this.getBalance(bank.publicKey); @@ -881,7 +881,10 @@ export class MarginfiAccount { assetQuantityUi, liabBank ); - const tx = new Transaction().add(...ixw.instructions, ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 })); + const tx = new Transaction().add( + ...ixw.instructions, + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }) + ); return this.client.processTransaction(tx); } From 3a68a884b81c3aecaae27637f47d11514fd03694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Mon, 11 Sep 2023 04:00:40 +0900 Subject: [PATCH 04/24] fix: rm deprecated package --- packages/marginfi-client-v2/src/account.ts | 1165 -------------------- 1 file changed, 1165 deletions(-) delete mode 100644 packages/marginfi-client-v2/src/account.ts diff --git a/packages/marginfi-client-v2/src/account.ts b/packages/marginfi-client-v2/src/account.ts deleted file mode 100644 index 8a0a826138..0000000000 --- a/packages/marginfi-client-v2/src/account.ts +++ /dev/null @@ -1,1165 +0,0 @@ -import { - Amount, - aprToApy, - createAssociatedTokenAccountIdempotentInstruction, - createCloseAccountInstruction, - createSyncNativeInstruction, - DEFAULT_COMMITMENT, - InstructionsWrapper, - NATIVE_MINT, - nativeToUi, - shortenAddress, - uiToNative, - WrappedI80F48, - wrappedI80F48toBigNumber, -} from "@mrgnlabs/mrgn-common"; -import { Address, BN, BorshCoder, translateAddress } from "@project-serum/anchor"; -import { associatedAddress } from "@project-serum/anchor/dist/cjs/utils/token"; -import { - AccountInfo, - AccountMeta, - Commitment, - ComputeBudgetProgram, - PublicKey, - SystemProgram, - Transaction, - TransactionInstruction, -} from "@solana/web3.js"; -import BigNumber from "bignumber.js"; -import { MarginfiClient } from "."; -import { Bank, BankData, getOraclePriceData, PriceBias } from "./bank"; -import MarginfiGroup from "./group"; -import { MARGINFI_IDL } from "./idl"; -import instructions from "./instructions"; -import { AccountType, MarginfiConfig, MarginfiProgram } from "./types"; - -/** - * Wrapper class around a specific marginfi account. - */ -export class MarginfiAccount { - public readonly publicKey: PublicKey; - - private _group: MarginfiGroup; - private _authority: PublicKey; - private _lendingBalances: Balance[]; - - /** - * @internal - */ - private constructor( - marginfiAccountPk: PublicKey, - readonly client: MarginfiClient, - group: MarginfiGroup, - rawData: MarginfiAccountData - ) { - this.publicKey = marginfiAccountPk; - - this._group = group; - this._authority = rawData.authority; - - this._lendingBalances = rawData.lendingAccount.balances.map((la) => new Balance(la)); - } - - // --- Getters / Setters - - /** - * Marginfi account authority address - */ - get authority(): PublicKey { - return this._authority; - } - - /** - * Marginfi group address - */ - get group(): MarginfiGroup { - return this._group; - } - - /** - * Marginfi group address - */ - get activeBalances(): Balance[] { - return this._lendingBalances.filter((la) => la.active); - } - - /** @internal */ - private get _program() { - return this.client.program; - } - - /** @internal */ - private get _config() { - return this.client.config; - } - - // --- Factories - - /** - * MarginfiAccount network factory - * - * Fetch account data according to the config and instantiate the corresponding MarginfiAccount. - * - * @param marginfiAccountPk Address of the target account - * @param client marginfi client - * @param commitment Commitment level override - * @returns MarginfiAccount instance - */ - static async fetch( - marginfiAccountPk: Address, - client: MarginfiClient, - commitment?: Commitment - ): Promise { - const { config, program } = client; - const _marginfiAccountPk = translateAddress(marginfiAccountPk); - - const accountData = await MarginfiAccount._fetchAccountData(_marginfiAccountPk, config, program, commitment); - - const marginfiAccount = new MarginfiAccount( - _marginfiAccountPk, - client, - await MarginfiGroup.fetch(config, program, commitment), - accountData - ); - - require("debug")("mfi:margin-account")("Loaded marginfi account %s", _marginfiAccountPk); - - return marginfiAccount; - } - - /** - * MarginfiAccount local factory (decoded) - * - * Instantiate a MarginfiAccount according to the provided decoded data. - * Check sanity against provided config. - * - * @param marginfiAccountPk Address of the target account - * @param client marginfi client - * @param accountData Decoded marginfi marginfi account data - * @param marginfiGroup MarginfiGroup instance - * @returns MarginfiAccount instance - */ - static fromAccountData( - marginfiAccountPk: Address, - client: MarginfiClient, - accountData: MarginfiAccountData, - marginfiGroup: MarginfiGroup - ) { - if (!accountData.group.equals(client.config.groupPk)) - throw Error( - `Marginfi account tied to group ${accountData.group.toBase58()}. Expected: ${client.config.groupPk.toBase58()}` - ); - - const _marginfiAccountPk = translateAddress(marginfiAccountPk); - - return new MarginfiAccount(_marginfiAccountPk, client, marginfiGroup, accountData); - } - - /** - * MarginfiAccount local factory (encoded) - * - * Instantiate a MarginfiAccount according to the provided encoded data. - * Check sanity against provided config. - * - * @param marginfiAccountPk Address of the target account - * @param client marginfi client - * @param marginfiAccountRawData Encoded marginfi marginfi account data - * @param marginfiGroup MarginfiGroup instance - * @returns MarginfiAccount instance - */ - static fromAccountDataRaw( - marginfiAccountPk: PublicKey, - client: MarginfiClient, - marginfiAccountRawData: Buffer, - marginfiGroup: MarginfiGroup - ) { - const marginfiAccountData = MarginfiAccount.decode(marginfiAccountRawData); - - return MarginfiAccount.fromAccountData(marginfiAccountPk, client, marginfiAccountData, marginfiGroup); - } - - /** - * Create transaction instruction to deposit collateral into the marginfi account. - * - * @param amount Amount to deposit (UI unit) - * @param bank Bank to deposit to - * @returns `MarginDepositCollateral` transaction instruction - */ - async makeDepositIx(amount: Amount, bank: Bank): Promise { - const userTokenAtaPk = await associatedAddress({ - mint: bank.mint, - owner: this.client.provider.wallet.publicKey, - }); - - const remainingAccounts = this.getHealthCheckAccounts([bank]); - - const ix = await instructions.makeDepositIx( - this._program, - { - marginfiGroupPk: this.group.publicKey, - marginfiAccountPk: this.publicKey, - authorityPk: this.client.provider.wallet.publicKey, - signerTokenAccountPk: userTokenAtaPk, - bankPk: bank.publicKey, - }, - { amount: uiToNative(amount, bank.mintDecimals) }, - remainingAccounts - ); - - return { - instructions: bank.mint.equals(NATIVE_MINT) ? await this.wrapInstructionForWSol(ix, amount) : [ix], - keys: [], - }; - } - - /** - * Deposit collateral into the marginfi account. - * - * @param amount Amount to deposit (UI unit) - * @param bank Bank to deposit to - * @returns Transaction signature - */ - async deposit(amount: Amount, bank: Bank): Promise { - const debug = require("debug")(`mfi:margin-account:${this.publicKey.toString()}:deposit`); - - debug("Depositing %s %s into marginfi account", amount, bank.mint); - const ixs = await this.makeDepositIx(amount, bank); - const tx = new Transaction().add(...ixs.instructions); - const sig = await this.client.processTransaction(tx, []); - debug("Depositing successful %s", sig); - await this.reload(); - return sig; - } - - /** - * Create transaction instruction to deposit collateral into the marginfi account. - * - * @param amount Amount to deposit (UI unit) - * @param bank Bank to deposit to - * @param repayAll (optional) Repay all the liability - * @returns `LendingPool` transaction instruction - */ - async makeRepayIx(amount: Amount, bank: Bank, repayAll: boolean = false): Promise { - const userTokenAtaPk = await associatedAddress({ - mint: bank.mint, - owner: this.client.provider.wallet.publicKey, - }); - - const remainingAccounts = repayAll - ? this.getHealthCheckAccounts([], [bank]) - : this.getHealthCheckAccounts([bank], []); - - const ix = await instructions.makeRepayIx( - this._program, - { - marginfiGroupPk: this.group.publicKey, - marginfiAccountPk: this.publicKey, - authorityPk: this.client.provider.wallet.publicKey, - signerTokenAccountPk: userTokenAtaPk, - bankPk: bank.publicKey, - }, - { amount: uiToNative(amount, bank.mintDecimals), repayAll }, - remainingAccounts - ); - - return { - instructions: bank.mint.equals(NATIVE_MINT) ? await this.wrapInstructionForWSol(ix, amount) : [ix], - keys: [], - }; - } - - /** - * Deposit collateral into the marginfi account. - * - * @param amount Amount to deposit (UI unit) - * @param bank Bank to deposit to - * @param repayAll (optional) Repay all the liability - * @returns Transaction signature - */ - async repay(amount: Amount, bank: Bank, repayAll: boolean = false): Promise { - const debug = require("debug")(`mfi:margin-account:${this.publicKey.toString()}:repay`); - debug("Repaying %s %s into marginfi account, repay all: %s", amount, bank.mint, repayAll); - - let ixs = []; - - if (this.activeBalances.length >= 4) { - ixs.push(ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 })); - } - - if (repayAll && !bank.emissionsMint.equals(PublicKey.default)) { - const userAta = await associatedAddress({ - mint: bank.emissionsMint, - owner: this.client.provider.wallet.publicKey, - }); - const createAtaIdempotentIx = createAssociatedTokenAccountIdempotentInstruction( - this.client.provider.wallet.publicKey, - userAta, - this.client.provider.wallet.publicKey, - bank.emissionsMint - ); - - ixs.push(createAtaIdempotentIx); - ixs.push(...(await this.makeWithdrawEmissionsIx(bank)).instructions); - } - - const ixws = await this.makeRepayIx(amount, bank, repayAll); - ixs.push(...ixws.instructions); - - const tx = new Transaction().add(...ixs); - const sig = await this.client.processTransaction(tx); - debug("Depositing successful %s", sig); - await this.reload(); - return sig; - } - - /** - * Create transaction instruction to withdraw collateral from the marginfi account. - * - * @param amount Amount to withdraw (mint native unit) - * @param bank Bank to withdraw from - * @param withdrawAll (optional) Withdraw all the asset - * @returns `MarginWithdrawCollateral` transaction instruction - */ - async makeWithdrawIx(amount: Amount, bank: Bank, withdrawAll: boolean = false): Promise { - const userTokenAtaPk = await associatedAddress({ - mint: bank.mint, - owner: this.client.provider.wallet.publicKey, - }); - - const remainingAccounts = withdrawAll - ? this.getHealthCheckAccounts([], [bank]) - : this.getHealthCheckAccounts([bank], []); - - const ix = await instructions.makeWithdrawIx( - this._program, - { - marginfiGroupPk: this.group.publicKey, - marginfiAccountPk: this.publicKey, - signerPk: this.client.provider.wallet.publicKey, - bankPk: bank.publicKey, - destinationTokenAccountPk: userTokenAtaPk, - }, - { amount: uiToNative(amount, bank.mintDecimals), withdrawAll }, - remainingAccounts - ); - - return { instructions: bank.mint.equals(NATIVE_MINT) ? await this.wrapInstructionForWSol(ix) : [ix], keys: [] }; - } - - /** - * Withdraw collateral from the marginfi account. - * - * @param amount Amount to withdraw (UI unit) - * @param bank Bank to withdraw from - * @param withdrawAll (optional) Withdraw all the asset - * @returns Transaction signature - */ - async withdraw(amount: Amount, bank: Bank, withdrawAll: boolean = false): Promise { - const debug = require("debug")(`mfi:margin-account:${this.publicKey.toString()}:withdraw`); - debug("Withdrawing %s from marginfi account", amount); - - let ixs = []; - - if (this.activeBalances.length >= 4) { - ixs.push(ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 })); - } - - if (withdrawAll && !bank.emissionsMint.equals(PublicKey.default)) { - const userAta = await associatedAddress({ - mint: bank.emissionsMint, - owner: this.client.provider.wallet.publicKey, - }); - const createAtaIdempotentIx = createAssociatedTokenAccountIdempotentInstruction( - this.client.provider.wallet.publicKey, - userAta, - this.client.provider.wallet.publicKey, - bank.emissionsMint - ); - - ixs.push(createAtaIdempotentIx); - ixs.push(...(await this.makeWithdrawEmissionsIx(bank)).instructions); - } - - const userAta = await associatedAddress({ - mint: bank.mint, - owner: this.client.provider.wallet.publicKey, - }); - const createAtaIdempotentIx = createAssociatedTokenAccountIdempotentInstruction( - this.client.provider.wallet.publicKey, - userAta, - this.client.provider.wallet.publicKey, - bank.mint - ); - ixs.push(createAtaIdempotentIx); - - const ixw = await this.makeWithdrawIx(amount, bank, withdrawAll); - ixs.push(...ixw.instructions); - - const tx = new Transaction().add(...ixs); - const sig = await this.client.processTransaction(tx); - debug("Withdrawing successful %s", sig); - await this.reload(); - return sig; - } - - /** - * Create transaction instruction to withdraw collateral from the marginfi account. - * - * @param amount Amount to withdraw (mint native unit) - * @param bank Bank to withdraw from - * @returns `MarginWithdrawCollateral` transaction instruction - */ - async makeBorrowIx( - amount: Amount, - bank: Bank, - opt?: { remainingAccountsBankOverride?: Bank[] } | undefined - ): Promise { - const userTokenAtaPk = await associatedAddress({ - mint: bank.mint, - owner: this.client.provider.wallet.publicKey, - }); - - const remainingAccounts = this.getHealthCheckAccounts( - (opt?.remainingAccountsBankOverride?.length ?? 0) > 0 ? opt?.remainingAccountsBankOverride : [bank] - ); - - const ix = await instructions.makeBorrowIx( - this._program, - { - marginfiGroupPk: this.group.publicKey, - marginfiAccountPk: this.publicKey, - signerPk: this.client.provider.wallet.publicKey, - bankPk: bank.publicKey, - destinationTokenAccountPk: userTokenAtaPk, - }, - { amount: uiToNative(amount, bank.mintDecimals) }, - remainingAccounts - ); - - return { instructions: bank.mint.equals(NATIVE_MINT) ? await this.wrapInstructionForWSol(ix) : [ix], keys: [] }; - } - - /** - * Withdraw collateral from the marginfi account. - * - * @param amount Amount to withdraw (UI unit) - * @param bank Bank to withdraw from - * @returns Transaction signature - */ - async borrow(amount: Amount, bank: Bank): Promise { - const debug = require("debug")(`mfi:margin-account:${this.publicKey.toString()}:borrow`); - debug("Borrowing %s from marginfi account", amount); - - let ixs = []; - - if (this.activeBalances.length >= 4) { - ixs.push(ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 })); - } - - const userAta = await associatedAddress({ - mint: bank.mint, - owner: this.client.provider.wallet.publicKey, - }); - const createAtaIdempotentIx = createAssociatedTokenAccountIdempotentInstruction( - this.client.provider.wallet.publicKey, - userAta, - this.client.provider.wallet.publicKey, - bank.mint - ); - ixs.push(createAtaIdempotentIx); - - const ixw = await this.makeBorrowIx(amount, bank); - ixs.push(...ixw.instructions); - - const tx = new Transaction().add(...ixs); - const sig = await this.client.processTransaction(tx); - debug("Borrowing successful %s", sig); - await this.reload(); - return sig; - } - - async makeWithdrawEmissionsIx(bank: Bank): Promise { - const userAta = await associatedAddress({ - mint: bank.emissionsMint, - owner: this.client.provider.wallet.publicKey, - }); - const ix = await instructions.makelendingAccountWithdrawEmissionIx(this._program, { - marginfiGroup: this.group.publicKey, - marginfiAccount: this.publicKey, - signer: this.client.provider.wallet.publicKey, - bank: bank.publicKey, - destinationTokenAccount: userAta, - emissionsMint: bank.emissionsMint, - }); - - return { instructions: [ix], keys: [] }; - } - - async withdrawEmissions(bank: Bank): Promise { - const tx = new Transaction(); - const userAta = await associatedAddress({ - mint: bank.emissionsMint, - owner: this.client.provider.wallet.publicKey, - }); - const createAtaIdempotentIx = createAssociatedTokenAccountIdempotentInstruction( - this.client.provider.wallet.publicKey, - userAta, - this.client.provider.wallet.publicKey, - bank.emissionsMint - ); - - tx.add(createAtaIdempotentIx); - tx.add(...(await this.makeWithdrawEmissionsIx(bank)).instructions); - - const sig = await this.client.processTransaction(tx); - await this.reload(); - return sig; - } - - // --- Others - - getHealthCheckAccounts(mandatoryBanks: Bank[] = [], excludedBanks: Bank[] = []): AccountMeta[] { - const mandatoryBanksSet = new Set(mandatoryBanks.map((b) => b.publicKey.toBase58())); - const excludedBanksSet = new Set(excludedBanks.map((b) => b.publicKey.toBase58())); - const activeBanks = new Set(this.activeBalances.map((b) => b.bankPk.toBase58())); - const banksToAdd = new Set([...mandatoryBanksSet].filter((x) => !activeBanks.has(x))); - - let slotsToKeep = banksToAdd.size; - return this._lendingBalances - .filter((balance) => { - if (balance.active) { - return !excludedBanksSet.has(balance.bankPk.toBase58()); - } else if (slotsToKeep > 0) { - slotsToKeep--; - return true; - } else { - return false; - } - }) - .map((balance) => { - if (balance.active) { - return balance.bankPk.toBase58(); - } - const newBank = [...banksToAdd.values()][0]; - banksToAdd.delete(newBank); - return newBank; - }) - .flatMap((bankPk) => { - const bank = this._group.getBankByPk(bankPk); - if (bank === null) throw Error(`Could not find bank ${bankPk}`); - return [ - { - pubkey: new PublicKey(bankPk), - isSigner: false, - isWritable: false, - }, - { - pubkey: bank.config.oracleKeys[0], - isSigner: false, - isWritable: false, - }, - ]; - }); - } - - /** - * Fetch marginfi account data. - * Check sanity against provided config. - * - * @param accountAddress account address - * @param config marginfi config - * @param program marginfi Anchor program - * @param commitment commitment override - * @returns Decoded marginfi account data struct - */ - private static async _fetchAccountData( - accountAddress: Address, - config: MarginfiConfig, - program: MarginfiProgram, - commitment?: Commitment - ): Promise { - const mergedCommitment = commitment ?? program.provider.connection.commitment ?? DEFAULT_COMMITMENT; - - const data: MarginfiAccountData = (await program.account.marginfiAccount.fetch( - accountAddress, - mergedCommitment - )) as any; - - if (!data.group.equals(config.groupPk)) - throw Error(`Marginfi account tied to group ${data.group.toBase58()}. Expected: ${config.groupPk.toBase58()}`); - - return data; - } - - /** - * Decode marginfi account data according to the Anchor IDL. - * - * @param encoded Raw data buffer - * @returns Decoded marginfi account data struct - */ - static decode(encoded: Buffer): MarginfiAccountData { - const coder = new BorshCoder(MARGINFI_IDL); - return coder.accounts.decode(AccountType.MarginfiAccount, encoded); - } - - /** - * Decode marginfi account data according to the Anchor IDL. - * - * @param decoded Marginfi account data struct - * @returns Raw data buffer - */ - static async encode(decoded: MarginfiAccountData): Promise { - const coder = new BorshCoder(MARGINFI_IDL); - return await coder.accounts.encode(AccountType.MarginfiAccount, decoded); - } - - /** - * Update instance data by fetching and storing the latest on-chain state. - */ - async reload() { - require("debug")(`mfi:margin-account:${this.publicKey.toBase58().toString()}:loader`)("Reloading account data"); - const [marginfiGroupAi, marginfiAccountAi] = await this._loadGroupAndAccountAi(); - const marginfiAccountData = MarginfiAccount.decode(marginfiAccountAi.data); - if (!marginfiAccountData.group.equals(this._config.groupPk)) - throw Error( - `Marginfi account tied to group ${marginfiAccountData.group.toBase58()}. Expected: ${this._config.groupPk.toBase58()}` - ); - - const bankAccountsData = await this._program.account.bank.all([ - { memcmp: { offset: 8 + 32 + 1, bytes: this._config.groupPk.toBase58() } }, - ]); - - const banks = await Promise.all( - bankAccountsData.map(async (accountData) => { - let bankData = accountData.account as any as BankData; - return new Bank( - accountData.publicKey, - bankData, - await getOraclePriceData( - this._program.provider.connection, - bankData.config.oracleSetup, - bankData.config.oracleKeys - ) - ); - }) - ); - - this._group = MarginfiGroup.fromAccountDataRaw(this._config, this._program, marginfiGroupAi.data, banks); - this._updateFromAccountData(marginfiAccountData); - } - - /** - * Update instance data from provided data struct. - * - * @param data Marginfi account data struct - */ - private _updateFromAccountData(data: MarginfiAccountData) { - this._authority = data.authority; - - this._lendingBalances = data.lendingAccount.balances.map((la) => new Balance(la)); - } - - private async _loadGroupAndAccountAi(): Promise[]> { - const debug = require("debug")(`mfi:margin-account:${this.publicKey.toString()}:loader`); - debug("Loading marginfi account %s, and group %s", this.publicKey, this._config.groupPk); - - let [marginfiGroupAi, marginfiAccountAi] = await this.client.provider.connection.getMultipleAccountsInfo( - [this._config.groupPk, this.publicKey], - DEFAULT_COMMITMENT - ); - - if (!marginfiAccountAi) { - throw Error("Marginfi account no found"); - } - if (!marginfiGroupAi) { - throw Error("Marginfi Group Account no found"); - } - - return [marginfiGroupAi, marginfiAccountAi]; - } - - public getHealthComponents(marginReqType: MarginRequirementType): { - assets: BigNumber; - liabilities: BigNumber; - } { - const [assets, liabilities] = this.activeBalances - .map((accountBalance) => { - const bank = this._group.banks.get(accountBalance.bankPk.toBase58()); - if (!bank) throw Error(`Bank ${shortenAddress(accountBalance.bankPk)} not found`); - const { assets, liabilities } = accountBalance.getUsdValueWithPriceBias(bank, marginReqType); - return [assets, liabilities]; - }) - .reduce( - ([asset, liability], [d, l]) => { - return [asset.plus(d), liability.plus(l)]; - }, - [new BigNumber(0), new BigNumber(0)] - ); - - return { assets, liabilities }; - } - - public canBeLiquidated(): boolean { - const { assets, liabilities } = this.getHealthComponents(MarginRequirementType.Maint); - - return assets.lt(liabilities); - } - - public getBalance(bankPk: PublicKey): Balance { - return this.activeBalances.find((b) => b.bankPk.equals(bankPk)) ?? Balance.newEmpty(bankPk); - } - - public getFreeCollateral(clamped: boolean = true): BigNumber { - const { assets, liabilities } = this.getHealthComponents(MarginRequirementType.Init); - const signedFreeCollateral = assets.minus(liabilities); - return clamped ? BigNumber.max(0, signedFreeCollateral) : signedFreeCollateral; - } - - public getHealthComponentsWithoutBias(marginReqType: MarginRequirementType): { - assets: BigNumber; - liabilities: BigNumber; - } { - const [assets, liabilities] = this.activeBalances - .map((accountBalance) => { - const bank = this._group.banks.get(accountBalance.bankPk.toBase58()); - if (!bank) throw Error(`Bank ${shortenAddress(accountBalance.bankPk)} not found`); - const { assets, liabilities } = accountBalance.getUsdValue(bank, marginReqType); - return [assets, liabilities]; - }) - .reduce( - ([asset, liability], [d, l]) => { - return [asset.plus(d), liability.plus(l)]; - }, - [new BigNumber(0), new BigNumber(0)] - ); - - return { assets, liabilities }; - } - - public computeNetApy(): number { - const { assets, liabilities } = this.getHealthComponentsWithoutBias(MarginRequirementType.Equity); - const totalUsdValue = assets.minus(liabilities); - const apr = this.activeBalances - .reduce((weightedApr, balance) => { - const bank = this._group.getBankByPk(balance.bankPk); - if (!bank) throw Error(`Bank ${balance.bankPk.toBase58()} not found`); - return weightedApr - .minus( - bank - .getInterestRates() - .borrowingRate.times(balance.getUsdValue(bank, MarginRequirementType.Equity).liabilities) - .div(totalUsdValue.isEqualTo(0) ? 1 : totalUsdValue) - ) - .plus( - bank - .getInterestRates() - .lendingRate.times(balance.getUsdValue(bank, MarginRequirementType.Equity).assets) - .div(totalUsdValue.isEqualTo(0) ? 1 : totalUsdValue) - ); - }, new BigNumber(0)) - .toNumber(); - - return aprToApy(apr); - } - - /** - * Calculate the maximum amount of asset that can be withdrawn from a bank given existing deposits of the asset - * and the untied collateral of the margin account. - * - * fc = free collateral - * ucb = untied collateral for bank - * - * q = (min(fc, ucb) / (price_lowest_bias * deposit_weight)) + (fc - min(fc, ucb)) / (price_highest_bias * liab_weight) - * - * - * - * NOTE FOR LIQUIDATORS - * This function doesn't take into account the collateral received when liquidating an account. - */ - public getMaxBorrowForBank(bank: Bank): BigNumber { - const balance = this.getBalance(bank.publicKey); - - const freeCollateral = this.getFreeCollateral(); - const untiedCollateralForBank = BigNumber.min( - bank.getAssetUsdValue(balance.assetShares, MarginRequirementType.Init, PriceBias.Lowest), - freeCollateral - ); - - const priceLowestBias = bank.getPrice(PriceBias.Lowest); - const priceHighestBias = bank.getPrice(PriceBias.Highest); - const assetWeight = bank.getAssetWeight(MarginRequirementType.Init); - const liabWeight = bank.getLiabilityWeight(MarginRequirementType.Init); - - if (assetWeight.eq(0)) { - return balance - .getQuantityUi(bank) - .assets.plus(freeCollateral.minus(untiedCollateralForBank).div(priceHighestBias.times(liabWeight))); - } else { - return untiedCollateralForBank - .div(priceLowestBias.times(assetWeight)) - .plus(freeCollateral.minus(untiedCollateralForBank).div(priceHighestBias.times(liabWeight))); - } - } - - /** - * Calculate the maximum amount that can be withdrawn form a bank without borrowing. - */ - public getMaxWithdrawForBank(bankPk: PublicKey, volatilityFactor: number = 1): BigNumber { - const bank = this._group.getBankByPk(bankPk); - if (!bank) throw Error(`Bank ${bankPk.toBase58()} not found`); - - const assetWeight = bank.getAssetWeight(MarginRequirementType.Init); - const balance = this.getBalance(bank.publicKey); - - if (assetWeight.eq(0)) { - return balance.getQuantityUi(bank).assets; - } else { - const freeCollateral = this.getFreeCollateral(); - const collateralForBank = bank.getAssetUsdValue( - balance.assetShares, - MarginRequirementType.Init, - PriceBias.Lowest - ); - let untiedCollateralForBank: BigNumber; - if (collateralForBank.lte(freeCollateral)) { - untiedCollateralForBank = collateralForBank; - } else { - untiedCollateralForBank = freeCollateral.times(volatilityFactor); - } - - const priceLowestBias = bank.getPrice(PriceBias.Lowest); - - return untiedCollateralForBank.div(priceLowestBias.times(assetWeight)); - } - } - - public async makeLendingAccountLiquidateIx( - liquidateeMarginfiAccount: MarginfiAccount, - assetBank: Bank, - assetQuantityUi: Amount, - liabBank: Bank - ): Promise { - const ix = await instructions.makeLendingAccountLiquidateIx( - this._program, - { - marginfiGroup: this._config.groupPk, - signer: this.client.provider.wallet.publicKey, - assetBank: assetBank.publicKey, - liabBank: liabBank.publicKey, - liquidatorMarginfiAccount: this.publicKey, - liquidateeMarginfiAccount: liquidateeMarginfiAccount.publicKey, - }, - { assetAmount: uiToNative(assetQuantityUi, assetBank.mintDecimals) }, - [ - { - pubkey: assetBank.config.oracleKeys[0], - isSigner: false, - isWritable: false, - }, - { - pubkey: liabBank.config.oracleKeys[0], - isSigner: false, - isWritable: false, - }, - ...this.getHealthCheckAccounts([liabBank, assetBank]), - ...liquidateeMarginfiAccount.getHealthCheckAccounts(), - ] - ); - - return { instructions: [ix], keys: [] }; - } - - public async lendingAccountLiquidate( - liquidateeMarginfiAccount: MarginfiAccount, - assetBank: Bank, - assetQuantityUi: Amount, - liabBank: Bank - ): Promise { - const ixw = await this.makeLendingAccountLiquidateIx( - liquidateeMarginfiAccount, - assetBank, - assetQuantityUi, - liabBank - ); - const tx = new Transaction().add( - ...ixw.instructions, - ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }) - ); - return this.client.processTransaction(tx); - } - - public toString() { - const { assets, liabilities } = this.getHealthComponents(MarginRequirementType.Equity); - - let str = `----------------- - Marginfi account: - Address: ${this.publicKey.toBase58()} - Group: ${this.group.publicKey.toBase58()} - Authority: ${this.authority.toBase58()} - Equity: ${this.getHealthComponents(MarginRequirementType.Equity).assets.toFixed(6)} - Equity: ${assets.minus(liabilities).toFixed(6)} - Assets: ${assets.toFixed(6)}, - Liabilities: ${liabilities.toFixed(6)}`; - - const activeLendingAccounts = this.activeBalances.filter((la) => la.active); - if (activeLendingAccounts.length > 0) { - str = str.concat("\n-----------------\nBalances:"); - } - for (let lendingAccount of activeLendingAccounts) { - const bank = this._group.getBankByPk(lendingAccount.bankPk); - if (!bank) { - console.log(`Bank ${lendingAccount.bankPk} not found`); - continue; - } - const utpStr = `\n Bank ${bank.publicKey.toBase58()}: - Mint: ${bank.mint.toBase58()} - Equity: ${lendingAccount.getUsdValue(bank, MarginRequirementType.Equity)}`; - str = str.concat(utpStr); - } - - return str; - } - - // Calculate the max amount of collateral to liquidate to bring an account maint health to 0 (assuming negative health). - // - // The asset amount is bounded by 2 constraints, - // (1) the amount of liquidated collateral cannot be more than the balance, - // (2) the amount of covered liablity cannot be more than existing liablity. - public getMaxLiquidatableAssetAmount(assetBank: Bank, liabBank: Bank): BigNumber { - const debug = require("debug")("mfi:getMaxLiquidatableAssetAmount"); - const { assets, liabilities } = this.getHealthComponents(MarginRequirementType.Maint); - const currentHealth = assets.minus(liabilities); - - const priceAssetLower = assetBank.getPrice(PriceBias.Lowest); - const priceAssetMarket = assetBank.getPrice(PriceBias.None); - const assetMaintWeight = assetBank.config.assetWeightMaint; - - const liquidationDiscount = new BigNumber(1 - 0.05); - - const priceLiabHighest = liabBank.getPrice(PriceBias.Highest); - const priceLiabMarket = liabBank.getPrice(PriceBias.None); - const liabMaintWeight = liabBank.config.liabilityWeightMaint; - - // MAX amount of asset to liquidate to bring account maint health to 0, regardless of existing balances - const maxLiquidatableUnboundedAssetAmount = currentHealth.div( - priceAssetLower - .times(assetMaintWeight) - .minus( - priceAssetMarket - .times(liquidationDiscount) - .times(priceLiabHighest) - .times(liabMaintWeight) - .div(priceLiabMarket) - ) - ); - - // MAX asset amount bounded by available asset amount - const assetBalanceBound = this.getBalance(assetBank.publicKey).getQuantityUi(assetBank).assets; - - const liabBalance = this.getBalance(liabBank.publicKey).getQuantityUi(liabBank).liabilities; - // MAX asset amount bounded by availalbe liability amount - const liabBalanceBound = liabBalance.times(priceLiabMarket).div(priceAssetMarket.times(liquidationDiscount)); - - debug("maxLiquidatableUnboundedAssetAmount", maxLiquidatableUnboundedAssetAmount.toFixed(6)); - debug("assetBalanceBound", assetBalanceBound.toFixed(6)); - debug("liabBalanceBound", liabBalanceBound.toFixed(6)); - - return BigNumber.min(assetBalanceBound, liabBalanceBound, maxLiquidatableUnboundedAssetAmount); - } - - public describe(): string { - const { assets, liabilities } = this.getHealthComponents(MarginRequirementType.Equity); - return ` -- Marginfi account: ${this.publicKey} -- Total deposits: $${assets.toFixed(6)} -- Total liabilities: $${liabilities.toFixed(6)} -- Equity: $${assets.minus(liabilities).toFixed(6)} -- Health: ${assets.minus(liabilities).div(assets).times(100).toFixed(2)}% -- Balances: ${this.activeBalances.map((la) => { - const bank = this._group.getBankByPk(la.bankPk)!; - return la.describe(bank); - })}`; - } - - private async wrapInstructionForWSol( - ix: TransactionInstruction, - amount: Amount = new BigNumber(0) - ): Promise { - const debug = require("debug")("mfi:wrapInstructionForWSol"); - debug("creating a wsol account, and minting %s wsol", amount); - return [...(await this.makeWrapSolIxs(new BigNumber(amount))), ix, await this.makeUnwrapSolIx()]; - } - - private async makeWrapSolIxs(amount: BigNumber): Promise { - const address = await associatedAddress({ mint: NATIVE_MINT, owner: this.client.wallet.publicKey }); - const ixs = [ - createAssociatedTokenAccountIdempotentInstruction( - this.client.wallet.publicKey, - address, - this.client.wallet.publicKey, - NATIVE_MINT - ), - ]; - - if (amount.gt(0)) { - const debug = require("debug")("mfi:wrapInstructionForWSol"); - const nativeAmount = uiToNative(amount, 9).toNumber() + 10000; - debug("wrapping %s wsol", nativeAmount); - - ixs.push( - SystemProgram.transfer({ fromPubkey: this.client.wallet.publicKey, toPubkey: address, lamports: nativeAmount }), - createSyncNativeInstruction(address) - ); - } - - return ixs; - } - - private async makeUnwrapSolIx(): Promise { - const address = await associatedAddress({ mint: NATIVE_MINT, owner: this.client.wallet.publicKey }); - - return createCloseAccountInstruction(address, this.client.wallet.publicKey, this.client.wallet.publicKey); - } -} - -export default MarginfiAccount; - -// Client types - -export class Balance { - active: boolean; - bankPk: PublicKey; - assetShares: BigNumber; - liabilityShares: BigNumber; - private emissionsOutstanding: BigNumber; - lastUpdate: number; - - constructor(data: BalanceData) { - this.active = data.active; - this.bankPk = data.bankPk; - this.assetShares = wrappedI80F48toBigNumber(data.assetShares); - this.liabilityShares = wrappedI80F48toBigNumber(data.liabilityShares); - this.emissionsOutstanding = wrappedI80F48toBigNumber(data.emissionsOutstanding); - this.lastUpdate = data.lastUpdate; - } - - public static newEmpty(bankPk: PublicKey): Balance { - return new Balance({ - active: false, - bankPk, - assetShares: { value: new BN(0) }, - liabilityShares: { value: new BN(0) }, - emissionsOutstanding: { value: new BN(0) }, - lastUpdate: 0, - }); - } - - public getUsdValue( - bank: Bank, - marginReqType: MarginRequirementType = MarginRequirementType.Equity - ): { assets: BigNumber; liabilities: BigNumber } { - return { - assets: bank.getAssetUsdValue(this.assetShares, marginReqType, PriceBias.None), - liabilities: bank.getLiabilityUsdValue(this.liabilityShares, marginReqType, PriceBias.None), - }; - } - - public getUsdValueWithPriceBias( - bank: Bank, - marginReqType: MarginRequirementType - ): { assets: BigNumber; liabilities: BigNumber } { - return { - assets: bank.getAssetUsdValue(this.assetShares, marginReqType, PriceBias.Lowest), - liabilities: bank.getLiabilityUsdValue(this.liabilityShares, marginReqType, PriceBias.Highest), - }; - } - - public getQuantity(bank: Bank): { - assets: BigNumber; - liabilities: BigNumber; - } { - return { - assets: bank.getAssetQuantity(this.assetShares), - liabilities: bank.getLiabilityQuantity(this.liabilityShares), - }; - } - - public getQuantityUi(bank: Bank): { - assets: BigNumber; - liabilities: BigNumber; - } { - return { - assets: new BigNumber(nativeToUi(bank.getAssetQuantity(this.assetShares), bank.mintDecimals)), - liabilities: new BigNumber(nativeToUi(bank.getLiabilityQuantity(this.liabilityShares), bank.mintDecimals)), - }; - } - - public getTotalOutstandingEmissions(bank: Bank): BigNumber { - const claimedEmissions = this.emissionsOutstanding; - - const unclaimedEmissions = this.calcClaimedEmissions(bank, Date.now() / 1000); - - return claimedEmissions.plus(unclaimedEmissions); - } - - private calcClaimedEmissions(bank: Bank, currentTimestamp: number): BigNumber { - const lendingActive = bank.emissionsActiveLending; - const borrowActive = bank.emissionsActiveBorrowing; - - const { assets, liabilities } = this.getQuantity(bank); - - let balanceAmount: BigNumber | null = null; - - if (lendingActive) { - balanceAmount = assets; - } else if (borrowActive) { - balanceAmount = liabilities; - } - - if (balanceAmount) { - const lastUpdate = this.lastUpdate; - const period = new BigNumber(currentTimestamp - lastUpdate); - const emissionsRate = new BigNumber(bank.emissionsRate); - const emissions = period.times(balanceAmount).times(emissionsRate).div(31_536_000_000_000); - const emissionsReal = BigNumber.min(emissions, new BigNumber(bank.emissionsRemaining)); - - return emissionsReal; - } - - return new BigNumber(0); - } - - public describe(bank: Bank): string { - let { assets: assetsQt, liabilities: liabsQt } = this.getQuantityUi(bank); - let { assets: assetsUsd, liabilities: liabsUsd } = this.getUsdValue(bank, MarginRequirementType.Equity); - - return ` -${bank.publicKey} Balance: -- Deposits: ${assetsQt.toFixed(5)} (${assetsUsd.toFixed(5)} USD) -- Borrows: ${liabsQt.toFixed(5)} (${liabsUsd.toFixed(5)} USD) -`; - } -} - -// On-chain types - -export interface MarginfiAccountData { - group: PublicKey; - authority: PublicKey; - lendingAccount: { balances: BalanceData[] }; -} - -export interface BalanceData { - active: boolean; - bankPk: PublicKey; - assetShares: WrappedI80F48; - liabilityShares: WrappedI80F48; - emissionsOutstanding: WrappedI80F48; - lastUpdate: number; -} - -export enum MarginRequirementType { - Init = 0, - Maint = 1, - Equity = 2, -} From a4721364f667f41a1f46786fa8134de08bf486ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Mon, 11 Sep 2023 04:02:12 +0900 Subject: [PATCH 05/24] fix: use MAX cus for liquidations --- packages/marginfi-client-v2/src/models/account/pure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/marginfi-client-v2/src/models/account/pure.ts b/packages/marginfi-client-v2/src/models/account/pure.ts index 7d38a0079d..3afbc8442f 100644 --- a/packages/marginfi-client-v2/src/models/account/pure.ts +++ b/packages/marginfi-client-v2/src/models/account/pure.ts @@ -636,7 +636,7 @@ class MarginfiAccount { let ixs = []; - ixs.push(ComputeBudgetProgram.setComputeUnitLimit({ units: 600_000 })); + ixs.push(ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 })); const liquidateIx = await instructions.makeLendingAccountLiquidateIx( program, { From fdf873c55bbc6ce0121a40859a96522bedcef91f Mon Sep 17 00:00:00 2001 From: j Date: Wed, 4 Oct 2023 14:02:54 +0200 Subject: [PATCH 06/24] fix: remove runLiquidator file --- apps/alpha-liquidator/src/runLiquidator.ts | 1 - yarn.lock | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) delete mode 100644 apps/alpha-liquidator/src/runLiquidator.ts diff --git a/apps/alpha-liquidator/src/runLiquidator.ts b/apps/alpha-liquidator/src/runLiquidator.ts deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/alpha-liquidator/src/runLiquidator.ts +++ /dev/null @@ -1 +0,0 @@ - diff --git a/yarn.lock b/yarn.lock index ce4a7d790e..1c3f35494b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10747,6 +10747,13 @@ cross-fetch@3.0.6: dependencies: node-fetch "2.6.1" +cross-fetch@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-fetch@3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" From 22daa0d68dadc65add9f424bb3329879c9c520da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Sat, 21 Oct 2023 22:28:14 +0200 Subject: [PATCH 07/24] feat: configurable slippage --- apps/alpha-liquidator/src/config.ts | 1 + apps/alpha-liquidator/src/liquidator.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/alpha-liquidator/src/config.ts b/apps/alpha-liquidator/src/config.ts index a12ef6d209..aed94323e5 100644 --- a/apps/alpha-liquidator/src/config.ts +++ b/apps/alpha-liquidator/src/config.ts @@ -48,6 +48,7 @@ let envSchema = z.object({ return pkArrayStr.split(",").map((pkStr) => new PublicKey(pkStr)); }) .optional(), + MAX_SLIPPAGE_BPS: z.string().optional().default("250").transform((s) => Number.parseInt(s, 10)), MIN_LIQUIDATION_AMOUNT_USD_UI: z .string() .default("0.1") diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 805f67092e..4085b56e84 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -22,7 +22,7 @@ const MIN_LIQUIDATION_AMOUNT_USD_UI = env_config.MIN_LIQUIDATION_AMOUNT_USD_UI; const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); const MIN_SOL_BALANCE = env_config.MIN_SOL_BALANCE * LAMPORTS_PER_SOL; -const SLIPPAGE_BPS = 10000; +const SLIPPAGE_BPS = env_config.MAX_SLIPPAGE_BPS; const EXCLUDE_ISOLATED_BANKS: boolean = process.env.EXCLUDE_ISOLATED_BANKS === "true"; // eslint-disable-line From cfc58c801b70d0cba22012153bcc7b5781cf8be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Wed, 8 Nov 2023 23:51:48 +0100 Subject: [PATCH 08/24] various fixes --- apps/alpha-liquidator/src/liquidator.ts | 63 ++++++++++++++----- .../src/models/account/pure.ts | 38 +++++++---- 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 4085b56e84..25464ecd2a 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -11,12 +11,12 @@ import BigNumber from "bignumber.js"; import { associatedAddress } from "@project-serum/anchor/dist/cjs/utils/token"; import { NATIVE_MINT } from "@solana/spl-token"; import { captureException, captureMessage, env_config } from "./config"; -import BN from "bn.js"; +import BN, { min } from "bn.js"; import { BankMetadataMap, loadBankMetadatas } from "./utils/bankMetadata"; import { Bank } from "@mrgnlabs/marginfi-client-v2/dist/models/bank"; const DUST_THRESHOLD = new BigNumber(10).pow(USDC_DECIMALS - 2); -const DUST_THRESHOLD_UI = new BigNumber(0.1); +const DUST_THRESHOLD_UI = new BigNumber(0.01); const MIN_LIQUIDATION_AMOUNT_USD_UI = env_config.MIN_LIQUIDATION_AMOUNT_USD_UI; const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); @@ -73,12 +73,18 @@ class Liquidator { await this.mainLoop(); } + private async reload() { + await this.client.reload(); + await this.account.reload(); + } + private async mainLoop() { const debug = getDebugLogger("main-loop"); drawSpinner("Scanning"); try { - await this.swapNonUsdcInTokenAccounts(); + this.reload(); while (true) { + await this.swapNonUsdcInTokenAccounts(); debug("Started main loop iteration"); if (await this.needsToBeRebalanced()) { await this.rebalancingStage(); @@ -88,6 +94,7 @@ class Liquidator { // Don't sleep after liquidating an account, start rebalance immediately if (!(await this.liquidationStage())) { await sleep(env_config.SLEEP_INTERVAL); + this.reload(); } } } catch (e) { @@ -103,7 +110,7 @@ class Liquidator { private async swap(mintIn: PublicKey, mintOut: PublicKey, amountIn: BN) { const debug = getDebugLogger("swap"); - debug("Swapping %s %s to %s", amountIn, mintIn.toBase58(), mintOut.toBase58()); + console.log("Swapping %s %s to %s", amountIn, mintIn.toBase58(), mintOut.toBase58()); const swapUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${mintIn.toBase58()}&outputMint=${mintOut.toBase58()}&amount=${amountIn.toString()}&slippageBps=${SLIPPAGE_BPS}`; const quoteApiResponse = await fetch(swapUrl); @@ -186,6 +193,7 @@ class Liquidator { const balance = await this.getTokenAccountBalance(bank.mint); await this.swap(bank.mint, USDC_MINT, uiToNative(balance, bank.mintDecimals)); + await this.account.reload(); } } @@ -266,6 +274,8 @@ class Liquidator { const depositSig = await this.account.repay(liabBalance, bank.address, liabBalance.gte(liabsUi)); debug("Deposit tx: %s", depositSig); + + await this.reload(); } } @@ -301,6 +311,7 @@ class Liquidator { } private async getTokenAccountBalance(mint: PublicKey, ignoreNativeMint: boolean = false): Promise { + const debug = getDebugLogger("getTokenAccountBalances"); const tokenAccount = await associatedAddress({ mint, owner: this.wallet.publicKey }); const nativeAmount = nativeToUi( mint.equals(NATIVE_MINT) @@ -313,11 +324,14 @@ class Liquidator { 9 ); + debug!("Checking token account %s for %s", tokenAccount, mint); + try { return new BigNumber((await this.connection.getTokenAccountBalance(tokenAccount)).value.uiAmount!).plus( nativeAmount ); } catch (e) { + debug("Error getting token account balance: %s", e); return new BigNumber(0).plus(nativeAmount); } } @@ -333,9 +347,16 @@ class Liquidator { continue; } - let amount = await this.getTokenAccountBalance(bank.mint); + let uiAmount = await this.getTokenAccountBalance(bank.mint); + let price = this.client.getOraclePriceByBank(bank.address)!; + let usdValue = bank.computeUsdValue(price, new BigNumber(uiAmount), PriceBias.None, undefined, false); + + let amount = uiToNative(uiAmount, bank.mintDecimals).toNumber(); - if (amount.lte(DUST_THRESHOLD_UI)) { + debug("Account has %d %s", uiAmount, this.getTokenSymbol(bank)); + + if (usdValue.lte(DUST_THRESHOLD_UI)) { + debug!("Not enough %s to swap, skipping...", this.getTokenSymbol(bank)); continue; } @@ -344,17 +365,22 @@ class Liquidator { if (liabilities.gt(0)) { debug("Account has %d liabilities in %s", liabilities, this.getTokenSymbol(bank)); - const depositAmount = BigNumber.min(amount, liabilities); + const depositAmount = BigNumber.min(uiAmount, liabilities); debug("Paying off %d %s liabilities", depositAmount, this.getTokenSymbol(bank)); - await this.account.repay(depositAmount, bank.address, amount.gte(liabilities)); + await this.account.repay(depositAmount, bank.address, uiAmount.gte(liabilities)); + + uiAmount = await this.getTokenAccountBalance(bank.mint); - amount = await this.getTokenAccountBalance(bank.mint); + if (uiAmount.lte(DUST_THRESHOLD_UI)) { + debug("Account has no more %s, skipping...", this.getTokenSymbol(bank)); + continue; + } } - debug("Swapping %d %s to USDC", amount, this.getTokenSymbol(bank)); + debug("Swapping %d %s to USDC", uiAmount, this.getTokenSymbol(bank)); - await this.swap(bank.mint, USDC_MINT, uiToNative(amount, bank.mintDecimals)); + await this.swap(bank.mint, USDC_MINT, uiToNative(uiAmount, bank.mintDecimals)); } const usdcBalance = await this.getTokenAccountBalance(USDC_MINT); @@ -369,14 +395,15 @@ class Liquidator { const tx = await this.account.deposit(usdcBalance, usdcBank.address); debug("Deposit tx: %s", tx); + + await this.reload(); } private async needsToBeRebalanced(): Promise { const debug = getDebugLogger("rebalance-check"); debug("Checking if liquidator needs to be rebalanced"); - await this.client.reload(); - await this.account.reload(); + await this.reload(); const lendingAccountToRebalance = this.account.activeBalances .map((lendingAccount) => { @@ -531,7 +558,7 @@ class Liquidator { } debug( - "Max collateral USD value: %d, mint: %s", + "Max collateral $%d, mint: %s", maxCollateralUsd, this.client.getBankByPk(marginfiAccount.activeBalances[bestCollateralIndex].bankPk)!.mint ); @@ -551,8 +578,6 @@ class Liquidator { liabBank.address ); - debug("Max collateral amount to liquidate: %d", maxCollateralAmountToLiquidate); - // MAX collateral amount to liquidate given liquidators current margin account const liquidatorMaxLiquidationCapacityLiabAmount = liquidatorAccount.computeMaxBorrowForBank(liabBank.address); const liquidatorMaxLiquidationCapacityUsd = liabBank.computeUsdValue( @@ -575,6 +600,12 @@ class Liquidator { liabBank.mint ); + debug( + "Collateral amount to liquidate: $%d for bank %s", + maxCollateralAmountToLiquidate, + collateralBank.mint + ) + const collateralAmountToLiquidate = BigNumber.min( maxCollateralAmountToLiquidate, liquidatorMaxLiqCapacityAssetAmount diff --git a/packages/marginfi-client-v2/src/models/account/pure.ts b/packages/marginfi-client-v2/src/models/account/pure.ts index 7a46479f40..0296c2d4c6 100644 --- a/packages/marginfi-client-v2/src/models/account/pure.ts +++ b/packages/marginfi-client-v2/src/models/account/pure.ts @@ -301,17 +301,25 @@ class MarginfiAccount { const liabMaintWeight = liabilityBank.config.liabilityWeightMaint; // MAX amount of asset to liquidate to bring account maint health to 0, regardless of existing balances - const underwaterMaintValue = currentHealth.div( - priceAssetLower - .times(assetMaintWeight) - .minus( - priceAssetMarket - .times(liquidationDiscount) - .times(priceLiabHighest) - .times(liabMaintWeight) - .div(priceLiabMarket) - ) - ); + // h + // q_a = ---------------------------------- + // p_al * w_a - p_al * d * p_lh * w_l + // ---------------------- + // p_lm + // const underwaterMaintValue = currentHealth.div( + // priceAssetLower + // .times(assetMaintWeight) + // .minus( + // priceAssetLower + // .times(liquidationDiscount) + // .times(priceLiabHighest) + // .times(liabMaintWeight) + // .div(priceLiabMarket) + // ) + // ); + + + const underwaterMaintValue = currentHealth.div(assetMaintWeight.minus(liabMaintWeight.times(liquidationDiscount))); // MAX asset amount bounded by available asset amount const assetBalance = this.getBalance(assetBankAddress); @@ -319,14 +327,18 @@ class MarginfiAccount { // MAX asset amount bounded by available liability amount const liabilityBalance = this.getBalance(liabilityBankAddress); - const liabilitiesForBank = liabilityBalance.computeQuantityUi(assetBank).liabilities; + const liabilitiesForBank = liabilityBalance.computeQuantityUi(liabilityBank).liabilities; const liabilityCap = liabilitiesForBank.times(priceLiabMarket).div(priceAssetMarket.times(liquidationDiscount)); + debug("Liab amount: %d, price: %d, value: %d", liabilitiesForBank.toFixed(6), priceLiabMarket.toFixed(6), liabilitiesForBank.times(priceLiabMarket).toFixed(6)); + + debug("Collateral amount: %d, price: %d, value: %d", assetsCap.toFixed(6), priceAssetMarket.toFixed(6), assetsCap.times(priceAssetMarket).toFixed(6)); + debug("underwaterValue", underwaterMaintValue.toFixed(6)); debug("assetsCap", assetsCap.toFixed(6)); debug("liabilityCap", liabilityCap.toFixed(6)); - return BigNumber.min(assetsCap, liabilityCap, underwaterMaintValue); + return BigNumber.min(assetsCap, underwaterMaintValue, liabilityCap); } getHealthCheckAccounts( From 94026e50c4c949908e5f6a4662c2046fa6d262a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Wed, 8 Nov 2023 23:53:39 +0100 Subject: [PATCH 09/24] fix: liquidation amount --- apps/alpha-liquidator/src/liquidator.ts | 2 +- .../src/models/account/pure.ts | 19 ------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 25464ecd2a..5fbf48ab1b 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -611,7 +611,7 @@ class Liquidator { liquidatorMaxLiqCapacityAssetAmount ); - const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.75); + const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.5); if (slippageAdjustedCollateralAmountToLiquidate.lt(MIN_LIQUIDATION_AMOUNT_USD_UI)) { debug("No collateral to liquidate"); diff --git a/packages/marginfi-client-v2/src/models/account/pure.ts b/packages/marginfi-client-v2/src/models/account/pure.ts index 0296c2d4c6..29c8b98f13 100644 --- a/packages/marginfi-client-v2/src/models/account/pure.ts +++ b/packages/marginfi-client-v2/src/models/account/pure.ts @@ -300,25 +300,6 @@ class MarginfiAccount { const priceLiabMarket = liabilityBank.getPrice(liabilityPriceInfo, PriceBias.None); const liabMaintWeight = liabilityBank.config.liabilityWeightMaint; - // MAX amount of asset to liquidate to bring account maint health to 0, regardless of existing balances - // h - // q_a = ---------------------------------- - // p_al * w_a - p_al * d * p_lh * w_l - // ---------------------- - // p_lm - // const underwaterMaintValue = currentHealth.div( - // priceAssetLower - // .times(assetMaintWeight) - // .minus( - // priceAssetLower - // .times(liquidationDiscount) - // .times(priceLiabHighest) - // .times(liabMaintWeight) - // .div(priceLiabMarket) - // ) - // ); - - const underwaterMaintValue = currentHealth.div(assetMaintWeight.minus(liabMaintWeight.times(liquidationDiscount))); // MAX asset amount bounded by available asset amount From a8b3e9ee7b23390ba8b5ca7e092521f764bbe644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Thu, 9 Nov 2023 10:57:38 +0100 Subject: [PATCH 10/24] fix: many fixes --- apps/alpha-liquidator/src/liquidator.ts | 125 +++++++++++++++++------- 1 file changed, 89 insertions(+), 36 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 5fbf48ab1b..062af77839 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -58,6 +58,8 @@ class Liquidator { console.log("Whitelist: %s", this.account_whitelist); } + this.bankMetadataMap = await loadBankMetadatas(); + setInterval(async () => { try { this.bankMetadataMap = await loadBankMetadatas(); @@ -66,6 +68,9 @@ class Liquidator { } }, 10 * 60 * 1000); // refresh cache every 10 minutes + setInterval(this.printAccountValue, 30 * 1000); + this.printAccountValue(); + console.log("Liquidating on %s banks", this.client.banks.size); console.log("Start with DEBUG=mfi:* to see more logs"); @@ -73,6 +78,17 @@ class Liquidator { await this.mainLoop(); } + private async printAccountValue() { + try { + const { assets, liabilities } = await this.account.computeHealthComponentsWithoutBias(MarginRequirementType.Equity); + const accountValue = assets.minus(liabilities); + console.log("Account Value: $%s", accountValue); + } catch (e) { + console.error("Failed to fetch account value"); + } + } + + private async reload() { await this.client.reload(); await this.account.reload(); @@ -107,12 +123,35 @@ class Liquidator { } } - private async swap(mintIn: PublicKey, mintOut: PublicKey, amountIn: BN) { + private async swap(mintIn: PublicKey, mintOut: PublicKey, amount: BN, swapModeExactOut: boolean = false) { const debug = getDebugLogger("swap"); - console.log("Swapping %s %s to %s", amountIn, mintIn.toBase58(), mintOut.toBase58()); + if (!swapModeExactOut) { + const mintInBank = this.client.getBankByMint(mintIn)!; + const mintOutBank = this.client.getBankByMint(mintOut)!; + const mintInSymbol = this.getTokenSymbol(mintInBank); + const mintOutSymbol = this.getTokenSymbol(mintOutBank); + const amountScaled = nativeToUi(amount, mintInBank.mintDecimals); + console.log("Swapping %s %s to %s", + amountScaled, + mintInSymbol, + mintOutSymbol + ); + } else { + const mintInBank = this.client.getBankByMint(mintIn)!; + const mintOutBank = this.client.getBankByMint(mintOut)!; + const mintInSymbol = this.getTokenSymbol(mintInBank); + const mintOutSymbol = this.getTokenSymbol(mintOutBank); + const amountScaled = nativeToUi(amount, mintOutBank.mintDecimals); + console.log("Swapping %s to %s %s", + mintInSymbol, + amountScaled, + mintOutSymbol, + ); + } - const swapUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${mintIn.toBase58()}&outputMint=${mintOut.toBase58()}&amount=${amountIn.toString()}&slippageBps=${SLIPPAGE_BPS}`; + const swapMode = swapModeExactOut ? 'ExactOut' : 'ExactIn'; + const swapUrl = `https://quote-api.jup.ag/v6/quote?inputMint=${mintIn.toBase58()}&outputMint=${mintOut.toBase58()}&amount=${amount.toString()}&slippageBps=${SLIPPAGE_BPS}&swapMode=${swapMode}`; const quoteApiResponse = await fetch(swapUrl); const data = await quoteApiResponse.json(); @@ -171,10 +210,15 @@ class Liquidator { return { assets, bank, priceInfo }; }) - .filter(({ assets, bank }) => !bank.mint.equals(USDC_MINT) && assets.gt(DUST_THRESHOLD)); + .filter(({ assets, bank }) => !bank.mint.equals(USDC_MINT)); for (let { bank } of balancesWithNonUsdcDeposits) { - let maxWithdrawAmount = this.account.computeMaxWithdrawForBank(bank.address); + // const maxWithdrawAmount = nativeToBigNumber(uiToNative(this.account.computeMaxWithdrawForBank(bank.address), bank.mintDecimals)); + const balanceAssetAmount = this.account.getBalance(bank.address).computeQuantity(bank).assets; + // TODO: Fix dumbass conversion + const maxWithdrawAmount = balanceAssetAmount; + + debug("Balance: %d, max withdraw: %d", balanceAssetAmount, maxWithdrawAmount); if (maxWithdrawAmount.eq(0)) { debug("No untied %s to withdraw", this.getTokenSymbol(bank)); @@ -182,19 +226,17 @@ class Liquidator { } debug("Withdrawing %d %s", maxWithdrawAmount, this.getTokenSymbol(bank)); - let withdrawSig = await this.account.withdraw(maxWithdrawAmount, bank.address); - - debug("Withdraw tx: %s", withdrawSig); - - await this.account.reload(); - - debug("Swapping %s to USDC", bank.mint); - - const balance = await this.getTokenAccountBalance(bank.mint); + let withdrawSig = await this.account.withdraw( + maxWithdrawAmount, + bank.address, + true + ); - await this.swap(bank.mint, USDC_MINT, uiToNative(balance, bank.mintDecimals)); - await this.account.reload(); + debug("Withdraw tx: %s", withdrawSig) + this.reload(); } + + this.swapNonUsdcInTokenAccounts(); } /** @@ -224,7 +266,7 @@ class Liquidator { .filter(({ liabilities, bank }) => liabilities.gt(new BigNumber(0)) && !bank.mint.equals(USDC_MINT)); for (let { liabilities, bank } of balancesWithNonUsdcLiabilities) { - debug("Repaying %d %si", nativeToUi(liabilities, bank.mintDecimals), this.getTokenSymbol(bank)); + debug("Repaying %d %s", nativeToUi(liabilities, bank.mintDecimals), this.getTokenSymbol(bank)); let availableUsdcInTokenAccount = await this.getTokenAccountBalance(USDC_MINT); await this.client.reload(); @@ -238,7 +280,7 @@ class Liquidator { liabilities, MarginRequirementType.Equity, // We might need to use a Higher price bias to account for worst case scenario. - PriceBias.None + PriceBias.Highest ); /// When a liab value is super small (1 BONK), we cannot feasibly buy it for the exact amount, @@ -265,7 +307,11 @@ class Liquidator { debug("Swapping %d USDC to %s", usdcBuyingPower, this.getTokenSymbol(bank)); - await this.swap(USDC_MINT, bank.mint, uiToNative(usdcBuyingPower, USDC_DECIMALS)); + await this.swap( + USDC_MINT, + bank.mint, + uiToNative(usdcBuyingPower, usdcBank.mintDecimals), + ); const liabsUi = new BigNumber(nativeToUi(liabilities, bank.mintDecimals)); const liabBalance = BigNumber.min(await this.getTokenAccountBalance(bank.mint, true), liabsUi); @@ -324,14 +370,13 @@ class Liquidator { 9 ); - debug!("Checking token account %s for %s", tokenAccount, mint); + // debug!("Checking token account %s for %s", tokenAccount, mint); try { return new BigNumber((await this.connection.getTokenAccountBalance(tokenAccount)).value.uiAmount!).plus( nativeAmount ); } catch (e) { - debug("Error getting token account balance: %s", e); return new BigNumber(0).plus(nativeAmount); } } @@ -341,23 +386,20 @@ class Liquidator { debug("Swapping any remaining non-usdc to usdc"); const banks = this.client.banks.values(); const usdcBank = this.client.getBankByMint(USDC_MINT)!; - for (let bankInterEntry = banks.next(); !bankInterEntry.done; bankInterEntry = banks.next()) { - const bank = bankInterEntry.value; + await Promise.all([...banks].map(async (bank) => { if (bank.mint.equals(USDC_MINT) || bank.mint.equals(NATIVE_MINT)) { - continue; + return; } let uiAmount = await this.getTokenAccountBalance(bank.mint); let price = this.client.getOraclePriceByBank(bank.address)!; let usdValue = bank.computeUsdValue(price, new BigNumber(uiAmount), PriceBias.None, undefined, false); - let amount = uiToNative(uiAmount, bank.mintDecimals).toNumber(); - - debug("Account has %d %s", uiAmount, this.getTokenSymbol(bank)); - if (usdValue.lte(DUST_THRESHOLD_UI)) { - debug!("Not enough %s to swap, skipping...", this.getTokenSymbol(bank)); - continue; + // debug!("Not enough %s to swap, skipping...", this.getTokenSymbol(bank)); + return; + } else { + debug("Account has %d ($%d) %s", uiAmount, usdValue, this.getTokenSymbol(bank)); } const balance = this.account.getBalance(bank.address); @@ -374,14 +416,14 @@ class Liquidator { if (uiAmount.lte(DUST_THRESHOLD_UI)) { debug("Account has no more %s, skipping...", this.getTokenSymbol(bank)); - continue; + return; } } debug("Swapping %d %s to USDC", uiAmount, this.getTokenSymbol(bank)); await this.swap(bank.mint, USDC_MINT, uiToNative(uiAmount, bank.mintDecimals)); - } + })); const usdcBalance = await this.getTokenAccountBalance(USDC_MINT); @@ -611,16 +653,23 @@ class Liquidator { liquidatorMaxLiqCapacityAssetAmount ); - const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.5); + const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.25); + + const collateralUsdValue = collateralBank.computeUsdValue( + collateralPriceInfo, + new BigNumber(uiToNative(slippageAdjustedCollateralAmountToLiquidate, collateralBank.mintDecimals).toNumber()), + PriceBias.None, + ); - if (slippageAdjustedCollateralAmountToLiquidate.lt(MIN_LIQUIDATION_AMOUNT_USD_UI)) { - debug("No collateral to liquidate"); + if (collateralUsdValue.lt(MIN_LIQUIDATION_AMOUNT_USD_UI)) { + debug("Collateral amount to liquidate is too small: $%d", collateralUsdValue); return false; } console.log( - "Liquidating %d %s for %s", + "Liquidating %d ($%d) %s for %s", slippageAdjustedCollateralAmountToLiquidate, + collateralUsdValue, this.getTokenSymbol(collateralBank), this.getTokenSymbol(liabBank) ); @@ -671,3 +720,7 @@ function drawSpinner(message: string) { frameIndex = (frameIndex + 1) % spinnerFrames.length; }, 100); } + +function nativeToBigNumber(amount: BN): BigNumber { + return new BigNumber(amount.toString()); +} \ No newline at end of file From f6fa0f14bfd7821db113a38c66fff18f4d02fc53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Thu, 9 Nov 2023 11:47:29 +0100 Subject: [PATCH 11/24] fix: more fixes --- apps/alpha-liquidator/src/liquidator.ts | 4 +-- .../src/models/account/pure.ts | 35 +++++++++++++------ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 062af77839..ae88ed6cab 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -643,7 +643,7 @@ class Liquidator { ); debug( - "Collateral amount to liquidate: $%d for bank %s", + "Collateral amount to liquidate: %d for bank %s", maxCollateralAmountToLiquidate, collateralBank.mint ) @@ -653,7 +653,7 @@ class Liquidator { liquidatorMaxLiqCapacityAssetAmount ); - const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.25); + const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.95); const collateralUsdValue = collateralBank.computeUsdValue( collateralPriceInfo, diff --git a/packages/marginfi-client-v2/src/models/account/pure.ts b/packages/marginfi-client-v2/src/models/account/pure.ts index 29c8b98f13..6ac79c376d 100644 --- a/packages/marginfi-client-v2/src/models/account/pure.ts +++ b/packages/marginfi-client-v2/src/models/account/pure.ts @@ -294,32 +294,45 @@ class MarginfiAccount { const priceAssetMarket = assetBank.getPrice(assetPriceInfo, PriceBias.None); const assetMaintWeight = assetBank.config.assetWeightMaint; - const liquidationDiscount = new BigNumber(1 - 0.05); + const liquidationDiscount = new BigNumber(0.95); const priceLiabHighest = liabilityBank.getPrice(liabilityPriceInfo, PriceBias.Highest); const priceLiabMarket = liabilityBank.getPrice(liabilityPriceInfo, PriceBias.None); const liabMaintWeight = liabilityBank.config.liabilityWeightMaint; - const underwaterMaintValue = currentHealth.div(assetMaintWeight.minus(liabMaintWeight.times(liquidationDiscount))); + debug("h: %d, w_a: %d, w_l: %d, d: %d", currentHealth.toFixed(6), assetMaintWeight, liabMaintWeight, liquidationDiscount); + + const underwaterMaintUsdValue = currentHealth.div(assetMaintWeight.minus(liabMaintWeight.times(liquidationDiscount))); + + debug("Underwater maint usd to adjust: $%d", underwaterMaintUsdValue.toFixed(6)); // MAX asset amount bounded by available asset amount const assetBalance = this.getBalance(assetBankAddress); - const assetsCap = assetBalance.computeQuantityUi(assetBank).assets; + const assetsAmountUi = assetBalance.computeQuantityUi(assetBank).assets; + const assetsUsdValue = assetsAmountUi.times(priceAssetLower); // MAX asset amount bounded by available liability amount const liabilityBalance = this.getBalance(liabilityBankAddress); - const liabilitiesForBank = liabilityBalance.computeQuantityUi(liabilityBank).liabilities; - const liabilityCap = liabilitiesForBank.times(priceLiabMarket).div(priceAssetMarket.times(liquidationDiscount)); + const liabilitiesAmountUi = liabilityBalance.computeQuantityUi(liabilityBank).liabilities; + const liabUsdValue = liabilitiesAmountUi.times(liquidationDiscount).times(priceLiabHighest); - debug("Liab amount: %d, price: %d, value: %d", liabilitiesForBank.toFixed(6), priceLiabMarket.toFixed(6), liabilitiesForBank.times(priceLiabMarket).toFixed(6)); + debug("Collateral amount: %d, price: %d, value: %d", + assetsAmountUi.toFixed(6), + priceAssetMarket.toFixed(6), + assetsUsdValue.times(priceAssetMarket).toFixed(6) + ); + + debug("Liab amount: %d, price: %d, value: %d", + liabilitiesAmountUi.toFixed(6), + priceLiabMarket.toFixed(6), + liabUsdValue.toFixed(6) + ); - debug("Collateral amount: %d, price: %d, value: %d", assetsCap.toFixed(6), priceAssetMarket.toFixed(6), assetsCap.times(priceAssetMarket).toFixed(6)); + const maxLiquidatableUsdValue = BigNumber.min(assetsAmountUi, underwaterMaintUsdValue, liabUsdValue); - debug("underwaterValue", underwaterMaintValue.toFixed(6)); - debug("assetsCap", assetsCap.toFixed(6)); - debug("liabilityCap", liabilityCap.toFixed(6)); + debug("Max liquidatable usd value: %d", maxLiquidatableUsdValue.toFixed(6)); - return BigNumber.min(assetsCap, underwaterMaintValue, liabilityCap); + return maxLiquidatableUsdValue.div(priceAssetLower); } getHealthCheckAccounts( From 55987b2400ec0742fcfccc0093f3afe4ca73f21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Thu, 9 Nov 2023 11:52:30 +0100 Subject: [PATCH 12/24] fix: liq math --- packages/marginfi-client-v2/src/models/account/pure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/marginfi-client-v2/src/models/account/pure.ts b/packages/marginfi-client-v2/src/models/account/pure.ts index 6ac79c376d..6b1eabd195 100644 --- a/packages/marginfi-client-v2/src/models/account/pure.ts +++ b/packages/marginfi-client-v2/src/models/account/pure.ts @@ -328,7 +328,7 @@ class MarginfiAccount { liabUsdValue.toFixed(6) ); - const maxLiquidatableUsdValue = BigNumber.min(assetsAmountUi, underwaterMaintUsdValue, liabUsdValue); + const maxLiquidatableUsdValue = BigNumber.min(assetsUsdValue, underwaterMaintUsdValue, liabUsdValue); debug("Max liquidatable usd value: %d", maxLiquidatableUsdValue.toFixed(6)); From 266cc20a65469ff312d37340e4b379c4472c9580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Thu, 9 Nov 2023 12:04:57 +0100 Subject: [PATCH 13/24] fix: log account value --- apps/alpha-liquidator/src/liquidator.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index ae88ed6cab..02790549d0 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -68,7 +68,8 @@ class Liquidator { } }, 10 * 60 * 1000); // refresh cache every 10 minutes - setInterval(this.printAccountValue, 30 * 1000); + setInterval(() => this.printAccountValue(), 30 * 1000); + this.printAccountValue(); console.log("Liquidating on %s banks", this.client.banks.size); @@ -84,11 +85,10 @@ class Liquidator { const accountValue = assets.minus(liabilities); console.log("Account Value: $%s", accountValue); } catch (e) { - console.error("Failed to fetch account value"); + console.error("Failed to fetch account value %s", e); } } - private async reload() { await this.client.reload(); await this.account.reload(); From 45c42f61d7c6b5acb0f35d0d4a39573ac573c713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Thu, 9 Nov 2023 13:39:35 +0100 Subject: [PATCH 14/24] fix: don't die on error --- apps/alpha-liquidator/src/liquidator.ts | 9 +++++---- apps/alpha-liquidator/src/runLiquidatorJupApi.ts | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 02790549d0..8df50974c3 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -667,11 +667,12 @@ class Liquidator { } console.log( - "Liquidating %d ($%d) %s for %s", - slippageAdjustedCollateralAmountToLiquidate, - collateralUsdValue, + "Liquidating %d ($%d) %s for %s, account: %s", + slippageAdjustedCollateralAmountToLiquidate.toFixed(6), + collateralUsdValue.toFixed(3), this.getTokenSymbol(collateralBank), - this.getTokenSymbol(liabBank) + this.getTokenSymbol(liabBank), + marginfiAccount.address.toBase58() ); const sig = await liquidatorAccount.lendingAccountLiquidate( diff --git a/apps/alpha-liquidator/src/runLiquidatorJupApi.ts b/apps/alpha-liquidator/src/runLiquidatorJupApi.ts index 040dfbb938..ebb85d3b20 100644 --- a/apps/alpha-liquidator/src/runLiquidatorJupApi.ts +++ b/apps/alpha-liquidator/src/runLiquidatorJupApi.ts @@ -25,7 +25,15 @@ async function start() { await liquidator.start(); } -start().catch((e) => { - console.log(e); - process.exit(1); -}); +async function startWithRestart() { + try { + await start(); + } catch (e) { + console.log(e); + console.log("Restarting due to crash..."); + startWithRestart(); + } +} + +startWithRestart(); + From 4b492c403c83b2a9b3687d84776b19ef717be323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Thu, 9 Nov 2023 14:21:41 +0100 Subject: [PATCH 15/24] fix: pre flight check --- apps/alpha-liquidator/src/liquidator.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 8df50974c3..cb6b4e0fe2 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -183,13 +183,10 @@ class Liquidator { const rawTransaction = transaction.serialize() const txid = await this.connection.sendRawTransaction(rawTransaction, { - skipPreflight: true, maxRetries: 2 }); debug("Swap transaction sent: %s", txid); - - await this.connection.confirmTransaction(txid); } /** From 3aef80f0e24e0d4c79702e883811a7d345bf9dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Sat, 11 Nov 2023 00:37:32 +0100 Subject: [PATCH 16/24] fix: native balance amount --- apps/alpha-liquidator/src/liquidator.ts | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index cb6b4e0fe2..d2d0bd56ff 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -97,29 +97,28 @@ class Liquidator { private async mainLoop() { const debug = getDebugLogger("main-loop"); drawSpinner("Scanning"); - try { - this.reload(); - while (true) { - await this.swapNonUsdcInTokenAccounts(); - debug("Started main loop iteration"); - if (await this.needsToBeRebalanced()) { - await this.rebalancingStage(); - continue; - } - - // Don't sleep after liquidating an account, start rebalance immediately - if (!(await this.liquidationStage())) { - await sleep(env_config.SLEEP_INTERVAL); - this.reload(); + while (true) { + try { + this.reload(); + while (true) { + await this.swapNonUsdcInTokenAccounts(); + debug("Started main loop iteration"); + if (await this.needsToBeRebalanced()) { + await this.rebalancingStage(); + continue; + } + + // Don't sleep after liquidating an account, start rebalance immediately + if (!(await this.liquidationStage())) { + await sleep(env_config.SLEEP_INTERVAL); + this.reload(); + } } + } catch (e) { + console.error(e); + captureException(e); + await sleep(env_config.SLEEP_INTERVAL); } - } catch (e) { - console.error(e); - - captureException(e); - - await sleep(env_config.SLEEP_INTERVAL); - await this.mainLoop(); } } @@ -311,7 +310,7 @@ class Liquidator { ); const liabsUi = new BigNumber(nativeToUi(liabilities, bank.mintDecimals)); - const liabBalance = BigNumber.min(await this.getTokenAccountBalance(bank.mint, true), liabsUi); + const liabBalance = BigNumber.min(await this.getTokenAccountBalance(bank.mint, false), liabsUi); debug("Got %s of %s, depositing to marginfi", liabBalance, bank.mint); @@ -360,13 +359,14 @@ class Liquidator { mint.equals(NATIVE_MINT) ? Math.max( (await this.connection.getBalance(this.wallet.publicKey)) - - (ignoreNativeMint ? MIN_SOL_BALANCE / 2 : MIN_SOL_BALANCE), + (ignoreNativeMint ? MIN_SOL_BALANCE / 2 : MIN_SOL_BALANCE) * LAMPORTS_PER_SOL, 0 ) : 0, 9 ); + // debug!("Checking token account %s for %s", tokenAccount, mint); try { @@ -388,7 +388,7 @@ class Liquidator { return; } - let uiAmount = await this.getTokenAccountBalance(bank.mint); + let uiAmount = await this.getTokenAccountBalance(bank.mint, false); let price = this.client.getOraclePriceByBank(bank.address)!; let usdValue = bank.computeUsdValue(price, new BigNumber(uiAmount), PriceBias.None, undefined, false); From 7cf39dbf19d83e63a5b2a9a2da199e06813e14c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Sat, 11 Nov 2023 00:53:24 +0100 Subject: [PATCH 17/24] fix: sol handling --- apps/alpha-liquidator/src/liquidator.ts | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index d2d0bd56ff..ba33157dd0 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -355,26 +355,25 @@ class Liquidator { private async getTokenAccountBalance(mint: PublicKey, ignoreNativeMint: boolean = false): Promise { const debug = getDebugLogger("getTokenAccountBalances"); const tokenAccount = await associatedAddress({ mint, owner: this.wallet.publicKey }); - const nativeAmount = nativeToUi( - mint.equals(NATIVE_MINT) - ? Math.max( - (await this.connection.getBalance(this.wallet.publicKey)) - - (ignoreNativeMint ? MIN_SOL_BALANCE / 2 : MIN_SOL_BALANCE) * LAMPORTS_PER_SOL, - 0 - ) - : 0, - 9 - ); + debug!("Checking token account %s for %s", tokenAccount, mint); + + let nativeAmountUi = 0; + + if (mint.equals(NATIVE_MINT)) { - // debug!("Checking token account %s for %s", tokenAccount, mint); + let nativeAmount = await this.connection.getBalance(this.wallet.publicKey); + nativeAmountUi = nativeToUi(Math.max(nativeAmount - MIN_SOL_BALANCE, 0), 9); + + debug("Native amount: %d", nativeAmountUi); + } try { return new BigNumber((await this.connection.getTokenAccountBalance(tokenAccount)).value.uiAmount!).plus( - nativeAmount + nativeAmountUi ); } catch (e) { - return new BigNumber(0).plus(nativeAmount); + return new BigNumber(0).plus(nativeAmountUi); } } @@ -384,11 +383,12 @@ class Liquidator { const banks = this.client.banks.values(); const usdcBank = this.client.getBankByMint(USDC_MINT)!; await Promise.all([...banks].map(async (bank) => { - if (bank.mint.equals(USDC_MINT) || bank.mint.equals(NATIVE_MINT)) { + if (bank.mint.equals(USDC_MINT)) { return; } let uiAmount = await this.getTokenAccountBalance(bank.mint, false); + debug("Account has %d %s", uiAmount, this.getTokenSymbol(bank)); let price = this.client.getOraclePriceByBank(bank.address)!; let usdValue = bank.computeUsdValue(price, new BigNumber(uiAmount), PriceBias.None, undefined, false); From f90ec31a677f0bdd5619e2aa7e268c6cfaa8232a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Sat, 11 Nov 2023 01:15:13 +0100 Subject: [PATCH 18/24] fix: wait for tx confirmation --- apps/alpha-liquidator/src/liquidator.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index ba33157dd0..69a4d11284 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -185,6 +185,8 @@ class Liquidator { maxRetries: 2 }); + await this.connection.confirmTransaction(txid, 'confirmed'); + debug("Swap transaction sent: %s", txid); } @@ -310,11 +312,13 @@ class Liquidator { ); const liabsUi = new BigNumber(nativeToUi(liabilities, bank.mintDecimals)); - const liabBalance = BigNumber.min(await this.getTokenAccountBalance(bank.mint, false), liabsUi); + const liabsTokenAccountUi = await this.getTokenAccountBalance(bank.mint, false); + const liabsUiAmountToRepay = BigNumber.min(liabsTokenAccountUi, liabsUi); - debug("Got %s of %s, depositing to marginfi", liabBalance, bank.mint); + debug("Got %d %s (debt: %d), depositing to marginfi", liabsUiAmountToRepay, this.getTokenSymbol(bank), liabsUi); + debug("Paying off %d %s liabilities", liabsUiAmountToRepay, this.getTokenSymbol(bank)); - const depositSig = await this.account.repay(liabBalance, bank.address, liabBalance.gte(liabsUi)); + const depositSig = await this.account.repay(liabsUiAmountToRepay, bank.address, liabsUiAmountToRepay.gte(liabsUi)); debug("Deposit tx: %s", depositSig); await this.reload(); @@ -358,22 +362,21 @@ class Liquidator { debug!("Checking token account %s for %s", tokenAccount, mint); - let nativeAmountUi = 0; + let nativeAmoutnUi = 0; if (mint.equals(NATIVE_MINT)) { - let nativeAmount = await this.connection.getBalance(this.wallet.publicKey); - nativeAmountUi = nativeToUi(Math.max(nativeAmount - MIN_SOL_BALANCE, 0), 9); + nativeAmoutnUi = nativeToUi(Math.max(nativeAmount - MIN_SOL_BALANCE, 0), 9); - debug("Native amount: %d", nativeAmountUi); + debug("Native amount: %d", nativeAmoutnUi); } try { return new BigNumber((await this.connection.getTokenAccountBalance(tokenAccount)).value.uiAmount!).plus( - nativeAmountUi + nativeAmoutnUi ); } catch (e) { - return new BigNumber(0).plus(nativeAmountUi); + return new BigNumber(0).plus(nativeAmoutnUi); } } From e22518ba3ee97afbe7da084668e0f1c2877860a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Sat, 11 Nov 2023 16:13:05 +0100 Subject: [PATCH 19/24] fix: liq increase fee --- apps/alpha-liquidator/src/liquidator.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 69a4d11284..1ed092a325 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -169,6 +169,7 @@ class Liquidator { wrapUnwrapSOL: true, // feeAccount is optional. Use if you want to charge a fee. feeBps must have been passed in /quote API. // feeAccount: "fee_account_public_key" + computeUnitPriceMicroLamports: 50 }) }) ).json(); @@ -182,7 +183,7 @@ class Liquidator { const rawTransaction = transaction.serialize() const txid = await this.connection.sendRawTransaction(rawTransaction, { - maxRetries: 2 + maxRetries: 2, }); await this.connection.confirmTransaction(txid, 'confirmed'); From c198e9ab17c55635c9eb5fac87a50e27c0ab3a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Sat, 11 Nov 2023 16:21:10 +0100 Subject: [PATCH 20/24] fix: higher swap fee --- apps/alpha-liquidator/src/liquidator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 1ed092a325..0fc6171e95 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -169,7 +169,7 @@ class Liquidator { wrapUnwrapSOL: true, // feeAccount is optional. Use if you want to charge a fee. feeBps must have been passed in /quote API. // feeAccount: "fee_account_public_key" - computeUnitPriceMicroLamports: 50 + computeUnitPriceMicroLamports: 50000, }) }) ).json(); From 8d8259d616058dcd1d9778d8a694c9e8179c06b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Sun, 12 Nov 2023 11:14:02 +0100 Subject: [PATCH 21/24] fix: add sorted account mode, withdraw amount conversion --- apps/alpha-liquidator/src/config.ts | 6 ++ apps/alpha-liquidator/src/liquidator.ts | 89 +++++++++++++++++++------ packages/mrgn-common/src/conversion.ts | 5 ++ 3 files changed, 80 insertions(+), 20 deletions(-) diff --git a/apps/alpha-liquidator/src/config.ts b/apps/alpha-liquidator/src/config.ts index aed94323e5..693a0c80f2 100644 --- a/apps/alpha-liquidator/src/config.ts +++ b/apps/alpha-liquidator/src/config.ts @@ -30,6 +30,7 @@ if (!process.env.RPC_ENDPOINT) { /*eslint sort-keys: "error"*/ let envSchema = z.object({ + ACCOUNT_COOL_DOWN_SECONDS: z.string().default("120").transform((s) => parseInt(s, 10)), IS_DEV: z .string() .optional() @@ -69,6 +70,11 @@ let envSchema = z.object({ .string() .default("10000") .transform((s) => parseInt(s, 10)), + SORT_ACCOUNTS_MODE: z + .string() + .optional() + .default("false") + .transform((s) => s === "true" || s === "1"), WALLET_KEYPAIR: z.string().transform((keypairStr) => { if (fs.existsSync(resolveHome(keypairStr))) { return loadKeypair(keypairStr); diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 0fc6171e95..9a5de2255c 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -1,12 +1,13 @@ import { Connection, LAMPORTS_PER_SOL, PublicKey, VersionedTransaction } from "@solana/web3.js"; import { MarginRequirementType, + MarginfiAccount, MarginfiAccountWrapper, MarginfiClient, PriceBias, USDC_DECIMALS, } from "@mrgnlabs/marginfi-client-v2"; -import { nativeToUi, NodeWallet, shortenAddress, sleep, uiToNative } from "@mrgnlabs/mrgn-common"; +import { nativeToUi, NodeWallet, shortenAddress, sleep, toBigNumber, uiToNative, uiToNativeBigNumber } from "@mrgnlabs/mrgn-common"; import BigNumber from "bignumber.js"; import { associatedAddress } from "@project-serum/anchor/dist/cjs/utils/token"; import { NATIVE_MINT } from "@solana/spl-token"; @@ -32,6 +33,7 @@ function getDebugLogger(context: string) { class Liquidator { private bankMetadataMap: BankMetadataMap; + private accountCooldowns: Map = new Map(); constructor( readonly connection: Connection, @@ -212,23 +214,22 @@ class Liquidator { .filter(({ assets, bank }) => !bank.mint.equals(USDC_MINT)); for (let { bank } of balancesWithNonUsdcDeposits) { - // const maxWithdrawAmount = nativeToBigNumber(uiToNative(this.account.computeMaxWithdrawForBank(bank.address), bank.mintDecimals)); - const balanceAssetAmount = this.account.getBalance(bank.address).computeQuantity(bank).assets; - // TODO: Fix dumbass conversion - const maxWithdrawAmount = balanceAssetAmount; + const maxWithdrawAmount = this.account.computeMaxWithdrawForBank(bank.address); + const balanceAssetAmount = nativeToUi(this.account.getBalance(bank.address).computeQuantity(bank).assets, bank.mintDecimals); + const withdrawAmount = BigNumber.min(maxWithdrawAmount, balanceAssetAmount); - debug("Balance: %d, max withdraw: %d", balanceAssetAmount, maxWithdrawAmount); + debug("Balance: %d, max withdraw: %d", balanceAssetAmount, withdrawAmount); - if (maxWithdrawAmount.eq(0)) { + if (withdrawAmount.eq(0)) { debug("No untied %s to withdraw", this.getTokenSymbol(bank)); continue; } - debug("Withdrawing %d %s", maxWithdrawAmount, this.getTokenSymbol(bank)); + debug("Withdrawing %d %s", withdrawAmount, this.getTokenSymbol(bank)); let withdrawSig = await this.account.withdraw( - maxWithdrawAmount, + withdrawAmount, bank.address, - true + withdrawAmount.gte(balanceAssetAmount) ); debug("Withdraw tx: %s", withdrawSig) @@ -284,7 +285,7 @@ class Liquidator { /// When a liab value is super small (1 BONK), we cannot feasibly buy it for the exact amount, // so the solution is to buy more (trivial amount more), and then over repay. - const liabUsdcValue = BigNumber.max(baseLiabUsdcValue, new BigNumber(1)); + const liabUsdcValue = BigNumber.max(baseLiabUsdcValue, new BigNumber(0.05)); debug("Liab usd value %s", liabUsdcValue); @@ -489,7 +490,14 @@ class Liquidator { return true; }); - const accounts = shuffle(targetAccounts); + let accounts = []; + + if (env_config.SORT_ACCOUNTS_MODE) { + accounts = this.sortByLiquidationAmount(targetAccounts); + } else { + accounts = shuffle(targetAccounts); + } + debug("Found %s accounts in total", allAccounts.length); debug("Monitoring %s accounts", targetAccounts.length); @@ -654,7 +662,7 @@ class Liquidator { liquidatorMaxLiqCapacityAssetAmount ); - const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.95); + const slippageAdjustedCollateralAmountToLiquidate = collateralAmountToLiquidate.times(0.9); const collateralUsdValue = collateralBank.computeUsdValue( collateralPriceInfo, @@ -676,17 +684,58 @@ class Liquidator { marginfiAccount.address.toBase58() ); - const sig = await liquidatorAccount.lendingAccountLiquidate( - marginfiAccount.data, - collateralBank.address, - slippageAdjustedCollateralAmountToLiquidate, - liabBank.address - ); - console.log("Liquidation tx: %s", sig); + try { + const sig = await liquidatorAccount.lendingAccountLiquidate( + marginfiAccount.data, + collateralBank.address, + slippageAdjustedCollateralAmountToLiquidate, + liabBank.address + ); + + console.log("Liquidation tx: %s", sig); + + } catch (e) { + console.error("Failed to liquidate account %s", marginfiAccount.address.toBase58()); + console.error(e); + + this.addAccountToCoolDown(marginfiAccount.address); + + return false; + } return true; } + isAccountInCoolDown(address: PublicKey): boolean { + const cooldown = this.accountCooldowns.get(address.toBase58()); + if (!cooldown) { + return false; + } + + return cooldown > Date.now(); + } + + addAccountToCoolDown(address: PublicKey) { + const debug = getDebugLogger("add-account-to-cooldown"); + debug("Adding account %s to cooldown for %d seconds", address.toBase58(), env_config.ACCOUNT_COOL_DOWN_SECONDS); + this.accountCooldowns.set(address.toBase58(), Date.now() + env_config.ACCOUNT_COOL_DOWN_SECONDS * 1000); + } + + sortByLiquidationAmount(accounts: MarginfiAccountWrapper[]): MarginfiAccountWrapper[] { + return accounts + .filter(a => a.canBeLiquidated()) + .filter(a => !this.isAccountInCoolDown(a.address)) + .sort((a, b) => { + const { assets: aAssets, liabilities: aLiabilities } = a.computeHealthComponents(MarginRequirementType.Maintenance); + const { assets: bAssets, liabilities: bLiabilities } = b.computeHealthComponents(MarginRequirementType.Maintenance); + + const aMaxLiabilityPaydown = aAssets.minus(aLiabilities); + const bMaxLiabilityPaydown = bAssets.minus(bLiabilities); + + return aMaxLiabilityPaydown.comparedTo(bMaxLiabilityPaydown); + }); + } + getTokenSymbol(bank: Bank): string { const bankMetadata = this.bankMetadataMap[bank.address.toBase58()]; if (!bankMetadata) { diff --git a/packages/mrgn-common/src/conversion.ts b/packages/mrgn-common/src/conversion.ts index 0e5679e5da..c87364b2bc 100644 --- a/packages/mrgn-common/src/conversion.ts +++ b/packages/mrgn-common/src/conversion.ts @@ -48,6 +48,11 @@ export function uiToNative(amount: Amount, decimals: number): BN { return new BN(amt.times(10 ** decimals).toFixed(0, BigNumber.ROUND_FLOOR)); } +export function uiToNativeBigNumber(amount: Amount, decimals: number): BigNumber { + let amt = toBigNumber(amount); + return amt.times(10 ** decimals); +} + /** * Converts a native representation of a token amount into its UI value as `number`, given the specified mint decimal amount (default to 6 for USDC). */ From 5120b79ebc0bc20446816356aaa2536aa2ddc19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Tue, 21 Nov 2023 17:18:35 +0100 Subject: [PATCH 22/24] fix: multi bank fix --- apps/alpha-liquidator/src/liquidator.ts | 10 +++++----- yarn.lock | 7 ------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 9a5de2255c..28fc339c0e 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -387,9 +387,9 @@ class Liquidator { debug("Swapping any remaining non-usdc to usdc"); const banks = this.client.banks.values(); const usdcBank = this.client.getBankByMint(USDC_MINT)!; - await Promise.all([...banks].map(async (bank) => { + for (const bank of banks) { if (bank.mint.equals(USDC_MINT)) { - return; + continue; } let uiAmount = await this.getTokenAccountBalance(bank.mint, false); @@ -399,7 +399,7 @@ class Liquidator { if (usdValue.lte(DUST_THRESHOLD_UI)) { // debug!("Not enough %s to swap, skipping...", this.getTokenSymbol(bank)); - return; + continue; } else { debug("Account has %d ($%d) %s", uiAmount, usdValue, this.getTokenSymbol(bank)); } @@ -418,14 +418,14 @@ class Liquidator { if (uiAmount.lte(DUST_THRESHOLD_UI)) { debug("Account has no more %s, skipping...", this.getTokenSymbol(bank)); - return; + continue; } } debug("Swapping %d %s to USDC", uiAmount, this.getTokenSymbol(bank)); await this.swap(bank.mint, USDC_MINT, uiToNative(uiAmount, bank.mintDecimals)); - })); + } const usdcBalance = await this.getTokenAccountBalance(USDC_MINT); diff --git a/yarn.lock b/yarn.lock index 9eba0fd8d8..0efb4c5d5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12132,13 +12132,6 @@ cross-fetch@3.0.6: dependencies: node-fetch "2.6.1" -cross-fetch@3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" - integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== - dependencies: - node-fetch "2.6.7" - cross-fetch@3.1.6: version "3.1.6" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" From d6388a21345d5830ae7ada4e46032b960c303349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Mon, 4 Dec 2023 12:09:08 +0100 Subject: [PATCH 23/24] fix: start and keep alive script --- apps/alpha-liquidator/scripts/start.sh | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 apps/alpha-liquidator/scripts/start.sh diff --git a/apps/alpha-liquidator/scripts/start.sh b/apps/alpha-liquidator/scripts/start.sh new file mode 100644 index 0000000000..79bd6eb74c --- /dev/null +++ b/apps/alpha-liquidator/scripts/start.sh @@ -0,0 +1,4 @@ +while true; do + yarn start + sleep 1 # Sleep for 1 second before restarting, adjust as needed +done From babe4c7512afb2ea06c0a7bcea28a48d5db39802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20Pov=C5=A1i=C4=8D?= Date: Wed, 6 Dec 2023 14:10:07 +0100 Subject: [PATCH 24/24] fix: readme --- apps/alpha-liquidator/README.md | 77 +++++++++++++++++++++++-- apps/alpha-liquidator/scripts/start.sh | 0 apps/alpha-liquidator/src/liquidator.ts | 2 +- 3 files changed, 74 insertions(+), 5 deletions(-) mode change 100644 => 100755 apps/alpha-liquidator/scripts/start.sh diff --git a/apps/alpha-liquidator/README.md b/apps/alpha-liquidator/README.md index c5242f0e76..4c2d9ac278 100644 --- a/apps/alpha-liquidator/README.md +++ b/apps/alpha-liquidator/README.md @@ -2,11 +2,80 @@ > This bot structure is based on the Jupiter quote API, check it out [here](https://github.com/jup-ag/jupiter-quote-api) -## Prerequisite +## Setup -- [redis](https://redis.io/docs/getting-started/installation/install-redis-on-mac-os/) +Before running the alpha liquidator bot, ensure that you have the following prerequisites installed: + +- Node.js (v14 or higher) +- Yarn package manager + +Then, in the root of the mrgn-ts repo, install the dependencies and build the project by running: + +```sh +yarn +yarn build +``` + +Make sure to configure the environment variables as described in the [Alpha Liquidator Configuration](#alpha-liquidator-configuration) section. ## How to run -1. `yarn` -2. `RPC_URL=xxxxxxx yarn start` +1. `./scripts/start.sh` + +## Alpha Liquidator Configuration + +The Alpha Liquidator application uses an environment configuration schema to manage its settings. This configuration is defined in `apps/alpha-liquidator/src/config.ts` and uses the `zod` library for schema definition and validation. + +### Configuration Schema + +Below are the environment variables used by the application, along with their expected types and default values: + +- `RPC_ENDPOINT`: The RPC endpoint URL as a string. + +- `LIQUIDATOR_PK`: The public key of the liquidator. It is a string that gets converted into a `PublicKey` object. + +- `WALLET_KEYPAIR`: The wallet keypair for the liquidator. It is a string that either gets loaded from a file if it exists or gets converted from a JSON string into a `Keypair` object. + +- `MIN_LIQUIDATION_AMOUNT_USD_UI`: The minimum liquidation amount in USD. It is a string that gets converted into a `BigNumber` object. Default is `"0.1"`. + +- `MAX_SLIPPAGE_BPS`: The maximum slippage in basis points. It is a string that gets parsed into an integer. Default is `"250"`. + +- `MIN_SOL_BALANCE`: The minimum balance of SOL required. It is a number with a default value of `0.5`. + +- `ACCOUNT_COOL_DOWN_SECONDS`: The cool down period in seconds before reattempting a liquidation on an account that previously failed when using the SORT_ACCOUNTS_MODE. It is a string that gets parsed into an integer. Default is `"120"`. + +- `SLEEP_INTERVAL`: The interval in milliseconds between checks. It is a string that gets parsed into an integer. Default is `"10000"`. + +- `SENTRY`: A flag indicating whether Sentry is enabled for error logging. It accepts a string and converts it to a boolean. Default is `"false"`. + +- `SENTRY_DSN`: The Sentry DSN string for error reporting. This field is optional. + +- `SORT_ACCOUNTS_MODE`: An experimental feature flag indicating whether accounts should be sorted by the liquidation amount, with accounts having more to liquidate being prioritized. It accepts a string and converts it to a boolean. Default is `"false"`. + +- `MARGINFI_ACCOUNT_BLACKLIST`: A comma-separated string of MarginFi account public keys to be blacklisted. It gets transformed into an array of `PublicKey` objects. This field is optional. + +- `MARGINFI_ACCOUNT_WHITELIST`: A comma-separated string of MarginFi account public keys to be whitelisted. It gets transformed into an array of `PublicKey` objects. This field is optional. + +### Required Configuration Fields + +The following environment variables are mandatory for the application to run: + +- `RPC_ENDPOINT`: The RPC endpoint URL as a string. +- `LIQUIDATOR_PK`: The public key of the liquidator. It is a string that gets converted into a `PublicKey` object. +- `WALLET_KEYPAIR`: The wallet keypair for the liquidator. It is a string that either gets loaded from a file if it exists or gets converted from a JSON string into a `Keypair` object. + +## Validation and Parsing + +The `env_config` object is created by parsing the `process.env` object through the `envSchema`. If any of the required environment variables are missing or invalid, the application will throw an error during the parsing process. + +## Mutually Exclusive Fields + +The application ensures that `MARGINFI_ACCOUNT_BLACKLIST` and `MARGINFI_ACCOUNT_WHITELIST` are mutually exclusive. If both are provided, an error is thrown. + +## Sentry Integration + +If Sentry is enabled (`SENTRY` is `true`), and a `SENTRY_DSN` is provided, the application initializes Sentry for error tracking. It also captures a startup message indicating that the Alpha Liquidator has started. + +## Usage + +To use the configuration, import `env_config` from the `config.ts` file. Ensure that the required environment variables are set before starting the application. diff --git a/apps/alpha-liquidator/scripts/start.sh b/apps/alpha-liquidator/scripts/start.sh old mode 100644 new mode 100755 diff --git a/apps/alpha-liquidator/src/liquidator.ts b/apps/alpha-liquidator/src/liquidator.ts index 28fc339c0e..94b9f6c69e 100644 --- a/apps/alpha-liquidator/src/liquidator.ts +++ b/apps/alpha-liquidator/src/liquidator.ts @@ -229,7 +229,7 @@ class Liquidator { let withdrawSig = await this.account.withdraw( withdrawAmount, bank.address, - withdrawAmount.gte(balanceAssetAmount) + withdrawAmount.gte(balanceAssetAmount * 0.95) ); debug("Withdraw tx: %s", withdrawSig)