From 9aa0a94794281294a80e1951f47e874e2f7ce69c Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 22 Aug 2022 12:39:34 +0200 Subject: [PATCH 01/50] make consistent, always return positive values for deposits or borrows Signed-off-by: microwavedcola1 --- anchor | 2 +- ts/client/src/accounts/mangoAccount.ts | 99 ++++++++++++++------------ 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/anchor b/anchor index 9e546f52d..b52f23614 160000 --- a/anchor +++ b/anchor @@ -1 +1 @@ -Subproject commit 9e546f52d967e95fea4ae105f4bc7bf3720b9464 +Subproject commit b52f23614601652a99ec6c27aec77bd327363b31 diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index d943afc4c..3918bdcdf 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -3,16 +3,10 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; import { nativeI80F48ToUi } from '../utils'; -import { Bank, QUOTE_DECIMALS } from './bank'; +import { Bank } from './bank'; import { Group } from './group'; import { HealthCache, HealthCacheDto } from './healthCache'; -import { - HUNDRED_I80F48, - I80F48, - I80F48Dto, - ONE_I80F48, - ZERO_I80F48, -} from './I80F48'; +import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; export class MangoAccount { public tokens: TokenPosition[]; public serum3: Serum3Orders[]; @@ -110,7 +104,7 @@ export class MangoAccount { nativeTokenPosition: TokenPosition, ): I80F48 { return nativeTokenPosition - ? nativeTokenPosition.native(sourceBank).mul(sourceBank.price) + ? nativeTokenPosition.balance(sourceBank).mul(sourceBank.price) : ZERO_I80F48; } @@ -124,47 +118,47 @@ export class MangoAccount { /** * * @param bank - * @returns native balance for a token + * @returns native balance for a token, is signed */ getTokenBalance(bank: Bank): I80F48 { - const ta = this.findToken(bank.tokenIndex); - return ta ? ta.native(bank) : ZERO_I80F48; + const tp = this.findToken(bank.tokenIndex); + return tp ? tp.balance(bank) : ZERO_I80F48; } /** * * @param bank - * @returns native balance for a token, 0 or more + * @returns native deposits for a token, 0 if position has borrows */ getTokenDeposits(bank: Bank): I80F48 { - const native = this.getTokenBalance(bank); - return native.gte(ZERO_I80F48) ? native : ZERO_I80F48; + const tp = this.findToken(bank.tokenIndex); + return tp ? tp.deposits(bank) : ZERO_I80F48; } /** * * @param bank - * @returns native balance for a token, 0 or less + * @returns native borrows for a token, 0 if position has deposits */ getTokenBorrows(bank: Bank): I80F48 { - const native = this.getTokenBalance(bank); - return native.lte(ZERO_I80F48) ? native : ZERO_I80F48; + const tp = this.findToken(bank.tokenIndex); + return tp ? tp.borrows(bank) : ZERO_I80F48; } /** * * @param bank - * @returns UI balance for a token + * @returns UI balance for a token, is signed */ getTokenBalanceUi(bank: Bank): number { - const ta = this.findToken(bank.tokenIndex); - return ta ? ta.balanceUi(bank) : 0; + const tp = this.findToken(bank.tokenIndex); + return tp ? tp.balanceUi(bank) : 0; } /** * * @param bank - * @returns UI balance for a token, 0 or more + * @returns UI deposits for a token, 0 or more */ getTokenDepositsUi(bank: Bank): number { const ta = this.findToken(bank.tokenIndex); @@ -174,7 +168,7 @@ export class MangoAccount { /** * * @param bank - * @returns UI balance for a token, 0 or less + * @returns UI borrows for a token, 0 or less */ getTokenBorrowsUi(bank: Bank): number { const ta = this.findToken(bank.tokenIndex); @@ -412,7 +406,12 @@ export class TokenPosition { return this.tokenIndex !== TokenPosition.TokenIndexUnset; } - public native(bank: Bank): I80F48 { + /** + * + * @param bank + * @returns native balance + */ + public balance(bank: Bank): I80F48 { if (this.indexedPosition.isPos()) { return bank.depositIndex.mul(this.indexedPosition); } else { @@ -421,41 +420,51 @@ export class TokenPosition { } /** + * * @param bank - * @returns position in UI decimals, is signed + * @returns native deposits, 0 if position has borrows + */ + public deposits(bank: Bank): I80F48 { + if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48)) { + return ZERO_I80F48; + } + return this.balance(bank); + } + + /** + * + * @param bank + * @returns native borrows, 0 if position has deposits + */ + public borrows(bank: Bank): I80F48 { + if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48)) { + return ZERO_I80F48; + } + return this.balance(bank).abs(); + } + + /** + * @param bank + * @returns UI balance, is signed */ public balanceUi(bank: Bank): number { - return nativeI80F48ToUi(this.native(bank), bank.mintDecimals).toNumber(); + return nativeI80F48ToUi(this.balance(bank), bank.mintDecimals).toNumber(); } /** * @param bank - * @returns position in UI decimals, 0 if position has borrows + * @returns UI deposits, 0 if position has borrows */ public depositsUi(bank: Bank): number { - if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48)) { - return 0; - } - - return nativeI80F48ToUi( - bank.depositIndex.mul(this.indexedPosition), - bank.mintDecimals, - ).toNumber(); + return nativeI80F48ToUi(this.deposits(bank), bank.mintDecimals).toNumber(); } /** * @param bank - * @returns position in UI decimals, can be 0 or negative, 0 if position has deposits + * @returns UI borrows, 0 if position has deposits */ public borrowsUi(bank: Bank): number { - if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48)) { - return 0; - } - - return nativeI80F48ToUi( - bank.borrowIndex.mul(this.indexedPosition), - bank.mintDecimals, - ).toNumber(); + return nativeI80F48ToUi(this.borrows(bank), bank.mintDecimals).toNumber(); } public toString(group?: Group, index?: number): string { @@ -463,7 +472,7 @@ export class TokenPosition { if (group) { const bank: Bank = group.getFirstBankByTokenIndex(this.tokenIndex); if (bank) { - const native = this.native(bank); + const native = this.balance(bank); extra += ', native: ' + native.toNumber(); extra += ', ui: ' + this.balanceUi(bank); extra += ', tokenName: ' + bank.name; From 357710dc24f36e2e6ebd354b5e2a1f4c9f1ec1af Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 22 Aug 2022 13:02:43 +0200 Subject: [PATCH 02/50] ts: Fix getMaxWithdrawWithBorrowForToken and add getGroupTokenVaultBalanceByMint Signed-off-by: microwavedcola1 --- ts/client/src/accounts/group.ts | 29 +++++++++++++++++-- ts/client/src/accounts/healthCache.ts | 4 +-- ts/client/src/accounts/mangoAccount.ts | 28 +++++++++--------- ts/client/src/debug-scripts/mb-debug-banks.ts | 5 ++-- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 86d2b8a0b..5f78e0484 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -1,16 +1,17 @@ import { BorshAccountsCoder } from '@project-serum/anchor'; +import { coder } from '@project-serum/anchor/dist/cjs/spl/associated-token'; import { Market } from '@project-serum/serum'; import { parsePriceData, PriceData } from '@pythnetwork/client'; import { PublicKey } from '@solana/web3.js'; +import BN from 'bn.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; import { Id } from '../ids'; +import { toNativeDecimals } from '../utils'; import { Bank, MintInfo } from './bank'; import { I80F48, ONE_I80F48 } from './I80F48'; import { PerpMarket } from './perp'; import { Serum3Market } from './serum3'; -import { toNativeDecimals } from '../utils'; -import BN from 'bn.js'; export class Group { static from( @@ -267,6 +268,30 @@ export class Group { return this.banksMapByTokenIndex.get(tokenIndex)[0]; } + /** + * + * @param client + * @param mintPk + * @returns sum of native balances of all vaults for a token + */ + public async getGroupTokenVaultBalanceByMint( + client: MangoClient, + mintPk: PublicKey, + ): Promise { + const banks = this.banksMapByMint.get(mintPk.toString()); + const amount = new BN(0); + for (const bank of banks) { + amount.add( + coder().accounts.decode( + 'token', + (await client.program.provider.connection.getAccountInfo(bank.vault)) + .data, + ).amount, + ); + } + return new I80F48(amount); + } + public consoleLogBanks() { for (const mintBanks of this.banksMapByMint.values()) { for (const bank of mintBanks) { diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 6efbe6fbd..d087168a9 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -365,8 +365,8 @@ export class HealthCache { return amount .div(source.oraclePrice) - .mul( - ONE_I80F48.sub( + .div( + ONE_I80F48.add( group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate, ), ); diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 3918bdcdf..83984c236 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -101,11 +101,9 @@ export class MangoAccount { static getEquivalentUsdcPosition( sourceBank: Bank, - nativeTokenPosition: TokenPosition, + tp: TokenPosition, ): I80F48 { - return nativeTokenPosition - ? nativeTokenPosition.balance(sourceBank).mul(sourceBank.price) - : ZERO_I80F48; + return tp ? tp.balance(sourceBank).mul(sourceBank.price) : ZERO_I80F48; } static getEquivalentTokenPosition( @@ -244,23 +242,27 @@ export class MangoAccount { } /** - * The amount of given native token you can borrow, considering all existing assets as collateral except the deposits for this token. - * Note 1: The existing native deposits need to be added to get the full amount that could be withdrawn. - * Note 2: The group might have less native deposits than what this returns. TODO: loan origination fees - * @returns amount of given native token you can borrow, considering all existing assets as collateral except the deposits for this token, in native token + * The amount of given native token you can withdraw including borrows, considering all existing assets as collateral. + * @returns amount of given native token you can borrow, considering all existing assets as collateral, in native token */ getMaxWithdrawWithBorrowForToken(group: Group, mintPk: PublicKey): I80F48 { const bank: Bank = group.getFirstBankByMint(mintPk); const initHealth = (this.accountData as MangoAccountData).initHealth; - const inUsdcUnits = MangoAccount.getEquivalentUsdcPosition( + const existingPositioninUsdcUnits = MangoAccount.getEquivalentUsdcPosition( bank, this.findToken(bank.tokenIndex), ).max(ZERO_I80F48); - const newInitHealth = initHealth.sub(inUsdcUnits.mul(bank.initAssetWeight)); - return MangoAccount.getEquivalentTokenPosition( - bank, - newInitHealth.div(bank.initLiabWeight), + const initHealthWithoutExistingPosition = initHealth.sub( + existingPositioninUsdcUnits.mul(bank.initAssetWeight), ); + const maxBorrowNative = MangoAccount.getEquivalentTokenPosition( + bank, + initHealthWithoutExistingPosition.div(bank.initLiabWeight), + ); + const maxBorrowNativeWithoutFees = maxBorrowNative.div( + ONE_I80F48.add(bank.loanOriginationFeeRate), + ); + return maxBorrowNativeWithoutFees.add(this.getTokenBalance(bank)); } /** diff --git a/ts/client/src/debug-scripts/mb-debug-banks.ts b/ts/client/src/debug-scripts/mb-debug-banks.ts index 6357aac06..b53c205a8 100644 --- a/ts/client/src/debug-scripts/mb-debug-banks.ts +++ b/ts/client/src/debug-scripts/mb-debug-banks.ts @@ -73,9 +73,8 @@ async function main() { coder() .accounts.decode( 'token', - await ( - await client.program.provider.connection.getAccountInfo(bank.vault) - ).data, + (await client.program.provider.connection.getAccountInfo(bank.vault)) + .data, ) .amount.toNumber(), ); From f273129b59841cc100289f225649e5c892bb86de Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 22 Aug 2022 18:56:40 +0200 Subject: [PATCH 03/50] testing Signed-off-by: microwavedcola1 --- ts/client/src/accounts/mangoAccount.ts | 13 ++++- ts/client/src/debug-scripts/mb-debug-user.ts | 58 ++++++++++++++------ 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 83984c236..dd51fdf59 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -2,7 +2,7 @@ import { BN } from '@project-serum/anchor'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; -import { nativeI80F48ToUi } from '../utils'; +import { nativeI80F48ToUi, toUiDecimals } from '../utils'; import { Bank } from './bank'; import { Group } from './group'; import { HealthCache, HealthCacheDto } from './healthCache'; @@ -262,7 +262,16 @@ export class MangoAccount { const maxBorrowNativeWithoutFees = maxBorrowNative.div( ONE_I80F48.add(bank.loanOriginationFeeRate), ); - return maxBorrowNativeWithoutFees.add(this.getTokenBalance(bank)); + return maxBorrowNativeWithoutFees + .add(this.getTokenBalance(bank)) + .mul(I80F48.fromString('0.99')); + } + + getMaxWithdrawWithBorrowForTokenUi(group: Group, mintPk: PublicKey): number { + return toUiDecimals( + this.getMaxWithdrawWithBorrowForToken(group, mintPk), + group.getMintDecimals(mintPk), + ); } /** diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 2f58c0136..18e3fd2a6 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -1,13 +1,18 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair } from '@solana/web3.js'; import fs from 'fs'; +import { Group } from '../accounts/group'; import { I80F48 } from '../accounts/I80F48'; -import { HealthType } from '../accounts/mangoAccount'; +import { HealthType, MangoAccount } from '../accounts/mangoAccount'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; import { toUiDecimalsForQuote } from '../utils'; -async function debugUser(client, group, mangoAccount) { +async function debugUser( + client: MangoClient, + group: Group, + mangoAccount: MangoAccount, +) { console.log(mangoAccount.toString(group)); await mangoAccount.reload(client, group); @@ -64,17 +69,33 @@ async function debugUser(client, group, mangoAccount) { console.log(group.banksMapByName.get('SOL')[0].mint.toBase58()); - console.log( - "mangoAccount.getMaxWithdrawWithBorrowForToken(group, 'SOL') " + - toUiDecimalsForQuote( - ( - await mangoAccount.getMaxWithdrawWithBorrowForToken( - group, - group.banksMapByName.get('SOL')[0].mint, - ) - ).toNumber(), - ), - ); + async function getMaxWithdrawWithBorrowForTokenUiWrapper(token) { + console.log( + `mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, ${token}) ` + + mangoAccount.getMaxWithdrawWithBorrowForTokenUi( + group, + group.banksMapByName.get(token)[0].mint, + ), + ); + try { + await client.tokenWithdraw( + group, + mangoAccount, + group.banksMapByName.get(token)[0].mint, + mangoAccount.getMaxWithdrawWithBorrowForTokenUi( + group, + group.banksMapByName.get(token)[0].mint, + ), + true, + ); + } catch (error) { + console.log(error); + } + } + await getMaxWithdrawWithBorrowForTokenUiWrapper('SOL'); + await getMaxWithdrawWithBorrowForTokenUiWrapper('MSOL'); + await getMaxWithdrawWithBorrowForTokenUiWrapper('USDC'); + await getMaxWithdrawWithBorrowForTokenUiWrapper('BTC'); console.log( 'mangoAccount.simHealthRatioWithTokenPositionChanges ' + @@ -138,7 +159,7 @@ async function main() { ); const group = await client.getGroupForCreator(admin.publicKey, 2); - console.log(`${group.toString()}`); + // console.log(`${group.toString()}`); for (const keypair of [ process.env.MB_PAYER_KEYPAIR, @@ -155,8 +176,13 @@ async function main() { user.publicKey, ); for (const mangoAccount of mangoAccounts) { - console.log(`MangoAccount ${mangoAccount.publicKey}`); - await debugUser(client, group, mangoAccount); + if ( + '9B8uwqH8FJqLn9kvGPVb5GEksLvmyXb3B8UKCFtRs5cq' === + mangoAccount.publicKey.toBase58() + ) { + console.log(`MangoAccount ${mangoAccount.publicKey}`); + await debugUser(client, group, mangoAccount); + } } } From b6b22ce903e102a4148a4d2e13315220ab5cbd9a Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 22 Aug 2022 19:09:22 +0200 Subject: [PATCH 04/50] debugging Signed-off-by: microwavedcola1 --- ts/client/src/accounts/group.ts | 28 +++++++++++++++----- ts/client/src/debug-scripts/mb-debug-user.ts | 9 ++++++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 5f78e0484..4e24ece31 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -1,5 +1,5 @@ import { BorshAccountsCoder } from '@project-serum/anchor'; -import { coder } from '@project-serum/anchor/dist/cjs/spl/associated-token'; +import { coder } from '@project-serum/anchor/dist/cjs/spl/token'; import { Market } from '@project-serum/serum'; import { parsePriceData, PriceData } from '@pythnetwork/client'; import { PublicKey } from '@solana/web3.js'; @@ -7,7 +7,7 @@ import BN from 'bn.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; import { Id } from '../ids'; -import { toNativeDecimals } from '../utils'; +import { toNativeDecimals, toUiDecimals } from '../utils'; import { Bank, MintInfo } from './bank'; import { I80F48, ONE_I80F48 } from './I80F48'; import { PerpMarket } from './perp'; @@ -274,14 +274,14 @@ export class Group { * @param mintPk * @returns sum of native balances of all vaults for a token */ - public async getGroupTokenVaultBalanceByMint( + public async getTokenVaultBalanceByMint( client: MangoClient, mintPk: PublicKey, ): Promise { const banks = this.banksMapByMint.get(mintPk.toString()); - const amount = new BN(0); + let amount = new BN(0); for (const bank of banks) { - amount.add( + amount = amount.add( coder().accounts.decode( 'token', (await client.program.provider.connection.getAccountInfo(bank.vault)) @@ -289,7 +289,23 @@ export class Group { ).amount, ); } - return new I80F48(amount); + return I80F48.fromNumber(amount.toNumber()); + } + + /** + * + * @param client + * @param mintPk + * @returns sum of ui balances of all vaults for a token + */ + public async getTokenVaultBalanceByMintUi( + client: MangoClient, + mintPk: PublicKey, + ): Promise { + return toUiDecimals( + await this.getTokenVaultBalanceByMint(client, mintPk), + this.getMintDecimals(mintPk), + ); } public consoleLogBanks() { diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 18e3fd2a6..21353815c 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -70,6 +70,13 @@ async function debugUser( console.log(group.banksMapByName.get('SOL')[0].mint.toBase58()); async function getMaxWithdrawWithBorrowForTokenUiWrapper(token) { + console.log( + `group.getTokenVaultBalanceByMintUi ${token} ${await group.getTokenVaultBalanceByMintUi( + client, + group.banksMapByName.get(token)[0].mint, + )}`, + ); + console.log( `mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, ${token}) ` + mangoAccount.getMaxWithdrawWithBorrowForTokenUi( @@ -159,7 +166,7 @@ async function main() { ); const group = await client.getGroupForCreator(admin.publicKey, 2); - // console.log(`${group.toString()}`); + console.log(`${group.toString()}`); for (const keypair of [ process.env.MB_PAYER_KEYPAIR, From 844bca622d996068a62ba7f9053a4b605101bf8d Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 22 Aug 2022 19:20:47 +0200 Subject: [PATCH 05/50] debugging Signed-off-by: microwavedcola1 --- ts/client/src/accounts/mangoAccount.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index dd51fdf59..086b44f39 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -262,8 +262,21 @@ export class MangoAccount { const maxBorrowNativeWithoutFees = maxBorrowNative.div( ONE_I80F48.add(bank.loanOriginationFeeRate), ); + + // console.log(`initHealth ${initHealth.toNumber()}`); + // console.log( + // `existingPositioninUsdcUnits ${existingPositioninUsdcUnits.toNumber()}`, + // ); + // console.log( + // `initHealthWithoutExistingPosition ${initHealthWithoutExistingPosition.toNumber()}`, + // ); + // console.log(`maxBorrowNative ${maxBorrowNative.toNumber()}`); + // console.log( + // `maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees.toNumber()}`, + // ); + return maxBorrowNativeWithoutFees - .add(this.getTokenBalance(bank)) + .add(this.getTokenDeposits(bank)) .mul(I80F48.fromString('0.99')); } From 2f5436d2398164422b0d522b7a5c66695829a41c Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 22 Aug 2022 20:01:36 +0200 Subject: [PATCH 06/50] fix Signed-off-by: microwavedcola1 --- ts/client/src/accounts/group.ts | 38 ++++++++++++++++++-------- ts/client/src/accounts/mangoAccount.ts | 21 ++------------ 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 4e24ece31..88a3b6567 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -46,6 +46,7 @@ export class Group { new Map(), // mintInfosMapByTokenIndex new Map(), // mintInfosMapByMint new Map(), // oraclesMap + new Map(), // vaultAmountsMap ); } @@ -67,7 +68,8 @@ export class Group { public perpMarketsMap: Map, public mintInfosMapByTokenIndex: Map, public mintInfosMapByMint: Map, - public oraclesMap: Map, + private oraclesMap: Map, // UNUSED + public vaultAmountsMap: Map, ) {} public findSerum3Market(marketIndex: number): Serum3Market | undefined { @@ -100,6 +102,8 @@ export class Group { this.reloadBankPrices(client, ids), // requires reloadSerum3Markets to have finished loading this.reloadSerum3ExternalMarkets(client, ids), + // requires reloadBanks to have finished loading + this.reloadVaults(client, ids), ]); // console.timeEnd('group.reload'); } @@ -256,6 +260,22 @@ export class Group { } } + public async reloadVaults(client: MangoClient, ids?: Id): Promise { + const vaultPks = Array.from(this.banksMapByMint.values()) + .flat() + .map((bank) => bank.vault); + this.vaultAmountsMap = new Map( + ( + await client.program.provider.connection.getMultipleAccountsInfo( + vaultPks, + ) + ).map((vaultAi, i) => [ + vaultPks[i].toBase58(), + coder().accounts.decode('token', vaultAi.data).amount.toNumber(), + ]), + ); + } + public getMintDecimals(mintPk: PublicKey) { return this.banksMapByMint.get(mintPk.toString())[0].mintDecimals; } @@ -272,31 +292,25 @@ export class Group { * * @param client * @param mintPk - * @returns sum of native balances of all vaults for a token + * @returns sum of native balances of vaults for all banks for a token (fetched from vaultAmountsMap cache) */ public async getTokenVaultBalanceByMint( client: MangoClient, mintPk: PublicKey, ): Promise { const banks = this.banksMapByMint.get(mintPk.toString()); - let amount = new BN(0); + let amount = 0; for (const bank of banks) { - amount = amount.add( - coder().accounts.decode( - 'token', - (await client.program.provider.connection.getAccountInfo(bank.vault)) - .data, - ).amount, - ); + amount += this.vaultAmountsMap.get(bank.vault.toBase58()); } - return I80F48.fromNumber(amount.toNumber()); + return I80F48.fromNumber(amount); } /** * * @param client * @param mintPk - * @returns sum of ui balances of all vaults for a token + * @returns sum of ui balances of vaults for all banks for a token */ public async getTokenVaultBalanceByMintUi( client: MangoClient, diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 086b44f39..feeea2373 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -248,36 +248,19 @@ export class MangoAccount { getMaxWithdrawWithBorrowForToken(group: Group, mintPk: PublicKey): I80F48 { const bank: Bank = group.getFirstBankByMint(mintPk); const initHealth = (this.accountData as MangoAccountData).initHealth; - const existingPositioninUsdcUnits = MangoAccount.getEquivalentUsdcPosition( - bank, - this.findToken(bank.tokenIndex), - ).max(ZERO_I80F48); - const initHealthWithoutExistingPosition = initHealth.sub( - existingPositioninUsdcUnits.mul(bank.initAssetWeight), - ); const maxBorrowNative = MangoAccount.getEquivalentTokenPosition( bank, - initHealthWithoutExistingPosition.div(bank.initLiabWeight), + initHealth.div(bank.initLiabWeight), ); const maxBorrowNativeWithoutFees = maxBorrowNative.div( ONE_I80F48.add(bank.loanOriginationFeeRate), ); - // console.log(`initHealth ${initHealth.toNumber()}`); - // console.log( - // `existingPositioninUsdcUnits ${existingPositioninUsdcUnits.toNumber()}`, - // ); - // console.log( - // `initHealthWithoutExistingPosition ${initHealthWithoutExistingPosition.toNumber()}`, - // ); // console.log(`maxBorrowNative ${maxBorrowNative.toNumber()}`); // console.log( // `maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees.toNumber()}`, // ); - - return maxBorrowNativeWithoutFees - .add(this.getTokenDeposits(bank)) - .mul(I80F48.fromString('0.99')); + return maxBorrowNativeWithoutFees; } getMaxWithdrawWithBorrowForTokenUi(group: Group, mintPk: PublicKey): number { From 50e9f39b7600d418026d83f1c3bd533a7bbb9fc9 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Mon, 22 Aug 2022 20:09:31 +0200 Subject: [PATCH 07/50] fix Signed-off-by: microwavedcola1 --- ts/client/src/accounts/mangoAccount.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index feeea2373..ae0a8e96b 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -248,19 +248,25 @@ export class MangoAccount { getMaxWithdrawWithBorrowForToken(group: Group, mintPk: PublicKey): I80F48 { const bank: Bank = group.getFirstBankByMint(mintPk); const initHealth = (this.accountData as MangoAccountData).initHealth; + if (initHealth.lte(ZERO_I80F48)) { + return ZERO_I80F48; + } + const maxBorrowNative = MangoAccount.getEquivalentTokenPosition( bank, initHealth.div(bank.initLiabWeight), ); - const maxBorrowNativeWithoutFees = maxBorrowNative.div( + const maxBorrowNativeMinusFees = maxBorrowNative.div( ONE_I80F48.add(bank.loanOriginationFeeRate), ); + // console.log(`initHealth ${initHealth.toNumber()}`); // console.log(`maxBorrowNative ${maxBorrowNative.toNumber()}`); // console.log( // `maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees.toNumber()}`, // ); - return maxBorrowNativeWithoutFees; + + return maxBorrowNativeMinusFees; } getMaxWithdrawWithBorrowForTokenUi(group: Group, mintPk: PublicKey): number { From 2f8436d8f727868c5ba04762e68717d0834209b6 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 23 Aug 2022 08:34:44 +0200 Subject: [PATCH 08/50] fix Signed-off-by: microwavedcola1 --- ts/client/src/accounts/healthCache.ts | 16 +++- ts/client/src/accounts/mangoAccount.ts | 40 +++++++++- ts/client/src/debug-scripts/mb-debug-user.ts | 77 +++++++++++--------- 3 files changed, 93 insertions(+), 40 deletions(-) diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index d087168a9..55fed7d91 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -174,19 +174,27 @@ export class HealthCache { simHealthRatioWithTokenPositionChanges( group: Group, - tokenChanges: { - tokenAmount: number; + nativeTokenChanges: { + nativeTokenAmount: I80F48; mintPk: PublicKey; }[], healthType: HealthType = HealthType.init, ): I80F48 { const adjustedCache: HealthCache = _.cloneDeep(this); - for (const change of tokenChanges) { + for (const change of nativeTokenChanges) { const bank: Bank = group.getFirstBankByMint(change.mintPk); const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank); adjustedCache.tokenInfos[changeIndex].balance = adjustedCache.tokenInfos[ changeIndex - ].balance.add(I80F48.fromNumber(change.tokenAmount).mul(bank.price)); + ].balance.add(change.nativeTokenAmount.mul(bank.price)); + console.log(' '); + console.log(`change.mintPk ${change.mintPk.toBase58()}`); + console.log(`changeIndex ${changeIndex}`); + console.log( + `adjustedCache.tokenInfos[changeIndex].balance ${adjustedCache.tokenInfos[ + changeIndex + ].balance.toNumber()}`, + ); } return adjustedCache.healthRatio(healthType); } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index ae0a8e96b..34f40f99f 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -301,21 +301,55 @@ export class MangoAccount { /** * Simulates new health ratio after applying tokenChanges to the token positions. + * Note: token changes are expected in native amounts + * * e.g. useful to simulate health after a potential swap. * Note: health ratio is technically ∞ if liabs are 0 * @returns health ratio, in percentage form */ simHealthRatioWithTokenPositionChanges( group: Group, - tokenChanges: { - tokenAmount: number; + nativeTokenChanges: { + nativeTokenAmount: I80F48; mintPk: PublicKey; }[], healthType: HealthType = HealthType.init, ): I80F48 { return this.accountData.healthCache.simHealthRatioWithTokenPositionChanges( group, - tokenChanges, + nativeTokenChanges, + healthType, + ); + } + + /** + * Simulates new health ratio after applying tokenChanges to the token positions. + * Note: token changes are expected in ui amounts + * + * e.g. useful to simulate health after a potential swap. + * Note: health ratio is technically ∞ if liabs are 0 + * @returns health ratio, in percentage form + */ + simHealthRatioWithTokenPositionUiChanges( + group: Group, + uiTokenChanges: { + uiTokenAmount: number; + mintPk: PublicKey; + }[], + healthType: HealthType = HealthType.init, + ): I80F48 { + const nativeTokenChanges = uiTokenChanges.map((tokenChange) => { + return { + nativeTokenAmount: I80F48.fromNumber( + tokenChange.uiTokenAmount * + Math.pow(10, group.getMintDecimals(tokenChange.mintPk)), + ), + mintPk: tokenChange.mintPk, + }; + }); + return this.accountData.healthCache.simHealthRatioWithTokenPositionChanges( + group, + nativeTokenChanges, healthType, ); } diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 21353815c..f38e7ddb2 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -84,45 +84,56 @@ async function debugUser( group.banksMapByName.get(token)[0].mint, ), ); - try { - await client.tokenWithdraw( - group, - mangoAccount, - group.banksMapByName.get(token)[0].mint, - mangoAccount.getMaxWithdrawWithBorrowForTokenUi( - group, - group.banksMapByName.get(token)[0].mint, - ), - true, - ); - } catch (error) { - console.log(error); - } } await getMaxWithdrawWithBorrowForTokenUiWrapper('SOL'); await getMaxWithdrawWithBorrowForTokenUiWrapper('MSOL'); await getMaxWithdrawWithBorrowForTokenUiWrapper('USDC'); await getMaxWithdrawWithBorrowForTokenUiWrapper('BTC'); - console.log( - 'mangoAccount.simHealthRatioWithTokenPositionChanges ' + - ( - await mangoAccount.simHealthRatioWithTokenPositionChanges(group, [ - { - mintPk: group.banksMapByName.get('USDC')[0].mint, - tokenAmount: - -95_000 * - Math.pow(10, group.banksMapByName.get('USDC')[0]!.mintDecimals!), - }, - { - mintPk: group.banksMapByName.get('BTC')[0].mint, - tokenAmount: - 4 * - Math.pow(10, group.banksMapByName.get('BTC')[0]!.mintDecimals!), - }, - ]) - ).toNumber(), - ); + function simHealthRatioWithTokenPositionChangesWrapper(debug, change) { + console.log( + `mangoAccount.simHealthRatioWithTokenPositionChanges ${debug}` + + mangoAccount + .simHealthRatioWithTokenPositionUiChanges(group, [change]) + .toNumber(), + ); + } + simHealthRatioWithTokenPositionChangesWrapper('sol 1 ', { + mintPk: group.banksMapByName.get('SOL')[0].mint, + uiTokenAmount: 1, + }); + simHealthRatioWithTokenPositionChangesWrapper('sol -1 ', { + mintPk: group.banksMapByName.get('SOL')[0].mint, + uiTokenAmount: -1, + }); + simHealthRatioWithTokenPositionChangesWrapper('msol 1 ', { + mintPk: group.banksMapByName.get('MSOL')[0].mint, + uiTokenAmount: 1, + }); + simHealthRatioWithTokenPositionChangesWrapper('msol -1 ', { + mintPk: group.banksMapByName.get('MSOL')[0].mint, + uiTokenAmount: -1, + }); + simHealthRatioWithTokenPositionChangesWrapper('usdc 10 ', { + mintPk: group.banksMapByName.get('USDC')[0].mint, + uiTokenAmount: 10, + }); + simHealthRatioWithTokenPositionChangesWrapper('usdc -10 ', { + mintPk: group.banksMapByName.get('USDC')[0].mint, + uiTokenAmount: -10, + }); + simHealthRatioWithTokenPositionChangesWrapper('btc 0.001 ', { + mintPk: group.banksMapByName.get('BTC')[0].mint, + uiTokenAmount: 0.001, + }); + simHealthRatioWithTokenPositionChangesWrapper('btc -0.001 ', { + mintPk: group.banksMapByName.get('BTC')[0].mint, + uiTokenAmount: -0.001, + }); + simHealthRatioWithTokenPositionChangesWrapper('soETH -0.001 ', { + mintPk: group.banksMapByName.get('soETH')[0].mint, + uiTokenAmount: -0.001, + }); function getMaxSourceForTokenSwapWrapper(src, tgt) { console.log( From 7f2fb0c04c74b0e16498345be3467bbec9a3c665 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 23 Aug 2022 09:20:03 +0200 Subject: [PATCH 09/50] fix Signed-off-by: microwavedcola1 --- ts/client/src/accounts/mangoAccount.ts | 71 ++++++++++------ ts/client/src/debug-scripts/mb-debug-user.ts | 86 ++++++++++---------- 2 files changed, 90 insertions(+), 67 deletions(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 34f40f99f..31b8a78a7 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -99,20 +99,6 @@ export class MangoAccount { // * functions try to be explicit by having native or ui in the name to better reflect the value // * some values might appear unexpected large or small, usually the doc contains a "note" - static getEquivalentUsdcPosition( - sourceBank: Bank, - tp: TokenPosition, - ): I80F48 { - return tp ? tp.balance(sourceBank).mul(sourceBank.price) : ZERO_I80F48; - } - - static getEquivalentTokenPosition( - targetBank: Bank, - nativeUsdcPosition: I80F48, - ): I80F48 { - return nativeUsdcPosition.div(targetBank.price); - } - /** * * @param bank @@ -246,27 +232,64 @@ export class MangoAccount { * @returns amount of given native token you can borrow, considering all existing assets as collateral, in native token */ getMaxWithdrawWithBorrowForToken(group: Group, mintPk: PublicKey): I80F48 { - const bank: Bank = group.getFirstBankByMint(mintPk); + const tokenBank: Bank = group.getFirstBankByMint(mintPk); const initHealth = (this.accountData as MangoAccountData).initHealth; + + // Case 1: + // Cannot withdraw if init health is below 0 if (initHealth.lte(ZERO_I80F48)) { return ZERO_I80F48; } - const maxBorrowNative = MangoAccount.getEquivalentTokenPosition( - bank, - initHealth.div(bank.initLiabWeight), + // Deposits need special treatment since they would neither count towards liabilities + // nor would be charged loanOriginationFeeRate when withdrawn + + const tp = this.findToken(tokenBank.tokenIndex); + let existingPositionHealthContrib = ZERO_I80F48; + if (tp && tp.deposits(tokenBank).gte(ZERO_I80F48)) { + existingPositionHealthContrib = tp + .deposits(tokenBank) + .mul(tokenBank.price) + .mul(tokenBank.initAssetWeight); + } + + // Case 2: token deposits have higher contribution than initHealth, + // can withdraw without borrowing until initHealth reaches 0 + if (existingPositionHealthContrib.gt(initHealth)) { + const withdrawAbleExistingPositionHealthContrib = + existingPositionHealthContrib.sub(initHealth); + // console.log(`initHealth ${initHealth}`); + // console.log( + // `existingPositionHealthContrib ${existingPositionHealthContrib}`, + // ); + // console.log( + // `withdrawAbleExistingPositionHealthContrib ${withdrawAbleExistingPositionHealthContrib}`, + // ); + return withdrawAbleExistingPositionHealthContrib.div(tokenBank.price); + } + + // Case 3: withdraw = withdraw existing deposits + borrows until initHealth reaches 0 + const initHealthWithoutExistingPosition = initHealth.sub( + existingPositionHealthContrib, ); - const maxBorrowNativeMinusFees = maxBorrowNative.div( - ONE_I80F48.add(bank.loanOriginationFeeRate), + const maxBorrowNative = initHealthWithoutExistingPosition + .div(tokenBank.initLiabWeight) + .div(tokenBank.price); + const maxBorrowNativeWithoutFees = maxBorrowNative.div( + ONE_I80F48.add(tokenBank.loanOriginationFeeRate), ); - // console.log(`initHealth ${initHealth.toNumber()}`); - // console.log(`maxBorrowNative ${maxBorrowNative.toNumber()}`); + // console.log(`initHealth ${initHealth}`); // console.log( - // `maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees.toNumber()}`, + // `existingPositionHealthContrib ${existingPositionHealthContrib}`, // ); + // console.log( + // `initHealthWithoutExistingPosition ${initHealthWithoutExistingPosition}`, + // ); + // console.log(`maxBorrowNative ${maxBorrowNative}`); + // console.log(`maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees}`); - return maxBorrowNativeMinusFees; + return maxBorrowNativeWithoutFees.add(this.getTokenDeposits(tokenBank)); } getMaxWithdrawWithBorrowForTokenUi(group: Group, mintPk: PublicKey): number { diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index f38e7ddb2..e80e71df3 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -70,13 +70,7 @@ async function debugUser( console.log(group.banksMapByName.get('SOL')[0].mint.toBase58()); async function getMaxWithdrawWithBorrowForTokenUiWrapper(token) { - console.log( - `group.getTokenVaultBalanceByMintUi ${token} ${await group.getTokenVaultBalanceByMintUi( - client, - group.banksMapByName.get(token)[0].mint, - )}`, - ); - + console.log(); console.log( `mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, ${token}) ` + mangoAccount.getMaxWithdrawWithBorrowForTokenUi( @@ -84,6 +78,12 @@ async function debugUser( group.banksMapByName.get(token)[0].mint, ), ); + console.log( + `group.getTokenVaultBalanceByMintUi ${token} ${await group.getTokenVaultBalanceByMintUi( + client, + group.banksMapByName.get(token)[0].mint, + )}`, + ); } await getMaxWithdrawWithBorrowForTokenUiWrapper('SOL'); await getMaxWithdrawWithBorrowForTokenUiWrapper('MSOL'); @@ -98,42 +98,42 @@ async function debugUser( .toNumber(), ); } - simHealthRatioWithTokenPositionChangesWrapper('sol 1 ', { - mintPk: group.banksMapByName.get('SOL')[0].mint, - uiTokenAmount: 1, - }); - simHealthRatioWithTokenPositionChangesWrapper('sol -1 ', { - mintPk: group.banksMapByName.get('SOL')[0].mint, - uiTokenAmount: -1, - }); - simHealthRatioWithTokenPositionChangesWrapper('msol 1 ', { - mintPk: group.banksMapByName.get('MSOL')[0].mint, - uiTokenAmount: 1, - }); - simHealthRatioWithTokenPositionChangesWrapper('msol -1 ', { - mintPk: group.banksMapByName.get('MSOL')[0].mint, - uiTokenAmount: -1, - }); - simHealthRatioWithTokenPositionChangesWrapper('usdc 10 ', { - mintPk: group.banksMapByName.get('USDC')[0].mint, - uiTokenAmount: 10, - }); - simHealthRatioWithTokenPositionChangesWrapper('usdc -10 ', { - mintPk: group.banksMapByName.get('USDC')[0].mint, - uiTokenAmount: -10, - }); - simHealthRatioWithTokenPositionChangesWrapper('btc 0.001 ', { - mintPk: group.banksMapByName.get('BTC')[0].mint, - uiTokenAmount: 0.001, - }); - simHealthRatioWithTokenPositionChangesWrapper('btc -0.001 ', { - mintPk: group.banksMapByName.get('BTC')[0].mint, - uiTokenAmount: -0.001, - }); - simHealthRatioWithTokenPositionChangesWrapper('soETH -0.001 ', { - mintPk: group.banksMapByName.get('soETH')[0].mint, - uiTokenAmount: -0.001, - }); + // simHealthRatioWithTokenPositionChangesWrapper('sol 1 ', { + // mintPk: group.banksMapByName.get('SOL')[0].mint, + // uiTokenAmount: 1, + // }); + // simHealthRatioWithTokenPositionChangesWrapper('sol -1 ', { + // mintPk: group.banksMapByName.get('SOL')[0].mint, + // uiTokenAmount: -1, + // }); + // simHealthRatioWithTokenPositionChangesWrapper('msol 1 ', { + // mintPk: group.banksMapByName.get('MSOL')[0].mint, + // uiTokenAmount: 1, + // }); + // simHealthRatioWithTokenPositionChangesWrapper('msol -1 ', { + // mintPk: group.banksMapByName.get('MSOL')[0].mint, + // uiTokenAmount: -1, + // }); + // simHealthRatioWithTokenPositionChangesWrapper('usdc 10 ', { + // mintPk: group.banksMapByName.get('USDC')[0].mint, + // uiTokenAmount: 10, + // }); + // simHealthRatioWithTokenPositionChangesWrapper('usdc -10 ', { + // mintPk: group.banksMapByName.get('USDC')[0].mint, + // uiTokenAmount: -10, + // }); + // simHealthRatioWithTokenPositionChangesWrapper('btc 0.001 ', { + // mintPk: group.banksMapByName.get('BTC')[0].mint, + // uiTokenAmount: 0.001, + // }); + // simHealthRatioWithTokenPositionChangesWrapper('btc -0.001 ', { + // mintPk: group.banksMapByName.get('BTC')[0].mint, + // uiTokenAmount: -0.001, + // }); + // simHealthRatioWithTokenPositionChangesWrapper('soETH -0.001 ', { + // mintPk: group.banksMapByName.get('soETH')[0].mint, + // uiTokenAmount: -0.001, + // }); function getMaxSourceForTokenSwapWrapper(src, tgt) { console.log( From 7321df31bef53b67c32b2e8a479dc0b7530bb6c9 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 23 Aug 2022 09:42:00 +0200 Subject: [PATCH 10/50] fix Signed-off-by: microwavedcola1 --- ts/client/src/accounts/healthCache.ts | 28 ++++-- ts/client/src/debug-scripts/mb-debug-user.ts | 89 ++++++++++---------- 2 files changed, 66 insertions(+), 51 deletions(-) diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 55fed7d91..6dc5ab840 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -172,6 +172,20 @@ export class HealthCache { return this.findTokenInfoIndex(bank.tokenIndex); } + private static logHealthCache(debug: string, healthCache: HealthCache) { + console.log(debug); + for (const token of healthCache.tokenInfos.sort( + (a, b) => a.tokenIndex - b.tokenIndex, + )) { + console.log(`${token.toString()}`); + } + console.log( + `assets ${healthCache.assets(HealthType.init)}, liabs ${healthCache.liabs( + HealthType.init, + )}, `, + ); + } + simHealthRatioWithTokenPositionChanges( group: Group, nativeTokenChanges: { @@ -181,21 +195,15 @@ export class HealthCache { healthType: HealthType = HealthType.init, ): I80F48 { const adjustedCache: HealthCache = _.cloneDeep(this); + // HealthCache.logHealthCache('beforeChange', adjustedCache); for (const change of nativeTokenChanges) { const bank: Bank = group.getFirstBankByMint(change.mintPk); const changeIndex = adjustedCache.getOrCreateTokenInfoIndex(bank); adjustedCache.tokenInfos[changeIndex].balance = adjustedCache.tokenInfos[ changeIndex ].balance.add(change.nativeTokenAmount.mul(bank.price)); - console.log(' '); - console.log(`change.mintPk ${change.mintPk.toBase58()}`); - console.log(`changeIndex ${changeIndex}`); - console.log( - `adjustedCache.tokenInfos[changeIndex].balance ${adjustedCache.tokenInfos[ - changeIndex - ].balance.toNumber()}`, - ); } + // HealthCache.logHealthCache('afterChange', adjustedCache); return adjustedCache.healthRatio(healthType); } @@ -441,6 +449,10 @@ export class TokenInfo { : this.assetWeight(healthType) ).mul(this.balance); } + + toString() { + return `tokenIndex: ${this.tokenIndex}, balance: ${this.balance}`; + } } export class Serum3Info { diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index e80e71df3..1ae33d69d 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -70,7 +70,6 @@ async function debugUser( console.log(group.banksMapByName.get('SOL')[0].mint.toBase58()); async function getMaxWithdrawWithBorrowForTokenUiWrapper(token) { - console.log(); console.log( `mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, ${token}) ` + mangoAccount.getMaxWithdrawWithBorrowForTokenUi( @@ -78,12 +77,12 @@ async function debugUser( group.banksMapByName.get(token)[0].mint, ), ); - console.log( - `group.getTokenVaultBalanceByMintUi ${token} ${await group.getTokenVaultBalanceByMintUi( - client, - group.banksMapByName.get(token)[0].mint, - )}`, - ); + // console.log( + // `group.getTokenVaultBalanceByMintUi ${token} ${await group.getTokenVaultBalanceByMintUi( + // client, + // group.banksMapByName.get(token)[0].mint, + // )}`, + // ); } await getMaxWithdrawWithBorrowForTokenUiWrapper('SOL'); await getMaxWithdrawWithBorrowForTokenUiWrapper('MSOL'); @@ -98,42 +97,46 @@ async function debugUser( .toNumber(), ); } - // simHealthRatioWithTokenPositionChangesWrapper('sol 1 ', { - // mintPk: group.banksMapByName.get('SOL')[0].mint, - // uiTokenAmount: 1, - // }); - // simHealthRatioWithTokenPositionChangesWrapper('sol -1 ', { - // mintPk: group.banksMapByName.get('SOL')[0].mint, - // uiTokenAmount: -1, - // }); - // simHealthRatioWithTokenPositionChangesWrapper('msol 1 ', { - // mintPk: group.banksMapByName.get('MSOL')[0].mint, - // uiTokenAmount: 1, - // }); - // simHealthRatioWithTokenPositionChangesWrapper('msol -1 ', { - // mintPk: group.banksMapByName.get('MSOL')[0].mint, - // uiTokenAmount: -1, - // }); - // simHealthRatioWithTokenPositionChangesWrapper('usdc 10 ', { - // mintPk: group.banksMapByName.get('USDC')[0].mint, - // uiTokenAmount: 10, - // }); - // simHealthRatioWithTokenPositionChangesWrapper('usdc -10 ', { - // mintPk: group.banksMapByName.get('USDC')[0].mint, - // uiTokenAmount: -10, - // }); - // simHealthRatioWithTokenPositionChangesWrapper('btc 0.001 ', { - // mintPk: group.banksMapByName.get('BTC')[0].mint, - // uiTokenAmount: 0.001, - // }); - // simHealthRatioWithTokenPositionChangesWrapper('btc -0.001 ', { - // mintPk: group.banksMapByName.get('BTC')[0].mint, - // uiTokenAmount: -0.001, - // }); - // simHealthRatioWithTokenPositionChangesWrapper('soETH -0.001 ', { - // mintPk: group.banksMapByName.get('soETH')[0].mint, - // uiTokenAmount: -0.001, - // }); + simHealthRatioWithTokenPositionChangesWrapper('sol 1 ', { + mintPk: group.banksMapByName.get('SOL')[0].mint, + uiTokenAmount: 1, + }); + simHealthRatioWithTokenPositionChangesWrapper('sol -1 ', { + mintPk: group.banksMapByName.get('SOL')[0].mint, + uiTokenAmount: -1, + }); + simHealthRatioWithTokenPositionChangesWrapper('msol 1 ', { + mintPk: group.banksMapByName.get('MSOL')[0].mint, + uiTokenAmount: 1, + }); + simHealthRatioWithTokenPositionChangesWrapper('msol -1 ', { + mintPk: group.banksMapByName.get('MSOL')[0].mint, + uiTokenAmount: -1, + }); + simHealthRatioWithTokenPositionChangesWrapper('btc 0.001 ', { + mintPk: group.banksMapByName.get('BTC')[0].mint, + uiTokenAmount: 0.001, + }); + simHealthRatioWithTokenPositionChangesWrapper('btc -0.001 ', { + mintPk: group.banksMapByName.get('BTC')[0].mint, + uiTokenAmount: -0.001, + }); + simHealthRatioWithTokenPositionChangesWrapper('usdc 10 ', { + mintPk: group.banksMapByName.get('USDC')[0].mint, + uiTokenAmount: 10, + }); + simHealthRatioWithTokenPositionChangesWrapper('usdc -10 ', { + mintPk: group.banksMapByName.get('USDC')[0].mint, + uiTokenAmount: -10, + }); + simHealthRatioWithTokenPositionChangesWrapper('soETH 0.001 ', { + mintPk: group.banksMapByName.get('soETH')[0].mint, + uiTokenAmount: 0.001, + }); + simHealthRatioWithTokenPositionChangesWrapper('soETH -0.001 ', { + mintPk: group.banksMapByName.get('soETH')[0].mint, + uiTokenAmount: -0.001, + }); function getMaxSourceForTokenSwapWrapper(src, tgt) { console.log( From 363d7a306c5040949e2cc75d138a48728c503573 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 23 Aug 2022 11:43:25 +0200 Subject: [PATCH 11/50] fix Signed-off-by: microwavedcola1 --- ts/client/src/accounts/healthCache.ts | 41 ++++++++++++++------ ts/client/src/accounts/mangoAccount.ts | 2 +- ts/client/src/debug-scripts/mb-debug-user.ts | 12 ++++-- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 6dc5ab840..55b6e0a7a 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -174,15 +174,21 @@ export class HealthCache { private static logHealthCache(debug: string, healthCache: HealthCache) { console.log(debug); - for (const token of healthCache.tokenInfos.sort( - (a, b) => a.tokenIndex - b.tokenIndex, - )) { + for (const token of healthCache.tokenInfos) { console.log(`${token.toString()}`); } console.log( - `assets ${healthCache.assets(HealthType.init)}, liabs ${healthCache.liabs( + ` assets ${healthCache.assets( HealthType.init, - )}, `, + )}, liabs ${healthCache.liabs(HealthType.init)}, `, + ); + console.log( + ` health(HealthType.init) ${healthCache.health(HealthType.init)}`, + ); + console.log( + ` healthRatio(HealthType.init) ${healthCache.healthRatio( + HealthType.init, + )}`, ); } @@ -249,7 +255,6 @@ export class HealthCache { const healthCacheClone: HealthCache = _.cloneDeep(this); const sourceIndex = healthCacheClone.getOrCreateTokenInfoIndex(sourceBank); const targetIndex = healthCacheClone.getOrCreateTokenInfoIndex(targetBank); - const source = healthCacheClone.tokenInfos[sourceIndex]; const target = healthCacheClone.tokenInfos[targetIndex]; @@ -261,10 +266,12 @@ export class HealthCache { function cacheAfterSwap(amount: I80F48) { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); + // HealthCache.logHealthCache('beforeSwap', adjustedCache); adjustedCache.tokenInfos[sourceIndex].balance = adjustedCache.tokenInfos[sourceIndex].balance.sub(amount); adjustedCache.tokenInfos[targetIndex].balance = adjustedCache.tokenInfos[targetIndex].balance.add(amount); + // HealthCache.logHealthCache('afterSwap', adjustedCache); return adjustedCache; } @@ -278,10 +285,12 @@ export class HealthCache { const point1Amount = source.balance .max(target.balance.neg()) .max(ZERO_I80F48); - const point0Ratio = healthRatioAfterSwap(point0Amount); - const cache = cacheAfterSwap(point1Amount); - const point1Ratio = cache.healthRatio(HealthType.init); - const point1Health = cache.health(HealthType.init); + const cache0 = cacheAfterSwap(point0Amount); + const point0Ratio = cache0.healthRatio(HealthType.init); + const point0Health = cache0.health(HealthType.init); + const cache1 = cacheAfterSwap(point1Amount); + const point1Ratio = cache1.healthRatio(HealthType.init); + const point1Health = cache1.health(HealthType.init); function binaryApproximationSearch( left: I80F48, @@ -292,7 +301,7 @@ export class HealthCache { ) { const maxIterations = 20; // TODO: make relative to health ratio decimals? Might be over engineering - const targetError = I80F48.fromString('0.001'); // ONE_I80F48; + const targetError = I80F48.fromString('0.001'); if ( (leftRatio.sub(targetRatio).isPos() && @@ -356,7 +365,15 @@ export class HealthCache { const zeroHealthAmount = point1Amount.add( point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)), ); + // console.log(`point1Amount ${point1Amount}`); + // console.log(`point1Health ${point1Health}`); + // console.log(`point1Ratio ${point1Ratio}`); + // console.log(`point0Amount ${point0Amount}`); + // console.log(`point0Health ${point0Health}`); + // console.log(`point0Ratio ${point0Ratio}`); + // console.log(`zeroHealthAmount ${zeroHealthAmount}`); const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount); + // console.log(`zeroHealthRatio ${zeroHealthRatio}`); amount = binaryApproximationSearch( point1Amount, point1Ratio, @@ -451,7 +468,7 @@ export class TokenInfo { } toString() { - return `tokenIndex: ${this.tokenIndex}, balance: ${this.balance}`; + return ` tokenIndex: ${this.tokenIndex}, balance: ${this.balance}`; } } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 31b8a78a7..b4b411640 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -317,7 +317,7 @@ export class MangoAccount { group, sourceMintPk, targetMintPk, - ONE_I80F48, // target 1% health + ZERO_I80F48, // target 1% health ) .mul(I80F48.fromNumber(slippageAndFeesFactor)); } diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 1ae33d69d..f4a69db4e 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -139,6 +139,7 @@ async function debugUser( }); function getMaxSourceForTokenSwapWrapper(src, tgt) { + // console.log(); console.log( `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` + mangoAccount @@ -146,7 +147,7 @@ async function debugUser( group, group.banksMapByName.get(src)[0].mint, group.banksMapByName.get(tgt)[0].mint, - 0.9, + 1, ) .div( I80F48.fromNumber( @@ -156,8 +157,13 @@ async function debugUser( .toNumber(), ); } - getMaxSourceForTokenSwapWrapper('SOL', 'BTC'); - getMaxSourceForTokenSwapWrapper('USDC', 'USDC'); + for (const srcToken of Array.from(group.banksMapByName.keys())) { + for (const tgtToken of Array.from(group.banksMapByName.keys())) { + // if (srcToken === 'SOL') + // if (tgtToken === 'MSOL') + getMaxSourceForTokenSwapWrapper(srcToken, tgtToken); + } + } } async function main() { From 8a460352a02ece65fff7b3b658d66fdb909f3316 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 23 Aug 2022 11:51:45 +0200 Subject: [PATCH 12/50] fix Signed-off-by: microwavedcola1 --- ts/client/src/accounts/mangoAccount.ts | 5 +- ts/client/src/debug-scripts/mb-debug-user.ts | 63 ++++---------------- 2 files changed, 14 insertions(+), 54 deletions(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index b4b411640..fdaf8a0cc 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -256,8 +256,7 @@ export class MangoAccount { // Case 2: token deposits have higher contribution than initHealth, // can withdraw without borrowing until initHealth reaches 0 if (existingPositionHealthContrib.gt(initHealth)) { - const withdrawAbleExistingPositionHealthContrib = - existingPositionHealthContrib.sub(initHealth); + const withdrawAbleExistingPositionHealthContrib = initHealth; // console.log(`initHealth ${initHealth}`); // console.log( // `existingPositionHealthContrib ${existingPositionHealthContrib}`, @@ -278,7 +277,6 @@ export class MangoAccount { const maxBorrowNativeWithoutFees = maxBorrowNative.div( ONE_I80F48.add(tokenBank.loanOriginationFeeRate), ); - // console.log(`initHealth ${initHealth}`); // console.log( // `existingPositionHealthContrib ${existingPositionHealthContrib}`, @@ -288,7 +286,6 @@ export class MangoAccount { // ); // console.log(`maxBorrowNative ${maxBorrowNative}`); // console.log(`maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees}`); - return maxBorrowNativeWithoutFees.add(this.getTokenDeposits(tokenBank)); } diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index f4a69db4e..61a2c4bfd 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -77,17 +77,10 @@ async function debugUser( group.banksMapByName.get(token)[0].mint, ), ); - // console.log( - // `group.getTokenVaultBalanceByMintUi ${token} ${await group.getTokenVaultBalanceByMintUi( - // client, - // group.banksMapByName.get(token)[0].mint, - // )}`, - // ); } - await getMaxWithdrawWithBorrowForTokenUiWrapper('SOL'); - await getMaxWithdrawWithBorrowForTokenUiWrapper('MSOL'); - await getMaxWithdrawWithBorrowForTokenUiWrapper('USDC'); - await getMaxWithdrawWithBorrowForTokenUiWrapper('BTC'); + for (const srcToken of Array.from(group.banksMapByName.keys())) { + await getMaxWithdrawWithBorrowForTokenUiWrapper(srcToken); + } function simHealthRatioWithTokenPositionChangesWrapper(debug, change) { console.log( @@ -97,46 +90,16 @@ async function debugUser( .toNumber(), ); } - simHealthRatioWithTokenPositionChangesWrapper('sol 1 ', { - mintPk: group.banksMapByName.get('SOL')[0].mint, - uiTokenAmount: 1, - }); - simHealthRatioWithTokenPositionChangesWrapper('sol -1 ', { - mintPk: group.banksMapByName.get('SOL')[0].mint, - uiTokenAmount: -1, - }); - simHealthRatioWithTokenPositionChangesWrapper('msol 1 ', { - mintPk: group.banksMapByName.get('MSOL')[0].mint, - uiTokenAmount: 1, - }); - simHealthRatioWithTokenPositionChangesWrapper('msol -1 ', { - mintPk: group.banksMapByName.get('MSOL')[0].mint, - uiTokenAmount: -1, - }); - simHealthRatioWithTokenPositionChangesWrapper('btc 0.001 ', { - mintPk: group.banksMapByName.get('BTC')[0].mint, - uiTokenAmount: 0.001, - }); - simHealthRatioWithTokenPositionChangesWrapper('btc -0.001 ', { - mintPk: group.banksMapByName.get('BTC')[0].mint, - uiTokenAmount: -0.001, - }); - simHealthRatioWithTokenPositionChangesWrapper('usdc 10 ', { - mintPk: group.banksMapByName.get('USDC')[0].mint, - uiTokenAmount: 10, - }); - simHealthRatioWithTokenPositionChangesWrapper('usdc -10 ', { - mintPk: group.banksMapByName.get('USDC')[0].mint, - uiTokenAmount: -10, - }); - simHealthRatioWithTokenPositionChangesWrapper('soETH 0.001 ', { - mintPk: group.banksMapByName.get('soETH')[0].mint, - uiTokenAmount: 0.001, - }); - simHealthRatioWithTokenPositionChangesWrapper('soETH -0.001 ', { - mintPk: group.banksMapByName.get('soETH')[0].mint, - uiTokenAmount: -0.001, - }); + for (const srcToken of Array.from(group.banksMapByName.keys())) { + simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} 1 `, { + mintPk: group.banksMapByName.get(srcToken)[0].mint, + uiTokenAmount: 1, + }); + simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} -1 `, { + mintPk: group.banksMapByName.get(srcToken)[0].mint, + uiTokenAmount: -1, + }); + } function getMaxSourceForTokenSwapWrapper(src, tgt) { // console.log(); From 6bde327a9e5c43d8b25e960ec9968b2682c756cb Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 23 Aug 2022 11:57:32 +0200 Subject: [PATCH 13/50] fix Signed-off-by: microwavedcola1 --- ts/client/src/accounts/group.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index 88a3b6567..1316302dc 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -91,20 +91,18 @@ export class Group { // console.time('group.reload'); await Promise.all([ - this.reloadBanks(client, ids), + this.reloadBanks(client, ids).then(() => + Promise.all([ + this.reloadBankPrices(client, ids), + this.reloadVaults(client, ids), + ]), + ), this.reloadMintInfos(client, ids), - this.reloadSerum3Markets(client, ids), + this.reloadSerum3Markets(client, ids).then(() => + this.reloadSerum3ExternalMarkets(client, ids), + ), this.reloadPerpMarkets(client, ids), ]); - - await Promise.all([ - // requires reloadBanks to have finished loading - this.reloadBankPrices(client, ids), - // requires reloadSerum3Markets to have finished loading - this.reloadSerum3ExternalMarkets(client, ids), - // requires reloadBanks to have finished loading - this.reloadVaults(client, ids), - ]); // console.timeEnd('group.reload'); } From 53b43bfdd1d4e4d56fca65aa9633dbc356879f29 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 23 Aug 2022 13:33:47 +0200 Subject: [PATCH 14/50] fixes from review Signed-off-by: microwavedcola1 --- ts/client/src/accounts/mangoAccount.ts | 14 ++++++++------ ts/client/src/utils.ts | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index fdaf8a0cc..e21187948 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -2,7 +2,7 @@ import { BN } from '@project-serum/anchor'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; import { PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; -import { nativeI80F48ToUi, toUiDecimals } from '../utils'; +import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils'; import { Bank } from './bank'; import { Group } from './group'; import { HealthCache, HealthCacheDto } from './healthCache'; @@ -264,7 +264,9 @@ export class MangoAccount { // console.log( // `withdrawAbleExistingPositionHealthContrib ${withdrawAbleExistingPositionHealthContrib}`, // ); - return withdrawAbleExistingPositionHealthContrib.div(tokenBank.price); + return withdrawAbleExistingPositionHealthContrib + .div(tokenBank.initAssetWeight) + .div(tokenBank.price); } // Case 3: withdraw = withdraw existing deposits + borrows until initHealth reaches 0 @@ -314,7 +316,7 @@ export class MangoAccount { group, sourceMintPk, targetMintPk, - ZERO_I80F48, // target 1% health + ONE_I80F48, // target 1% health ) .mul(I80F48.fromNumber(slippageAndFeesFactor)); } @@ -360,9 +362,9 @@ export class MangoAccount { ): I80F48 { const nativeTokenChanges = uiTokenChanges.map((tokenChange) => { return { - nativeTokenAmount: I80F48.fromNumber( - tokenChange.uiTokenAmount * - Math.pow(10, group.getMintDecimals(tokenChange.mintPk)), + nativeTokenAmount: toNative( + tokenChange.uiTokenAmount, + group.getMintDecimals(tokenChange.mintPk), ), mintPk: tokenChange.mintPk, }; diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index 2f9cc6da1..2643d5f95 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -98,6 +98,12 @@ export async function createAssociatedTokenAccountIdempotentInstruction( }); } +export function toNative(uiAmount: number, decimals: number): I80F48 { + return I80F48.fromNumber(uiAmount).mul( + I80F48.fromNumber(Math.pow(10, decimals)), + ); +} + export function toNativeDecimals(amount: number, decimals: number): BN { return new BN(Math.trunc(amount * Math.pow(10, decimals))); } From 026a58d856326b667dca241265ef5d349dea2488 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 23 Aug 2022 13:39:32 +0200 Subject: [PATCH 15/50] fixes from review Signed-off-by: microwavedcola1 --- ts/client/src/accounts/mangoAccount.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index e21187948..02dab7c8c 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -245,10 +245,10 @@ export class MangoAccount { // nor would be charged loanOriginationFeeRate when withdrawn const tp = this.findToken(tokenBank.tokenIndex); + const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48; let existingPositionHealthContrib = ZERO_I80F48; - if (tp && tp.deposits(tokenBank).gte(ZERO_I80F48)) { - existingPositionHealthContrib = tp - .deposits(tokenBank) + if (existingTokenDeposits.gt(ZERO_I80F48)) { + existingPositionHealthContrib = existingTokenDeposits .mul(tokenBank.price) .mul(tokenBank.initAssetWeight); } @@ -288,7 +288,7 @@ export class MangoAccount { // ); // console.log(`maxBorrowNative ${maxBorrowNative}`); // console.log(`maxBorrowNativeWithoutFees ${maxBorrowNativeWithoutFees}`); - return maxBorrowNativeWithoutFees.add(this.getTokenDeposits(tokenBank)); + return maxBorrowNativeWithoutFees.add(existingTokenDeposits); } getMaxWithdrawWithBorrowForTokenUi(group: Group, mintPk: PublicKey): number { From 0849354924d812bdeed90d02e961696c0dbc62f4 Mon Sep 17 00:00:00 2001 From: riordanp Date: Wed, 24 Aug 2022 08:53:46 +0100 Subject: [PATCH 16/50] Add metrics output to crank (#186) --- keeper/src/crank.rs | 48 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/keeper/src/crank.rs b/keeper/src/crank.rs index 430fc6ecb..0fdf8c56a 100644 --- a/keeper/src/crank.rs +++ b/keeper/src/crank.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{sync::Arc, time::Duration, time::Instant}; use crate::MangoClient; use itertools::Itertools; @@ -76,7 +76,7 @@ pub async fn loop_update_index_and_rate( let token_names = token_indices_clone .iter() .map(|token_index| client.context.token(*token_index).name.to_owned()) - .join(", "); + .join(","); let program = client.program(); let mut req = program.request(); @@ -112,16 +112,24 @@ pub async fn loop_update_index_and_rate( ix.accounts.append(&mut banks); req = req.instruction(ix); } + let pre = Instant::now(); let sig_result = req.send().map_err(prettify_client_error); if let Err(e) = sig_result { + log::info!( + "metricName=UpdateTokensV4Failure tokens={} durationMs={} error={}", + token_names, + pre.elapsed().as_millis(), + e + ); log::error!("{:?}", e) } else { log::info!( - "update_index_and_rate {} {:?}", + "metricName=UpdateTokensV4Success tokens={} durationMs={}", token_names, - sig_result.unwrap() - ) + pre.elapsed().as_millis(), + ); + log::info!("{:?}", sig_result); } Ok(()) @@ -191,6 +199,7 @@ pub async fn loop_consume_events( event_queue.pop_front()?; } + let pre = Instant::now(); let sig_result = client .program() .request() @@ -216,13 +225,22 @@ pub async fn loop_consume_events( .map_err(prettify_client_error); if let Err(e) = sig_result { + log::info!( + "metricName=ConsumeEventsV4Failure market={} durationMs={} consumed={} error={}", + perp_market.name(), + pre.elapsed().as_millis(), + ams_.len(), + e.to_string() + ); log::error!("{:?}", e) } else { log::info!( - "consume_event {} {:?}", + "metricName=ConsumeEventsV4Success market={} durationMs={} consumed={}", perp_market.name(), - sig_result.unwrap() - ) + pre.elapsed().as_millis(), + ams_.len(), + ); + log::info!("{:?}", sig_result); } Ok(()) @@ -253,6 +271,7 @@ pub async fn loop_update_funding( let client = mango_client.clone(); let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { + let pre = Instant::now(); let sig_result = client .program() .request() @@ -275,13 +294,20 @@ pub async fn loop_update_funding( .send() .map_err(prettify_client_error); if let Err(e) = sig_result { + log::error!( + "metricName=UpdateFundingV4Error market={} durationMs={} error={}", + perp_market.name(), + pre.elapsed().as_millis(), + e.to_string() + ); log::error!("{:?}", e) } else { log::info!( - "update_funding {} {:?}", + "metricName=UpdateFundingV4Success market={} durationMs={}", perp_market.name(), - sig_result.unwrap() - ) + pre.elapsed().as_millis(), + ); + log::info!("{:?}", sig_result); } Ok(()) From c9fb4c71334edbe6f60385e4affb8d91b76776af Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 22 Aug 2022 14:13:25 +0200 Subject: [PATCH 17/50] Tests: Fix program log capture --- Cargo.lock | 1 + programs/mango-v4/Cargo.toml | 1 + programs/mango-v4/tests/program_test/mod.rs | 13 ++++++++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a5d75251..e29b18d00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3208,6 +3208,7 @@ dependencies = [ "fixed", "fixed-macro", "itertools 0.10.3", + "lazy_static", "log 0.4.17", "mango-macro", "margin-trade", diff --git a/programs/mango-v4/Cargo.toml b/programs/mango-v4/Cargo.toml index bdfa792d3..b693ad05d 100644 --- a/programs/mango-v4/Cargo.toml +++ b/programs/mango-v4/Cargo.toml @@ -58,3 +58,4 @@ async-trait = "0.1.52" margin-trade = { path = "../margin-trade", features = ["cpi"] } itertools = "0.10.3" rand = "0.8.4" +lazy_static = "1.4.0" diff --git a/programs/mango-v4/tests/program_test/mod.rs b/programs/mango-v4/tests/program_test/mod.rs index d6da75484..726a8e7d8 100644 --- a/programs/mango-v4/tests/program_test/mod.rs +++ b/programs/mango-v4/tests/program_test/mod.rs @@ -89,10 +89,12 @@ pub struct TestContextBuilder { mint0: Pubkey, } +lazy_static::lazy_static! { + static ref PROGRAM_LOG_CAPTURE: Arc>> = Arc::new(RwLock::new(vec![])); +} + impl TestContextBuilder { pub fn new() -> Self { - let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry)); - // We need to intercept logs to capture program log output let log_filter = "solana_rbpf=trace,\ solana_runtime::message_processor=debug,\ @@ -102,18 +104,19 @@ impl TestContextBuilder { env_logger::Builder::from_env(env_logger::Env::new().default_filter_or(log_filter)) .format_timestamp_nanos() .build(); - let program_log_capture = Arc::new(RwLock::new(vec![])); let _ = log::set_boxed_logger(Box::new(LoggerWrapper { inner: env_logger, - program_log: program_log_capture.clone(), + program_log: PROGRAM_LOG_CAPTURE.clone(), })); + let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry)); + // intentionally set to as tight as possible, to catch potential problems early test.set_compute_max_units(87000); Self { test, - program_log_capture, + program_log_capture: PROGRAM_LOG_CAPTURE.clone(), mint0: Pubkey::new_unique(), } } From 2d2cef35a0b78e083e91614fc7f7b18cd7dfde32 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 22 Aug 2022 11:02:01 +0200 Subject: [PATCH 18/50] Add HealthRegion instructions You can do - HealthRegionBegin - ... mango instructions ... - HealthRegionEnd and the account health will only be checked at the start and end instead of for every instruction. --- CHANGELOG.md | 3 + client/src/client.rs | 2 + liquidator/src/liquidate.rs | 13 +- liquidator/src/rebalance.rs | 11 +- programs/mango-v4/src/error.rs | 2 + .../src/instructions/health_region.rs | 126 +++++++++++++ .../src/instructions/liq_token_bankruptcy.rs | 8 +- .../src/instructions/liq_token_with_token.rs | 8 +- programs/mango-v4/src/instructions/mod.rs | 2 + .../src/instructions/perp_place_order.rs | 13 +- .../src/instructions/serum3_place_order.rs | 13 +- .../src/instructions/token_deposit.rs | 25 ++- .../token_update_index_and_rate.rs | 9 +- .../src/instructions/token_withdraw.rs | 27 ++- programs/mango-v4/src/lib.rs | 12 ++ programs/mango-v4/src/state/bank.rs | 11 ++ programs/mango-v4/src/state/health.rs | 22 +-- programs/mango-v4/src/state/mango_account.rs | 57 ++++-- .../tests/program_test/mango_client.rs | 90 +++++++++ .../tests/program_test/mango_setup.rs | 1 + programs/mango-v4/tests/test_health_region.rs | 177 ++++++++++++++++++ ts/client/src/client.ts | 2 + ts/client/src/mango_v4.ts | 134 ++++++++++++- 23 files changed, 678 insertions(+), 90 deletions(-) create mode 100644 programs/mango-v4/src/instructions/health_region.rs create mode 100644 programs/mango-v4/tests/test_health_region.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e58551f..a42f430da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Update this for each mainnet deployment. ## not on mainnet +- Add HealthRegionBegin, -End instructions +- Add explicit "oracle" account argument for TokenDeposit and TokenWithdraw instructions + ## mainnet Aug 20, 2022 at 19:58:29 Central European Summer Time diff --git a/client/src/client.rs b/client/src/client.rs index c7815874a..61ba70ba4 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -374,6 +374,7 @@ impl MangoClient { account: self.mango_account_address, bank: mint_info.first_bank(), vault: mint_info.first_vault(), + oracle: mint_info.oracle, token_account: get_associated_token_address( &self.owner(), &mint_info.mint, @@ -425,6 +426,7 @@ impl MangoClient { owner: self.owner(), bank: mint_info.first_bank(), vault: mint_info.first_vault(), + oracle: mint_info.oracle, token_account: get_associated_token_address( &self.owner(), &mint_info.mint, diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index fd6219558..8ef545450 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -4,8 +4,8 @@ use crate::account_shared_data::KeyedAccountSharedData; use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext}; use mango_v4::state::{ - new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, - MangoAccountValue, TokenIndex, QUOTE_TOKEN_INDEX, + new_health_cache, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, MangoAccountValue, + TokenIndex, QUOTE_TOKEN_INDEX, }; use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; @@ -143,11 +143,10 @@ pub fn maybe_liquidate_account( let token = mango_client.context.token(token_position.token_index); let bank = account_fetcher.fetch::(&token.mint_info.first_bank())?; let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?; - let price = oracle_price( - &KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()), - bank.oracle_config.conf_filter, - bank.mint_decimals, - )?; + let price = bank.oracle_price(&KeyedAccountSharedData::new( + token.mint_info.oracle, + oracle.into(), + ))?; Ok(( token_position.token_index, price, diff --git a/liquidator/src/rebalance.rs b/liquidator/src/rebalance.rs index 34e59c6df..28dad5162 100644 --- a/liquidator/src/rebalance.rs +++ b/liquidator/src/rebalance.rs @@ -1,7 +1,7 @@ use crate::{account_shared_data::KeyedAccountSharedData, AnyhowWrap}; use client::{chain_data, AccountFetcher, MangoClient, TokenContext}; -use mango_v4::state::{oracle_price, Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX}; +use mango_v4::state::{Bank, TokenIndex, TokenPosition, QUOTE_TOKEN_INDEX}; use {fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; @@ -44,11 +44,10 @@ impl TokenState { account_fetcher: &chain_data::AccountFetcher, ) -> anyhow::Result { let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?; - oracle_price( - &KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()), - bank.oracle_config.conf_filter, - bank.mint_decimals, - ) + bank.oracle_price(&KeyedAccountSharedData::new( + token.mint_info.oracle, + oracle.into(), + )) .map_err_anyhow() } } diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index 65a216383..7231a218e 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -35,6 +35,8 @@ pub enum MangoError { Serum3OpenOrdersExistAlready, #[msg("bank vault has insufficent funds")] InsufficentBankVaultFunds, + #[msg("account is currently being liquidated")] + BeingLiquidated, } pub trait Contextable { diff --git a/programs/mango-v4/src/instructions/health_region.rs b/programs/mango-v4/src/instructions/health_region.rs new file mode 100644 index 000000000..25dcd6ad5 --- /dev/null +++ b/programs/mango-v4/src/instructions/health_region.rs @@ -0,0 +1,126 @@ +use crate::error::*; +use crate::state::*; +use anchor_lang::prelude::*; +use anchor_lang::solana_program::sysvar::instructions as tx_instructions; +use anchor_lang::Discriminator; + +/// Sets up for a health region +/// +/// The same transaction must have the corresponding HealthRegionEnd call. +/// +/// remaining_accounts: health accounts for account +#[derive(Accounts)] +pub struct HealthRegionBegin<'info> { + /// Instructions Sysvar for instruction introspection + /// CHECK: fixed instructions sysvar account + #[account(address = tx_instructions::ID)] + pub instructions: UncheckedAccount<'info>, + + #[account(mut)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, +} + +/// Ends a health region. +/// +/// remaining_accounts: health accounts for account +#[derive(Accounts)] +pub struct HealthRegionEnd<'info> { + #[account(mut)] + pub account: AccountLoaderDynamic<'info, MangoAccount>, +} + +pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>( + ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionBegin<'info>>, +) -> Result<()> { + // Check if the other instructions in the transactions are compatible + { + let ixs = ctx.accounts.instructions.as_ref(); + let current_index = tx_instructions::load_current_index_checked(ixs)? as usize; + + // There must be a matching HealthRegionEnd instruction + let mut index = current_index + 1; + let mut found_end = false; + loop { + let ix = match tx_instructions::load_instruction_at_checked(index, ixs) { + Ok(ix) => ix, + Err(ProgramError::InvalidArgument) => break, // past the last instruction + Err(e) => return Err(e.into()), + }; + index += 1; + + if ix.program_id != crate::id() { + continue; + } + if ix.data[0..8] != crate::instruction::HealthRegionEnd::discriminator() { + continue; + } + + // check that it's for the same account + require_keys_eq!(ix.accounts[0].pubkey, ctx.accounts.account.key()); + + found_end = true; + index += 1; + } + require_msg!( + found_end, + "found no HealthRegionEnd instruction in transaction" + ); + } + + let mut account = ctx.accounts.account.load_mut()?; + require_msg!( + !account.fixed.is_in_health_region(), + "account must not already be health wrapped" + ); + account.fixed.set_in_health_region(true); + + let group = account.fixed.group; + let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group) + .context("create account retriever")?; + + // Compute pre-health and store it on the account + let pre_health = compute_health(&account.borrow(), HealthType::Init, &account_retriever) + .context("compute health")?; + msg!("pre_health {:?}", pre_health); + account.fixed.health_region_begin_init_health = pre_health.ceil().checked_to_num().unwrap(); + + account + .fixed + .maybe_recover_from_being_liquidated(pre_health); + + require!( + !account.fixed.being_liquidated(), + MangoError::BeingLiquidated + ); + + Ok(()) +} + +pub fn health_region_end<'key, 'accounts, 'remaining, 'info>( + ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionEnd<'info>>, +) -> Result<()> { + let mut account = ctx.accounts.account.load_mut()?; + require_msg!( + account.fixed.is_in_health_region(), + "account must be health wrapped" + ); + account.fixed.set_in_health_region(false); + + let group = account.fixed.group; + let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group) + .context("create account retriever")?; + + let post_health = compute_health(&account.borrow(), HealthType::Init, &account_retriever) + .context("compute health")?; + msg!("post_health {:?}", post_health); + require!( + post_health >= 0 || post_health > account.fixed.health_region_begin_init_health, + MangoError::HealthMustBePositive + ); + account + .fixed + .maybe_recover_from_being_liquidated(post_health); + account.fixed.health_region_begin_init_health = 0; + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs index c65282b34..3b64ee6e4 100644 --- a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs @@ -178,9 +178,11 @@ pub fn liq_token_bankruptcy( liab_bank.withdraw_with_fee(liqor_liab, liab_transfer)?; // Check liqor's health - let liqor_health = - compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?; - require!(liqor_health >= 0, MangoError::HealthMustBePositive); + if !liqor.fixed.is_in_health_region() { + let liqor_health = + compute_health(&liqor.borrow(), HealthType::Init, &account_retriever)?; + require!(liqor_health >= 0, MangoError::HealthMustBePositive); + } // liqor quote emit!(TokenBalanceLog { diff --git a/programs/mango-v4/src/instructions/liq_token_with_token.rs b/programs/mango-v4/src/instructions/liq_token_with_token.rs index 46fa2d59b..433a1ea94 100644 --- a/programs/mango-v4/src/instructions/liq_token_with_token.rs +++ b/programs/mango-v4/src/instructions/liq_token_with_token.rs @@ -251,9 +251,11 @@ pub fn liq_token_with_token( .maybe_recover_from_being_liquidated(liqee_init_health); // Check liqor's health - let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever) - .context("compute liqor health")?; - require!(liqor_health >= 0, MangoError::HealthMustBePositive); + if !liqor.fixed.is_in_health_region() { + let liqor_health = compute_health(&liqor.borrow(), HealthType::Init, &account_retriever) + .context("compute liqor health")?; + require!(liqor_health >= 0, MangoError::HealthMustBePositive); + } emit!(LiquidateTokenAndTokenLog { mango_group: ctx.accounts.group.key(), diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index 7d8dece86..03283ea81 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -8,6 +8,7 @@ pub use flash_loan::*; pub use group_close::*; pub use group_create::*; pub use group_edit::*; +pub use health_region::*; pub use liq_token_bankruptcy::*; pub use liq_token_with_token::*; pub use perp_cancel_all_orders::*; @@ -51,6 +52,7 @@ mod flash_loan; mod group_close; mod group_create; mod group_edit; +mod health_region; mod liq_token_bankruptcy; mod liq_token_with_token; mod perp_cancel_all_orders; diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index 3602fe30a..49bb5b69d 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -132,11 +132,14 @@ pub fn perp_place_order( )?; } - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; - msg!("health: {}", health); - require!(health >= 0, MangoError::HealthMustBePositive); - account.fixed.maybe_recover_from_being_liquidated(health); + if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; + msg!("health: {}", health); + require!(health >= 0, MangoError::HealthMustBePositive); + account.fixed.maybe_recover_from_being_liquidated(health); + } Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 3b787185d..37db488cb 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -293,11 +293,14 @@ pub fn serum3_place_order( // // Health check // - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; - msg!("health: {}", health); - require!(health >= 0, MangoError::HealthMustBePositive); - account.fixed.maybe_recover_from_being_liquidated(health); + if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; + msg!("health: {}", health); + require!(health >= 0, MangoError::HealthMustBePositive); + account.fixed.maybe_recover_from_being_liquidated(health); + } vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index 1cae507f9..cd9ede702 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -4,6 +4,7 @@ use anchor_spl::token::Token; use anchor_spl::token::TokenAccount; use fixed::types::I80F48; +use crate::accounts_zerocopy::AccountInfoRef; use crate::error::*; use crate::state::*; use crate::util::checked_math as cm; @@ -21,6 +22,7 @@ pub struct TokenDeposit<'info> { mut, has_one = group, has_one = vault, + has_one = oracle, // the mints of bank/vault/token_account are implicitly the same because // spl::token::transfer succeeds between token_account and vault )] @@ -29,6 +31,9 @@ pub struct TokenDeposit<'info> { #[account(mut)] pub vault: Account<'info, TokenAccount>, + /// CHECK: The oracle can be one of several different account types + pub oracle: UncheckedAccount<'info>, + #[account(mut)] pub token_account: Box>, pub token_authority: Signer<'info>, @@ -56,7 +61,7 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // Get the account's position for that token index let mut account = ctx.accounts.account.load_mut()?; - let (position, raw_token_index, active_token_index) = + let (position, raw_token_index, _active_token_index) = account.ensure_token_position(token_index)?; let amount_i80f48 = I80F48::from(amount); @@ -69,10 +74,8 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { token::transfer(ctx.accounts.transfer_ctx(), amount)?; let indexed_position = position.indexed_position; - - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let (bank, oracle_price) = - retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?; + let bank = ctx.accounts.bank.load()?; + let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::(); @@ -91,10 +94,14 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // // Health computation // - let health = compute_health(&account.borrow(), HealthType::Init, &retriever) - .context("post-deposit init health")?; - msg!("health: {}", health); - account.fixed.maybe_recover_from_being_liquidated(health); + if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health = compute_health(&account.borrow(), HealthType::Init, &retriever) + .context("post-deposit init health")?; + msg!("health: {}", health); + account.fixed.maybe_recover_from_being_liquidated(health); + } // // Deactivate the position only after the health check because the user passed in diff --git a/programs/mango-v4/src/instructions/token_update_index_and_rate.rs b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs index 0a24adb2a..adc274cf5 100644 --- a/programs/mango-v4/src/instructions/token_update_index_and_rate.rs +++ b/programs/mango-v4/src/instructions/token_update_index_and_rate.rs @@ -5,7 +5,7 @@ use crate::logs::{UpdateIndexLog, UpdateRateLog}; use crate::state::HOUR; use crate::{ accounts_zerocopy::{AccountInfoRef, LoadMutZeroCopyRef, LoadZeroCopyRef}, - state::{oracle_price, Bank, Group, MintInfo}, + state::{Bank, Group, MintInfo}, }; use anchor_lang::solana_program::sysvar::instructions as tx_instructions; use anchor_lang::Discriminator; @@ -106,11 +106,8 @@ pub fn token_update_index_and_rate(ctx: Context) -> Res now_ts, ); - let price = oracle_price( - &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, - some_bank.oracle_config.conf_filter, - some_bank.mint_decimals, - )?; + let price = + some_bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; emit!(UpdateIndexLog { mango_group: mint_info.group.key(), token_index: mint_info.token_index, diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 2b0680826..522b6e74d 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -1,3 +1,4 @@ +use crate::accounts_zerocopy::*; use crate::error::*; use crate::state::*; use anchor_lang::prelude::*; @@ -24,6 +25,7 @@ pub struct TokenWithdraw<'info> { mut, has_one = group, has_one = vault, + has_one = oracle, // the mints of bank/vault/token_account are implicitly the same because // spl::token::transfer succeeds between token_account and vault )] @@ -32,6 +34,9 @@ pub struct TokenWithdraw<'info> { #[account(mut)] pub vault: Account<'info, TokenAccount>, + /// CHECK: The oracle can be one of several different account types + pub oracle: UncheckedAccount<'info>, + #[account(mut)] pub token_account: Box>, @@ -59,7 +64,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // Get the account's position for that token index let mut account = ctx.accounts.account.load_mut()?; - let (position, raw_token_index, active_token_index) = + let (position, raw_token_index, _active_token_index) = account.ensure_token_position(token_index)?; // The bank will also be passed in remainingAccounts. Use an explicit scope @@ -113,10 +118,8 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo }; let indexed_position = position.indexed_position; - - let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let (bank, oracle_price) = - retriever.bank_and_oracle(&ctx.accounts.group.key(), active_token_index, token_index)?; + let bank = ctx.accounts.bank.load()?; + let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::(); @@ -135,11 +138,15 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo // // Health check // - let health = compute_health(&account.borrow(), HealthType::Init, &retriever) - .context("post-withdraw init health")?; - msg!("health: {}", health); - require!(health >= 0, MangoError::HealthMustBePositive); - account.fixed.maybe_recover_from_being_liquidated(health); + if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health = compute_health(&account.borrow(), HealthType::Init, &retriever) + .context("post-withdraw init health")?; + msg!("health: {}", health); + require!(health >= 0, MangoError::HealthMustBePositive); + account.fixed.maybe_recover_from_being_liquidated(health); + } // // Deactivate the position only after the health check because the user passed in diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 347069be5..2f4d98fb6 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -236,6 +236,18 @@ pub mod mango_v4 { instructions::flash_loan_end(ctx, flash_loan_type) } + pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>( + ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionBegin<'info>>, + ) -> Result<()> { + instructions::health_region_begin(ctx) + } + + pub fn health_region_end<'key, 'accounts, 'remaining, 'info>( + ctx: Context<'key, 'accounts, 'remaining, 'info, HealthRegionEnd<'info>>, + ) -> Result<()> { + instructions::health_region_end(ctx) + } + /// /// Serum /// diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 51caf833f..444c05ae3 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -1,4 +1,6 @@ use super::{OracleConfig, TokenIndex, TokenPosition}; +use crate::accounts_zerocopy::KeyedAccountReader; +use crate::state::oracle; use crate::util; use crate::util::checked_math as cm; use anchor_lang::prelude::*; @@ -545,6 +547,15 @@ impl Bank { (self.rate0, self.rate1, self.max_rate) } } + + pub fn oracle_price(&self, oracle_acc: &impl KeyedAccountReader) -> Result { + require_keys_eq!(self.oracle, *oracle_acc.key()); + oracle::oracle_price( + oracle_acc, + self.oracle_config.conf_filter, + self.mint_decimals, + ) + } } #[macro_export] diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 6dda44174..8afa24c6c 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use crate::accounts_zerocopy::*; use crate::error::*; use crate::serum3_cpi; -use crate::state::{oracle_price, Bank, PerpMarket, PerpMarketIndex, TokenIndex}; +use crate::state::{Bank, PerpMarket, PerpMarketIndex, TokenIndex}; use crate::util::checked_math as cm; use super::MangoAccountRef; @@ -89,8 +89,7 @@ impl FixedOrderAccountRetriever { fn oracle_price(&self, account_index: usize, bank: &Bank) -> Result { let oracle = &self.ais[cm!(self.n_banks + account_index)]; - require_keys_eq!(bank.oracle, *oracle.key()); - oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals) + bank.oracle_price(oracle) } } @@ -275,8 +274,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { let index = self.bank_index(token_index1)?; let bank = self.banks[index].load_mut_fully_unchecked::()?; let oracle = &self.oracles[index]; - require_keys_eq!(bank.oracle, *oracle.key); - let price = oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?; + let price = bank.oracle_price(oracle)?; return Ok((bank, price, None)); } let index1 = self.bank_index(token_index1)?; @@ -294,12 +292,8 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { let bank2 = second_bank_part[second - (first + 1)].load_mut_fully_unchecked::()?; let oracle1 = &self.oracles[first]; let oracle2 = &self.oracles[second]; - require_keys_eq!(bank1.oracle, *oracle1.key); - require_keys_eq!(bank2.oracle, *oracle2.key); - let mint_decimals1 = bank1.mint_decimals; - let mint_decimals2 = bank2.mint_decimals; - let price1 = oracle_price(oracle1, bank1.oracle_config.conf_filter, mint_decimals1)?; - let price2 = oracle_price(oracle2, bank2.oracle_config.conf_filter, mint_decimals2)?; + let price1 = bank1.oracle_price(oracle1)?; + let price2 = bank2.oracle_price(oracle2)?; if swap { Ok((bank2, price2, Some((bank1, price1)))) } else { @@ -311,11 +305,7 @@ impl<'a, 'info> ScanningAccountRetriever<'a, 'info> { let index = self.bank_index(token_index)?; let bank = self.banks[index].load_fully_unchecked::()?; let oracle = &self.oracles[index]; - require_keys_eq!(bank.oracle, *oracle.key); - Ok(( - bank, - oracle_price(oracle, bank.oracle_config.conf_filter, bank.mint_decimals)?, - )) + Ok((bank, bank.oracle_price(oracle)?)) } pub fn scanned_perp_market(&self, perp_market_index: PerpMarketIndex) -> Result<&PerpMarket> { diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index df017df14..9fcde508c 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -56,9 +56,16 @@ pub struct MangoAccount { /// Normally accounts can not be liquidated while maint_health >= 0. But when an account /// reaches maint_health < 0, liquidators will call a liquidation instruction and thereby /// set this flag. Now the account may be liquidated until init_health >= 0. - being_liquidated: u8, + /// + /// Many actions should be disabled while the account is being liquidated, even if + /// its maint health has recovered to positive. Creating new open orders would, for example, + /// confuse liquidators. + pub being_liquidated: u8, - padding2: u8, + /// The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd. + /// + /// Must never be set after a transaction ends. + pub in_health_region: u8, pub bump: u8, @@ -72,7 +79,10 @@ pub struct MangoAccount { // TODO: unimplemented pub net_settled: i64, - pub reserved: [u8; 248], + /// Init health as calculated during HealthReginBegin, rounded up. + pub health_region_pre_init_health: i64, + + pub reserved: [u8; 240], // dynamic pub header_version: u8, @@ -100,13 +110,14 @@ impl Default for MangoAccount { owner: Pubkey::default(), delegate: Pubkey::default(), being_liquidated: 0, - padding2: 0, + in_health_region: 0, account_num: 0, bump: 0, padding: Default::default(), net_deposits: 0, net_settled: 0, - reserved: [0; 248], + health_region_pre_init_health: 0, + reserved: [0; 240], header_version: DEFAULT_MANGO_ACCOUNT_VERSION, padding3: Default::default(), padding4: Default::default(), @@ -179,14 +190,17 @@ fn test_serialization_match() { account.name = crate::util::fill_from_str("abcdef").unwrap(); account.delegate = Pubkey::new_unique(); account.account_num = 1; - account.bump = 2; - account.net_deposits = 3; - account.net_settled = 4; + account.being_liquidated = 2; + account.in_health_region = 3; + account.bump = 4; + account.net_deposits = 5; + account.net_settled = 6; + account.health_region_pre_init_health = 7; account.tokens.resize(8, TokenPosition::default()); - account.tokens[0].token_index = 5; + account.tokens[0].token_index = 8; account.serum3.resize(8, Serum3Orders::default()); account.perps.resize(8, PerpPosition::default()); - account.perps[0].market_index = 6; + account.perps[0].market_index = 9; account .perp_open_orders .resize(8, PerpOpenOrders::default()); @@ -203,9 +217,15 @@ fn test_serialization_match() { assert_eq!(account.name, account2.fixed.name); assert_eq!(account.delegate, account2.fixed.delegate); assert_eq!(account.account_num, account2.fixed.account_num); + assert_eq!(account.being_liquidated, account2.fixed.being_liquidated); + assert_eq!(account.in_health_region, account2.fixed.in_health_region); assert_eq!(account.bump, account2.fixed.bump); assert_eq!(account.net_deposits, account2.fixed.net_deposits); assert_eq!(account.net_settled, account2.fixed.net_settled); + assert_eq!( + account.health_region_pre_init_health, + account2.fixed.health_region_begin_init_health + ); assert_eq!( account.tokens[0].token_index, account2.token_position_by_raw_index(0).token_index @@ -230,14 +250,15 @@ pub struct MangoAccountFixed { pub delegate: Pubkey, pub account_num: u32, being_liquidated: u8, - padding2: u8, + in_health_region: u8, pub bump: u8, pub padding: [u8; 1], pub net_deposits: i64, pub net_settled: i64, - pub reserved: [u8; 248], + pub health_region_begin_init_health: i64, + pub reserved: [u8; 240], } -const_assert_eq!(size_of::(), 32 * 4 + 8 + 2 * 8 + 248); +const_assert_eq!(size_of::(), 32 * 4 + 8 + 3 * 8 + 240); const_assert_eq!(size_of::() % 8, 0); unsafe impl bytemuck::Pod for MangoAccountFixed {} @@ -255,13 +276,21 @@ impl MangoAccountFixed { } pub fn being_liquidated(&self) -> bool { - self.being_liquidated != 0 + self.being_liquidated == 1 } pub fn set_being_liquidated(&mut self, b: bool) { self.being_liquidated = if b { 1 } else { 0 }; } + pub fn is_in_health_region(&self) -> bool { + self.in_health_region == 1 + } + + pub fn set_in_health_region(&mut self, b: bool) { + self.in_health_region = if b { 1 } else { 0 }; + } + pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) { // This is used as threshold to flip flag instead of 0 because of dust issues let one_native_usdc = I80F48::ONE; diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 1d12ecc6d..eb69e5a55 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -502,6 +502,7 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> { owner: self.owner.pubkey(), bank: mint_info.banks[self.bank_index], vault: mint_info.vaults[self.bank_index], + oracle: mint_info.oracle, token_account: self.token_account, token_program: Token::id(), }; @@ -569,6 +570,7 @@ impl ClientInstruction for TokenDepositInstruction { account: self.account, bank: mint_info.banks[self.bank_index], vault: mint_info.vaults[self.bank_index], + oracle: mint_info.oracle, token_account: self.token_account, token_authority: self.token_authority.pubkey(), token_program: Token::id(), @@ -2541,3 +2543,91 @@ impl ClientInstruction for ComputeAccountDataInstruction { vec![] } } + +pub struct HealthRegionBeginInstruction { + pub account: Pubkey, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for HealthRegionBeginInstruction { + type Accounts = mango_v4::accounts::HealthRegionBegin; + type Instruction = mango_v4::instruction::HealthRegionBegin; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction {}; + + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); + + let health_check_metas = derive_health_check_remaining_account_metas( + &account_loader, + &account, + None, + false, + None, + ) + .await; + + let accounts = Self::Accounts { + instructions: solana_program::sysvar::instructions::id(), + account: self.account, + }; + + let mut instruction = make_instruction(program_id, &accounts, instruction); + instruction.accounts.extend(health_check_metas.into_iter()); + + (accounts, instruction) + } + + fn signers(&self) -> Vec<&Keypair> { + vec![] + } +} + +pub struct HealthRegionEndInstruction { + pub account: Pubkey, + pub affected_bank: Option, +} +#[async_trait::async_trait(?Send)] +impl ClientInstruction for HealthRegionEndInstruction { + type Accounts = mango_v4::accounts::HealthRegionEnd; + type Instruction = mango_v4::instruction::HealthRegionEnd; + async fn to_instruction( + &self, + account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction {}; + + let account = account_loader + .load_mango_account(&self.account) + .await + .unwrap(); + + let health_check_metas = derive_health_check_remaining_account_metas( + &account_loader, + &account, + self.affected_bank, + false, + None, + ) + .await; + + let accounts = Self::Accounts { + account: self.account, + }; + + let mut instruction = make_instruction(program_id, &accounts, instruction); + instruction.accounts.extend(health_check_metas.into_iter()); + + (accounts, instruction) + } + + fn signers(&self) -> Vec<&Keypair> { + vec![] + } +} diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index d375ebbaf..2aacef1fa 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -13,6 +13,7 @@ pub struct GroupWithTokensConfig<'a> { pub mints: &'a [MintCookie], } +#[derive(Clone)] pub struct Token { pub index: u16, pub mint: MintCookie, diff --git a/programs/mango-v4/tests/test_health_region.rs b/programs/mango-v4/tests/test_health_region.rs new file mode 100644 index 000000000..693adec22 --- /dev/null +++ b/programs/mango-v4/tests/test_health_region.rs @@ -0,0 +1,177 @@ +#![cfg(feature = "test-bpf")] + +use solana_program_test::*; +use solana_sdk::{signature::Keypair, transport::TransportError}; + +use mango_v4::state::MangoAccount; + +use program_test::*; + +mod program_test; + +#[tokio::test] +async fn test_health_wrap() -> Result<(), TransportError> { + let context = TestContext::new().await; + let solana = &context.solana.clone(); + + let admin = &Keypair::new(); + let owner = &context.users[0].key; + let payer = &context.users[1].key; + let mints = &context.mints[0..2]; + let payer_mint_accounts = &context.users[1].token_accounts; + + // + // SETUP: Create a group, account, register a token (mint0) + // + + let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + admin, + payer, + mints, + } + .create(solana) + .await; + + // SETUP: Create an account with deposits, so the second account can borrow more than it has + let setup_account = send_tx( + solana, + AccountCreateInstruction { + account_num: 0, + token_count: 8, + serum3_count: 0, + perp_count: 0, + perp_oo_count: 0, + group, + owner, + payer, + }, + ) + .await + .unwrap() + .account; + + send_tx( + solana, + TokenDepositInstruction { + amount: 1000, + account: setup_account, + token_account: payer_mint_accounts[0], + token_authority: payer.clone(), + bank_index: 0, + }, + ) + .await + .unwrap(); + + // SETUP: Make a second account + let account = send_tx( + solana, + AccountCreateInstruction { + account_num: 1, + token_count: 8, + serum3_count: 0, + perp_count: 0, + perp_oo_count: 0, + group, + owner, + payer, + }, + ) + .await + .unwrap() + .account; + + // SETUP: deposit something, so only one new token position needs to be created + // simply because the test code can't deal with two affected banks right now + send_tx( + solana, + TokenDepositInstruction { + amount: 1, + account, + token_account: payer_mint_accounts[0], + token_authority: payer.clone(), + bank_index: 0, + }, + ) + .await + .unwrap(); + + let send_test_tx = |repay_amount| { + let tokens = tokens.clone(); + async move { + let mut tx = ClientTransaction::new(solana); + tx.add_instruction(HealthRegionBeginInstruction { account }) + .await; + tx.add_instruction(TokenWithdrawInstruction { + amount: 1000, // more than the 1 token that's on the account + allow_borrow: true, + account, + owner, + token_account: payer_mint_accounts[0], + bank_index: 0, + }) + .await; + tx.add_instruction(TokenDepositInstruction { + amount: repay_amount, + account, + token_account: payer_mint_accounts[1], + token_authority: payer.clone(), + bank_index: 0, + }) + .await; + tx.add_instruction(HealthRegionEndInstruction { + account, + affected_bank: Some(tokens[1].bank), + }) + .await; + tx.send().await + } + }; + + // + // TEST: Borrow a lot of token0 without collateral, but repay too little + // + { + send_test_tx(1000).await.unwrap_err(); + let logs = solana.program_log(); + // reaches the End instruction + assert!(logs + .iter() + .any(|line| line.contains("Instruction: HealthRegionEnd"))); + // errors due to health + assert!(logs + .iter() + .any(|line| line.contains("Error Code: HealthMustBePositive"))); + } + + // + // TEST: Borrow a lot of token0 without collateral, and repay in token1 in the same tx + // + { + let start_payer_mint0 = solana.token_account_balance(payer_mint_accounts[0]).await; + let start_payer_mint1 = solana.token_account_balance(payer_mint_accounts[1]).await; + + send_test_tx(3000).await.unwrap(); + + assert_eq!( + solana.token_account_balance(payer_mint_accounts[0]).await - start_payer_mint0, + 1000 + ); + assert_eq!( + start_payer_mint1 - solana.token_account_balance(payer_mint_accounts[1]).await, + 3000 + ); + assert_eq!( + account_position(solana, account, tokens[0].bank).await, + -999 + ); + assert_eq!( + account_position(solana, account, tokens[1].bank).await, + 3000 + ); + let account_data: MangoAccount = solana.get_account(account).await; + assert_eq!(account_data.in_health_region, 0); + } + + Ok(()) +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 23dbe3407..fd106191a 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -791,6 +791,7 @@ export class MangoClient { account: mangoAccount.publicKey, bank: bank.publicKey, vault: bank.vault, + oracle: bank.oracle, tokenAccount: wrappedSolAccount?.publicKey ?? tokenAccountPk, tokenAuthority: mangoAccount.owner, }) @@ -864,6 +865,7 @@ export class MangoClient { account: mangoAccount.publicKey, bank: bank.publicKey, vault: bank.vault, + oracle: bank.oracle, tokenAccount: tokenAccountPk, owner: mangoAccount.owner, }) diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 695f61f99..f6a907150 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1106,6 +1106,11 @@ export type MangoV4 = { "isMut": true, "isSigner": false }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, { "name": "tokenAccount", "isMut": true, @@ -1157,6 +1162,11 @@ export type MangoV4 = { "isMut": true, "isSigner": false }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, { "name": "tokenAccount", "isMut": true, @@ -1233,6 +1243,36 @@ export type MangoV4 = { } ] }, + { + "name": "healthRegionBegin", + "accounts": [ + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "healthRegionEnd", + "accounts": [ + { + "name": "account", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "serum3RegisterMarket", "docs": [ @@ -3000,12 +3040,21 @@ export type MangoV4 = { "", "Normally accounts can not be liquidated while maint_health >= 0. But when an account", "reaches maint_health < 0, liquidators will call a liquidation instruction and thereby", - "set this flag. Now the account may be liquidated until init_health >= 0." + "set this flag. Now the account may be liquidated until init_health >= 0.", + "", + "Many actions should be disabled while the account is being liquidated, even if", + "its maint health has recovered to positive. Creating new open orders would, for example,", + "confuse liquidators." ], "type": "u8" }, { - "name": "padding2", + "name": "inHealthRegion", + "docs": [ + "The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd.", + "", + "Must never be set after a transaction ends." + ], "type": "u8" }, { @@ -3029,12 +3078,19 @@ export type MangoV4 = { "name": "netSettled", "type": "i64" }, + { + "name": "healthRegionPreInitHealth", + "docs": [ + "Init health as calculated during HealthReginBegin, rounded up." + ], + "type": "i64" + }, { "name": "reserved", "type": { "array": [ "u8", - 248 + 240 ] } }, @@ -5183,6 +5239,11 @@ export type MangoV4 = { "code": 6014, "name": "InsufficentBankVaultFunds", "msg": "bank vault has insufficent funds" + }, + { + "code": 6015, + "name": "BeingLiquidated", + "msg": "account is currently being liquidated" } ] }; @@ -6295,6 +6356,11 @@ export const IDL: MangoV4 = { "isMut": true, "isSigner": false }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, { "name": "tokenAccount", "isMut": true, @@ -6346,6 +6412,11 @@ export const IDL: MangoV4 = { "isMut": true, "isSigner": false }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + }, { "name": "tokenAccount", "isMut": true, @@ -6422,6 +6493,36 @@ export const IDL: MangoV4 = { } ] }, + { + "name": "healthRegionBegin", + "accounts": [ + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "account", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "healthRegionEnd", + "accounts": [ + { + "name": "account", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, { "name": "serum3RegisterMarket", "docs": [ @@ -8189,12 +8290,21 @@ export const IDL: MangoV4 = { "", "Normally accounts can not be liquidated while maint_health >= 0. But when an account", "reaches maint_health < 0, liquidators will call a liquidation instruction and thereby", - "set this flag. Now the account may be liquidated until init_health >= 0." + "set this flag. Now the account may be liquidated until init_health >= 0.", + "", + "Many actions should be disabled while the account is being liquidated, even if", + "its maint health has recovered to positive. Creating new open orders would, for example,", + "confuse liquidators." ], "type": "u8" }, { - "name": "padding2", + "name": "inHealthRegion", + "docs": [ + "The account is currently inside a health region marked by HealthRegionBegin...HealthRegionEnd.", + "", + "Must never be set after a transaction ends." + ], "type": "u8" }, { @@ -8218,12 +8328,19 @@ export const IDL: MangoV4 = { "name": "netSettled", "type": "i64" }, + { + "name": "healthRegionPreInitHealth", + "docs": [ + "Init health as calculated during HealthReginBegin, rounded up." + ], + "type": "i64" + }, { "name": "reserved", "type": { "array": [ "u8", - 248 + 240 ] } }, @@ -10372,6 +10489,11 @@ export const IDL: MangoV4 = { "code": 6014, "name": "InsufficentBankVaultFunds", "msg": "bank vault has insufficent funds" + }, + { + "code": 6015, + "name": "BeingLiquidated", + "msg": "account is currently being liquidated" } ] }; From 1ebbac7d6fbd10b8119701791bc8b4503b273069 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 23 Aug 2022 09:18:19 +0200 Subject: [PATCH 19/50] Withdraw: Add pre-health check --- .../src/instructions/token_deposit.rs | 3 + .../src/instructions/token_withdraw.rs | 147 ++++++++++-------- 2 files changed, 84 insertions(+), 66 deletions(-) diff --git a/programs/mango-v4/src/instructions/token_deposit.rs b/programs/mango-v4/src/instructions/token_deposit.rs index cd9ede702..05a2d9e51 100644 --- a/programs/mango-v4/src/instructions/token_deposit.rs +++ b/programs/mango-v4/src/instructions/token_deposit.rs @@ -94,6 +94,9 @@ pub fn token_deposit(ctx: Context, amount: u64) -> Result<()> { // // Health computation // + // Since depositing can only increase health, we can skip the usual pre-health computation. + // Also, TokenDeposit is one of the rare instructions that is allowed even during being_liquidated. + // if !account.fixed.is_in_health_region() { let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 522b6e74d..45ca975ba 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -10,7 +10,6 @@ use fixed::types::I80F48; use crate::logs::{ LoanOriginationFeeInstruction, TokenBalanceLog, WithdrawLoanOriginationFeeLog, WithdrawLog, }; -use crate::state::new_fixed_order_account_retriever; use crate::util::checked_math as cm; #[derive(Accounts)] @@ -61,91 +60,107 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo let group = ctx.accounts.group.load()?; let token_index = ctx.accounts.bank.load()?.token_index; - // Get the account's position for that token index + // Create the account's position for that token index let mut account = ctx.accounts.account.load_mut()?; + let (_, raw_token_index, _) = account.ensure_token_position(token_index)?; - let (position, raw_token_index, _active_token_index) = - account.ensure_token_position(token_index)?; - - // The bank will also be passed in remainingAccounts. Use an explicit scope - // to drop the &mut before we borrow it immutably again later. - let (position_is_active, amount_i80f48, loan_origination_fee) = { - let mut bank = ctx.accounts.bank.load_mut()?; - let native_position = position.native(&bank); - - // Handle amount special case for withdrawing everything - let amount = if amount == u64::MAX && !allow_borrow { - if native_position.is_positive() { - // TODO: This rounding may mean that if we deposit and immediately withdraw - // we can't withdraw the full amount! - native_position.floor().to_num::() - } else { - return Ok(()); - } - } else { - amount - }; - + // Health check _after_ the token position is guaranteed to exist + let pre_health_opt = if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health_cache = + new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; + let pre_health = health_cache.health(HealthType::Init); + msg!("pre_health: {}", pre_health); + account + .fixed + .maybe_recover_from_being_liquidated(pre_health); require!( - allow_borrow || amount <= native_position, - MangoError::SomeError + !account.fixed.being_liquidated(), + MangoError::BeingLiquidated ); - - let amount_i80f48 = I80F48::from(amount); - - // Update the bank and position - let (position_is_active, loan_origination_fee) = - bank.withdraw_with_fee(position, amount_i80f48)?; - - // Provide a readable error message in case the vault doesn't have enough tokens - if ctx.accounts.vault.amount < amount { - return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { - format!( - "bank vault does not have enough tokens, need {} but have {}", - amount, ctx.accounts.vault.amount - ) - }); - } - - // Transfer the actual tokens - let group_seeds = group_seeds!(group); - token::transfer( - ctx.accounts.transfer_ctx().with_signer(&[group_seeds]), - amount, - )?; - - (position_is_active, amount_i80f48, loan_origination_fee) + Some((health_cache, pre_health)) + } else { + None }; - let indexed_position = position.indexed_position; - let bank = ctx.accounts.bank.load()?; - let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; + let mut bank = ctx.accounts.bank.load_mut()?; + let position = account.token_position_mut_by_raw_index(raw_token_index); + let native_position = position.native(&bank); - // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) - let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::(); - account.fixed.net_deposits = cm!(account.fixed.net_deposits - amount_usd); + // Handle amount special case for withdrawing everything + let amount = if amount == u64::MAX && !allow_borrow { + if native_position.is_positive() { + // TODO: This rounding may mean that if we deposit and immediately withdraw + // we can't withdraw the full amount! + native_position.floor().to_num::() + } else { + return Ok(()); + } + } else { + amount + }; + + require!( + allow_borrow || amount <= native_position, + MangoError::SomeError + ); + + let amount_i80f48 = I80F48::from(amount); + + // Update the bank and position + let (position_is_active, loan_origination_fee) = + bank.withdraw_with_fee(position, amount_i80f48)?; + + // Provide a readable error message in case the vault doesn't have enough tokens + if ctx.accounts.vault.amount < amount { + return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { + format!( + "bank vault does not have enough tokens, need {} but have {}", + amount, ctx.accounts.vault.amount + ) + }); + } + + // Transfer the actual tokens + let group_seeds = group_seeds!(group); + token::transfer( + ctx.accounts.transfer_ctx().with_signer(&[group_seeds]), + amount, + )?; + + let native_position_after = position.native(&bank); + let oracle_price = bank.oracle_price(&AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?)?; emit!(TokenBalanceLog { mango_group: ctx.accounts.group.key(), mango_account: ctx.accounts.account.key(), token_index, - indexed_position: indexed_position.to_bits(), + indexed_position: position.indexed_position.to_bits(), deposit_index: bank.deposit_index.to_bits(), borrow_index: bank.borrow_index.to_bits(), price: oracle_price.to_bits(), }); + // Update the net deposits - adjust by price so different tokens are on the same basis (in USD terms) + let amount_usd = cm!(amount_i80f48 * oracle_price).to_num::(); + account.fixed.net_deposits = cm!(account.fixed.net_deposits - amount_usd); + // // Health check // - if !account.fixed.is_in_health_region() { - let retriever = - new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever) - .context("post-withdraw init health")?; - msg!("health: {}", health); - require!(health >= 0, MangoError::HealthMustBePositive); - account.fixed.maybe_recover_from_being_liquidated(health); + if let Some((mut health_cache, pre_health)) = pre_health_opt { + health_cache + .adjust_token_balance(token_index, cm!(native_position_after - native_position))?; + let post_health = health_cache.health(HealthType::Init); + msg!("post_health: {}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositive + ); + account + .fixed + .maybe_recover_from_being_liquidated(post_health); } // From 40f467024d2c7d00a9ca6783c0a1a64c1d307623 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 23 Aug 2022 14:01:10 +0200 Subject: [PATCH 20/50] FlashLoan: Add pre-health check --- .../mango-v4/src/instructions/flash_loan.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index 2616ff5a0..b56b43afa 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -316,7 +316,15 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Check health before balance adjustments let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; - let _pre_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; + let pre_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; + msg!("pre_health {:?}", pre_health); + account + .fixed + .maybe_recover_from_being_liquidated(pre_health); + require!( + !account.fixed.being_liquidated(), + MangoError::BeingLiquidated + ); // Prices for logging let mut prices = vec![]; @@ -390,8 +398,11 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Check health after account position changes let post_health = compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?; - msg!("post_cpi_health {:?}", post_health); - require!(post_health >= 0, MangoError::HealthMustBePositive); + msg!("post_health {:?}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositive + ); account .fixed .maybe_recover_from_being_liquidated(post_health); From 45f52ae535b126274b4c0066311d7d4c7a304dff Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 23 Aug 2022 14:10:38 +0200 Subject: [PATCH 21/50] Serum3PlaceOrder: compute pre-health --- .../serum3_liq_force_cancel_orders.rs | 47 ++-- .../src/instructions/serum3_place_order.rs | 244 +++++++++++++----- .../src/instructions/serum3_settle_funds.rs | 57 ++-- programs/mango-v4/src/state/health.rs | 68 ++++- programs/mango-v4/tests/program_test/mod.rs | 2 +- programs/mango-v4/tests/test_liq_tokens.rs | 4 +- programs/mango-v4/tests/test_serum.rs | 4 +- 7 files changed, 284 insertions(+), 142 deletions(-) diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 165f85ada..70b2979ca 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -5,7 +5,7 @@ use crate::error::*; use crate::instructions::apply_vault_difference; use crate::state::*; -use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; +use crate::logs::LoanOriginationFeeInstruction; #[derive(Accounts)] pub struct Serum3LiqForceCancelOrders<'info> { @@ -140,36 +140,21 @@ pub fn serum3_liq_force_cancel_orders( let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = - apply_vault_difference( - &mut account.borrow_mut(), - &mut base_bank, - after_base_vault, - before_base_vault, - &mut quote_bank, - after_quote_vault, - before_quote_vault, - )?; - vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); - - if base_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.base_token_index, - loan_origination_fee: base_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders - }); - } - if quote_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.quote_token_index, - loan_origination_fee: quote_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders - }); - } + let difference_result = apply_vault_difference( + &mut account.borrow_mut(), + &mut base_bank, + after_base_vault, + before_base_vault, + &mut quote_bank, + after_quote_vault, + before_quote_vault, + )?; + difference_result.log_loan_origination_fees( + &ctx.accounts.group.key(), + &ctx.accounts.account.key(), + LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders, + ); + difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 37db488cb..7553209d8 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -20,6 +20,7 @@ pub struct OpenOrdersSlim { pub native_coin_total: u64, pub native_pc_free: u64, pub native_pc_total: u64, + pub referrer_rebates_accrued: u64, } impl OpenOrdersSlim { pub fn from_oo(oo: &OpenOrders) -> Self { @@ -28,30 +29,45 @@ impl OpenOrdersSlim { native_coin_total: oo.native_coin_total, native_pc_free: oo.native_pc_free, native_pc_total: oo.native_pc_total, + referrer_rebates_accrued: oo.referrer_rebates_accrued, } } } -pub trait OpenOrdersReserved { - fn native_coin_reserved(&self) -> u64; - fn native_pc_reserved(&self) -> u64; +pub trait OpenOrdersAmounts { + fn native_base_reserved(&self) -> u64; + fn native_quote_reserved(&self) -> u64; + fn native_base_free(&self) -> u64; + fn native_quote_free(&self) -> u64; // includes settleable referrer rebates } -impl OpenOrdersReserved for OpenOrdersSlim { - fn native_coin_reserved(&self) -> u64 { - self.native_coin_total - self.native_coin_free +impl OpenOrdersAmounts for OpenOrdersSlim { + fn native_base_reserved(&self) -> u64 { + cm!(self.native_coin_total - self.native_coin_free) } - fn native_pc_reserved(&self) -> u64 { - self.native_pc_total - self.native_pc_free + fn native_quote_reserved(&self) -> u64 { + cm!(self.native_pc_total - self.native_pc_free) + } + fn native_base_free(&self) -> u64 { + self.native_coin_free + } + fn native_quote_free(&self) -> u64 { + cm!(self.native_pc_free + self.referrer_rebates_accrued) } } -impl OpenOrdersReserved for OpenOrders { - fn native_coin_reserved(&self) -> u64 { - self.native_coin_total - self.native_coin_free +impl OpenOrdersAmounts for OpenOrders { + fn native_base_reserved(&self) -> u64 { + cm!(self.native_coin_total - self.native_coin_free) } - fn native_pc_reserved(&self) -> u64 { - self.native_pc_total - self.native_pc_free + fn native_quote_reserved(&self) -> u64 { + cm!(self.native_pc_total - self.native_pc_free) + } + fn native_base_free(&self) -> u64 { + self.native_coin_free + } + fn native_quote_free(&self) -> u64 { + cm!(self.native_pc_free + self.referrer_rebates_accrued) } } @@ -223,7 +239,28 @@ pub fn serum3_place_order( }); } - // TODO: pre-health check + // + // Pre-health computation + // + let mut account = ctx.accounts.account.load_mut()?; + let pre_health_opt = if !account.fixed.is_in_health_region() { + let retriever = + new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; + let health_cache = + new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; + let pre_health = health_cache.health(HealthType::Init); + msg!("pre_health: {}", pre_health); + account + .fixed + .maybe_recover_from_being_liquidated(pre_health); + require!( + !account.fixed.being_liquidated(), + MangoError::BeingLiquidated + ); + Some((health_cache, pre_health)) + } else { + None + }; // // Apply the order to serum. Also immediately settle, in case the order @@ -248,22 +285,22 @@ pub fn serum3_place_order( let open_orders = load_open_orders_ref(oo_ai)?; OpenOrdersSlim::from_oo(&open_orders) }; - cpi_place_order(ctx.accounts, order)?; - { + cpi_place_order(ctx.accounts, order)?; + cpi_settle_funds(ctx.accounts)?; + + let oo_difference = { let oo_ai = &ctx.accounts.open_orders.as_ref(); let open_orders = load_open_orders_ref(oo_ai)?; let after_oo = OpenOrdersSlim::from_oo(&open_orders); - let mut account = ctx.accounts.account.load_mut()?; inc_maybe_loan( serum_market.market_index, &mut account.borrow_mut(), &before_oo, &after_oo, ); - } - - cpi_settle_funds(ctx.accounts)?; + OODifference::new(&before_oo, &after_oo) + }; // // After-order tracking @@ -274,8 +311,7 @@ pub fn serum3_place_order( let after_quote_vault = ctx.accounts.quote_vault.amount; // Charge the difference in vault balances to the user's account - let mut account = ctx.accounts.account.load_mut()?; - let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = { + let vault_difference = { let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; @@ -293,35 +329,27 @@ pub fn serum3_place_order( // // Health check // - if !account.fixed.is_in_health_region() { - let retriever = - new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; - msg!("health: {}", health); - require!(health >= 0, MangoError::HealthMustBePositive); - account.fixed.maybe_recover_from_being_liquidated(health); + if let Some((mut health_cache, pre_health)) = pre_health_opt { + vault_difference.adjust_health_cache(&mut health_cache)?; + oo_difference.adjust_health_cache(&mut health_cache, &serum_market)?; + + let post_health = health_cache.health(HealthType::Init); + msg!("post_health: {}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositive + ); + account + .fixed + .maybe_recover_from_being_liquidated(post_health); } - vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); - - if base_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.base_token_index, - loan_origination_fee: base_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder - }); - } - if quote_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.quote_token_index, - loan_origination_fee: quote_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3PlaceOrder - }); - } + vault_difference.log_loan_origination_fees( + &ctx.accounts.group.key(), + &ctx.accounts.account.key(), + LoanOriginationFeeInstruction::Serum3PlaceOrder, + ); + vault_difference.deactivate_inactive_token_accounts(&mut account.borrow_mut()); Ok(()) } @@ -335,25 +363,70 @@ pub fn inc_maybe_loan( ) { let serum3_account = account.serum3_orders_mut(market_index).unwrap(); - if after_oo.native_coin_reserved() > before_oo.native_coin_reserved() { + if after_oo.native_base_reserved() > before_oo.native_base_reserved() { let native_coin_reserved_increase = - after_oo.native_coin_reserved() - before_oo.native_coin_reserved(); + after_oo.native_base_reserved() - before_oo.native_base_reserved(); serum3_account.previous_native_coin_reserved = cm!(serum3_account.previous_native_coin_reserved + native_coin_reserved_increase); } - if after_oo.native_pc_reserved() > before_oo.native_pc_reserved() { - let reserved_pc_increase = after_oo.native_pc_reserved() - before_oo.native_pc_reserved(); + if after_oo.native_quote_reserved() > before_oo.native_quote_reserved() { + let reserved_pc_increase = + after_oo.native_quote_reserved() - before_oo.native_quote_reserved(); serum3_account.previous_native_pc_reserved = cm!(serum3_account.previous_native_pc_reserved + reserved_pc_increase); } } +pub struct OODifference { + reserved_base_change: I80F48, + reserved_quote_change: I80F48, + free_base_change: I80F48, + free_quote_change: I80F48, +} + +impl OODifference { + pub fn new(before_oo: &OpenOrdersSlim, after_oo: &OpenOrdersSlim) -> Self { + Self { + reserved_base_change: cm!(I80F48::from(after_oo.native_base_reserved()) + - I80F48::from(before_oo.native_base_reserved())), + reserved_quote_change: cm!(I80F48::from(after_oo.native_quote_reserved()) + - I80F48::from(before_oo.native_quote_reserved())), + free_base_change: cm!(I80F48::from(after_oo.native_base_free()) + - I80F48::from(before_oo.native_base_free())), + free_quote_change: cm!(I80F48::from(after_oo.native_quote_free()) + - I80F48::from(before_oo.native_quote_free())), + } + } + + pub fn adjust_health_cache( + &self, + health_cache: &mut HealthCache, + market: &Serum3Market, + ) -> Result<()> { + health_cache.adjust_serum3_reserved( + market.market_index, + market.base_token_index, + self.reserved_base_change, + self.free_base_change, + market.quote_token_index, + self.reserved_quote_change, + self.free_quote_change, + ) + } +} + pub struct VaultDifferenceResult { base_raw_index: usize, + base_index: TokenIndex, base_active: bool, quote_raw_index: usize, + quote_index: TokenIndex, quote_active: bool, + base_loan_origination_fee: I80F48, + quote_loan_origination_fee: I80F48, + base_native_change: I80F48, + quote_native_change: I80F48, } impl VaultDifferenceResult { @@ -365,6 +438,38 @@ impl VaultDifferenceResult { account.deactivate_token_position(self.quote_raw_index); } } + + pub fn log_loan_origination_fees( + &self, + group: &Pubkey, + account: &Pubkey, + instruction: LoanOriginationFeeInstruction, + ) { + if self.base_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: *group, + mango_account: *account, + token_index: self.base_index, + loan_origination_fee: self.base_loan_origination_fee.to_bits(), + instruction, + }); + } + if self.quote_loan_origination_fee.is_positive() { + emit!(WithdrawLoanOriginationFeeLog { + mango_group: *group, + mango_account: *account, + token_index: self.quote_index, + loan_origination_fee: self.quote_loan_origination_fee.to_bits(), + instruction, + }); + } + } + + pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> { + health_cache.adjust_token_balance(self.base_index, self.base_native_change)?; + health_cache.adjust_token_balance(self.quote_index, self.quote_native_change)?; + Ok(()) + } } pub fn apply_vault_difference( @@ -375,31 +480,38 @@ pub fn apply_vault_difference( quote_bank: &mut Bank, after_quote_vault: u64, before_quote_vault: u64, -) -> Result<(VaultDifferenceResult, I80F48, I80F48)> { +) -> Result { // TODO: Applying the loan origination fee here may be too early: it should only be // charged if an order executes and the loan materializes? Otherwise MMs that place // an order without having the funds will be charged for each place_order! let (base_position, base_raw_index) = account.token_position_mut(base_bank.token_index)?; - let base_change = I80F48::from(after_base_vault) - I80F48::from(before_base_vault); + let base_native_before = base_position.native(&base_bank); + let base_needed_change = cm!(I80F48::from(after_base_vault) - I80F48::from(before_base_vault)); let (base_active, base_loan_origination_fee) = - base_bank.change_with_fee(base_position, base_change)?; + base_bank.change_with_fee(base_position, base_needed_change)?; + let base_native_after = base_position.native(&base_bank); let (quote_position, quote_raw_index) = account.token_position_mut(quote_bank.token_index)?; - let quote_change = I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault); + let quote_native_before = quote_position.native("e_bank); + let quote_needed_change = + cm!(I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault)); let (quote_active, quote_loan_origination_fee) = - quote_bank.change_with_fee(quote_position, quote_change)?; + quote_bank.change_with_fee(quote_position, quote_needed_change)?; + let quote_native_after = quote_position.native("e_bank); - Ok(( - VaultDifferenceResult { - base_raw_index, - base_active, - quote_raw_index, - quote_active, - }, + Ok(VaultDifferenceResult { + base_raw_index, + base_index: base_bank.token_index, + base_active, + quote_raw_index, + quote_index: quote_bank.token_index, + quote_active, base_loan_origination_fee, quote_loan_origination_fee, - )) + base_native_change: cm!(base_native_after - base_native_before), + quote_native_change: cm!(quote_native_after - quote_native_before), + }) } fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> { diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 8b4d92d7a..2b4530fe0 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -9,8 +9,8 @@ use crate::error::*; use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; -use super::{apply_vault_difference, OpenOrdersReserved, OpenOrdersSlim}; -use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; +use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim}; +use crate::logs::LoanOriginationFeeInstruction; #[derive(Accounts)] pub struct Serum3SettleFunds<'info> { @@ -150,36 +150,21 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - let (vault_difference_result, base_loan_origination_fee, quote_loan_origination_fee) = - apply_vault_difference( - &mut account.borrow_mut(), - &mut base_bank, - after_base_vault, - before_base_vault, - &mut quote_bank, - after_quote_vault, - before_quote_vault, - )?; - vault_difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); - - if base_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.base_token_index, - loan_origination_fee: base_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3SettleFunds - }); - } - if quote_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: ctx.accounts.group.key(), - mango_account: ctx.accounts.account.key(), - token_index: serum_market.quote_token_index, - loan_origination_fee: quote_loan_origination_fee.to_bits(), - instruction: LoanOriginationFeeInstruction::Serum3SettleFunds - }); - } + let difference_result = apply_vault_difference( + &mut account.borrow_mut(), + &mut base_bank, + after_base_vault, + before_base_vault, + &mut quote_bank, + after_quote_vault, + before_quote_vault, + )?; + difference_result.log_loan_origination_fees( + &ctx.accounts.group.key(), + &ctx.accounts.account.key(), + LoanOriginationFeeInstruction::Serum3SettleFunds, + ); + difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); } Ok(()) @@ -198,11 +183,11 @@ pub fn charge_maybe_fees( let maybe_actualized_coin_loan = I80F48::from_num::( serum3_account .previous_native_coin_reserved - .saturating_sub(after_oo.native_coin_reserved()), + .saturating_sub(after_oo.native_base_reserved()), ); if maybe_actualized_coin_loan > 0 { - serum3_account.previous_native_coin_reserved = after_oo.native_coin_reserved(); + serum3_account.previous_native_coin_reserved = after_oo.native_base_reserved(); // loan origination fees let coin_token_account = account.token_position_mut(coin_bank.token_index)?.0; @@ -223,11 +208,11 @@ pub fn charge_maybe_fees( let maybe_actualized_pc_loan = I80F48::from_num::( serum3_account .previous_native_pc_reserved - .saturating_sub(after_oo.native_pc_reserved()), + .saturating_sub(after_oo.native_quote_reserved()), ); if maybe_actualized_pc_loan > 0 { - serum3_account.previous_native_pc_reserved = after_oo.native_pc_reserved(); + serum3_account.previous_native_pc_reserved = after_oo.native_quote_reserved(); // loan origination fees let pc_token_account = account.token_position_mut(pc_bank.token_index)?.0; diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 8afa24c6c..e775ef0ec 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use crate::accounts_zerocopy::*; use crate::error::*; use crate::serum3_cpi; -use crate::state::{Bank, PerpMarket, PerpMarketIndex, TokenIndex}; +use crate::state::{Bank, PerpMarket, PerpMarketIndex, Serum3MarketIndex, TokenIndex}; use crate::util::checked_math as cm; use super::MangoAccountRef; @@ -427,6 +427,7 @@ pub struct Serum3Info { reserved: I80F48, base_index: usize, quote_index: usize, + market_index: Serum3MarketIndex, } impl Serum3Info { @@ -526,16 +527,70 @@ impl HealthCache { health } + fn token_entry_index(&mut self, token_index: TokenIndex) -> Result { + self.token_infos + .iter() + .position(|t| t.token_index == token_index) + .ok_or_else(|| error_msg!("token index {} not found", token_index)) + } + pub fn adjust_token_balance(&mut self, token_index: TokenIndex, change: I80F48) -> Result<()> { - let mut entry = self - .token_infos - .iter_mut() - .find(|t| t.token_index == token_index) - .ok_or_else(|| error_msg!("token index {} not found", token_index))?; + let entry_index = self.token_entry_index(token_index)?; + let mut entry = &mut self.token_infos[entry_index]; entry.balance = cm!(entry.balance + change * entry.oracle_price); Ok(()) } + pub fn adjust_serum3_reserved( + &mut self, + market_index: Serum3MarketIndex, + base_token_index: TokenIndex, + reserved_base_change: I80F48, + free_base_change: I80F48, + quote_token_index: TokenIndex, + reserved_quote_change: I80F48, + free_quote_change: I80F48, + ) -> Result<()> { + let base_entry_index = self.token_entry_index(base_token_index)?; + let quote_entry_index = self.token_entry_index(quote_token_index)?; + + // Compute the total reserved amount change in health reference units + let mut reserved_amount; + { + let base_entry = &mut self.token_infos[base_entry_index]; + reserved_amount = cm!(reserved_base_change * base_entry.oracle_price); + } + { + let quote_entry = &mut self.token_infos[quote_entry_index]; + reserved_amount = + cm!(reserved_amount + reserved_quote_change * quote_entry.oracle_price); + } + + // Apply it to the tokens + { + let base_entry = &mut self.token_infos[base_entry_index]; + base_entry.serum3_max_reserved = cm!(base_entry.serum3_max_reserved + reserved_amount); + base_entry.balance = + cm!(base_entry.balance + free_base_change * base_entry.oracle_price); + } + { + let quote_entry = &mut self.token_infos[quote_entry_index]; + quote_entry.serum3_max_reserved = + cm!(quote_entry.serum3_max_reserved + reserved_amount); + quote_entry.balance = + cm!(quote_entry.balance + free_quote_change * quote_entry.oracle_price); + } + + // Apply it to the serum3 info + let market_entry = self + .serum3_infos + .iter_mut() + .find(|m| m.market_index == market_index) + .ok_or_else(|| error_msg!("serum3 market {} not found", market_index))?; + market_entry.reserved = cm!(market_entry.reserved + reserved_amount); + Ok(()) + } + pub fn has_liquidatable_assets(&self) -> bool { let spot_liquidatable = self .token_infos @@ -832,6 +887,7 @@ pub fn new_health_cache( reserved: reserved_balance, base_index, quote_index, + market_index: serum_account.market_index, }); } diff --git a/programs/mango-v4/tests/program_test/mod.rs b/programs/mango-v4/tests/program_test/mod.rs index 726a8e7d8..bbb71dc47 100644 --- a/programs/mango-v4/tests/program_test/mod.rs +++ b/programs/mango-v4/tests/program_test/mod.rs @@ -112,7 +112,7 @@ impl TestContextBuilder { let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry)); // intentionally set to as tight as possible, to catch potential problems early - test.set_compute_max_units(87000); + test.set_compute_max_units(75000); Self { test, diff --git a/programs/mango-v4/tests/test_liq_tokens.rs b/programs/mango-v4/tests/test_liq_tokens.rs index d89a2b59c..84b9e69e3 100644 --- a/programs/mango-v4/tests/test_liq_tokens.rs +++ b/programs/mango-v4/tests/test_liq_tokens.rs @@ -11,7 +11,9 @@ mod program_test; #[tokio::test] async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> { - let context = TestContext::new().await; + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k + let context = test_builder.start_default().await; let solana = &context.solana.clone(); let admin = &Keypair::new(); diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index b7d9b63f9..25d92968b 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -10,7 +10,9 @@ mod program_test; #[tokio::test] async fn test_serum() -> Result<(), TransportError> { - let context = TestContext::new().await; + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k + let context = test_builder.start_default().await; let solana = &context.solana.clone(); let admin = &Keypair::new(); From c8159746e7a98c9eae63579cdd00cab3902a50dc Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 24 Aug 2022 13:33:11 +0200 Subject: [PATCH 22/50] Tests: Remove unnecessary argument --- programs/mango-v4/tests/program_test/mango_client.rs | 1 - programs/mango-v4/tests/test_basic.rs | 12 +++--------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index eb69e5a55..aa9ab48ee 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -2501,7 +2501,6 @@ impl ClientInstruction for TokenUpdateIndexAndRateInstruction { pub struct ComputeAccountDataInstruction { pub account: Pubkey, - pub health_type: HealthType, } #[async_trait::async_trait(?Send)] impl ClientInstruction for ComputeAccountDataInstruction { diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 6a0673d91..ed3ac06a0 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -111,15 +111,9 @@ async fn test_basic() -> Result<(), TransportError> { // // TEST: Compute the account health // - send_tx( - solana, - ComputeAccountDataInstruction { - account, - health_type: HealthType::Init, - }, - ) - .await - .unwrap(); + send_tx(solana, ComputeAccountDataInstruction { account }) + .await + .unwrap(); // // TEST: Withdraw funds From cb0a9e5d2b2fdc5dcac5e96e7886db48acbce071 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 24 Aug 2022 14:08:08 +0200 Subject: [PATCH 23/50] Tests: allow checking of logged "post_health" --- .../tests/program_test/mango_client.rs | 22 +++++++++++++++++++ programs/mango-v4/tests/program_test/mod.rs | 2 ++ .../mango-v4/tests/program_test/solana.rs | 15 +++++++++++++ programs/mango-v4/tests/test_basic.rs | 7 ++++++ programs/mango-v4/tests/test_serum.rs | 2 ++ 5 files changed, 48 insertions(+) diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index aa9ab48ee..379e9f1b7 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -319,6 +319,28 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank: native.to_num::() } +// Verifies that the "post_health: ..." log emitted by the previous instruction +// matches the init health of the account. +pub async fn check_prev_instruction_post_health(solana: &SolanaCookie, account: Pubkey) { + let logs = solana.program_log(); + let post_health_str = logs + .iter() + .find_map(|line| line.strip_prefix("post_health: ")) + .unwrap(); + let post_health = post_health_str.parse::().unwrap(); + + solana.advance_by_slots(1).await; // ugly, just to avoid sending the same tx next + send_tx(solana, ComputeAccountDataInstruction { account }) + .await + .unwrap(); + + let health_data = solana + .program_log_events::() + .pop() + .unwrap(); + assert_eq!(health_data.init_health.to_num::(), post_health); +} + // // a struct for each instruction along with its // ClientInstruction impl diff --git a/programs/mango-v4/tests/program_test/mod.rs b/programs/mango-v4/tests/program_test/mod.rs index bbb71dc47..50f3a2cf1 100644 --- a/programs/mango-v4/tests/program_test/mod.rs +++ b/programs/mango-v4/tests/program_test/mod.rs @@ -68,6 +68,8 @@ impl Log for LoggerWrapper { let msg = record.args().to_string(); if let Some(data) = msg.strip_prefix("Program log: ") { self.program_log.write().unwrap().push(data.into()); + } else if let Some(data) = msg.strip_prefix("Program data: ") { + self.program_log.write().unwrap().push(data.into()); } } self.inner.log(record); diff --git a/programs/mango-v4/tests/program_test/solana.rs b/programs/mango-v4/tests/program_test/solana.rs index 293909683..170cf102c 100644 --- a/programs/mango-v4/tests/program_test/solana.rs +++ b/programs/mango-v4/tests/program_test/solana.rs @@ -223,4 +223,19 @@ impl SolanaCookie { pub fn program_log(&self) -> Vec { self.program_log.read().unwrap().clone() } + + pub fn program_log_events( + &self, + ) -> Vec { + self.program_log() + .iter() + .filter_map(|data| { + let bytes = base64::decode(data).ok()?; + if bytes[0..8] != T::discriminator() { + return None; + } + T::try_from_slice(&bytes[8..]).ok() + }) + .collect() + } } diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index ed3ac06a0..7cefa5771 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -114,6 +114,11 @@ async fn test_basic() -> Result<(), TransportError> { send_tx(solana, ComputeAccountDataInstruction { account }) .await .unwrap(); + let health_data = solana + .program_log_events::() + .pop() + .unwrap(); + assert_eq!(health_data.init_health.to_num::(), 60); // // TEST: Withdraw funds @@ -137,6 +142,8 @@ async fn test_basic() -> Result<(), TransportError> { .await .unwrap(); + check_prev_instruction_post_health(&solana, account).await; + assert_eq!(solana.token_account_balance(vault).await, withdraw_amount); assert_eq!( solana.token_account_balance(payer_mint0_account).await, diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index 25d92968b..3dd39494d 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -160,6 +160,8 @@ async fn test_serum() -> Result<(), TransportError> { .await .unwrap(); + check_prev_instruction_post_health(&solana, account).await; + let native0 = account_position(solana, account, base_token.bank).await; let native1 = account_position(solana, account, quote_token.bank).await; assert_eq!(native0, 1000); From 38b349a4019da92f3e474be079dbf1459924ee3a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 24 Aug 2022 15:22:55 +0200 Subject: [PATCH 24/50] Rename PerpOpenOrders -> PerpOpenOrder, since it's a single one --- programs/mango-v4/src/state/mango_account.rs | 24 +++++++++---------- .../src/state/mango_account_components.rs | 12 +++++----- ts/client/src/mango_v4.ts | 16 +++++++++---- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 9fcde508c..b5a796763 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -17,7 +17,7 @@ use super::FillEvent; use super::LeafNode; use super::PerpMarket; use super::PerpMarketIndex; -use super::PerpOpenOrders; +use super::PerpOpenOrder; use super::Serum3MarketIndex; use super::Side; use super::TokenIndex; @@ -99,7 +99,7 @@ pub struct MangoAccount { pub padding6: u32, pub perps: Vec, pub padding7: u32, - pub perp_open_orders: Vec, + pub perp_open_orders: Vec, } impl Default for MangoAccount { @@ -127,7 +127,7 @@ impl Default for MangoAccount { padding6: Default::default(), perps: vec![PerpPosition::default(); 2], padding7: Default::default(), - perp_open_orders: vec![PerpOpenOrders::default(); 2], + perp_open_orders: vec![PerpOpenOrder::default(); 2], } } } @@ -178,7 +178,7 @@ impl MangoAccount { perp_oo_count: u8, ) -> usize { Self::dynamic_perp_oo_vec_offset(token_count, serum3_count, perp_count) - + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(perp_oo_count)) + + (BORSH_VEC_SIZE_BYTES + size_of::() * usize::from(perp_oo_count)) } } @@ -201,9 +201,7 @@ fn test_serialization_match() { account.serum3.resize(8, Serum3Orders::default()); account.perps.resize(8, PerpPosition::default()); account.perps[0].market_index = 9; - account - .perp_open_orders - .resize(8, PerpOpenOrders::default()); + account.perp_open_orders.resize(8, PerpOpenOrder::default()); let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap(); assert_eq!( @@ -403,7 +401,7 @@ impl MangoAccountDynamicHeader { self.serum3_count, self.perp_count, ) + BORSH_VEC_SIZE_BYTES - + raw_index * size_of::() + + raw_index * size_of::() } pub fn token_count(&self) -> usize { @@ -546,11 +544,11 @@ impl< .filter(|p| p.is_active()) } - pub fn perp_orders_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrders { + pub fn perp_orders_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrder { get_helper(self.dynamic(), self.header().perp_oo_offset(raw_index)) } - pub fn all_perp_orders(&self) -> impl Iterator { + pub fn all_perp_orders(&self) -> impl Iterator { (0..self.header().perp_oo_count()).map(|i| self.perp_orders_by_raw_index(i)) } @@ -739,7 +737,7 @@ impl< get_helper_mut(self.dynamic_mut(), offset) } - pub fn perp_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrders { + pub fn perp_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrder { let offset = self.header().perp_oo_offset(raw_index); get_helper_mut(self.dynamic_mut(), offset) } @@ -963,12 +961,12 @@ impl< sol_memmove( &mut dynamic[new_header.perp_oo_offset(0)], &mut dynamic[old_header.perp_oo_offset(0)], - size_of::() * old_header.perp_oo_count(), + size_of::() * old_header.perp_oo_count(), ); } for i in old_header.perp_oo_count..new_perp_oo_count { *get_helper_mut(dynamic, new_header.perp_oo_offset(i.into())) = - PerpOpenOrders::default(); + PerpOpenOrder::default(); } } diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 5a5cf1cbe..3bf2e606b 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -321,7 +321,7 @@ impl PerpPosition { #[zero_copy] #[derive(AnchorSerialize, AnchorDeserialize, Debug)] -pub struct PerpOpenOrders { +pub struct PerpOpenOrder { pub order_side: Side, // TODO: storing enums isn't POD pub padding1: [u8; 1], pub order_market: PerpMarketIndex, @@ -331,7 +331,7 @@ pub struct PerpOpenOrders { pub reserved: [u8; 64], } -impl Default for PerpOpenOrders { +impl Default for PerpOpenOrder { fn default() -> Self { Self { order_side: Side::Bid, @@ -345,11 +345,11 @@ impl Default for PerpOpenOrders { } } -unsafe impl bytemuck::Pod for PerpOpenOrders {} -unsafe impl bytemuck::Zeroable for PerpOpenOrders {} +unsafe impl bytemuck::Pod for PerpOpenOrder {} +unsafe impl bytemuck::Zeroable for PerpOpenOrder {} -const_assert_eq!(size_of::(), 1 + 1 + 2 + 4 + 8 + 16 + 64); -const_assert_eq!(size_of::() % 8, 0); +const_assert_eq!(size_of::(), 1 + 1 + 2 + 4 + 8 + 16 + 64); +const_assert_eq!(size_of::() % 8, 0); #[macro_export] macro_rules! account_seeds { diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index f6a907150..19a3a1682 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -3151,7 +3151,7 @@ export type MangoV4 = { "name": "perpOpenOrders", "type": { "vec": { - "defined": "PerpOpenOrders" + "defined": "PerpOpenOrder" } } } @@ -3845,6 +3845,10 @@ export type MangoV4 = { { "name": "quoteIndex", "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" } ] } @@ -4133,7 +4137,7 @@ export type MangoV4 = { } }, { - "name": "PerpOpenOrders", + "name": "PerpOpenOrder", "type": { "kind": "struct", "fields": [ @@ -8401,7 +8405,7 @@ export const IDL: MangoV4 = { "name": "perpOpenOrders", "type": { "vec": { - "defined": "PerpOpenOrders" + "defined": "PerpOpenOrder" } } } @@ -9095,6 +9099,10 @@ export const IDL: MangoV4 = { { "name": "quoteIndex", "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" } ] } @@ -9383,7 +9391,7 @@ export const IDL: MangoV4 = { } }, { - "name": "PerpOpenOrders", + "name": "PerpOpenOrder", "type": { "kind": "struct", "fields": [ From bba27ed6f0bc6ee2cfbb90ba74ad03a016f1bf9e Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 24 Aug 2022 16:07:22 +0200 Subject: [PATCH 25/50] PlacePerpOrder: pre-health computation --- programs/mango-v4/src/error.rs | 2 + .../mango-v4/src/instructions/flash_loan.rs | 2 +- .../src/instructions/health_region.rs | 2 +- .../src/instructions/perp_place_order.rs | 143 ++++++++----- .../src/instructions/serum3_place_order.rs | 2 +- .../src/instructions/token_withdraw.rs | 2 +- programs/mango-v4/src/state/health.rs | 191 ++++++++++-------- programs/mango-v4/tests/test_health_region.rs | 2 +- programs/mango-v4/tests/test_perp.rs | 7 + 9 files changed, 211 insertions(+), 142 deletions(-) diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index 7231a218e..84c1d55d6 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -19,6 +19,8 @@ pub enum MangoError { InvalidFlashLoanTargetCpiProgram, #[msg("health must be positive")] HealthMustBePositive, + #[msg("health must be positive or increase")] + HealthMustBePositiveOrIncrease, #[msg("health must be negative")] HealthMustBeNegative, #[msg("the account is bankrupt")] diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index b56b43afa..b42191d18 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -401,7 +401,7 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( msg!("post_health {:?}", post_health); require!( post_health >= 0 || post_health > pre_health, - MangoError::HealthMustBePositive + MangoError::HealthMustBePositiveOrIncrease ); account .fixed diff --git a/programs/mango-v4/src/instructions/health_region.rs b/programs/mango-v4/src/instructions/health_region.rs index 25dcd6ad5..6bc12fc91 100644 --- a/programs/mango-v4/src/instructions/health_region.rs +++ b/programs/mango-v4/src/instructions/health_region.rs @@ -115,7 +115,7 @@ pub fn health_region_end<'key, 'accounts, 'remaining, 'info>( msg!("post_health {:?}", post_health); require!( post_health >= 0 || post_health > account.fixed.health_region_begin_init_health, - MangoError::HealthMustBePositive + MangoError::HealthMustBePositiveOrIncrease ); account .fixed diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index 49bb5b69d..d9b7491be 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -4,7 +4,7 @@ use crate::accounts_zerocopy::*; use crate::error::*; use crate::state::MangoAccount; use crate::state::{ - compute_health, new_fixed_order_account_retriever, oracle_price, AccountLoaderDynamic, Book, + new_fixed_order_account_retriever, new_health_cache, oracle_price, AccountLoaderDynamic, Book, BookSide, EventQueue, Group, HealthType, OrderType, PerpMarket, Side, }; @@ -83,62 +83,97 @@ pub fn perp_place_order( let account_pk = ctx.accounts.account.key(); - { - let mut perp_market = ctx.accounts.perp_market.load_mut()?; - let bids = ctx.accounts.bids.load_mut()?; - let asks = ctx.accounts.asks.load_mut()?; - let mut book = Book::new(bids, asks); + let perp_market_index = { + let perp_market = ctx.accounts.perp_market.load()?; + perp_market.perp_market_index + }; + let (_, perp_position_raw_index) = account.ensure_perp_position(perp_market_index)?; - let mut event_queue = ctx.accounts.event_queue.load_mut()?; - - let oracle_price = oracle_price( - &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, - perp_market.oracle_config.conf_filter, - perp_market.base_token_decimals, - )?; - - let now_ts = Clock::get()?.unix_timestamp as u64; - let time_in_force = if expiry_timestamp != 0 { - // If expiry is far in the future, clamp to 255 seconds - let tif = expiry_timestamp.saturating_sub(now_ts).min(255); - if tif == 0 { - // If expiry is in the past, ignore the order - msg!("Order is already expired"); - return Ok(()); - } - tif as u8 - } else { - // Never expire - 0 - }; - - // TODO reduce_only based on event queue - - book.new_order( - side, - &mut perp_market, - &mut event_queue, - oracle_price, - &mut account.borrow_mut(), - &account_pk, - price_lots, - max_base_lots, - max_quote_lots, - order_type, - time_in_force, - client_order_id, - now_ts, - limit, - )?; - } - - if !account.fixed.is_in_health_region() { + // + // Pre-health computation, _after_ perp position is created + // + let pre_health_opt = if !account.fixed.is_in_health_region() { let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; - msg!("health: {}", health); - require!(health >= 0, MangoError::HealthMustBePositive); - account.fixed.maybe_recover_from_being_liquidated(health); + let health_cache = + new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; + let pre_health = health_cache.health(HealthType::Init); + msg!("pre_health: {}", pre_health); + account + .fixed + .maybe_recover_from_being_liquidated(pre_health); + require!( + !account.fixed.being_liquidated(), + MangoError::BeingLiquidated + ); + Some((health_cache, pre_health)) + } else { + None + }; + + let mut perp_market = ctx.accounts.perp_market.load_mut()?; + let bids = ctx.accounts.bids.load_mut()?; + let asks = ctx.accounts.asks.load_mut()?; + let mut book = Book::new(bids, asks); + + let mut event_queue = ctx.accounts.event_queue.load_mut()?; + + let oracle_price = oracle_price( + &AccountInfoRef::borrow(ctx.accounts.oracle.as_ref())?, + perp_market.oracle_config.conf_filter, + perp_market.base_token_decimals, + )?; + + let now_ts = Clock::get()?.unix_timestamp as u64; + let time_in_force = if expiry_timestamp != 0 { + // If expiry is far in the future, clamp to 255 seconds + let tif = expiry_timestamp.saturating_sub(now_ts).min(255); + if tif == 0 { + // If expiry is in the past, ignore the order + msg!("Order is already expired"); + return Ok(()); + } + tif as u8 + } else { + // Never expire + 0 + }; + + // TODO reduce_only based on event queue + + book.new_order( + side, + &mut perp_market, + &mut event_queue, + oracle_price, + &mut account.borrow_mut(), + &account_pk, + price_lots, + max_base_lots, + max_quote_lots, + order_type, + time_in_force, + client_order_id, + now_ts, + limit, + )?; + + // + // Health check + // + if let Some((mut health_cache, pre_health)) = pre_health_opt { + let perp_position = account.perp_position_by_raw_index(perp_position_raw_index); + health_cache.recompute_perp_info(perp_position, &perp_market)?; + + let post_health = health_cache.health(HealthType::Init); + msg!("post_health: {}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositiveOrIncrease + ); + account + .fixed + .maybe_recover_from_being_liquidated(post_health); } Ok(()) diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 7553209d8..844d298d6 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -337,7 +337,7 @@ pub fn serum3_place_order( msg!("post_health: {}", post_health); require!( post_health >= 0 || post_health > pre_health, - MangoError::HealthMustBePositive + MangoError::HealthMustBePositiveOrIncrease ); account .fixed diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 45ca975ba..7723d4d87 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -156,7 +156,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo msg!("post_health: {}", post_health); require!( post_health >= 0 || post_health > pre_health, - MangoError::HealthMustBePositive + MangoError::HealthMustBePositiveOrIncrease ); account .fixed diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index e775ef0ec..0c170c527 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -11,7 +11,9 @@ use std::collections::HashMap; use crate::accounts_zerocopy::*; use crate::error::*; use crate::serum3_cpi; -use crate::state::{Bank, PerpMarket, PerpMarketIndex, Serum3MarketIndex, TokenIndex}; +use crate::state::{ + Bank, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex, TokenIndex, +}; use crate::util::checked_math as cm; use super::MangoAccountRef; @@ -471,6 +473,7 @@ impl Serum3Info { #[derive(Clone, AnchorDeserialize, AnchorSerialize)] pub struct PerpInfo { + perp_market_index: PerpMarketIndex, maint_asset_weight: I80F48, init_asset_weight: I80F48, maint_liab_weight: I80F48, @@ -482,6 +485,93 @@ pub struct PerpInfo { } impl PerpInfo { + fn new( + perp_position: &PerpPosition, + perp_market: &PerpMarket, + token_infos: &[TokenInfo], + ) -> Result { + // find the TokenInfos for the market's base and quote tokens + let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?; + // TODO: base_index could be unset + let base_info = &token_infos[base_index]; + + let base_lot_size = I80F48::from(perp_market.base_lot_size); + + let base_lots = cm!(perp_position.base_position_lots + perp_position.taker_base_lots); + let taker_quote = I80F48::from(cm!( + perp_position.taker_quote_lots * perp_market.quote_lot_size + )); + let quote_current = cm!(perp_position.quote_position_native + taker_quote); + + // Two scenarios: + // 1. The price goes low and all bids execute, converting to base. + // That means the perp position is increased by `bids` and the quote position + // is decreased by `bids * base_lot_size * price`. + // The health for this case is: + // (weighted(base_lots + bids) - bids) * base_lot_size * price + quote + // 2. The price goes high and all asks execute, converting to quote. + // The health for this case is: + // (weighted(base_lots - asks) + asks) * base_lot_size * price + quote + // + // Comparing these makes it clear we need to pick the worse subfactor + // weighted(base_lots + bids) - bids =: scenario1 + // or + // weighted(base_lots - asks) + asks =: scenario2 + // + // Additionally, we want this scenario choice to be the same no matter whether we're + // computing init or maint health. This can be guaranteed by requiring the weights + // to satisfy the property (P): + // + // (1 - init_asset_weight) / (init_liab_weight - 1) + // == (1 - maint_asset_weight) / (maint_liab_weight - 1) + // + // Derivation: + // Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids. + // Now + // scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and + // scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots + // So with expanding weigthed(a) = weight_factor_for_a * a, the question + // scenario1 < scenario2 + // becomes: + // (weight_factor_for_bids_net_lots - 1) * bids_net_lots + // < (weight_factor_for_asks_net_lots - 1) * asks_net_lots + // Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows. + // + // We satisfy (P) by requiring + // asset_weight = 1 - x and liab_weight = 1 + x + // + // And with that assumption the scenario choice condition further simplifies to: + // scenario1 < scenario2 + // iff abs(bids_net_lots) > abs(asks_net_lots) + let bids_net_lots = cm!(base_lots + perp_position.bids_base_lots); + let asks_net_lots = cm!(base_lots - perp_position.asks_base_lots); + + let lots_to_quote = base_lot_size * base_info.oracle_price; + let base; + let quote; + if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) { + let bids_net_lots = I80F48::from(bids_net_lots); + let bids_base_lots = I80F48::from(perp_position.bids_base_lots); + base = cm!(bids_net_lots * lots_to_quote); + quote = cm!(quote_current - bids_base_lots * lots_to_quote); + } else { + let asks_net_lots = I80F48::from(asks_net_lots); + let asks_base_lots = I80F48::from(perp_position.asks_base_lots); + base = cm!(asks_net_lots * lots_to_quote); + quote = cm!(quote_current + asks_base_lots * lots_to_quote); + }; + + Ok(Self { + perp_market_index: perp_market.perp_market_index, + init_asset_weight: perp_market.init_asset_weight, + init_liab_weight: perp_market.init_liab_weight, + maint_asset_weight: perp_market.maint_asset_weight, + maint_liab_weight: perp_market.maint_liab_weight, + base, + quote, + }) + } + /// Total health contribution from perp balances /// /// Due to isolation of perp markets, users may never borrow against perp @@ -591,6 +681,20 @@ impl HealthCache { Ok(()) } + pub fn recompute_perp_info( + &mut self, + perp_position: &PerpPosition, + perp_market: &PerpMarket, + ) -> Result<()> { + let perp_entry = self + .perp_infos + .iter_mut() + .find(|m| m.perp_market_index == perp_market.perp_market_index) + .ok_or_else(|| error_msg!("perp market {} not found", perp_market.perp_market_index))?; + *perp_entry = PerpInfo::new(perp_position, perp_market, &self.token_infos)?; + Ok(()) + } + pub fn has_liquidatable_assets(&self) -> bool { let spot_liquidatable = self .token_infos @@ -894,89 +998,10 @@ pub fn new_health_cache( // TODO: also account for perp funding in health // health contribution from perp accounts let mut perp_infos = Vec::with_capacity(account.active_perp_positions().count()); - for (i, perp_account) in account.active_perp_positions().enumerate() { + for (i, perp_position) in account.active_perp_positions().enumerate() { let perp_market = - retriever.perp_market(&account.fixed.group, i, perp_account.market_index)?; - - // find the TokenInfos for the market's base and quote tokens - let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?; - // TODO: base_index could be unset - let base_info = &token_infos[base_index]; - - let base_lot_size = I80F48::from(perp_market.base_lot_size); - - let base_lots = cm!(perp_account.base_position_lots + perp_account.taker_base_lots); - let taker_quote = I80F48::from(cm!( - perp_account.taker_quote_lots * perp_market.quote_lot_size - )); - let quote_current = cm!(perp_account.quote_position_native + taker_quote); - - // Two scenarios: - // 1. The price goes low and all bids execute, converting to base. - // That means the perp position is increased by `bids` and the quote position - // is decreased by `bids * base_lot_size * price`. - // The health for this case is: - // (weighted(base_lots + bids) - bids) * base_lot_size * price + quote - // 2. The price goes high and all asks execute, converting to quote. - // The health for this case is: - // (weighted(base_lots - asks) + asks) * base_lot_size * price + quote - // - // Comparing these makes it clear we need to pick the worse subfactor - // weighted(base_lots + bids) - bids =: scenario1 - // or - // weighted(base_lots - asks) + asks =: scenario2 - // - // Additionally, we want this scenario choice to be the same no matter whether we're - // computing init or maint health. This can be guaranteed by requiring the weights - // to satisfy the property (P): - // - // (1 - init_asset_weight) / (init_liab_weight - 1) - // == (1 - maint_asset_weight) / (maint_liab_weight - 1) - // - // Derivation: - // Set asks_net_lots := base_lots - asks, bids_net_lots := base_lots + bids. - // Now - // scenario1 = weighted(bids_net_lots) - bids_net_lots + base_lots and - // scenario2 = weighted(asks_net_lots) - asks_net_lots + base_lots - // So with expanding weigthed(a) = weight_factor_for_a * a, the question - // scenario1 < scenario2 - // becomes: - // (weight_factor_for_bids_net_lots - 1) * bids_net_lots - // < (weight_factor_for_asks_net_lots - 1) * asks_net_lots - // Since asks_net_lots < 0 and bids_net_lots > 0 is the only interesting case, (P) follows. - // - // We satisfy (P) by requiring - // asset_weight = 1 - x and liab_weight = 1 + x - // - // And with that assumption the scenario choice condition further simplifies to: - // scenario1 < scenario2 - // iff abs(bids_net_lots) > abs(asks_net_lots) - let bids_net_lots = cm!(base_lots + perp_account.bids_base_lots); - let asks_net_lots = cm!(base_lots - perp_account.asks_base_lots); - - let lots_to_quote = base_lot_size * base_info.oracle_price; - let base; - let quote; - if cm!(bids_net_lots.abs()) > cm!(asks_net_lots.abs()) { - let bids_net_lots = I80F48::from(bids_net_lots); - let bids_base_lots = I80F48::from(perp_account.bids_base_lots); - base = cm!(bids_net_lots * lots_to_quote); - quote = cm!(quote_current - bids_base_lots * lots_to_quote); - } else { - let asks_net_lots = I80F48::from(asks_net_lots); - let asks_base_lots = I80F48::from(perp_account.asks_base_lots); - base = cm!(asks_net_lots * lots_to_quote); - quote = cm!(quote_current + asks_base_lots * lots_to_quote); - }; - - perp_infos.push(PerpInfo { - init_asset_weight: perp_market.init_asset_weight, - init_liab_weight: perp_market.init_liab_weight, - maint_asset_weight: perp_market.maint_asset_weight, - maint_liab_weight: perp_market.maint_liab_weight, - base, - quote, - }); + retriever.perp_market(&account.fixed.group, i, perp_position.market_index)?; + perp_infos.push(PerpInfo::new(perp_position, perp_market, &token_infos)?); } Ok(HealthCache { diff --git a/programs/mango-v4/tests/test_health_region.rs b/programs/mango-v4/tests/test_health_region.rs index 693adec22..4e85fb799 100644 --- a/programs/mango-v4/tests/test_health_region.rs +++ b/programs/mango-v4/tests/test_health_region.rs @@ -141,7 +141,7 @@ async fn test_health_wrap() -> Result<(), TransportError> { // errors due to health assert!(logs .iter() - .any(|line| line.contains("Error Code: HealthMustBePositive"))); + .any(|line| line.contains("Error Code: HealthMustBePositiveOrIncrease"))); } // diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index 64a8c5aef..8f481dcd7 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -204,6 +204,7 @@ async fn test_perp() -> Result<(), TransportError> { ) .await .unwrap(); + check_prev_instruction_post_health(&solana, account_0).await; let order_id_to_cancel = solana .get_account::(account_0) @@ -250,6 +251,7 @@ async fn test_perp() -> Result<(), TransportError> { ) .await .unwrap(); + check_prev_instruction_post_health(&solana, account_0).await; send_tx( solana, @@ -291,6 +293,7 @@ async fn test_perp() -> Result<(), TransportError> { ) .await .unwrap(); + check_prev_instruction_post_health(&solana, account_0).await; send_tx( solana, PerpPlaceOrderInstruction { @@ -311,6 +314,7 @@ async fn test_perp() -> Result<(), TransportError> { ) .await .unwrap(); + check_prev_instruction_post_health(&solana, account_0).await; send_tx( solana, PerpPlaceOrderInstruction { @@ -331,6 +335,7 @@ async fn test_perp() -> Result<(), TransportError> { ) .await .unwrap(); + check_prev_instruction_post_health(&solana, account_0).await; send_tx( solana, @@ -371,6 +376,7 @@ async fn test_perp() -> Result<(), TransportError> { ) .await .unwrap(); + check_prev_instruction_post_health(&solana, account_0).await; send_tx( solana, @@ -392,6 +398,7 @@ async fn test_perp() -> Result<(), TransportError> { ) .await .unwrap(); + check_prev_instruction_post_health(&solana, account_1).await; send_tx( solana, From dfa8166aee6e65f46a086f550d7bb0ee529346fa Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Wed, 24 Aug 2022 16:39:36 +0200 Subject: [PATCH 26/50] Health: Put common pre/post check logic into functions --- .../mango-v4/src/instructions/flash_loan.rs | 28 ++++------------ .../src/instructions/health_region.rs | 28 ++++------------ .../src/instructions/liq_token_bankruptcy.rs | 1 + .../src/instructions/liq_token_with_token.rs | 1 + .../src/instructions/perp_place_order.rs | 23 ++----------- .../src/instructions/serum3_place_order.rs | 21 ++---------- .../src/instructions/token_withdraw.rs | 20 ++---------- programs/mango-v4/src/state/health.rs | 26 ++++++++++++++- programs/mango-v4/src/state/mango_account.rs | 32 +++++++++++++++++++ 9 files changed, 79 insertions(+), 101 deletions(-) diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index b42191d18..d6a4a5181 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -4,8 +4,8 @@ use crate::group_seeds; use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog}; use crate::state::MangoAccount; use crate::state::{ - compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever, - AccountLoaderDynamic, AccountRetriever, Bank, Group, HealthType, TokenIndex, + new_fixed_order_account_retriever, new_health_cache, AccountLoaderDynamic, AccountRetriever, + Bank, Group, TokenIndex, }; use crate::util::checked_math as cm; use anchor_lang::prelude::*; @@ -316,15 +316,8 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( // Check health before balance adjustments let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; - let pre_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?; - msg!("pre_health {:?}", pre_health); - account - .fixed - .maybe_recover_from_being_liquidated(pre_health); - require!( - !account.fixed.being_liquidated(), - MangoError::BeingLiquidated - ); + let health_cache = new_health_cache(&account.borrow(), &retriever)?; + let pre_health = account.check_health_pre(&health_cache)?; // Prices for logging let mut prices = vec![]; @@ -396,16 +389,9 @@ pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>( }); // Check health after account position changes - let post_health = - compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?; - msg!("post_health {:?}", post_health); - require!( - post_health >= 0 || post_health > pre_health, - MangoError::HealthMustBePositiveOrIncrease - ); - account - .fixed - .maybe_recover_from_being_liquidated(post_health); + let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?; + let health_cache = new_health_cache(&account.borrow(), &retriever)?; + account.check_health_post(&health_cache, pre_health)?; // Deactivate inactive token accounts after health check for raw_token_index in deactivated_token_positions { diff --git a/programs/mango-v4/src/instructions/health_region.rs b/programs/mango-v4/src/instructions/health_region.rs index 6bc12fc91..05d10c67d 100644 --- a/programs/mango-v4/src/instructions/health_region.rs +++ b/programs/mango-v4/src/instructions/health_region.rs @@ -3,6 +3,7 @@ use crate::state::*; use anchor_lang::prelude::*; use anchor_lang::solana_program::sysvar::instructions as tx_instructions; use anchor_lang::Discriminator; +use fixed::types::I80F48; /// Sets up for a health region /// @@ -79,20 +80,10 @@ pub fn health_region_begin<'key, 'accounts, 'remaining, 'info>( .context("create account retriever")?; // Compute pre-health and store it on the account - let pre_health = compute_health(&account.borrow(), HealthType::Init, &account_retriever) - .context("compute health")?; - msg!("pre_health {:?}", pre_health); + let health_cache = new_health_cache(&account.borrow(), &account_retriever)?; + let pre_health = account.check_health_pre(&health_cache)?; account.fixed.health_region_begin_init_health = pre_health.ceil().checked_to_num().unwrap(); - account - .fixed - .maybe_recover_from_being_liquidated(pre_health); - - require!( - !account.fixed.being_liquidated(), - MangoError::BeingLiquidated - ); - Ok(()) } @@ -109,17 +100,10 @@ pub fn health_region_end<'key, 'accounts, 'remaining, 'info>( let group = account.fixed.group; let account_retriever = ScanningAccountRetriever::new(ctx.remaining_accounts, &group) .context("create account retriever")?; + let health_cache = new_health_cache(&account.borrow(), &account_retriever)?; - let post_health = compute_health(&account.borrow(), HealthType::Init, &account_retriever) - .context("compute health")?; - msg!("post_health {:?}", post_health); - require!( - post_health >= 0 || post_health > account.fixed.health_region_begin_init_health, - MangoError::HealthMustBePositiveOrIncrease - ); - account - .fixed - .maybe_recover_from_being_liquidated(post_health); + let pre_health = I80F48::from(account.fixed.health_region_begin_init_health); + account.check_health_post(&health_cache, pre_health)?; account.fixed.health_region_begin_init_health = 0; Ok(()) diff --git a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs index 3b64ee6e4..7021681f2 100644 --- a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs @@ -89,6 +89,7 @@ pub fn liq_token_bankruptcy( .is_owner_or_delegate(ctx.accounts.liqor_owner.key()), MangoError::SomeError ); + require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated); let mut account_retriever = ScanningAccountRetriever::new(health_ais, group_pk)?; diff --git a/programs/mango-v4/src/instructions/liq_token_with_token.rs b/programs/mango-v4/src/instructions/liq_token_with_token.rs index 433a1ea94..d33f850cb 100644 --- a/programs/mango-v4/src/instructions/liq_token_with_token.rs +++ b/programs/mango-v4/src/instructions/liq_token_with_token.rs @@ -50,6 +50,7 @@ pub fn liq_token_with_token( .is_owner_or_delegate(ctx.accounts.liqor_owner.key()), MangoError::SomeError ); + require!(!liqor.fixed.being_liquidated(), MangoError::BeingLiquidated); let mut liqee = ctx.accounts.liqee.load_mut()?; diff --git a/programs/mango-v4/src/instructions/perp_place_order.rs b/programs/mango-v4/src/instructions/perp_place_order.rs index d9b7491be..32abc1619 100644 --- a/programs/mango-v4/src/instructions/perp_place_order.rs +++ b/programs/mango-v4/src/instructions/perp_place_order.rs @@ -5,7 +5,7 @@ use crate::error::*; use crate::state::MangoAccount; use crate::state::{ new_fixed_order_account_retriever, new_health_cache, oracle_price, AccountLoaderDynamic, Book, - BookSide, EventQueue, Group, HealthType, OrderType, PerpMarket, Side, + BookSide, EventQueue, Group, OrderType, PerpMarket, Side, }; #[derive(Accounts)] @@ -97,15 +97,7 @@ pub fn perp_place_order( new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; let health_cache = new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; - let pre_health = health_cache.health(HealthType::Init); - msg!("pre_health: {}", pre_health); - account - .fixed - .maybe_recover_from_being_liquidated(pre_health); - require!( - !account.fixed.being_liquidated(), - MangoError::BeingLiquidated - ); + let pre_health = account.check_health_pre(&health_cache)?; Some((health_cache, pre_health)) } else { None @@ -164,16 +156,7 @@ pub fn perp_place_order( if let Some((mut health_cache, pre_health)) = pre_health_opt { let perp_position = account.perp_position_by_raw_index(perp_position_raw_index); health_cache.recompute_perp_info(perp_position, &perp_market)?; - - let post_health = health_cache.health(HealthType::Init); - msg!("post_health: {}", post_health); - require!( - post_health >= 0 || post_health > pre_health, - MangoError::HealthMustBePositiveOrIncrease - ); - account - .fixed - .maybe_recover_from_being_liquidated(post_health); + account.check_health_post(&health_cache, pre_health)?; } Ok(()) diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 844d298d6..c7c26e549 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -248,15 +248,7 @@ pub fn serum3_place_order( new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; let health_cache = new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; - let pre_health = health_cache.health(HealthType::Init); - msg!("pre_health: {}", pre_health); - account - .fixed - .maybe_recover_from_being_liquidated(pre_health); - require!( - !account.fixed.being_liquidated(), - MangoError::BeingLiquidated - ); + let pre_health = account.check_health_pre(&health_cache)?; Some((health_cache, pre_health)) } else { None @@ -332,16 +324,7 @@ pub fn serum3_place_order( if let Some((mut health_cache, pre_health)) = pre_health_opt { vault_difference.adjust_health_cache(&mut health_cache)?; oo_difference.adjust_health_cache(&mut health_cache, &serum_market)?; - - let post_health = health_cache.health(HealthType::Init); - msg!("post_health: {}", post_health); - require!( - post_health >= 0 || post_health > pre_health, - MangoError::HealthMustBePositiveOrIncrease - ); - account - .fixed - .maybe_recover_from_being_liquidated(post_health); + account.check_health_post(&health_cache, pre_health)?; } vault_difference.log_loan_origination_fees( diff --git a/programs/mango-v4/src/instructions/token_withdraw.rs b/programs/mango-v4/src/instructions/token_withdraw.rs index 7723d4d87..d5e946ddd 100644 --- a/programs/mango-v4/src/instructions/token_withdraw.rs +++ b/programs/mango-v4/src/instructions/token_withdraw.rs @@ -70,15 +70,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; let health_cache = new_health_cache(&account.borrow(), &retriever).context("pre-withdraw init health")?; - let pre_health = health_cache.health(HealthType::Init); - msg!("pre_health: {}", pre_health); - account - .fixed - .maybe_recover_from_being_liquidated(pre_health); - require!( - !account.fixed.being_liquidated(), - MangoError::BeingLiquidated - ); + let pre_health = account.check_health_pre(&health_cache)?; Some((health_cache, pre_health)) } else { None @@ -152,15 +144,7 @@ pub fn token_withdraw(ctx: Context, amount: u64, allow_borrow: bo if let Some((mut health_cache, pre_health)) = pre_health_opt { health_cache .adjust_token_balance(token_index, cm!(native_position_after - native_position))?; - let post_health = health_cache.health(HealthType::Init); - msg!("post_health: {}", post_health); - require!( - post_health >= 0 || post_health > pre_health, - MangoError::HealthMustBePositiveOrIncrease - ); - account - .fixed - .maybe_recover_from_being_liquidated(post_health); + account.check_health_post(&health_cache, pre_health)?; } // diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 0c170c527..74e9608fe 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -12,7 +12,8 @@ use crate::accounts_zerocopy::*; use crate::error::*; use crate::serum3_cpi; use crate::state::{ - Bank, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex, TokenIndex, + Bank, MangoAccountFixed, PerpMarket, PerpMarketIndex, PerpPosition, Serum3MarketIndex, + TokenIndex, }; use crate::util::checked_math as cm; @@ -617,6 +618,29 @@ impl HealthCache { health } + pub fn check_health_pre(&self, account: &mut MangoAccountFixed) -> Result { + let pre_health = self.health(HealthType::Init); + msg!("pre_health: {}", pre_health); + account.maybe_recover_from_being_liquidated(pre_health); + require!(!account.being_liquidated(), MangoError::BeingLiquidated); + Ok(pre_health) + } + + pub fn check_health_post( + &self, + account: &mut MangoAccountFixed, + pre_health: I80F48, + ) -> Result<()> { + let post_health = self.health(HealthType::Init); + msg!("post_health: {}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositiveOrIncrease + ); + account.maybe_recover_from_being_liquidated(post_health); + Ok(()) + } + fn token_entry_index(&mut self, token_index: TokenIndex) -> Result { self.token_infos .iter() diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index b5a796763..6eee288fc 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -22,6 +22,7 @@ use super::Serum3MarketIndex; use super::Side; use super::TokenIndex; use super::FREE_ORDER_SLOT; +use super::{HealthCache, HealthType}; use super::{PerpPosition, Serum3Orders, TokenPosition}; use checked_math as cm; @@ -607,6 +608,9 @@ impl< fn header_mut(&mut self) -> &mut MangoAccountDynamicHeader { self.header.deref_or_borrow_mut() } + fn fixed_mut(&mut self) -> &mut MangoAccountFixed { + self.fixed.deref_or_borrow_mut() + } fn dynamic_mut(&mut self) -> &mut [u8] { self.dynamic.deref_or_borrow_mut() } @@ -881,6 +885,34 @@ impl< Ok(()) } + pub fn check_health_pre(&mut self, health_cache: &HealthCache) -> Result { + let pre_health = health_cache.health(HealthType::Init); + msg!("pre_health: {}", pre_health); + self.fixed_mut() + .maybe_recover_from_being_liquidated(pre_health); + require!( + !self.fixed().being_liquidated(), + MangoError::BeingLiquidated + ); + Ok(pre_health) + } + + pub fn check_health_post( + &mut self, + health_cache: &HealthCache, + pre_health: I80F48, + ) -> Result<()> { + let post_health = health_cache.health(HealthType::Init); + msg!("post_health: {}", post_health); + require!( + post_health >= 0 || post_health > pre_health, + MangoError::HealthMustBePositiveOrIncrease + ); + self.fixed_mut() + .maybe_recover_from_being_liquidated(post_health); + Ok(()) + } + // writes length of tokens vec at appropriate offset so that borsh can infer the vector length // length used is that present in the header fn write_token_length(&mut self) { From 453248bbe5938bbba9e1896eeefc1757411526d3 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Thu, 25 Aug 2022 19:25:56 +0200 Subject: [PATCH 27/50] token_deregister: remove redundant token index and add doc (#192) * token_deregister: remove redundant token index Signed-off-by: microwavedcola1 * fix test Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- .../mango-v4/src/instructions/token_deregister.rs | 7 +++---- programs/mango-v4/src/lib.rs | 3 +-- .../mango-v4/tests/program_test/mango_client.rs | 4 +--- ts/client/src/client.ts | 2 +- ts/client/src/mango_v4.ts | 14 ++------------ 5 files changed, 8 insertions(+), 22 deletions(-) diff --git a/programs/mango-v4/src/instructions/token_deregister.rs b/programs/mango-v4/src/instructions/token_deregister.rs index d5df31373..687bc39f6 100644 --- a/programs/mango-v4/src/instructions/token_deregister.rs +++ b/programs/mango-v4/src/instructions/token_deregister.rs @@ -4,8 +4,9 @@ use anchor_spl::token::{self, CloseAccount, Token, TokenAccount}; use crate::{accounts_zerocopy::LoadZeroCopyRef, state::*}; use anchor_lang::AccountsClose; +/// In addition to these accounts, there must be remaining_accounts: +/// all n pairs of bank and its corresponding vault account for a token #[derive(Accounts)] -#[instruction(token_index: TokenIndex)] pub struct TokenDeregister<'info> { #[account( constraint = group.load()?.is_testing(), @@ -18,7 +19,6 @@ pub struct TokenDeregister<'info> { #[account( mut, has_one = group, - constraint = mint_info.load()?.token_index == token_index, close = sol_destination )] pub mint_info: AccountLoader<'info, MintInfo>, @@ -36,7 +36,6 @@ pub struct TokenDeregister<'info> { #[allow(clippy::too_many_arguments)] pub fn token_deregister<'key, 'accounts, 'remaining, 'info>( ctx: Context<'key, 'accounts, 'remaining, 'info, TokenDeregister<'info>>, - token_index: TokenIndex, ) -> Result<()> { let mint_info = ctx.accounts.mint_info.load()?; { @@ -61,7 +60,7 @@ pub fn token_deregister<'key, 'accounts, 'remaining, 'info>( { let bank = bank_ai.load::()?; require_keys_eq!(bank.group, ctx.accounts.group.key()); - require_eq!(bank.token_index, token_index); + require_eq!(bank.token_index, mint_info.token_index); require_keys_eq!(bank.vault, vault_ai.key()); } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index 2f4d98fb6..f5c95c773 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -142,9 +142,8 @@ pub mod mango_v4 { pub fn token_deregister<'key, 'accounts, 'remaining, 'info>( ctx: Context<'key, 'accounts, 'remaining, 'info, TokenDeregister<'info>>, - token_index: TokenIndex, ) -> Result<()> { - instructions::token_deregister(ctx, token_index) + instructions::token_deregister(ctx) } pub fn token_update_index_and_rate(ctx: Context) -> Result<()> { diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 379e9f1b7..c98253b9c 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -836,9 +836,7 @@ impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> { _loader: impl ClientAccountLoader + 'async_trait, ) -> (Self::Accounts, Instruction) { let program_id = mango_v4::id(); - let instruction = Self::Instruction { - token_index: self.token_index, - }; + let instruction = Self::Instruction {}; let accounts = Self::Accounts { admin: self.admin.pubkey(), diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index fd106191a..30bc972d4 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -348,7 +348,7 @@ export class MangoClient { } return await this.program.methods - .tokenDeregister(bank.tokenIndex) + .tokenDeregister() .accounts({ group: group.publicKey, admin: adminPk, diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 19a3a1682..863d23890 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -730,12 +730,7 @@ export type MangoV4 = { "isSigner": false } ], - "args": [ - { - "name": "tokenIndex", - "type": "u16" - } - ] + "args": [] }, { "name": "tokenUpdateIndexAndRate", @@ -5984,12 +5979,7 @@ export const IDL: MangoV4 = { "isSigner": false } ], - "args": [ - { - "name": "tokenIndex", - "type": "u16" - } - ] + "args": [] }, { "name": "tokenUpdateIndexAndRate", From 1a95a84ed4ff81ff04e3d42a000b538318b6f724 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 25 Aug 2022 18:24:11 +0200 Subject: [PATCH 28/50] Serum: Comments on account constraints --- .../src/instructions/serum3_cancel_all_orders.rs | 10 +++++++--- .../src/instructions/serum3_cancel_order.rs | 11 ++++++++--- .../src/instructions/serum3_close_open_orders.rs | 11 ++++++++--- .../instructions/serum3_create_open_orders.rs | 7 ++++++- .../serum3_liq_force_cancel_orders.rs | 8 ++++---- .../src/instructions/serum3_place_order.rs | 16 ++++++++++------ .../src/instructions/serum3_register_market.rs | 2 -- .../src/instructions/serum3_settle_funds.rs | 15 ++++++++++----- 8 files changed, 53 insertions(+), 27 deletions(-) diff --git a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs index 48ba91d2e..b50e6514e 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs @@ -7,12 +7,15 @@ use crate::state::*; pub struct Serum3CancelAllOrders<'info> { pub group: AccountLoader<'info, Group>, - #[account(has_one = group)] + #[account( + has_one = group + // owner is checked at #1 + )] pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] - /// CHECK: Validated inline by checking against the pubkey stored in the account + /// CHECK: Validated inline by checking against the pubkey stored in the account at #2 pub open_orders: UncheckedAccount<'info>, #[account( @@ -46,6 +49,7 @@ pub fn serum3_cancel_all_orders(ctx: Context, limit: u8) // { let account = ctx.accounts.account.load()?; + // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError @@ -53,7 +57,7 @@ pub fn serum3_cancel_all_orders(ctx: Context, limit: u8) let serum_market = ctx.accounts.serum_market.load()?; - // Validate open_orders + // Validate open_orders #2 require!( account .serum3_orders(serum_market.market_index) diff --git a/programs/mango-v4/src/instructions/serum3_cancel_order.rs b/programs/mango-v4/src/instructions/serum3_cancel_order.rs index c55c8590d..88c907adf 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_order.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_order.rs @@ -14,12 +14,16 @@ use checked_math as cm; pub struct Serum3CancelOrder<'info> { pub group: AccountLoader<'info, Group>, - #[account(mut, has_one = group)] + #[account( + mut, + has_one = group + // owner is checked at #1 + )] pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] - /// CHECK: Validated inline by checking against the pubkey stored in the account + /// CHECK: Validated inline by checking against the pubkey stored in the account at #2 pub open_orders: UncheckedAccount<'info>, #[account( @@ -59,12 +63,13 @@ pub fn serum3_cancel_order( // { let account = ctx.accounts.account.load()?; + // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError ); - // Validate open_orders + // Validate open_orders #2 require!( account .serum3_orders(serum_market.market_index) diff --git a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs index a457b6661..70e2656f6 100644 --- a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs @@ -7,7 +7,11 @@ use crate::state::*; pub struct Serum3CloseOpenOrders<'info> { pub group: AccountLoader<'info, Group>, - #[account(mut, has_one = group)] + #[account( + mut, + has_one = group + // owner is checked at #1 + )] pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, @@ -23,7 +27,7 @@ pub struct Serum3CloseOpenOrders<'info> { pub serum_market_external: UncheckedAccount<'info>, #[account(mut)] - /// CHECK: Validated inline by checking against the pubkey stored in the account + /// CHECK: Validated inline by checking against the pubkey stored in the account at #2 pub open_orders: UncheckedAccount<'info>, #[account(mut)] @@ -36,6 +40,7 @@ pub fn serum3_close_open_orders(ctx: Context) -> Result<( // Validation // let mut account = ctx.accounts.account.load_mut()?; + // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError @@ -43,7 +48,7 @@ pub fn serum3_close_open_orders(ctx: Context) -> Result<( let serum_market = ctx.accounts.serum_market.load()?; - // Validate open_orders + // Validate open_orders #2 require!( account .serum3_orders(serum_market.market_index) diff --git a/programs/mango-v4/src/instructions/serum3_create_open_orders.rs b/programs/mango-v4/src/instructions/serum3_create_open_orders.rs index e2dfd3ab4..ff6e34e97 100644 --- a/programs/mango-v4/src/instructions/serum3_create_open_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_create_open_orders.rs @@ -7,7 +7,11 @@ use crate::state::*; pub struct Serum3CreateOpenOrders<'info> { pub group: AccountLoader<'info, Group>, - #[account(mut, has_one = group)] + #[account( + mut, + has_one = group + // owner is checked at #1 + )] pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, @@ -48,6 +52,7 @@ pub fn serum3_create_open_orders(ctx: Context) -> Result let serum_market = ctx.accounts.serum_market.load()?; let mut account = ctx.accounts.account.load_mut()?; + // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 70b2979ca..77229138e 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -15,7 +15,7 @@ pub struct Serum3LiqForceCancelOrders<'info> { pub account: AccountLoaderDynamic<'info, MangoAccount>, #[account(mut)] - /// CHECK: Validated inline by checking against the pubkey stored in the account + /// CHECK: Validated inline by checking against the pubkey stored in the account at #2 pub open_orders: UncheckedAccount<'info>, #[account( @@ -50,7 +50,7 @@ pub struct Serum3LiqForceCancelOrders<'info> { /// CHECK: Validated by the serum cpi call pub market_vault_signer: UncheckedAccount<'info>, - // token_index and bank.vault == vault is validated inline + // token_index and bank.vault == vault is validated inline at #3 #[account(mut, has_one = group)] pub quote_bank: AccountLoader<'info, Bank>, #[account(mut)] @@ -74,7 +74,7 @@ pub fn serum3_liq_force_cancel_orders( { let account = ctx.accounts.account.load()?; - // Validate open_orders + // Validate open_orders #2 require!( account .serum3_orders(serum_market.market_index) @@ -84,7 +84,7 @@ pub fn serum3_liq_force_cancel_orders( MangoError::SomeError ); - // Validate banks and vaults + // Validate banks and vaults #3 let quote_bank = ctx.accounts.quote_bank.load()?; require!( quote_bank.vault == ctx.accounts.quote_vault.key(), diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index c7c26e549..87875411e 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -101,12 +101,16 @@ pub enum Serum3Side { pub struct Serum3PlaceOrder<'info> { pub group: AccountLoader<'info, Group>, - #[account(mut, has_one = group)] + #[account( + mut, + has_one = group + // owner is checked at #1 + )] pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] - /// CHECK: Validated inline by checking against the pubkey stored in the account + /// CHECK: Validated inline by checking against the pubkey stored in the account at #2 pub open_orders: UncheckedAccount<'info>, #[account( @@ -146,11 +150,10 @@ pub struct Serum3PlaceOrder<'info> { pub market_vault_signer: UncheckedAccount<'info>, // TODO: do we need to pass both, or just payer? - // TODO: if we potentially settle immediately, they all need to be mut? // TODO: Can we reduce the number of accounts by requiring the banks // to be in the remainingAccounts (where they need to be anyway, for // health checks - but they need to be mut) - // token_index and bank.vault == vault is validated inline + // token_index and bank.vault == vault is validated inline at #3 #[account(mut, has_one = group)] pub quote_bank: AccountLoader<'info, Bank>, #[account(mut)] @@ -182,12 +185,13 @@ pub fn serum3_place_order( // { let account = ctx.accounts.account.load()?; + // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError ); - // Validate open_orders + // Validate open_orders #2 require!( account .serum3_orders(serum_market.market_index) @@ -197,7 +201,7 @@ pub fn serum3_place_order( MangoError::SomeError ); - // Validate banks and vaults + // Validate banks and vaults #3 let quote_bank = ctx.accounts.quote_bank.load()?; require!( quote_bank.vault == ctx.accounts.quote_vault.key(), diff --git a/programs/mango-v4/src/instructions/serum3_register_market.rs b/programs/mango-v4/src/instructions/serum3_register_market.rs index a2d07b5d1..4d41688db 100644 --- a/programs/mango-v4/src/instructions/serum3_register_market.rs +++ b/programs/mango-v4/src/instructions/serum3_register_market.rs @@ -15,7 +15,6 @@ pub struct Serum3RegisterMarket<'info> { pub group: AccountLoader<'info, Group>, pub admin: Signer<'info>, - // TODO: limit? /// CHECK: Can register a market for any serum program pub serum_program: UncheckedAccount<'info>, /// CHECK: Can register any serum market @@ -42,7 +41,6 @@ pub struct Serum3RegisterMarket<'info> { pub system_program: Program<'info, System>, } -// TODO: should this be "configure_serum_market", which allows reconfiguring? pub fn serum3_register_market( ctx: Context, market_index: Serum3MarketIndex, diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 2b4530fe0..392ce1306 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -16,12 +16,16 @@ use crate::logs::LoanOriginationFeeInstruction; pub struct Serum3SettleFunds<'info> { pub group: AccountLoader<'info, Group>, - #[account(mut, has_one = group)] + #[account( + mut, + has_one = group + // owner is checked at #1 + )] pub account: AccountLoaderDynamic<'info, MangoAccount>, pub owner: Signer<'info>, #[account(mut)] - /// CHECK: Validated inline by checking against the pubkey stored in the account + /// CHECK: Validated inline by checking against the pubkey stored in the account at #2 pub open_orders: UncheckedAccount<'info>, #[account( @@ -48,7 +52,7 @@ pub struct Serum3SettleFunds<'info> { /// CHECK: Validated by the serum cpi call pub market_vault_signer: UncheckedAccount<'info>, - // token_index and bank.vault == vault is validated inline + // token_index and bank.vault == vault is validated inline at #3 #[account(mut, has_one = group)] pub quote_bank: AccountLoader<'info, Bank>, #[account(mut)] @@ -74,12 +78,13 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { // { let account = ctx.accounts.account.load()?; + // account constraint #1 require!( account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()), MangoError::SomeError ); - // Validate open_orders + // Validate open_orders #2 require!( account .serum3_orders(serum_market.market_index) @@ -89,7 +94,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { MangoError::SomeError ); - // Validate banks and vaults + // Validate banks and vaults #3 let quote_bank = ctx.accounts.quote_bank.load()?; require!( quote_bank.vault == ctx.accounts.quote_vault.key(), From d63c5bfc76b3de8ff7ba040e6f313a9687f83af6 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Thu, 25 Aug 2022 18:33:03 +0200 Subject: [PATCH 29/50] Serum: Don't attempt deactivating token positions after serum actions While a serum open orders is active, the base and quote token positions for it are locked to active. It's pointless to check if they need to be deactivated after place/settle etc. --- .../mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs | 1 - programs/mango-v4/src/instructions/serum3_place_order.rs | 1 - programs/mango-v4/src/instructions/serum3_settle_funds.rs | 1 - 3 files changed, 3 deletions(-) diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 77229138e..c6a35b62e 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -154,7 +154,6 @@ pub fn serum3_liq_force_cancel_orders( &ctx.accounts.account.key(), LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders, ); - difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 87875411e..6c788419a 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -336,7 +336,6 @@ pub fn serum3_place_order( &ctx.accounts.account.key(), LoanOriginationFeeInstruction::Serum3PlaceOrder, ); - vault_difference.deactivate_inactive_token_accounts(&mut account.borrow_mut()); Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 392ce1306..32ca92e92 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -169,7 +169,6 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { &ctx.accounts.account.key(), LoanOriginationFeeInstruction::Serum3SettleFunds, ); - difference_result.deactivate_inactive_token_accounts(&mut account.borrow_mut()); } Ok(()) From 7d508e5df57ed27d546997e0a12bf440abcb098a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Mon, 29 Aug 2022 15:36:21 +0200 Subject: [PATCH 30/50] MangoAccount: Fixes after audit --- programs/mango-v4/src/state/health.rs | 4 +- programs/mango-v4/src/state/mango_account.rs | 50 +++++++++---------- programs/mango-v4/src/state/orderbook/book.rs | 2 +- programs/mango-v4/src/state/orderbook/mod.rs | 24 ++++----- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index 74e9608fe..f4b4228c2 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -1159,7 +1159,7 @@ mod tests { // Run a health test that includes all the side values (like referrer_rebates_accrued) #[test] fn test_health0() { - let buffer = MangoAccount::default().try_to_vec().unwrap(); + let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); let group = Pubkey::new_unique(); @@ -1338,7 +1338,7 @@ mod tests { expected_health: f64, } fn test_health1_runner(testcase: &TestHealth1Case) { - let buffer = MangoAccount::default().try_to_vec().unwrap(); + let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); let group = Pubkey::new_unique(); diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 6eee288fc..809f1df7e 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -103,8 +103,8 @@ pub struct MangoAccount { pub perp_open_orders: Vec, } -impl Default for MangoAccount { - fn default() -> Self { +impl MangoAccount { + pub fn default_for_tests() -> Self { Self { name: Default::default(), group: Pubkey::default(), @@ -131,9 +131,8 @@ impl Default for MangoAccount { perp_open_orders: vec![PerpOpenOrder::default(); 2], } } -} -impl MangoAccount { + /// Number of bytes needed for the MangoAccount, including the discriminator pub fn space( token_count: u8, serum3_count: u8, @@ -185,7 +184,7 @@ impl MangoAccount { #[test] fn test_serialization_match() { - let mut account = MangoAccount::default(); + let mut account = MangoAccount::default_for_tests(); account.group = Pubkey::new_unique(); account.owner = Pubkey::new_unique(); account.name = crate::util::fill_from_str("abcdef").unwrap(); @@ -502,13 +501,11 @@ impl< // get iter over all active TokenPositions pub fn active_token_positions(&self) -> impl Iterator + '_ { - (0..self.header().token_count()) - .map(|i| self.token_position_by_raw_index(i)) - .filter(|token| token.is_active()) + self.all_token_positions().filter(|token| token.is_active()) } pub fn serum3_orders(&self, market_index: Serum3MarketIndex) -> Option<&Serum3Orders> { - self.active_serum3_orders() + self.all_serum3_orders() .find(|p| p.is_active_for_market(market_index)) } @@ -521,13 +518,12 @@ impl< } pub fn active_serum3_orders(&self) -> impl Iterator + '_ { - (0..self.header().serum3_count()) - .map(|i| self.serum3_orders_by_raw_index(i)) + self.all_serum3_orders() .filter(|serum3_order| serum3_order.is_active()) } pub fn perp_position(&self, market_index: PerpMarketIndex) -> Option<&PerpPosition> { - self.active_perp_positions() + self.all_perp_positions() .find(|p| p.is_active_for_market(market_index)) } @@ -540,17 +536,15 @@ impl< } pub fn active_perp_positions(&self) -> impl Iterator { - (0..self.header().perp_count()) - .map(|i| self.perp_position_by_raw_index(i)) - .filter(|p| p.is_active()) + self.all_perp_positions().filter(|p| p.is_active()) } - pub fn perp_orders_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrder { + pub fn perp_order_by_raw_index(&self, raw_index: usize) -> &PerpOpenOrder { get_helper(self.dynamic(), self.header().perp_oo_offset(raw_index)) } pub fn all_perp_orders(&self) -> impl Iterator { - (0..self.header().perp_oo_count()).map(|i| self.perp_orders_by_raw_index(i)) + (0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index(i)) } pub fn perp_next_order_slot(&self) -> Option { @@ -563,8 +557,7 @@ impl< market_index: PerpMarketIndex, client_order_id: u64, ) -> Option<(i128, Side)> { - for i in 0..self.header().perp_oo_count() { - let oo = self.perp_orders_by_raw_index(i); + for oo in self.all_perp_orders() { if oo.order_market == market_index && oo.client_order_id == client_order_id { return Some((oo.order_id, oo.order_side)); } @@ -577,8 +570,7 @@ impl< market_index: PerpMarketIndex, order_id: i128, ) -> Option { - for i in 0..self.header().perp_oo_count() { - let oo = self.perp_orders_by_raw_index(i); + for oo in self.all_perp_orders() { if oo.order_market == market_index && oo.order_id == order_id { return Some(oo.order_side); } @@ -730,7 +722,7 @@ impl< market_index: Serum3MarketIndex, ) -> Option<&mut Serum3Orders> { let raw_index_opt = self - .active_serum3_orders() + .all_serum3_orders() .position(|p| p.is_active_for_market(market_index)); raw_index_opt.map(|raw_index| self.serum3_orders_mut_by_raw_index(raw_index)) } @@ -741,7 +733,7 @@ impl< get_helper_mut(self.dynamic_mut(), offset) } - pub fn perp_orders_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrder { + pub fn perp_order_mut_by_raw_index(&mut self, raw_index: usize) -> &mut PerpOpenOrder { let offset = self.header().perp_oo_offset(raw_index); get_helper_mut(self.dynamic_mut(), offset) } @@ -751,7 +743,7 @@ impl< perp_market_index: PerpMarketIndex, ) -> Result<(&mut PerpPosition, usize)> { let mut raw_index_opt = self - .active_perp_positions() + .all_perp_positions() .position(|p| p.is_active_for_market(perp_market_index)); if raw_index_opt.is_none() { raw_index_opt = self.all_perp_positions().position(|p| !p.is_active()); @@ -779,6 +771,7 @@ impl< side: Side, order: &LeafNode, ) -> Result<()> { + // TODO: pass in the PerpPosition, currently has a creation side-effect let mut perp_account = self.ensure_perp_position(perp_market_index).unwrap().0; match side { Side::Bid => { @@ -790,7 +783,7 @@ impl< }; let slot = order.owner_slot as usize; - let mut oo = self.perp_orders_mut_by_raw_index(slot); + let mut oo = self.perp_order_mut_by_raw_index(slot); oo.order_market = perp_market_index; oo.order_side = side; oo.order_id = order.key; @@ -799,8 +792,9 @@ impl< } pub fn remove_perp_order(&mut self, slot: usize, quantity: i64) -> Result<()> { + // TODO: pass in the PerpPosition, currently has a creation side-effect { - let oo = self.perp_orders_mut_by_raw_index(slot); + let oo = self.perp_order_mut_by_raw_index(slot); require_neq!(oo.order_market, FREE_ORDER_SLOT); let order_side = oo.order_side; let perp_market_index = oo.order_market; @@ -818,7 +812,7 @@ impl< } // release space - let oo = self.perp_orders_mut_by_raw_index(slot); + let oo = self.perp_order_mut_by_raw_index(slot); oo.order_market = FREE_ORDER_SLOT; oo.order_side = Side::Bid; oo.order_id = 0i128; @@ -832,6 +826,7 @@ impl< perp_market: &mut PerpMarket, fill: &FillEvent, ) -> Result<()> { + // TODO: pass in the PerpPosition, currently has a creation side-effect let pa = self.ensure_perp_position(perp_market_index).unwrap().0; pa.settle_funding(perp_market); @@ -871,6 +866,7 @@ impl< perp_market: &mut PerpMarket, fill: &FillEvent, ) -> Result<()> { + // TODO: pass in the PerpPosition, currently has a creation side-effect let pa = self.ensure_perp_position(perp_market_index).unwrap().0; pa.settle_funding(perp_market); diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index da78ec8a0..4de66b880 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -393,7 +393,7 @@ impl<'a> Book<'a> { side_to_cancel_option: Option, ) -> Result<()> { for i in 0..mango_account.header.perp_oo_count() { - let oo = mango_account.perp_orders_by_raw_index(i); + let oo = mango_account.perp_order_by_raw_index(i); if oo.order_market == FREE_ORDER_SLOT || oo.order_market != perp_market.perp_market_index { diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index 6d144f863..9b3156b3e 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -101,7 +101,7 @@ mod tests { let mut new_order = |book: &mut Book, event_queue: &mut EventQueue, side, price, now_ts| -> i128 { - let buffer = MangoAccount::default().try_to_vec().unwrap(); + let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); let mut account = MangoAccountValue::from_bytes(&buffer).unwrap(); let quantity = 1; @@ -124,7 +124,7 @@ mod tests { u8::MAX, ) .unwrap(); - account.perp_orders_by_raw_index(0).order_id + account.perp_order_by_raw_index(0).order_id }; // insert bids until book side is full @@ -196,7 +196,7 @@ mod tests { market.maker_fee = I80F48::from_num(-0.001f64); market.taker_fee = I80F48::from_num(0.01f64); - let buffer = MangoAccount::default().try_to_vec().unwrap(); + let buffer = MangoAccount::default_for_tests().try_to_vec().unwrap(); let mut maker = MangoAccountValue::from_bytes(&buffer).unwrap(); let mut taker = MangoAccountValue::from_bytes(&buffer).unwrap(); @@ -225,19 +225,19 @@ mod tests { ) .unwrap(); assert_eq!( - maker.perp_orders_mut_by_raw_index(0).order_market, + maker.perp_order_mut_by_raw_index(0).order_market, market.perp_market_index ); assert_eq!( - maker.perp_orders_mut_by_raw_index(1).order_market, + maker.perp_order_mut_by_raw_index(1).order_market, FREE_ORDER_SLOT ); - assert_ne!(maker.perp_orders_mut_by_raw_index(0).order_id, 0); - assert_eq!(maker.perp_orders_mut_by_raw_index(0).client_order_id, 42); - assert_eq!(maker.perp_orders_mut_by_raw_index(0).order_side, Side::Bid); + assert_ne!(maker.perp_order_mut_by_raw_index(0).order_id, 0); + assert_eq!(maker.perp_order_mut_by_raw_index(0).client_order_id, 42); + assert_eq!(maker.perp_order_mut_by_raw_index(0).order_side, Side::Bid); assert!(bookside_contains_key( &book.bids, - maker.perp_orders_mut_by_raw_index(0).order_id + maker.perp_order_mut_by_raw_index(0).order_id )); assert!(bookside_contains_price(&book.bids, price)); assert_eq!( @@ -279,7 +279,7 @@ mod tests { // the remainder of the maker order is still on the book // (the maker account is unchanged: it was not even passed in) let order = - bookside_leaf_by_key(&book.bids, maker.perp_orders_by_raw_index(0).order_id).unwrap(); + bookside_leaf_by_key(&book.bids, maker.perp_order_by_raw_index(0).order_id).unwrap(); assert_eq!(order.price(), price); assert_eq!(order.quantity, bid_quantity - match_quantity); @@ -292,7 +292,7 @@ mod tests { // the taker account is updated assert_eq!( - taker.perp_orders_by_raw_index(0).order_market, + taker.perp_order_by_raw_index(0).order_market, FREE_ORDER_SLOT ); assert_eq!(taker.perp_position_by_raw_index(0).bids_base_lots, 0); @@ -334,7 +334,7 @@ mod tests { .unwrap(); assert_eq!(market.open_interest, 2 * match_quantity); - assert_eq!(maker.perp_orders_by_raw_index(0).order_market, 0); + assert_eq!(maker.perp_order_by_raw_index(0).order_market, 0); assert_eq!( maker.perp_position_by_raw_index(0).bids_base_lots, bid_quantity - match_quantity From d6d66402f792c489f6d8164b00aa33ac5cee7758 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 26 Aug 2022 15:59:47 +0200 Subject: [PATCH 31/50] Tests: Helper for create + fund --- .../tests/program_test/mango_setup.rs | 47 ++++++- .../mango-v4/tests/test_bankrupt_tokens.rs | 45 ++----- programs/mango-v4/tests/test_delegate.rs | 36 +----- .../mango-v4/tests/test_health_compute.rs | 83 +++--------- programs/mango-v4/tests/test_health_region.rs | 34 +---- programs/mango-v4/tests/test_liq_tokens.rs | 73 ++--------- programs/mango-v4/tests/test_margin_trade.rs | 56 ++------ programs/mango-v4/tests/test_perp.rs | 120 ++++-------------- .../mango-v4/tests/test_position_lifetime.rs | 49 ++----- programs/mango-v4/tests/test_serum.rs | 64 ++-------- .../tests/test_token_update_index_and_rate.rs | 74 ++--------- 11 files changed, 175 insertions(+), 506 deletions(-) diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index 2aacef1fa..f9f8438df 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -5,7 +5,7 @@ use solana_sdk::signature::Keypair; use super::mango_client::*; use super::solana::SolanaCookie; -use super::{send_tx, MintCookie}; +use super::{send_tx, ClonableKeypair, MintCookie, UserCookie}; pub struct GroupWithTokensConfig<'a> { pub admin: &'a Keypair, @@ -137,3 +137,48 @@ impl<'a> GroupWithTokensConfig<'a> { } } } + +pub async fn create_funded_account( + solana: &SolanaCookie, + group: Pubkey, + owner: &Keypair, + account_num: u32, + payer: &UserCookie, + mints: &[MintCookie], + amounts: u64, + bank_index: usize, +) -> Pubkey { + let account = send_tx( + solana, + AccountCreateInstruction { + account_num, + token_count: 16, + serum3_count: 8, + perp_count: 8, + perp_oo_count: 8, + group, + owner, + payer: &payer.key, + }, + ) + .await + .unwrap() + .account; + + for mint in mints { + send_tx( + solana, + TokenDepositInstruction { + amount: amounts, + account, + token_account: payer.token_accounts[mint.index], + token_authority: payer.key.clone(), + bank_index, + }, + ) + .await + .unwrap(); + } + + account +} diff --git a/programs/mango-v4/tests/test_bankrupt_tokens.rs b/programs/mango-v4/tests/test_bankrupt_tokens.rs index 69cff8a1f..2f02de6f3 100644 --- a/programs/mango-v4/tests/test_bankrupt_tokens.rs +++ b/programs/mango-v4/tests/test_bankrupt_tokens.rs @@ -10,6 +10,8 @@ use solana_sdk::{ use mango_v4::state::*; use program_test::*; +use mango_setup::*; + mod program_test; #[tokio::test] @@ -27,7 +29,7 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { // SETUP: Create a group and an account to fill the vaults // - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -40,37 +42,18 @@ async fn test_bankrupt_tokens_socialize_loss() -> Result<(), TransportError> { let collateral_token2 = &tokens[3]; // deposit some funds, to the vaults aren't empty - let vault_account = send_tx( - solana, - AccountCreateInstruction { - account_num: 2, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, - ) - .await - .unwrap() - .account; let vault_amount = 100000; - for &token_account in payer_mint_accounts { - send_tx( - solana, - TokenDepositInstruction { - amount: vault_amount, - account: vault_account, - token_account, - token_authority: payer.clone(), - bank_index: 1, - }, - ) - .await - .unwrap(); - } + let vault_account = create_funded_account( + &solana, + group, + owner, + 2, + &context.users[1], + mints, + vault_amount, + 1, + ) + .await; // also add a tiny amount to bank0 for borrow_token1, so we can test multi-bank socialized loss send_tx( diff --git a/programs/mango-v4/tests/test_delegate.rs b/programs/mango-v4/tests/test_delegate.rs index 18e669b83..500797c05 100644 --- a/programs/mango-v4/tests/test_delegate.rs +++ b/programs/mango-v4/tests/test_delegate.rs @@ -6,6 +6,8 @@ use solana_sdk::{signature::Keypair, signature::Signer, transport::TransportErro use mango_v4::state::*; use program_test::*; +use mango_setup::*; + mod program_test; #[tokio::test] @@ -24,7 +26,7 @@ async fn test_delegate() -> Result<(), TransportError> { // SETUP: Create a group, register a token (mint0), create an account // - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -33,36 +35,8 @@ async fn test_delegate() -> Result<(), TransportError> { .await; let bank = tokens[0].bank; - let account = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, - ) - .await - .unwrap() - .account; - - // deposit - send_tx( - solana, - TokenDepositInstruction { - amount: 100, - account, - token_account: payer_mint0_account, - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); + let account = + create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 100, 0).await; // // TEST: Edit account - Set delegate diff --git a/programs/mango-v4/tests/test_health_compute.rs b/programs/mango-v4/tests/test_health_compute.rs index cf338cabc..da4bb4c42 100644 --- a/programs/mango-v4/tests/test_health_compute.rs +++ b/programs/mango-v4/tests/test_health_compute.rs @@ -7,6 +7,8 @@ use solana_sdk::{signature::Keypair, transport::TransportError}; use program_test::*; +use mango_setup::*; + mod program_test; // Try to reach compute limits in health checks by having many different tokens in an account @@ -19,13 +21,12 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> { let owner = &context.users[0].key; let payer = &context.users[1].key; let mints = &context.mints[0..10]; - let payer_mint_accounts = &context.users[1].token_accounts[0..mints.len()]; // // SETUP: Create a group and an account // - let mango_setup::GroupWithTokens { group, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -33,43 +34,8 @@ async fn test_health_compute_tokens() -> Result<(), TransportError> { .create(solana) .await; - let account = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, - ) - .await - .unwrap() - .account; - - // - // TEST: Deposit user funds for all the mints - // each deposit will end with a health check - // - for &token_account in payer_mint_accounts { - let deposit_amount = 1000; - - send_tx( - solana, - TokenDepositInstruction { - amount: deposit_amount, - account, - token_account, - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } + // each deposit ends with a health check + create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 1000, 0).await; // TODO: actual explicit CU comparisons. // On 2022-5-25 the final deposit costs 36905 CU and each new token increases it by roughly 1600 CU @@ -216,36 +182,17 @@ async fn test_health_compute_perp() -> Result<(), TransportError> { .create(solana) .await; - let account = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + let account = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + &mints[..1], + 1000, + 0, ) - .await - .unwrap() - .account; - - // Give the account some quote currency - send_tx( - solana, - TokenDepositInstruction { - amount: 1000, - account, - token_account: payer_mint_accounts[0], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); + .await; // // SETUP: Create perp markets diff --git a/programs/mango-v4/tests/test_health_region.rs b/programs/mango-v4/tests/test_health_region.rs index 4e85fb799..9fadeae6d 100644 --- a/programs/mango-v4/tests/test_health_region.rs +++ b/programs/mango-v4/tests/test_health_region.rs @@ -7,6 +7,8 @@ use mango_v4::state::MangoAccount; use program_test::*; +use mango_setup::*; + mod program_test; #[tokio::test] @@ -24,7 +26,7 @@ async fn test_health_wrap() -> Result<(), TransportError> { // SETUP: Create a group, account, register a token (mint0) // - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -33,35 +35,7 @@ async fn test_health_wrap() -> Result<(), TransportError> { .await; // SETUP: Create an account with deposits, so the second account can borrow more than it has - let setup_account = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 8, - serum3_count: 0, - perp_count: 0, - perp_oo_count: 0, - group, - owner, - payer, - }, - ) - .await - .unwrap() - .account; - - send_tx( - solana, - TokenDepositInstruction { - amount: 1000, - account: setup_account, - token_account: payer_mint_accounts[0], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); + create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 1000, 0).await; // SETUP: Make a second account let account = send_tx( diff --git a/programs/mango-v4/tests/test_liq_tokens.rs b/programs/mango-v4/tests/test_liq_tokens.rs index 84b9e69e3..591371682 100644 --- a/programs/mango-v4/tests/test_liq_tokens.rs +++ b/programs/mango-v4/tests/test_liq_tokens.rs @@ -7,6 +7,8 @@ use solana_sdk::{signature::Keypair, transport::TransportError}; use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; use program_test::*; +use mango_setup::*; + mod program_test; #[tokio::test] @@ -26,7 +28,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> { // SETUP: Create a group and an account to fill the vaults // - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -37,36 +39,7 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> { let quote_token = &tokens[1]; // deposit some funds, to the vaults aren't empty - let vault_account = send_tx( - solana, - AccountCreateInstruction { - account_num: 2, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, - ) - .await - .unwrap() - .account; - for &token_account in payer_mint_accounts { - send_tx( - solana, - TokenDepositInstruction { - amount: 10000, - account: vault_account, - token_account, - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } + create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 10000, 0).await; // // SETUP: Create serum market @@ -96,36 +69,18 @@ async fn test_liq_tokens_force_cancel() -> Result<(), TransportError> { // // SETUP: Make an account and deposit some quote // - let account = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, - ) - .await - .unwrap() - .account; - let deposit_amount = 1000; - send_tx( - solana, - TokenDepositInstruction { - amount: deposit_amount, - account, - token_account: payer_mint_accounts[1], - token_authority: payer.clone(), - bank_index: 0, - }, + let account = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + &mints[1..2], + deposit_amount, + 0, ) - .await - .unwrap(); + .await; // // SETUP: Create an open orders account and an order diff --git a/programs/mango-v4/tests/test_margin_trade.rs b/programs/mango-v4/tests/test_margin_trade.rs index be485ab1a..9d3979cc1 100644 --- a/programs/mango-v4/tests/test_margin_trade.rs +++ b/programs/mango-v4/tests/test_margin_trade.rs @@ -6,6 +6,8 @@ use solana_sdk::signature::Signer; use program_test::*; +use mango_setup::*; + mod program_test; // This is an unspecific happy-case test that just runs a few instructions to check @@ -21,7 +23,6 @@ async fn test_margin_trade() -> Result<(), BanksClientError> { let payer = &context.users[1].key; let mints = &context.mints[0..2]; let payer_mint0_account = context.users[1].token_accounts[0]; - let payer_mint1_account = context.users[1].token_accounts[1]; let loan_origination_fee = 0.0005; // higher resolution that the loan_origination_fee for one token @@ -31,7 +32,7 @@ async fn test_margin_trade() -> Result<(), BanksClientError> { // SETUP: Create a group, account, register a token (mint0) // - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -45,48 +46,17 @@ async fn test_margin_trade() -> Result<(), BanksClientError> { // provide some funds for tokens, so the test user can borrow // let provided_amount = 1000; - - let provider_account = send_tx( - solana, - AccountCreateInstruction { - account_num: 1, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + mints, + provided_amount, + 0, ) - .await - .unwrap() - .account; - - send_tx( - solana, - TokenDepositInstruction { - amount: provided_amount, - account: provider_account, - token_account: payer_mint0_account, - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - send_tx( - solana, - TokenDepositInstruction { - amount: provided_amount, - account: provider_account, - token_account: payer_mint1_account, - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); + .await; // // create thes test user account diff --git a/programs/mango-v4/tests/test_perp.rs b/programs/mango-v4/tests/test_perp.rs index 8f481dcd7..c7dec62cd 100644 --- a/programs/mango-v4/tests/test_perp.rs +++ b/programs/mango-v4/tests/test_perp.rs @@ -7,6 +7,8 @@ use program_test::*; use solana_program_test::*; use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError}; +use mango_setup::*; + mod program_test; #[tokio::test] @@ -18,13 +20,12 @@ async fn test_perp() -> Result<(), TransportError> { let owner = &context.users[0].key; let payer = &context.users[1].key; let mints = &context.mints[0..2]; - let payer_mint_accounts = &context.users[1].token_accounts[0..=2]; // // SETUP: Create a group and an account // - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -32,102 +33,29 @@ async fn test_perp() -> Result<(), TransportError> { .create(solana) .await; - let account_0 = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + let deposit_amount = 1000; + let account_0 = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + mints, + deposit_amount, + 0, ) - .await - .unwrap() - .account; - - let account_1 = send_tx( - solana, - AccountCreateInstruction { - account_num: 1, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + .await; + let account_1 = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + mints, + deposit_amount, + 0, ) - .await - .unwrap() - .account; - - // - // SETUP: Deposit user funds - // - { - let deposit_amount = 1000; - - send_tx( - solana, - TokenDepositInstruction { - amount: deposit_amount, - account: account_0, - token_account: payer_mint_accounts[0], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - - send_tx( - solana, - TokenDepositInstruction { - amount: deposit_amount, - account: account_0, - token_account: payer_mint_accounts[1], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } - - { - let deposit_amount = 1000; - - send_tx( - solana, - TokenDepositInstruction { - amount: deposit_amount, - account: account_1, - token_account: payer_mint_accounts[0], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - - send_tx( - solana, - TokenDepositInstruction { - amount: deposit_amount, - account: account_1, - token_account: payer_mint_accounts[1], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } + .await; // // TEST: Create a perp market diff --git a/programs/mango-v4/tests/test_position_lifetime.rs b/programs/mango-v4/tests/test_position_lifetime.rs index ca79d613e..1f3448303 100644 --- a/programs/mango-v4/tests/test_position_lifetime.rs +++ b/programs/mango-v4/tests/test_position_lifetime.rs @@ -6,6 +6,8 @@ use solana_sdk::signature::Keypair; use program_test::*; +use crate::mango_setup::*; + mod program_test; // Check opening and closing positions @@ -50,43 +52,18 @@ async fn test_position_lifetime() -> Result<()> { .unwrap() .account; - let funding_account = send_tx( - solana, - AccountCreateInstruction { - account_num: 1, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + let funding_amount = 1000000; + create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + mints, + funding_amount, + 0, ) - .await - .unwrap() - .account; - - // - // SETUP: Put some tokens into the funding account to allow borrowing - // - { - let funding_amount = 1000000; - for &payer_token in payer_mint_accounts { - send_tx( - solana, - TokenDepositInstruction { - amount: funding_amount, - account: funding_account, - token_account: payer_token, - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } - } + .await; // // TEST: Deposit and withdraw tokens for all mints diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index 3dd39494d..cf2b71b27 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -8,6 +8,8 @@ use program_test::*; mod program_test; +use mango_setup::*; + #[tokio::test] async fn test_serum() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); @@ -19,13 +21,12 @@ async fn test_serum() -> Result<(), TransportError> { let owner = &context.users[0].key; let payer = &context.users[1].key; let mints = &context.mints[0..2]; - let payer_mint_accounts = &context.users[1].token_accounts[0..=2]; // // SETUP: Create a group and an account // - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -35,22 +36,18 @@ async fn test_serum() -> Result<(), TransportError> { let base_token = &tokens[0]; let quote_token = &tokens[1]; - let account = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + let deposit_amount = 1000; + let account = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + mints, + deposit_amount, + 0, ) - .await - .unwrap() - .account; + .await; // // SETUP: Create serum market @@ -60,39 +57,6 @@ async fn test_serum() -> Result<(), TransportError> { .list_spot_market(&base_token.mint, "e_token.mint) .await; - // - // SETUP: Deposit user funds - // - { - let deposit_amount = 1000; - - send_tx( - solana, - TokenDepositInstruction { - amount: deposit_amount, - account, - token_account: payer_mint_accounts[0], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - - send_tx( - solana, - TokenDepositInstruction { - amount: deposit_amount, - account, - token_account: payer_mint_accounts[1], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } - // // TEST: Register a serum market // diff --git a/programs/mango-v4/tests/test_token_update_index_and_rate.rs b/programs/mango-v4/tests/test_token_update_index_and_rate.rs index 635e1ae16..4164ac5f6 100644 --- a/programs/mango-v4/tests/test_token_update_index_and_rate.rs +++ b/programs/mango-v4/tests/test_token_update_index_and_rate.rs @@ -4,6 +4,7 @@ use mango_v4::state::*; use solana_program_test::*; use solana_sdk::{signature::Keypair, transport::TransportError}; +use mango_setup::*; use program_test::*; mod program_test; @@ -17,13 +18,12 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> { let owner = &context.users[0].key; let payer = &context.users[1].key; let mints = &context.mints[0..2]; - let payer_mint_accounts = &context.users[1].token_accounts[0..2]; // // SETUP: Create a group and an account to fill the vaults // - let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig { + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { admin, payer, mints, @@ -32,66 +32,18 @@ async fn test_token_update_index_and_rate() -> Result<(), TransportError> { .await; // deposit some funds, to the vaults aren't empty - let deposit_account = send_tx( - solana, - AccountCreateInstruction { - account_num: 0, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, + create_funded_account(&solana, group, owner, 0, &context.users[1], mints, 10000, 0).await; + let withdraw_account = create_funded_account( + &solana, + group, + owner, + 1, + &context.users[1], + &mints[1..2], + 100000, + 0, ) - .await - .unwrap() - .account; - for &token_account in payer_mint_accounts { - send_tx( - solana, - TokenDepositInstruction { - amount: 10000, - account: deposit_account, - token_account, - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); - } - - let withdraw_account = send_tx( - solana, - AccountCreateInstruction { - account_num: 1, - token_count: 16, - serum3_count: 8, - perp_count: 8, - perp_oo_count: 8, - group, - owner, - payer, - }, - ) - .await - .unwrap() - .account; - - send_tx( - solana, - TokenDepositInstruction { - amount: 100000, - account: withdraw_account, - token_account: payer_mint_accounts[1], - token_authority: payer.clone(), - bank_index: 0, - }, - ) - .await - .unwrap(); + .await; send_tx( solana, From e1adbf0217df3c4bda09ee3c2466265406d75539 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Sat, 27 Aug 2022 17:42:21 +0200 Subject: [PATCH 32/50] Tests: Fix concurrent log capture --- programs/mango-v4/tests/program_test/mod.rs | 19 ++++++++------ .../mango-v4/tests/program_test/solana.rs | 26 +++++++++++++++---- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/programs/mango-v4/tests/program_test/mod.rs b/programs/mango-v4/tests/program_test/mod.rs index 50f3a2cf1..2ca8772b3 100644 --- a/programs/mango-v4/tests/program_test/mod.rs +++ b/programs/mango-v4/tests/program_test/mod.rs @@ -52,7 +52,7 @@ impl AddPacked for ProgramTest { struct LoggerWrapper { inner: env_logger::Logger, - program_log: Arc>>, + capture: Arc>>, } impl Log for LoggerWrapper { @@ -67,9 +67,9 @@ impl Log for LoggerWrapper { { let msg = record.args().to_string(); if let Some(data) = msg.strip_prefix("Program log: ") { - self.program_log.write().unwrap().push(data.into()); + self.capture.write().unwrap().push(data.into()); } else if let Some(data) = msg.strip_prefix("Program data: ") { - self.program_log.write().unwrap().push(data.into()); + self.capture.write().unwrap().push(data.into()); } } self.inner.log(record); @@ -87,12 +87,13 @@ pub struct MarginTradeCookie { pub struct TestContextBuilder { test: ProgramTest, - program_log_capture: Arc>>, + logger_capture: Arc>>, mint0: Pubkey, } lazy_static::lazy_static! { - static ref PROGRAM_LOG_CAPTURE: Arc>> = Arc::new(RwLock::new(vec![])); + static ref LOGGER_CAPTURE: Arc>> = Arc::new(RwLock::new(vec![])); + static ref LOGGER_LOCK: Arc> = Arc::new(RwLock::new(())); } impl TestContextBuilder { @@ -108,7 +109,7 @@ impl TestContextBuilder { .build(); let _ = log::set_boxed_logger(Box::new(LoggerWrapper { inner: env_logger, - program_log: PROGRAM_LOG_CAPTURE.clone(), + capture: LOGGER_CAPTURE.clone(), })); let mut test = ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry)); @@ -118,7 +119,7 @@ impl TestContextBuilder { Self { test, - program_log_capture: PROGRAM_LOG_CAPTURE.clone(), + logger_capture: LOGGER_CAPTURE.clone(), mint0: Pubkey::new_unique(), } } @@ -282,7 +283,9 @@ impl TestContextBuilder { let solana = Arc::new(SolanaCookie { context: RefCell::new(context), rent, - program_log: self.program_log_capture.clone(), + logger_capture: self.logger_capture.clone(), + logger_lock: LOGGER_LOCK.clone(), + last_transaction_log: RefCell::new(vec![]), }); solana diff --git a/programs/mango-v4/tests/program_test/solana.rs b/programs/mango-v4/tests/program_test/solana.rs index 170cf102c..ac0195bca 100644 --- a/programs/mango-v4/tests/program_test/solana.rs +++ b/programs/mango-v4/tests/program_test/solana.rs @@ -17,7 +17,9 @@ use spl_token::*; pub struct SolanaCookie { pub context: RefCell, pub rent: Rent, - pub program_log: Arc>>, + pub logger_capture: Arc>>, + pub logger_lock: Arc>, + pub last_transaction_log: RefCell>, } impl SolanaCookie { @@ -27,7 +29,15 @@ impl SolanaCookie { instructions: &[Instruction], signers: Option<&[&Keypair]>, ) -> Result<(), BanksClientError> { - self.program_log.write().unwrap().clear(); + // The locking in this function is convoluted: + // We capture the program log output by overriding the global logger and capturing + // messages there. This logger is potentially shared among multiple tests that run + // concurrently. + // To allow each independent SolanaCookie to capture only the logs from the transaction + // passed to process_transaction, wo globally hold the "program_log_lock" for the + // duration that the tx needs to process. So only a single one can run at a time. + let tx_log_lock = Arc::new(self.logger_lock.write().unwrap()); + self.logger_capture.write().unwrap().clear(); let mut context = self.context.borrow_mut(); @@ -45,13 +55,19 @@ impl SolanaCookie { transaction.sign(&all_signers, context.last_blockhash); - context + let result = context .banks_client .process_transaction_with_commitment( transaction, solana_sdk::commitment_config::CommitmentLevel::Processed, ) - .await + .await; + + *self.last_transaction_log.borrow_mut() = self.logger_capture.read().unwrap().clone(); + + drop(tx_log_lock); + + result } pub async fn get_clock(&self) -> solana_program::clock::Clock { @@ -221,7 +237,7 @@ impl SolanaCookie { #[allow(dead_code)] pub fn program_log(&self) -> Vec { - self.program_log.read().unwrap().clone() + self.last_transaction_log.borrow().clone() } pub fn program_log_events( From dc4acd0dd7a98e9d9622668530e2cea9535a1e41 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Fri, 26 Aug 2022 12:45:32 +0200 Subject: [PATCH 33/50] Serum: loan origination fee, don't auto-settle, fix vault check - Loan origination fees: The previous approach of tracking the reserved amount did not work because OutEvents will also reduce the reserved amount. This means we can't know if it was an OutEvent-cancel or an order execution that caused the reduction. Instead, we now track the amount of borrows that was made (without applying origination fees) in place order. Whenever we try to settle and the amount of tokens on the oo account is less than the potential borrows, we can be certain that the borrow has actualized. - Place order is no longer automatically followed by a settle. This can reduce compute use when people want to place multiple orders in sequence. Now they can use the HealthRegion instructions to place their orders, settle once at the end, and then have health checked. - Vault check: Place order previously rejected valid orders because it didn't consider that there could be free tokens on the oo account. - Tests: Some infrastructure for less verbose serum testing. --- .../src/instructions/serum3_cancel_order.rs | 43 -- .../serum3_liq_force_cancel_orders.rs | 39 +- .../src/instructions/serum3_place_order.rs | 280 ++++------ .../src/instructions/serum3_settle_funds.rs | 131 ++--- .../src/state/mango_account_components.rs | 16 +- programs/mango-v4/tests/program_test/serum.rs | 6 +- programs/mango-v4/tests/test_serum.rs | 519 +++++++++++++++--- 7 files changed, 668 insertions(+), 366 deletions(-) diff --git a/programs/mango-v4/src/instructions/serum3_cancel_order.rs b/programs/mango-v4/src/instructions/serum3_cancel_order.rs index 88c907adf..6b35d8253 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_order.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_order.rs @@ -3,12 +3,9 @@ use anchor_lang::prelude::*; use serum_dex::instruction::CancelOrderInstructionV2; use crate::error::*; -use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; -use super::OpenOrdersSlim; use super::Serum3Side; -use checked_math as cm; #[derive(Accounts)] pub struct Serum3CancelOrder<'info> { @@ -83,55 +80,15 @@ pub fn serum3_cancel_order( // // Cancel // - let before_oo = { - let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?; - OpenOrdersSlim::from_oo(&open_orders) - }; let order = serum_dex::instruction::CancelOrderInstructionV2 { side: u8::try_from(side).unwrap().try_into().unwrap(), order_id, }; cpi_cancel_order(ctx.accounts, order)?; - { - let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?; - let after_oo = OpenOrdersSlim::from_oo(&open_orders); - let mut account = ctx.accounts.account.load_mut()?; - decrease_maybe_loan( - serum_market.market_index, - &mut account.borrow_mut(), - &before_oo, - &after_oo, - ); - }; - Ok(()) } -// if free has increased, the free increase is reduction in reserved, reduce this from -// the cached -pub fn decrease_maybe_loan( - market_index: Serum3MarketIndex, - account: &mut MangoAccountRefMut, - before_oo: &OpenOrdersSlim, - after_oo: &OpenOrdersSlim, -) { - let serum3_account = account.serum3_orders_mut(market_index).unwrap(); - - if after_oo.native_coin_free > before_oo.native_coin_free { - let native_coin_free_increase = after_oo.native_coin_free - before_oo.native_coin_free; - serum3_account.previous_native_coin_reserved = - cm!(serum3_account.previous_native_coin_reserved - native_coin_free_increase); - } - - // pc - if after_oo.native_pc_free > before_oo.native_pc_free { - let free_pc_increase = after_oo.native_pc_free - before_oo.native_pc_free; - serum3_account.previous_native_pc_reserved = - cm!(serum3_account.previous_native_pc_reserved - free_pc_increase); - } -} - fn cpi_cancel_order(ctx: &Serum3CancelOrder, order: CancelOrderInstructionV2) -> Result<()> { use crate::serum3_cpi; let group = ctx.group.load()?; diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index c6a35b62e..d4ba2cd18 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -2,11 +2,10 @@ use anchor_lang::prelude::*; use anchor_spl::token::{Token, TokenAccount}; use crate::error::*; -use crate::instructions::apply_vault_difference; +use crate::instructions::{apply_vault_difference, charge_loan_origination_fees, OpenOrdersSlim}; +use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; -use crate::logs::LoanOriginationFeeInstruction; - #[derive(Accounts)] pub struct Serum3LiqForceCancelOrders<'info> { pub group: AccountLoader<'info, Group>, @@ -116,6 +115,26 @@ pub fn serum3_liq_force_cancel_orders( require!(health < 0, MangoError::SomeError); } + // + // Charge any open loan origination fees + // + { + let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?; + let before_oo = OpenOrdersSlim::from_oo(&open_orders); + let mut account = ctx.accounts.account.load_mut()?; + let mut base_bank = ctx.accounts.base_bank.load_mut()?; + let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; + charge_loan_origination_fees( + &ctx.accounts.group.key(), + &ctx.accounts.account.key(), + serum_market.market_index, + &mut base_bank, + &mut quote_bank, + &mut account.borrow_mut(), + &before_oo, + )?; + } + // // Before-settle tracking // @@ -136,12 +155,17 @@ pub fn serum3_liq_force_cancel_orders( let after_base_vault = ctx.accounts.base_vault.amount; let after_quote_vault = ctx.accounts.quote_vault.amount; - // Charge the difference in vault balances to the user's account + // Settle cannot decrease vault balances + require_gte!(after_base_vault, before_base_vault); + require_gte!(after_quote_vault, before_quote_vault); + + // Credit the difference in vault balances to the user's account let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - let difference_result = apply_vault_difference( + apply_vault_difference( &mut account.borrow_mut(), + serum_market.market_index, &mut base_bank, after_base_vault, before_base_vault, @@ -149,11 +173,6 @@ pub fn serum3_liq_force_cancel_orders( after_quote_vault, before_quote_vault, )?; - difference_result.log_loan_origination_fees( - &ctx.accounts.group.key(), - &ctx.accounts.account.key(), - LoanOriginationFeeInstruction::Serum3LiqForceCancelOrders, - ); Ok(()) } diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 6c788419a..7a282c2d1 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -12,15 +12,14 @@ use serum_dex::instruction::NewOrderInstructionV3; use serum_dex::matching::Side; use serum_dex::state::OpenOrders; -use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; - /// For loan origination fees bookkeeping purposes +#[derive(Debug)] pub struct OpenOrdersSlim { - pub native_coin_free: u64, - pub native_coin_total: u64, - pub native_pc_free: u64, - pub native_pc_total: u64, - pub referrer_rebates_accrued: u64, + native_coin_free: u64, + native_coin_total: u64, + native_pc_free: u64, + native_pc_total: u64, + referrer_rebates_accrued: u64, } impl OpenOrdersSlim { pub fn from_oo(oo: &OpenOrders) -> Self { @@ -38,7 +37,10 @@ pub trait OpenOrdersAmounts { fn native_base_reserved(&self) -> u64; fn native_quote_reserved(&self) -> u64; fn native_base_free(&self) -> u64; - fn native_quote_free(&self) -> u64; // includes settleable referrer rebates + fn native_quote_free(&self) -> u64; + fn native_quote_free_plus_rebates(&self) -> u64; + fn native_base_total(&self) -> u64; + fn native_quote_total_plus_rebates(&self) -> u64; } impl OpenOrdersAmounts for OpenOrdersSlim { @@ -52,8 +54,17 @@ impl OpenOrdersAmounts for OpenOrdersSlim { self.native_coin_free } fn native_quote_free(&self) -> u64 { + self.native_pc_free + } + fn native_quote_free_plus_rebates(&self) -> u64 { cm!(self.native_pc_free + self.referrer_rebates_accrued) } + fn native_base_total(&self) -> u64 { + self.native_coin_total + } + fn native_quote_total_plus_rebates(&self) -> u64 { + cm!(self.native_pc_total + self.referrer_rebates_accrued) + } } impl OpenOrdersAmounts for OpenOrders { @@ -67,8 +78,17 @@ impl OpenOrdersAmounts for OpenOrders { self.native_coin_free } fn native_quote_free(&self) -> u64 { + self.native_pc_free + } + fn native_quote_free_plus_rebates(&self) -> u64 { cm!(self.native_pc_free + self.referrer_rebates_accrued) } + fn native_base_total(&self) -> u64 { + self.native_coin_total + } + fn native_quote_total_plus_rebates(&self) -> u64 { + cm!(self.native_pc_total + self.referrer_rebates_accrued) + } } /// Copy paste a bunch of enums so that we could AnchorSerialize & AnchorDeserialize them @@ -222,27 +242,6 @@ pub fn serum3_place_order( ); } - // - // Before-order tracking - // - - let before_base_vault = ctx.accounts.base_vault.amount; - let before_quote_vault = ctx.accounts.quote_vault.amount; - - // Provide a readable error message in case the vault doesn't have enough tokens - let (vault_amount, needed_amount) = match side { - Serum3Side::Ask => (before_base_vault, max_base_qty), - Serum3Side::Bid => (before_quote_vault, max_native_quote_qty_including_fees), - }; - if vault_amount < needed_amount { - return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { - format!( - "bank vault does not have enough tokens, need {} but have {}", - needed_amount, vault_amount - ) - }); - } - // // Pre-health computation // @@ -259,8 +258,40 @@ pub fn serum3_place_order( }; // - // Apply the order to serum. Also immediately settle, in case the order - // matched against an existing other order. + // Before-order tracking + // + + let before_base_vault = ctx.accounts.base_vault.amount; + let before_quote_vault = ctx.accounts.quote_vault.amount; + + let before_oo = { + let oo_ai = &ctx.accounts.open_orders.as_ref(); + let open_orders = load_open_orders_ref(oo_ai)?; + OpenOrdersSlim::from_oo(&open_orders) + }; + + // Provide a readable error message in case the vault doesn't have enough tokens + let (vault_amount, needed_amount) = match side { + Serum3Side::Ask => ( + before_base_vault, + max_base_qty.saturating_sub(before_oo.native_base_free()), + ), + Serum3Side::Bid => ( + before_quote_vault, + max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free()), + ), + }; + if vault_amount < needed_amount { + return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { + format!( + "bank vault does not have enough tokens, need {} but have {}", + needed_amount, vault_amount + ) + }); + } + + // + // Apply the order to serum // let order = serum_dex::instruction::NewOrderInstructionV3 { side: u8::try_from(side).unwrap().try_into().unwrap(), @@ -275,26 +306,12 @@ pub fn serum3_place_order( client_order_id, limit, }; - - let before_oo = { - let oo_ai = &ctx.accounts.open_orders.as_ref(); - let open_orders = load_open_orders_ref(oo_ai)?; - OpenOrdersSlim::from_oo(&open_orders) - }; - cpi_place_order(ctx.accounts, order)?; - cpi_settle_funds(ctx.accounts)?; let oo_difference = { let oo_ai = &ctx.accounts.open_orders.as_ref(); let open_orders = load_open_orders_ref(oo_ai)?; let after_oo = OpenOrdersSlim::from_oo(&open_orders); - inc_maybe_loan( - serum_market.market_index, - &mut account.borrow_mut(), - &before_oo, - &after_oo, - ); OODifference::new(&before_oo, &after_oo) }; @@ -306,6 +323,10 @@ pub fn serum3_place_order( let after_base_vault = ctx.accounts.base_vault.amount; let after_quote_vault = ctx.accounts.quote_vault.amount; + // Placing an order cannot increase vault balances + require_gte!(before_base_vault, after_base_vault); + require_gte!(before_quote_vault, after_quote_vault); + // Charge the difference in vault balances to the user's account let vault_difference = { let mut base_bank = ctx.accounts.base_bank.load_mut()?; @@ -313,6 +334,7 @@ pub fn serum3_place_order( apply_vault_difference( &mut account.borrow_mut(), + serum_market.market_index, &mut base_bank, after_base_vault, before_base_vault, @@ -331,39 +353,9 @@ pub fn serum3_place_order( account.check_health_post(&health_cache, pre_health)?; } - vault_difference.log_loan_origination_fees( - &ctx.accounts.group.key(), - &ctx.accounts.account.key(), - LoanOriginationFeeInstruction::Serum3PlaceOrder, - ); - Ok(()) } -// if reserved has increased, then increase cached value by the increase in reserved -pub fn inc_maybe_loan( - market_index: Serum3MarketIndex, - account: &mut MangoAccountRefMut, - before_oo: &OpenOrdersSlim, - after_oo: &OpenOrdersSlim, -) { - let serum3_account = account.serum3_orders_mut(market_index).unwrap(); - - if after_oo.native_base_reserved() > before_oo.native_base_reserved() { - let native_coin_reserved_increase = - after_oo.native_base_reserved() - before_oo.native_base_reserved(); - serum3_account.previous_native_coin_reserved = - cm!(serum3_account.previous_native_coin_reserved + native_coin_reserved_increase); - } - - if after_oo.native_quote_reserved() > before_oo.native_quote_reserved() { - let reserved_pc_increase = - after_oo.native_quote_reserved() - before_oo.native_quote_reserved(); - serum3_account.previous_native_pc_reserved = - cm!(serum3_account.previous_native_pc_reserved + reserved_pc_increase); - } -} - pub struct OODifference { reserved_base_change: I80F48, reserved_quote_change: I80F48, @@ -380,8 +372,8 @@ impl OODifference { - I80F48::from(before_oo.native_quote_reserved())), free_base_change: cm!(I80F48::from(after_oo.native_base_free()) - I80F48::from(before_oo.native_base_free())), - free_quote_change: cm!(I80F48::from(after_oo.native_quote_free()) - - I80F48::from(before_oo.native_quote_free())), + free_quote_change: cm!(I80F48::from(after_oo.native_quote_free_plus_rebates()) + - I80F48::from(before_oo.native_quote_free_plus_rebates())), } } @@ -402,55 +394,14 @@ impl OODifference { } } -pub struct VaultDifferenceResult { - base_raw_index: usize, +pub struct VaultDifference { base_index: TokenIndex, - base_active: bool, - quote_raw_index: usize, quote_index: TokenIndex, - quote_active: bool, - base_loan_origination_fee: I80F48, - quote_loan_origination_fee: I80F48, base_native_change: I80F48, quote_native_change: I80F48, } -impl VaultDifferenceResult { - pub fn deactivate_inactive_token_accounts(&self, account: &mut MangoAccountRefMut) { - if !self.base_active { - account.deactivate_token_position(self.base_raw_index); - } - if !self.quote_active { - account.deactivate_token_position(self.quote_raw_index); - } - } - - pub fn log_loan_origination_fees( - &self, - group: &Pubkey, - account: &Pubkey, - instruction: LoanOriginationFeeInstruction, - ) { - if self.base_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: *group, - mango_account: *account, - token_index: self.base_index, - loan_origination_fee: self.base_loan_origination_fee.to_bits(), - instruction, - }); - } - if self.quote_loan_origination_fee.is_positive() { - emit!(WithdrawLoanOriginationFeeLog { - mango_group: *group, - mango_account: *account, - token_index: self.quote_index, - loan_origination_fee: self.quote_loan_origination_fee.to_bits(), - instruction, - }); - } - } - +impl VaultDifference { pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> { health_cache.adjust_token_balance(self.base_index, self.base_native_change)?; health_cache.adjust_token_balance(self.quote_index, self.quote_native_change)?; @@ -458,45 +409,68 @@ impl VaultDifferenceResult { } } +/// Called in settle_funds, place_order, liq_force_cancel to adjust token positions after +/// changing the vault balances pub fn apply_vault_difference( account: &mut MangoAccountRefMut, + serum_market_index: Serum3MarketIndex, base_bank: &mut Bank, after_base_vault: u64, before_base_vault: u64, quote_bank: &mut Bank, after_quote_vault: u64, before_quote_vault: u64, -) -> Result { - // TODO: Applying the loan origination fee here may be too early: it should only be - // charged if an order executes and the loan materializes? Otherwise MMs that place - // an order without having the funds will be charged for each place_order! - - let (base_position, base_raw_index) = account.token_position_mut(base_bank.token_index)?; - let base_native_before = base_position.native(&base_bank); +) -> Result { let base_needed_change = cm!(I80F48::from(after_base_vault) - I80F48::from(before_base_vault)); - let (base_active, base_loan_origination_fee) = - base_bank.change_with_fee(base_position, base_needed_change)?; - let base_native_after = base_position.native(&base_bank); - let (quote_position, quote_raw_index) = account.token_position_mut(quote_bank.token_index)?; - let quote_native_before = quote_position.native("e_bank); + let (base_position, _) = account.token_position_mut(base_bank.token_index)?; + let base_native_before = base_position.native(&base_bank); + base_bank.change_without_fee(base_position, base_needed_change)?; + let base_native_after = base_position.native(&base_bank); + let base_native_change = cm!(base_native_after - base_native_before); + let base_borrows = base_native_change + .max(base_native_after) + .min(I80F48::ZERO) + .abs() + .to_num::(); + let quote_needed_change = cm!(I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault)); - let (quote_active, quote_loan_origination_fee) = - quote_bank.change_with_fee(quote_position, quote_needed_change)?; - let quote_native_after = quote_position.native("e_bank); - Ok(VaultDifferenceResult { - base_raw_index, + let (quote_position, _) = account.token_position_mut(quote_bank.token_index)?; + let quote_native_before = quote_position.native("e_bank); + quote_bank.change_without_fee(quote_position, quote_needed_change)?; + let quote_native_after = quote_position.native("e_bank); + let quote_native_change = cm!(quote_native_after - quote_native_before); + let quote_borrows = quote_native_change + .max(quote_native_after) + .min(I80F48::ZERO) + .abs() + .to_num::(); + + let market = account.serum3_orders_mut(serum_market_index).unwrap(); + + // Only for place: Add to potential borrow amounts + market.base_borrows_without_fee = cm!(market.base_borrows_without_fee + base_borrows); + market.quote_borrows_without_fee = cm!(market.quote_borrows_without_fee + quote_borrows); + + // Only for settle/liq_force_cancel: Reduce the potential borrow amounts + if base_needed_change > 0 { + market.base_borrows_without_fee = market + .base_borrows_without_fee + .saturating_sub(base_needed_change.to_num::()); + } + if quote_needed_change > 0 { + market.quote_borrows_without_fee = market + .quote_borrows_without_fee + .saturating_sub(quote_needed_change.to_num::()); + } + + Ok(VaultDifference { base_index: base_bank.token_index, - base_active, - quote_raw_index, quote_index: quote_bank.token_index, - quote_active, - base_loan_origination_fee, - quote_loan_origination_fee, - base_native_change: cm!(base_native_after - base_native_before), - quote_native_change: cm!(quote_native_after - quote_native_before), + base_native_change, + quote_native_change, }) } @@ -526,21 +500,3 @@ fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Resu } .call(&group, order) } - -fn cpi_settle_funds(ctx: &Serum3PlaceOrder) -> Result<()> { - use crate::serum3_cpi; - let group = ctx.group.load()?; - serum3_cpi::SettleFunds { - program: ctx.serum_program.to_account_info(), - market: ctx.serum_market_external.to_account_info(), - open_orders: ctx.open_orders.to_account_info(), - open_orders_authority: ctx.group.to_account_info(), - base_vault: ctx.market_base_vault.to_account_info(), - quote_vault: ctx.market_quote_vault.to_account_info(), - user_base_wallet: ctx.base_vault.to_account_info(), - user_quote_wallet: ctx.quote_vault.to_account_info(), - vault_signer: ctx.market_vault_signer.to_account_info(), - token_program: ctx.token_program.to_account_info(), - } - .call(&group) -} diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 32ca92e92..e50d43276 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -1,5 +1,3 @@ -use std::borrow::BorrowMut; - use anchor_lang::prelude::*; use anchor_spl::token::{Token, TokenAccount}; @@ -10,7 +8,7 @@ use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; use super::{apply_vault_difference, OpenOrdersAmounts, OpenOrdersSlim}; -use crate::logs::LoanOriginationFeeInstruction; +use crate::logs::{LoanOriginationFeeInstruction, WithdrawLoanOriginationFeeLog}; #[derive(Accounts)] pub struct Serum3SettleFunds<'info> { @@ -116,34 +114,35 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { } // - // Before-order tracking - // - - let before_base_vault = ctx.accounts.base_vault.amount; - let before_quote_vault = ctx.accounts.quote_vault.amount; - - // - // Settle + // Charge any open loan origination fees // { let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?; - cpi_settle_funds(ctx.accounts)?; - - let after_oo = OpenOrdersSlim::from_oo(&open_orders); + let before_oo = OpenOrdersSlim::from_oo(&open_orders); let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - charge_maybe_fees( + charge_loan_origination_fees( + &ctx.accounts.group.key(), + &ctx.accounts.account.key(), serum_market.market_index, &mut base_bank, &mut quote_bank, &mut account.borrow_mut(), - &after_oo, + &before_oo, )?; } // - // After-order tracking + // Settle + // + let before_base_vault = ctx.accounts.base_vault.amount; + let before_quote_vault = ctx.accounts.quote_vault.amount; + + cpi_settle_funds(ctx.accounts)?; + + // + // After-settle tracking // { ctx.accounts.base_vault.reload()?; @@ -151,12 +150,17 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { let after_base_vault = ctx.accounts.base_vault.amount; let after_quote_vault = ctx.accounts.quote_vault.amount; - // Charge the difference in vault balances to the user's account + // Settle cannot decrease vault balances + require_gte!(after_base_vault, before_base_vault); + require_gte!(after_quote_vault, before_quote_vault); + + // Credit the difference in vault balances to the user's account let mut account = ctx.accounts.account.load_mut()?; let mut base_bank = ctx.accounts.base_bank.load_mut()?; let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - let difference_result = apply_vault_difference( + apply_vault_difference( &mut account.borrow_mut(), + serum_market.market_index, &mut base_bank, after_base_vault, before_base_vault, @@ -164,73 +168,70 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { after_quote_vault, before_quote_vault, )?; - difference_result.log_loan_origination_fees( - &ctx.accounts.group.key(), - &ctx.accounts.account.key(), - LoanOriginationFeeInstruction::Serum3SettleFunds, - ); } Ok(()) } -// if reserved is less than cached, charge loan fee on the difference -pub fn charge_maybe_fees( +// Charge fees if the potential borrows are bigger than the funds on the open orders account +pub fn charge_loan_origination_fees( + group_pubkey: &Pubkey, + account_pubkey: &Pubkey, market_index: Serum3MarketIndex, - coin_bank: &mut Bank, - pc_bank: &mut Bank, + base_bank: &mut Bank, + quote_bank: &mut Bank, account: &mut MangoAccountRefMut, - after_oo: &OpenOrdersSlim, + before_oo: &OpenOrdersSlim, ) -> Result<()> { let serum3_account = account.serum3_orders_mut(market_index).unwrap(); - let maybe_actualized_coin_loan = I80F48::from_num::( + let oo_base_total = before_oo.native_base_total(); + let actualized_base_loan = I80F48::from_num( serum3_account - .previous_native_coin_reserved - .saturating_sub(after_oo.native_base_reserved()), + .base_borrows_without_fee + .saturating_sub(oo_base_total), ); + if actualized_base_loan > 0 { + serum3_account.base_borrows_without_fee = oo_base_total; - if maybe_actualized_coin_loan > 0 { - serum3_account.previous_native_coin_reserved = after_oo.native_base_reserved(); + // now that the loan is actually materialized, charge the loan origination fee + // note: the withdraw has already happened while placing the order + let base_token_account = account.token_position_mut(base_bank.token_index)?.0; + let (_, fee) = + base_bank.withdraw_loan_origination_fee(base_token_account, actualized_base_loan)?; - // loan origination fees - let coin_token_account = account.token_position_mut(coin_bank.token_index)?.0; - let coin_token_native = coin_token_account.native(coin_bank); - - if coin_token_native.is_negative() { - let actualized_loan = coin_token_native.abs().min(maybe_actualized_coin_loan); - // note: the withdraw has already happened while placing the order - // now that the loan is actually materialized (since the fill having taken place) - // charge the loan origination fee - coin_bank - .borrow_mut() - .withdraw_loan_origination_fee(coin_token_account, actualized_loan)?; - } + emit!(WithdrawLoanOriginationFeeLog { + mango_group: *group_pubkey, + mango_account: *account_pubkey, + token_index: base_bank.token_index, + loan_origination_fee: fee.to_bits(), + instruction: LoanOriginationFeeInstruction::Serum3SettleFunds, + }); } let serum3_account = account.serum3_orders_mut(market_index).unwrap(); - let maybe_actualized_pc_loan = I80F48::from_num::( + let oo_quote_total = before_oo.native_quote_total_plus_rebates(); + let actualized_quote_loan = I80F48::from_num::( serum3_account - .previous_native_pc_reserved - .saturating_sub(after_oo.native_quote_reserved()), + .quote_borrows_without_fee + .saturating_sub(oo_quote_total), ); + if actualized_quote_loan > 0 { + serum3_account.quote_borrows_without_fee = oo_quote_total; - if maybe_actualized_pc_loan > 0 { - serum3_account.previous_native_pc_reserved = after_oo.native_quote_reserved(); + // now that the loan is actually materialized, charge the loan origination fee + // note: the withdraw has already happened while placing the order + let quote_token_account = account.token_position_mut(quote_bank.token_index)?.0; + let (_, fee) = + quote_bank.withdraw_loan_origination_fee(quote_token_account, actualized_quote_loan)?; - // loan origination fees - let pc_token_account = account.token_position_mut(pc_bank.token_index)?.0; - let pc_token_native = pc_token_account.native(pc_bank); - - if pc_token_native.is_negative() { - let actualized_loan = pc_token_native.abs().min(maybe_actualized_pc_loan); - // note: the withdraw has already happened while placing the order - // now that the loan is actually materialized (since the fill having taken place) - // charge the loan origination fee - pc_bank - .borrow_mut() - .withdraw_loan_origination_fee(pc_token_account, actualized_loan)?; - } + emit!(WithdrawLoanOriginationFeeLog { + mango_group: *group_pubkey, + mango_account: *account_pubkey, + token_index: quote_bank.token_index, + loan_origination_fee: fee.to_bits(), + instruction: LoanOriginationFeeInstruction::Serum3SettleFunds, + }); } Ok(()) diff --git a/programs/mango-v4/src/state/mango_account_components.rs b/programs/mango-v4/src/state/mango_account_components.rs index 3bf2e606b..b13d3b46b 100644 --- a/programs/mango-v4/src/state/mango_account_components.rs +++ b/programs/mango-v4/src/state/mango_account_components.rs @@ -92,12 +92,12 @@ impl TokenPosition { pub struct Serum3Orders { pub open_orders: Pubkey, - // tracks reserved funds in open orders account, - // used for bookkeeping of potentital loans which - // can be charged with loan origination fees - // e.g. serum3 settle funds ix - pub previous_native_coin_reserved: u64, - pub previous_native_pc_reserved: u64, + /// Tracks the amount of borrows that have flowed into the serum open orders account. + /// These borrows did not have the loan origination fee applied, and that may happen + /// later (in serum3_settle_funds) if we can guarantee that the funds were used. + /// In particular a place-on-book, cancel, settle should not cost fees. + pub base_borrows_without_fee: u64, + pub quote_borrows_without_fee: u64, pub market_index: Serum3MarketIndex, @@ -138,8 +138,8 @@ impl Default for Serum3Orders { quote_token_index: TokenIndex::MAX, reserved: [0; 64], padding: Default::default(), - previous_native_coin_reserved: 0, - previous_native_pc_reserved: 0, + base_borrows_without_fee: 0, + quote_borrows_without_fee: 0, } } } diff --git a/programs/mango-v4/tests/program_test/serum.rs b/programs/mango-v4/tests/program_test/serum.rs index 7d78675dc..5704567ff 100644 --- a/programs/mango-v4/tests/program_test/serum.rs +++ b/programs/mango-v4/tests/program_test/serum.rs @@ -190,16 +190,16 @@ impl SerumCookie { pub async fn consume_spot_events( &self, spot_market_cookie: &SpotMarketCookie, - open_orders: Pubkey, + open_orders: &[Pubkey], ) { let instructions = [serum_dex::instruction::consume_events( &self.program_id, - vec![&open_orders], + open_orders.iter().collect(), &spot_market_cookie.market, &spot_market_cookie.event_q, &spot_market_cookie.coin_fee_account, &spot_market_cookie.pc_fee_account, - 5, + 10, ) .unwrap()]; self.solana diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index cf2b71b27..50f452f49 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -1,17 +1,153 @@ #![cfg(feature = "test-bpf")] use solana_program_test::*; -use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError}; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError}; -use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; +use mango_v4::instructions::{ + OpenOrdersSlim, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side, +}; +use mango_v4::state::Serum3Orders; use program_test::*; mod program_test; use mango_setup::*; +use std::sync::Arc; + +struct SerumOrderPlacer { + solana: Arc, + serum: Arc, + account: Pubkey, + owner: Keypair, + serum_market: Pubkey, + open_orders: Pubkey, + next_client_order_id: u64, +} + +impl SerumOrderPlacer { + fn inc_client_order_id(&mut self) -> u64 { + let id = self.next_client_order_id; + self.next_client_order_id += 1; + id + } + + async fn find_order_id_for_client_order_id(&self, client_order_id: u64) -> Option<(u128, u64)> { + let open_orders = self.serum.load_open_orders(self.open_orders).await; + for i in 0..128 { + if open_orders.free_slot_bits & (1u128 << i) != 0 { + continue; + } + if open_orders.client_order_ids[i] == client_order_id { + return Some((open_orders.orders[i], client_order_id)); + } + } + None + } + + async fn bid(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> { + let client_order_id = self.inc_client_order_id(); + send_tx( + &self.solana, + Serum3PlaceOrderInstruction { + side: Serum3Side::Bid, + limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100) + max_base_qty: max_base / 100, // in base lot (100) + max_native_quote_qty_including_fees: (limit_price * (max_base as f64)) as u64, + self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction, + order_type: Serum3OrderType::Limit, + client_order_id, + limit: 10, + account: self.account, + owner: &self.owner, + serum_market: self.serum_market, + }, + ) + .await + .unwrap(); + self.find_order_id_for_client_order_id(client_order_id) + .await + } + + async fn ask(&mut self, limit_price: f64, max_base: u64) -> Option<(u128, u64)> { + let client_order_id = self.inc_client_order_id(); + send_tx( + &self.solana, + Serum3PlaceOrderInstruction { + side: Serum3Side::Ask, + limit_price: (limit_price * 100.0 / 10.0) as u64, // in quote_lot (10) per base lot (100) + max_base_qty: max_base / 100, // in base lot (100) + max_native_quote_qty_including_fees: (limit_price * (max_base as f64)) as u64, + self_trade_behavior: Serum3SelfTradeBehavior::AbortTransaction, + order_type: Serum3OrderType::Limit, + client_order_id, + limit: 10, + account: self.account, + owner: &self.owner, + serum_market: self.serum_market, + }, + ) + .await + .unwrap(); + self.find_order_id_for_client_order_id(client_order_id) + .await + } + + async fn cancel(&self, order_id: u128) { + let side = { + let open_orders = self.serum.load_open_orders(self.open_orders).await; + let orders = open_orders.orders; + let idx = orders.iter().position(|&v| v == order_id).unwrap(); + if open_orders.is_bid_bits & (1u128 << idx) == 0 { + Serum3Side::Ask + } else { + Serum3Side::Bid + } + }; + send_tx( + &self.solana, + Serum3CancelOrderInstruction { + side, + order_id, + account: self.account, + owner: &self.owner, + serum_market: self.serum_market, + }, + ) + .await + .unwrap(); + } + + async fn settle(&self) { + // to avoid multiple settles looking like the same tx + self.solana.advance_by_slots(1).await; + send_tx( + &self.solana, + Serum3SettleFundsInstruction { + account: self.account, + owner: &self.owner, + serum_market: self.serum_market, + }, + ) + .await + .unwrap(); + } + + async fn mango_serum_orders(&self) -> Serum3Orders { + let account_data = get_mango_account(&self.solana, self.account).await; + let orders = account_data + .all_serum3_orders() + .find(|s| s.open_orders == self.open_orders) + .unwrap(); + orders.clone() + } + + async fn open_orders(&self) -> OpenOrdersSlim { + OpenOrdersSlim::from_oo(&self.serum.load_open_orders(self.open_orders).await) + } +} #[tokio::test] -async fn test_serum() -> Result<(), TransportError> { +async fn test_serum_basics() -> Result<(), TransportError> { let mut test_builder = TestContextBuilder::new(); test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k let context = test_builder.start_default().await; @@ -36,19 +172,6 @@ async fn test_serum() -> Result<(), TransportError> { let base_token = &tokens[0]; let quote_token = &tokens[1]; - let deposit_amount = 1000; - let account = create_funded_account( - &solana, - group, - owner, - 0, - &context.users[1], - mints, - deposit_amount, - 0, - ) - .await; - // // SETUP: Create serum market // @@ -77,6 +200,22 @@ async fn test_serum() -> Result<(), TransportError> { .unwrap() .serum_market; + // + // SETUP: Create account + // + let deposit_amount = 1000; + let account = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + mints, + deposit_amount, + 0, + ) + .await; + // // TEST: Create an open orders account // @@ -102,28 +241,20 @@ async fn test_serum() -> Result<(), TransportError> { [(open_orders, 0)] ); + let mut order_placer = SerumOrderPlacer { + solana: solana.clone(), + serum: context.serum.clone(), + account, + owner: owner.clone(), + serum_market, + open_orders, + next_client_order_id: 0, + }; + // // TEST: Place an order // - send_tx( - solana, - Serum3PlaceOrderInstruction { - side: Serum3Side::Bid, - limit_price: 10, // in quote_lot (10) per base lot (100) - max_base_qty: 1, // in base lot (100) - max_native_quote_qty_including_fees: 100, - self_trade_behavior: Serum3SelfTradeBehavior::DecrementTake, - order_type: Serum3OrderType::Limit, - client_order_id: 0, - limit: 10, - account, - owner, - serum_market, - }, - ) - .await - .unwrap(); - + let (order_id, _) = order_placer.bid(1.0, 100).await.unwrap(); check_prev_instruction_post_health(&solana, account).await; let native0 = account_position(solana, account, base_token.bank).await; @@ -131,62 +262,46 @@ async fn test_serum() -> Result<(), TransportError> { assert_eq!(native0, 1000); assert_eq!(native1, 900); - // get the order id - let open_orders_bytes = solana.get_account_data(open_orders).await.unwrap(); - let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes( - &open_orders_bytes[5..5 + std::mem::size_of::()], - ); - let order_id = open_orders_data.orders[0]; + let account_data = get_mango_account(solana, account).await; + let serum_orders = account_data.serum3_orders_by_raw_index(0); + assert_eq!(serum_orders.base_borrows_without_fee, 0); + assert_eq!(serum_orders.quote_borrows_without_fee, 0); + assert!(order_id != 0); // // TEST: Cancel the order // - send_tx( - solana, - Serum3CancelOrderInstruction { - side: Serum3Side::Bid, - order_id, - account, - owner, - serum_market, - }, - ) - .await - .unwrap(); + order_placer.cancel(order_id).await; // // TEST: Settle, moving the freed up funds back // - send_tx( - solana, - Serum3SettleFundsInstruction { - account, - owner, - serum_market, - }, - ) - .await - .unwrap(); + order_placer.settle().await; let native0 = account_position(solana, account, base_token.bank).await; let native1 = account_position(solana, account, quote_token.bank).await; assert_eq!(native0, 1000); assert_eq!(native1, 1000); + // Process events such that the OutEvent deactivates the closed order on open_orders + context + .serum + .consume_spot_events(&serum_market_cookie, &[open_orders]) + .await; + // close oo account - // TODO: custom program error: 0x2a TooManyOpenOrders https://github.com/project-serum/serum-dex/blob/master/dex/src/error.rs#L88 - // send_tx( - // solana, - // Serum3CloseOpenOrdersInstruction { - // account, - // serum_market, - // owner, - // sol_destination: payer.pubkey(), - // }, - // ) - // .await - // .unwrap(); + send_tx( + solana, + Serum3CloseOpenOrdersInstruction { + account, + serum_market, + owner, + sol_destination: payer.pubkey(), + }, + ) + .await + .unwrap(); // deregister serum3 market send_tx( @@ -203,3 +318,257 @@ async fn test_serum() -> Result<(), TransportError> { Ok(()) } + +#[tokio::test] +async fn test_serum_loan_origination_fees() -> Result<(), TransportError> { + let mut test_builder = TestContextBuilder::new(); + test_builder.test().set_compute_max_units(95_000); // Serum3PlaceOrder needs 92.8k + let context = test_builder.start_default().await; + let solana = &context.solana.clone(); + + let admin = &Keypair::new(); + let owner = &context.users[0].key; + let payer = &context.users[1].key; + let mints = &context.mints[0..3]; + + // + // SETUP: Create a group and an account + // + + let GroupWithTokens { group, tokens, .. } = GroupWithTokensConfig { + admin, + payer, + mints, + } + .create(solana) + .await; + let base_token = &tokens[0]; + let base_bank = base_token.bank; + let quote_token = &tokens[1]; + let quote_bank = quote_token.bank; + + // + // SETUP: Create serum market + // + let serum_market_cookie = context + .serum + .list_spot_market(&base_token.mint, "e_token.mint) + .await; + + // + // SETUP: Register a serum market + // + let serum_market = send_tx( + solana, + Serum3RegisterMarketInstruction { + group, + admin, + serum_program: context.serum.program_id, + serum_market_external: serum_market_cookie.market, + market_index: 0, + base_bank: base_token.bank, + quote_bank: quote_token.bank, + payer, + }, + ) + .await + .unwrap() + .serum_market; + + // + // SETUP: Create accounts + // + let deposit_amount = 180000; + let account = create_funded_account( + &solana, + group, + owner, + 0, + &context.users[1], + mints, + deposit_amount, + 0, + ) + .await; + let account2 = create_funded_account( + &solana, + group, + owner, + 2, + &context.users[1], + mints, + deposit_amount, + 0, + ) + .await; + // to have enough funds in the vaults + create_funded_account( + &solana, + group, + owner, + 3, + &context.users[1], + mints, + 10000000, + 0, + ) + .await; + + let open_orders = send_tx( + solana, + Serum3CreateOpenOrdersInstruction { + account, + serum_market, + owner, + payer, + }, + ) + .await + .unwrap() + .open_orders; + + let open_orders2 = send_tx( + solana, + Serum3CreateOpenOrdersInstruction { + account: account2, + serum_market, + owner, + payer, + }, + ) + .await + .unwrap() + .open_orders; + + let mut order_placer = SerumOrderPlacer { + solana: solana.clone(), + serum: context.serum.clone(), + account, + owner: owner.clone(), + serum_market, + open_orders, + next_client_order_id: 0, + }; + let mut order_placer2 = SerumOrderPlacer { + solana: solana.clone(), + serum: context.serum.clone(), + account: account2, + owner: owner.clone(), + serum_market, + open_orders: open_orders2, + next_client_order_id: 100000, + }; + + // + // TEST: Placing and canceling an order does not take loan origination fees even if borrows are needed + // + { + let (bid_order_id, _) = order_placer.bid(1.0, 200000).await.unwrap(); + let (ask_order_id, _) = order_placer.ask(2.0, 200000).await.unwrap(); + + let o = order_placer.mango_serum_orders().await; + assert_eq!(o.base_borrows_without_fee, 19999); // rounded + assert_eq!(o.quote_borrows_without_fee, 19999); + + order_placer.cancel(bid_order_id).await; + order_placer.cancel(ask_order_id).await; + + let o = order_placer.mango_serum_orders().await; + assert_eq!(o.base_borrows_without_fee, 19999); // unchanged + assert_eq!(o.quote_borrows_without_fee, 19999); + + // placing new, slightly larger orders increases the borrow_without_fee amount only by a small amount + let (bid_order_id, _) = order_placer.bid(1.0, 210000).await.unwrap(); + let (ask_order_id, _) = order_placer.ask(2.0, 300000).await.unwrap(); + + let o = order_placer.mango_serum_orders().await; + assert_eq!(o.base_borrows_without_fee, 119998); // rounded + assert_eq!(o.quote_borrows_without_fee, 29998); + + order_placer.cancel(bid_order_id).await; + order_placer.cancel(ask_order_id).await; + + // returns all the funds + order_placer.settle().await; + + let o = order_placer.mango_serum_orders().await; + assert_eq!(o.base_borrows_without_fee, 0); + assert_eq!(o.quote_borrows_without_fee, 0); + + assert_eq!( + account_position(solana, account, quote_bank).await, + deposit_amount as i64 + ); + assert_eq!( + account_position(solana, account, base_bank).await, + deposit_amount as i64 + ); + + // consume all the out events from the cancels + context + .serum + .consume_spot_events(&serum_market_cookie, &[open_orders]) + .await; + } + + let without_serum_taker_fee = |amount: i64| (amount as f64 * (1.0 - 0.0022)).trunc() as i64; + let serum_maker_rebate = |amount: i64| (amount as f64 * 0.0003).round() as i64; + let loan_origination_fee = |amount: i64| (amount as f64 * 0.0005).trunc() as i64; + + // + // TEST: Order execution and settling charges borrow fee + // + { + let deposit_amount = deposit_amount as i64; + let bid_amount = 200000; + let ask_amount = 210000; + let fill_amount = 200000; + + // account2 has an order on the book + order_placer2.bid(1.0, bid_amount as u64).await.unwrap(); + + // account takes + order_placer.ask(1.0, ask_amount as u64).await.unwrap(); + order_placer.settle().await; + + let o = order_placer.mango_serum_orders().await; + // parts of the order ended up on the book an may cause loan origination fees later + assert_eq!( + o.base_borrows_without_fee, + (ask_amount - fill_amount) as u64 + ); + assert_eq!(o.quote_borrows_without_fee, 0); + + assert_eq!( + account_position(solana, account, quote_bank).await, + deposit_amount + without_serum_taker_fee(fill_amount) + ); + assert_eq!( + account_position(solana, account, base_bank).await, + deposit_amount - ask_amount - loan_origination_fee(fill_amount - deposit_amount) + ); + + // check account2 balances too + context + .serum + .consume_spot_events(&serum_market_cookie, &[open_orders, open_orders2]) + .await; + order_placer2.settle().await; + + let o = order_placer2.mango_serum_orders().await; + assert_eq!(o.base_borrows_without_fee, 0); + assert_eq!(o.quote_borrows_without_fee, 0); + + assert_eq!( + account_position(solana, account2, base_bank).await, + deposit_amount + fill_amount + ); + assert_eq!( + account_position(solana, account2, quote_bank).await, + deposit_amount - fill_amount - loan_origination_fee(fill_amount - deposit_amount) + + serum_maker_rebate(fill_amount) + ); + } + + Ok(()) +} From 36723792a12ced32b731b21a67d495997feae66a Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 30 Aug 2022 08:37:53 +0200 Subject: [PATCH 34/50] SerumRegister: Block using the same market index twice --- .../instructions/serum3_register_market.rs | 12 +++ .../tests/program_test/mango_client.rs | 11 +++ ts/client/src/mango_v4.ts | 98 ++++++++++++++++--- 3 files changed, 105 insertions(+), 16 deletions(-) diff --git a/programs/mango-v4/src/instructions/serum3_register_market.rs b/programs/mango-v4/src/instructions/serum3_register_market.rs index 4d41688db..5dc86393f 100644 --- a/programs/mango-v4/src/instructions/serum3_register_market.rs +++ b/programs/mango-v4/src/instructions/serum3_register_market.rs @@ -6,6 +6,7 @@ use crate::state::*; use crate::util::fill_from_str; #[derive(Accounts)] +#[instruction(market_index: Serum3MarketIndex)] pub struct Serum3RegisterMarket<'info> { #[account( mut, @@ -30,6 +31,17 @@ pub struct Serum3RegisterMarket<'info> { )] pub serum_market: AccountLoader<'info, Serum3Market>, + /// CHECK: Unused account + #[account( + init, + // block using the same market index twice + seeds = [b"Serum3Index".as_ref(), group.key().as_ref(), &market_index.to_le_bytes()], + bump, + payer = payer, + space = 1, + )] + pub index_reservation: UncheckedAccount<'info>, + #[account(has_one = group)] pub quote_bank: AccountLoader<'info, Bank>, #[account(has_one = group)] diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index c98253b9c..2ae47747e 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1343,12 +1343,23 @@ impl<'keypair> ClientInstruction for Serum3RegisterMarketInstruction<'keypair> { ) .0; + let index_reservation = Pubkey::find_program_address( + &[ + b"Serum3Index".as_ref(), + self.group.as_ref(), + &self.market_index.to_le_bytes(), + ], + &program_id, + ) + .0; + let accounts = Self::Accounts { group: self.group, admin: self.admin.pubkey(), serum_program: self.serum_program, serum_market_external: self.serum_market_external, serum_market, + index_reservation, base_bank: self.base_bank, quote_bank: self.quote_bank, payer: self.payer.pubkey(), diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index 863d23890..e11622b3d 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1320,6 +1320,30 @@ export type MangoV4 = { ] } }, + { + "name": "indexReservation", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "Serum3Index" + }, + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "arg", + "type": "u16", + "path": "market_index" + } + ] + } + }, { "name": "quoteBank", "isMut": false, @@ -3853,6 +3877,10 @@ export type MangoV4 = { "type": { "kind": "struct", "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, { "name": "maintAssetWeight", "type": { @@ -5201,46 +5229,51 @@ export type MangoV4 = { }, { "code": 6007, + "name": "HealthMustBePositiveOrIncrease", + "msg": "health must be positive or increase" + }, + { + "code": 6008, "name": "HealthMustBeNegative", "msg": "health must be negative" }, { - "code": 6008, + "code": 6009, "name": "IsBankrupt", "msg": "the account is bankrupt" }, { - "code": 6009, + "code": 6010, "name": "IsNotBankrupt", "msg": "the account is not bankrupt" }, { - "code": 6010, + "code": 6011, "name": "NoFreeTokenPositionIndex", "msg": "no free token position index" }, { - "code": 6011, + "code": 6012, "name": "NoFreeSerum3OpenOrdersIndex", "msg": "no free serum3 open orders index" }, { - "code": 6012, + "code": 6013, "name": "NoFreePerpPositionIndex", "msg": "no free perp position index" }, { - "code": 6013, + "code": 6014, "name": "Serum3OpenOrdersExistAlready", "msg": "serum3 open orders exist already" }, { - "code": 6014, + "code": 6015, "name": "InsufficentBankVaultFunds", "msg": "bank vault has insufficent funds" }, { - "code": 6015, + "code": 6016, "name": "BeingLiquidated", "msg": "account is currently being liquidated" } @@ -6569,6 +6602,30 @@ export const IDL: MangoV4 = { ] } }, + { + "name": "indexReservation", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "const", + "type": "string", + "value": "Serum3Index" + }, + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "arg", + "type": "u16", + "path": "market_index" + } + ] + } + }, { "name": "quoteBank", "isMut": false, @@ -9102,6 +9159,10 @@ export const IDL: MangoV4 = { "type": { "kind": "struct", "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, { "name": "maintAssetWeight", "type": { @@ -10450,46 +10511,51 @@ export const IDL: MangoV4 = { }, { "code": 6007, + "name": "HealthMustBePositiveOrIncrease", + "msg": "health must be positive or increase" + }, + { + "code": 6008, "name": "HealthMustBeNegative", "msg": "health must be negative" }, { - "code": 6008, + "code": 6009, "name": "IsBankrupt", "msg": "the account is bankrupt" }, { - "code": 6009, + "code": 6010, "name": "IsNotBankrupt", "msg": "the account is not bankrupt" }, { - "code": 6010, + "code": 6011, "name": "NoFreeTokenPositionIndex", "msg": "no free token position index" }, { - "code": 6011, + "code": 6012, "name": "NoFreeSerum3OpenOrdersIndex", "msg": "no free serum3 open orders index" }, { - "code": 6012, + "code": 6013, "name": "NoFreePerpPositionIndex", "msg": "no free perp position index" }, { - "code": 6013, + "code": 6014, "name": "Serum3OpenOrdersExistAlready", "msg": "serum3 open orders exist already" }, { - "code": 6014, + "code": 6015, "name": "InsufficentBankVaultFunds", "msg": "bank vault has insufficent funds" }, { - "code": 6015, + "code": 6016, "name": "BeingLiquidated", "msg": "account is currently being liquidated" } From e0437305ee14d7b9e80549cd931026331cf529f5 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 30 Aug 2022 09:55:19 +0200 Subject: [PATCH 35/50] SerumPlaceOrder: Only pass the payer bank/vault --- client/src/client.rs | 10 +- .../serum3_liq_force_cancel_orders.rs | 4 + .../src/instructions/serum3_place_order.rs | 178 ++++++------------ .../src/instructions/serum3_settle_funds.rs | 4 + .../tests/program_test/mango_client.rs | 11 +- ts/client/src/client.ts | 17 +- ts/client/src/mango_v4.ts | 68 +++---- 7 files changed, 127 insertions(+), 165 deletions(-) diff --git a/client/src/client.rs b/client/src/client.rs index 61ba70ba4..b79965317 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -605,6 +605,10 @@ impl MangoClient { let rates = get_fee_rates(fee_tier); (s3.market.pc_lot_size as f64 * (1f64 + rates.0)) as u64 * (limit_price * max_base_qty) }; + let payer_mint_info = match side { + Serum3Side::Bid => s3.quote.mint_info, + Serum3Side::Ask => s3.base.mint_info, + }; self.program() .request() @@ -616,10 +620,8 @@ impl MangoClient { group: self.group(), account: self.mango_account_address, open_orders, - quote_bank: s3.quote.mint_info.first_bank(), - quote_vault: s3.quote.mint_info.first_vault(), - base_bank: s3.base.mint_info.first_bank(), - base_vault: s3.base.mint_info.first_vault(), + payer_bank: payer_mint_info.first_bank(), + payer_vault: payer_mint_info.first_vault(), serum_market: s3.market.address, serum_program: s3.market.market.serum_program, serum_market_external: s3.market.market.serum_market_external, diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index d4ba2cd18..94b1165c6 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -169,6 +169,10 @@ pub fn serum3_liq_force_cancel_orders( &mut base_bank, after_base_vault, before_base_vault, + )?; + apply_vault_difference( + &mut account.borrow_mut(), + serum_market.market_index, &mut quote_bank, after_quote_vault, before_quote_vault, diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 7a282c2d1..4a33b8c56 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -9,7 +9,6 @@ use fixed::types::I80F48; use num_enum::IntoPrimitive; use num_enum::TryFromPrimitive; use serum_dex::instruction::NewOrderInstructionV3; -use serum_dex::matching::Side; use serum_dex::state::OpenOrders; /// For loan origination fees bookkeeping purposes @@ -169,19 +168,13 @@ pub struct Serum3PlaceOrder<'info> { /// CHECK: Validated by the serum cpi call pub market_vault_signer: UncheckedAccount<'info>, - // TODO: do we need to pass both, or just payer? - // TODO: Can we reduce the number of accounts by requiring the banks - // to be in the remainingAccounts (where they need to be anyway, for - // health checks - but they need to be mut) - // token_index and bank.vault == vault is validated inline at #3 + /// The bank that pays for the order, if necessary + // token_index and payer_bank.vault == payer_vault is validated inline at #3 #[account(mut, has_one = group)] - pub quote_bank: AccountLoader<'info, Bank>, + pub payer_bank: AccountLoader<'info, Bank>, + /// The bank vault that pays for the order, if necessary #[account(mut)] - pub quote_vault: Box>, - #[account(mut, has_one = group)] - pub base_bank: AccountLoader<'info, Bank>, - #[account(mut)] - pub base_vault: Box>, + pub payer_vault: Box>, pub token_program: Program<'info, Token>, } @@ -221,25 +214,14 @@ pub fn serum3_place_order( MangoError::SomeError ); - // Validate banks and vaults #3 - let quote_bank = ctx.accounts.quote_bank.load()?; - require!( - quote_bank.vault == ctx.accounts.quote_vault.key(), - MangoError::SomeError - ); - require!( - quote_bank.token_index == serum_market.quote_token_index, - MangoError::SomeError - ); - let base_bank = ctx.accounts.base_bank.load()?; - require!( - base_bank.vault == ctx.accounts.base_vault.key(), - MangoError::SomeError - ); - require!( - base_bank.token_index == serum_market.base_token_index, - MangoError::SomeError - ); + // Validate bank and vault #3 + let payer_bank = ctx.accounts.payer_bank.load()?; + require_keys_eq!(payer_bank.vault, ctx.accounts.payer_vault.key()); + let payer_token_index = match side { + Serum3Side::Bid => serum_market.quote_token_index, + Serum3Side::Ask => serum_market.base_token_index, + }; + require_eq!(payer_bank.token_index, payer_token_index); } // @@ -261,8 +243,7 @@ pub fn serum3_place_order( // Before-order tracking // - let before_base_vault = ctx.accounts.base_vault.amount; - let before_quote_vault = ctx.accounts.quote_vault.amount; + let before_vault = ctx.accounts.payer_vault.amount; let before_oo = { let oo_ai = &ctx.accounts.open_orders.as_ref(); @@ -271,21 +252,17 @@ pub fn serum3_place_order( }; // Provide a readable error message in case the vault doesn't have enough tokens - let (vault_amount, needed_amount) = match side { - Serum3Side::Ask => ( - before_base_vault, - max_base_qty.saturating_sub(before_oo.native_base_free()), - ), - Serum3Side::Bid => ( - before_quote_vault, - max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free()), - ), + let needed_amount = match side { + Serum3Side::Ask => max_base_qty.saturating_sub(before_oo.native_base_free()), + Serum3Side::Bid => { + max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free()) + } }; - if vault_amount < needed_amount { + if before_vault < needed_amount { return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { format!( "bank vault does not have enough tokens, need {} but have {}", - needed_amount, vault_amount + needed_amount, before_vault ) }); } @@ -318,29 +295,21 @@ pub fn serum3_place_order( // // After-order tracking // - ctx.accounts.base_vault.reload()?; - ctx.accounts.quote_vault.reload()?; - let after_base_vault = ctx.accounts.base_vault.amount; - let after_quote_vault = ctx.accounts.quote_vault.amount; + ctx.accounts.payer_vault.reload()?; + let after_vault = ctx.accounts.payer_vault.amount; - // Placing an order cannot increase vault balances - require_gte!(before_base_vault, after_base_vault); - require_gte!(before_quote_vault, after_quote_vault); + // Placing an order cannot increase vault balance + require_gte!(before_vault, after_vault); - // Charge the difference in vault balances to the user's account + // Charge the difference in vault balance to the user's account let vault_difference = { - let mut base_bank = ctx.accounts.base_bank.load_mut()?; - let mut quote_bank = ctx.accounts.quote_bank.load_mut()?; - + let mut payer_bank = ctx.accounts.payer_bank.load_mut()?; apply_vault_difference( &mut account.borrow_mut(), serum_market.market_index, - &mut base_bank, - after_base_vault, - before_base_vault, - &mut quote_bank, - after_quote_vault, - before_quote_vault, + &mut payer_bank, + after_vault, + before_vault, )? }; @@ -395,16 +364,13 @@ impl OODifference { } pub struct VaultDifference { - base_index: TokenIndex, - quote_index: TokenIndex, - base_native_change: I80F48, - quote_native_change: I80F48, + token_index: TokenIndex, + native_change: I80F48, } impl VaultDifference { pub fn adjust_health_cache(&self, health_cache: &mut HealthCache) -> Result<()> { - health_cache.adjust_token_balance(self.base_index, self.base_native_change)?; - health_cache.adjust_token_balance(self.quote_index, self.quote_native_change)?; + health_cache.adjust_token_balance(self.token_index, self.native_change)?; Ok(()) } } @@ -414,74 +380,52 @@ impl VaultDifference { pub fn apply_vault_difference( account: &mut MangoAccountRefMut, serum_market_index: Serum3MarketIndex, - base_bank: &mut Bank, - after_base_vault: u64, - before_base_vault: u64, - quote_bank: &mut Bank, - after_quote_vault: u64, - before_quote_vault: u64, + bank: &mut Bank, + vault_after: u64, + vault_before: u64, ) -> Result { - let base_needed_change = cm!(I80F48::from(after_base_vault) - I80F48::from(before_base_vault)); + let needed_change = cm!(I80F48::from(vault_after) - I80F48::from(vault_before)); - let (base_position, _) = account.token_position_mut(base_bank.token_index)?; - let base_native_before = base_position.native(&base_bank); - base_bank.change_without_fee(base_position, base_needed_change)?; - let base_native_after = base_position.native(&base_bank); - let base_native_change = cm!(base_native_after - base_native_before); - let base_borrows = base_native_change - .max(base_native_after) - .min(I80F48::ZERO) - .abs() - .to_num::(); - - let quote_needed_change = - cm!(I80F48::from(after_quote_vault) - I80F48::from(before_quote_vault)); - - let (quote_position, _) = account.token_position_mut(quote_bank.token_index)?; - let quote_native_before = quote_position.native("e_bank); - quote_bank.change_without_fee(quote_position, quote_needed_change)?; - let quote_native_after = quote_position.native("e_bank); - let quote_native_change = cm!(quote_native_after - quote_native_before); - let quote_borrows = quote_native_change - .max(quote_native_after) + let (position, _) = account.token_position_mut(bank.token_index)?; + let native_before = position.native(&bank); + bank.change_without_fee(position, needed_change)?; + let native_after = position.native(&bank); + let native_change = cm!(native_after - native_before); + let new_borrows = native_change + .max(native_after) .min(I80F48::ZERO) .abs() .to_num::(); let market = account.serum3_orders_mut(serum_market_index).unwrap(); + let borrows_without_fee = if bank.token_index == market.base_token_index { + &mut market.base_borrows_without_fee + } else if bank.token_index == market.quote_token_index { + &mut market.quote_borrows_without_fee + } else { + return Err(error_msg!( + "assert failed: apply_vault_difference called with bad token index" + )); + }; - // Only for place: Add to potential borrow amounts - market.base_borrows_without_fee = cm!(market.base_borrows_without_fee + base_borrows); - market.quote_borrows_without_fee = cm!(market.quote_borrows_without_fee + quote_borrows); + // Only for place: Add to potential borrow amount + let old_value = *borrows_without_fee; + *borrows_without_fee = cm!(old_value + new_borrows); // Only for settle/liq_force_cancel: Reduce the potential borrow amounts - if base_needed_change > 0 { - market.base_borrows_without_fee = market - .base_borrows_without_fee - .saturating_sub(base_needed_change.to_num::()); - } - if quote_needed_change > 0 { - market.quote_borrows_without_fee = market - .quote_borrows_without_fee - .saturating_sub(quote_needed_change.to_num::()); + if needed_change > 0 { + *borrows_without_fee = (*borrows_without_fee).saturating_sub(needed_change.to_num::()); } Ok(VaultDifference { - base_index: base_bank.token_index, - quote_index: quote_bank.token_index, - base_native_change, - quote_native_change, + token_index: bank.token_index, + native_change, }) } fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Result<()> { use crate::serum3_cpi; - let order_payer_token_account = match order.side { - Side::Bid => &ctx.quote_vault, - Side::Ask => &ctx.base_vault, - }; - let group = ctx.group.load()?; serum3_cpi::PlaceOrder { program: ctx.serum_program.to_account_info(), @@ -495,7 +439,7 @@ fn cpi_place_order(ctx: &Serum3PlaceOrder, order: NewOrderInstructionV3) -> Resu token_program: ctx.token_program.to_account_info(), open_orders: ctx.open_orders.to_account_info(), - order_payer_token_account: order_payer_token_account.to_account_info(), + order_payer_token_account: ctx.payer_vault.to_account_info(), user_authority: ctx.group.to_account_info(), } .call(&group, order) diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index e50d43276..5de906917 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -164,6 +164,10 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { &mut base_bank, after_base_vault, before_base_vault, + )?; + apply_vault_difference( + &mut account.borrow_mut(), + serum_market.market_index, &mut quote_bank, after_quote_vault, before_quote_vault, diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index 2ae47747e..b38a14342 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -1600,14 +1600,17 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> { ) .await; + let (payer_bank, payer_vault) = match self.side { + Serum3Side::Bid => (quote_info.first_bank(), quote_info.first_vault()), + Serum3Side::Ask => (base_info.first_bank(), base_info.first_vault()), + }; + let accounts = Self::Accounts { group: account.fixed.group, account: self.account, open_orders, - quote_bank: quote_info.first_bank(), - quote_vault: quote_info.first_vault(), - base_bank: base_info.first_bank(), - base_vault: base_info.first_vault(), + payer_bank, + payer_vault, serum_market: self.serum_market, serum_program: serum_market.serum_program, serum_market_external: serum_market.serum_market_external, diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 30bc972d4..fefafda0e 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1079,6 +1079,13 @@ export class MangoClient { .baseSizeNumberToLots(size) .mul(serum3MarketExternal.priceNumberToLots(price)), ); + const payerTokenIndex = (() => { + if (side == Serum3Side.bid) { + return serum3Market.quoteTokenIndex; + } else { + return serum3Market.baseTokenIndex; + } + })(); return await this.program.methods .serum3PlaceOrder( @@ -1107,14 +1114,8 @@ export class MangoClient { marketBaseVault: serum3MarketExternal.decoded.baseVault, marketQuoteVault: serum3MarketExternal.decoded.quoteVault, marketVaultSigner: serum3MarketExternalVaultSigner, - quoteBank: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) - .publicKey, - quoteVault: group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) - .vault, - baseBank: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) - .publicKey, - baseVault: group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) - .vault, + payerBank: group.getFirstBankByTokenIndex(payerTokenIndex).publicKey, + payerVault: group.getFirstBankByTokenIndex(payerTokenIndex).vault, }) .remainingAccounts( healthRemainingAccounts.map( diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index e11622b3d..4b6a7c6c4 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -1605,24 +1605,20 @@ export type MangoV4 = { ] }, { - "name": "quoteBank", + "name": "payerBank", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The bank that pays for the order, if necessary" + ] }, { - "name": "quoteVault", + "name": "payerVault", "isMut": true, - "isSigner": false - }, - { - "name": "baseBank", - "isMut": true, - "isSigner": false - }, - { - "name": "baseVault", - "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The bank vault that pays for the order, if necessary" + ] }, { "name": "tokenProgram", @@ -4015,11 +4011,17 @@ export type MangoV4 = { "type": "publicKey" }, { - "name": "previousNativeCoinReserved", + "name": "baseBorrowsWithoutFee", + "docs": [ + "Tracks the amount of borrows that have flowed into the serum open orders account.", + "These borrows did not have the loan origination fee applied, and that may happen", + "later (in serum3_settle_funds) if we can guarantee that the funds were used.", + "In particular a place-on-book, cancel, settle should not cost fees." + ], "type": "u64" }, { - "name": "previousNativePcReserved", + "name": "quoteBorrowsWithoutFee", "type": "u64" }, { @@ -6887,24 +6889,20 @@ export const IDL: MangoV4 = { ] }, { - "name": "quoteBank", + "name": "payerBank", "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The bank that pays for the order, if necessary" + ] }, { - "name": "quoteVault", + "name": "payerVault", "isMut": true, - "isSigner": false - }, - { - "name": "baseBank", - "isMut": true, - "isSigner": false - }, - { - "name": "baseVault", - "isMut": true, - "isSigner": false + "isSigner": false, + "docs": [ + "The bank vault that pays for the order, if necessary" + ] }, { "name": "tokenProgram", @@ -9297,11 +9295,17 @@ export const IDL: MangoV4 = { "type": "publicKey" }, { - "name": "previousNativeCoinReserved", + "name": "baseBorrowsWithoutFee", + "docs": [ + "Tracks the amount of borrows that have flowed into the serum open orders account.", + "These borrows did not have the loan origination fee applied, and that may happen", + "later (in serum3_settle_funds) if we can guarantee that the funds were used.", + "In particular a place-on-book, cancel, settle should not cost fees." + ], "type": "u64" }, { - "name": "previousNativePcReserved", + "name": "quoteBorrowsWithoutFee", "type": "u64" }, { From 8e3087068963d70416f781bd861527f5d2e1e4fd Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 30 Aug 2022 10:14:47 +0200 Subject: [PATCH 36/50] SerumLiqForceClose: Proper health/liq checking --- .../src/instructions/liq_token_with_token.rs | 3 +- .../serum3_liq_force_cancel_orders.rs | 69 +++++++++++++++---- programs/mango-v4/src/state/mango_account.rs | 5 +- programs/mango-v4/tests/test_serum.rs | 1 + 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/programs/mango-v4/src/instructions/liq_token_with_token.rs b/programs/mango-v4/src/instructions/liq_token_with_token.rs index d33f850cb..a01adf5f0 100644 --- a/programs/mango-v4/src/instructions/liq_token_with_token.rs +++ b/programs/mango-v4/src/instructions/liq_token_with_token.rs @@ -63,8 +63,7 @@ pub fn liq_token_with_token( // we want to allow liquidation to continue until init_health is positive, // to prevent constant oscillation between the two states if liqee.being_liquidated() { - if init_health > I80F48::ZERO { - liqee.fixed.set_being_liquidated(false); + if liqee.fixed.maybe_recover_from_being_liquidated(init_health) { msg!("Liqee init_health above zero"); return Ok(()); } diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 94b1165c6..6ae6b40e3 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -1,8 +1,11 @@ use anchor_lang::prelude::*; use anchor_spl::token::{Token, TokenAccount}; +use fixed::types::I80F48; use crate::error::*; -use crate::instructions::{apply_vault_difference, charge_loan_origination_fees, OpenOrdersSlim}; +use crate::instructions::{ + apply_vault_difference, charge_loan_origination_fees, OODifference, OpenOrdersSlim, +}; use crate::serum3_cpi::load_open_orders_ref; use crate::state::*; @@ -104,21 +107,41 @@ pub fn serum3_liq_force_cancel_orders( ); } - // TODO: do the correct health / being_liquidated check - { - let account = ctx.accounts.account.load()?; - + // + // Check liqee health if liquidation is allowed + // + let mut health_cache = { + let mut account = ctx.accounts.account.load_mut()?; let retriever = new_fixed_order_account_retriever(ctx.remaining_accounts, &account.borrow())?; - let health = compute_health(&account.borrow(), HealthType::Maint, &retriever)?; - msg!("health: {}", health); - require!(health < 0, MangoError::SomeError); - } + let health_cache = + new_health_cache(&account.borrow(), &retriever).context("create health cache")?; + + if account.being_liquidated() { + let init_health = health_cache.health(HealthType::Init); + if account + .fixed + .maybe_recover_from_being_liquidated(init_health) + { + msg!("Liqee init_health above zero"); + return Ok(()); + } + } else { + let maint_health = health_cache.health(HealthType::Maint); + require!( + maint_health < I80F48::ZERO, + MangoError::HealthMustBeNegative + ); + account.fixed.set_being_liquidated(true); + } + + health_cache + }; // // Charge any open loan origination fees // - { + let before_oo = { let open_orders = load_open_orders_ref(ctx.accounts.open_orders.as_ref())?; let before_oo = OpenOrdersSlim::from_oo(&open_orders); let mut account = ctx.accounts.account.load_mut()?; @@ -133,7 +156,9 @@ pub fn serum3_liq_force_cancel_orders( &mut account.borrow_mut(), &before_oo, )?; - } + + before_oo + }; // // Before-settle tracking @@ -150,6 +175,14 @@ pub fn serum3_liq_force_cancel_orders( // // After-settle tracking // + { + let oo_ai = &ctx.accounts.open_orders.as_ref(); + let open_orders = load_open_orders_ref(oo_ai)?; + let after_oo = OpenOrdersSlim::from_oo(&open_orders); + OODifference::new(&before_oo, &after_oo) + .adjust_health_cache(&mut health_cache, &serum_market)?; + }; + ctx.accounts.base_vault.reload()?; ctx.accounts.quote_vault.reload()?; let after_base_vault = ctx.accounts.base_vault.amount; @@ -169,14 +202,24 @@ pub fn serum3_liq_force_cancel_orders( &mut base_bank, after_base_vault, before_base_vault, - )?; + )? + .adjust_health_cache(&mut health_cache)?; apply_vault_difference( &mut account.borrow_mut(), serum_market.market_index, &mut quote_bank, after_quote_vault, before_quote_vault, - )?; + )? + .adjust_health_cache(&mut health_cache)?; + + // + // Health check at the end + // + let init_health = health_cache.health(HealthType::Init); + account + .fixed + .maybe_recover_from_being_liquidated(init_health); Ok(()) } diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 809f1df7e..07cc7441a 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -289,11 +289,14 @@ impl MangoAccountFixed { self.in_health_region = if b { 1 } else { 0 }; } - pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) { + pub fn maybe_recover_from_being_liquidated(&mut self, init_health: I80F48) -> bool { // This is used as threshold to flip flag instead of 0 because of dust issues let one_native_usdc = I80F48::ONE; if self.being_liquidated() && init_health > -one_native_usdc { self.set_being_liquidated(false); + true + } else { + false } } } diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index 50f452f49..24e6e323a 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -1,4 +1,5 @@ #![cfg(feature = "test-bpf")] +#![allow(dead_code)] use solana_program_test::*; use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transport::TransportError}; From 1f0138d1de7002080c76588fa3274b438c01a87b Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 30 Aug 2022 10:28:53 +0200 Subject: [PATCH 37/50] SerumCloseOO: Reduce in_use_count of token positions --- .../src/instructions/serum3_close_open_orders.rs | 9 ++++++++- programs/mango-v4/tests/test_serum.rs | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs index 70e2656f6..0cd170166 100644 --- a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs @@ -63,7 +63,14 @@ pub fn serum3_close_open_orders(ctx: Context) -> Result<( // cpi_close_open_orders(ctx.accounts)?; - // TODO: decrement in_use_count on the base token and quote token + // Reduce the in_use_count on the token positions - they no longer need to be forced open. + // We cannot immediately dust tiny positions because we don't have the banks. + let (base_position, _) = account.token_position_mut(serum_market.base_token_index)?; + base_position.in_use_count = base_position.in_use_count.saturating_sub(1); + let (quote_position, _) = account.token_position_mut(serum_market.quote_token_index)?; + quote_position.in_use_count = quote_position.in_use_count.saturating_sub(1); + + // Deactivate the serum open orders account itself account.deactivate_serum3_orders(serum_market.market_index)?; Ok(()) diff --git a/programs/mango-v4/tests/test_serum.rs b/programs/mango-v4/tests/test_serum.rs index 24e6e323a..95d56e1f1 100644 --- a/programs/mango-v4/tests/test_serum.rs +++ b/programs/mango-v4/tests/test_serum.rs @@ -264,6 +264,9 @@ async fn test_serum_basics() -> Result<(), TransportError> { assert_eq!(native1, 900); let account_data = get_mango_account(solana, account).await; + assert_eq!(account_data.token_position_by_raw_index(0).in_use_count, 1); + assert_eq!(account_data.token_position_by_raw_index(1).in_use_count, 1); + assert_eq!(account_data.token_position_by_raw_index(2).in_use_count, 0); let serum_orders = account_data.serum3_orders_by_raw_index(0); assert_eq!(serum_orders.base_borrows_without_fee, 0); assert_eq!(serum_orders.quote_borrows_without_fee, 0); @@ -304,6 +307,10 @@ async fn test_serum_basics() -> Result<(), TransportError> { .await .unwrap(); + let account_data = get_mango_account(solana, account).await; + assert_eq!(account_data.token_position_by_raw_index(0).in_use_count, 0); + assert_eq!(account_data.token_position_by_raw_index(1).in_use_count, 0); + // deregister serum3 market send_tx( solana, From 77b8e6e8b97c3096bde8e5e6a18e696251736a22 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 30 Aug 2022 10:41:59 +0200 Subject: [PATCH 38/50] SerumPlaceOrder: Fix vault balance check for base_lot_size As spotted by microwavedcola --- .../src/instructions/serum3_place_order.rs | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index 4a33b8c56..c5c768fc6 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -1,6 +1,6 @@ use crate::error::*; -use crate::serum3_cpi::load_open_orders_ref; +use crate::serum3_cpi::{load_market_state, load_open_orders_ref}; use crate::state::*; use anchor_lang::prelude::*; use anchor_spl::token::{Token, TokenAccount}; @@ -252,19 +252,29 @@ pub fn serum3_place_order( }; // Provide a readable error message in case the vault doesn't have enough tokens - let needed_amount = match side { - Serum3Side::Ask => max_base_qty.saturating_sub(before_oo.native_base_free()), - Serum3Side::Bid => { - max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free()) + { + let base_lot_size = load_market_state( + &ctx.accounts.serum_market_external, + &ctx.accounts.serum_program.key(), + )? + .coin_lot_size; + + let needed_amount = match side { + Serum3Side::Ask => { + cm!(max_base_qty * base_lot_size).saturating_sub(before_oo.native_base_free()) + } + Serum3Side::Bid => { + max_native_quote_qty_including_fees.saturating_sub(before_oo.native_quote_free()) + } + }; + if before_vault < needed_amount { + return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { + format!( + "bank vault does not have enough tokens, need {} but have {}", + needed_amount, before_vault + ) + }); } - }; - if before_vault < needed_amount { - return err!(MangoError::InsufficentBankVaultFunds).with_context(|| { - format!( - "bank vault does not have enough tokens, need {} but have {}", - needed_amount, before_vault - ) - }); } // From 801b68b93cfc66cb02ec79921cf140a9edd723c7 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 30 Aug 2022 12:47:15 +0200 Subject: [PATCH 39/50] MangoAccount: add unittests --- keeper/src/taker.rs | 2 +- .../instructions/serum3_cancel_all_orders.rs | 3 +- .../src/instructions/serum3_cancel_order.rs | 3 +- .../instructions/serum3_close_open_orders.rs | 3 +- .../serum3_liq_force_cancel_orders.rs | 3 +- .../src/instructions/serum3_place_order.rs | 3 +- .../src/instructions/serum3_settle_funds.rs | 3 +- programs/mango-v4/src/state/mango_account.rs | 311 ++++++++++++++---- programs/mango-v4/src/state/orderbook/book.rs | 4 +- 9 files changed, 255 insertions(+), 80 deletions(-) diff --git a/keeper/src/taker.rs b/keeper/src/taker.rs index 8feaedf58..b2859e91f 100644 --- a/keeper/src/taker.rs +++ b/keeper/src/taker.rs @@ -77,7 +77,7 @@ fn ensure_oo(mango_client: &Arc) -> Result<(), anyhow::Error> { let account = mango_client.mango_account()?; for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() { - if account.serum3_orders(*market_index).is_none() { + if account.serum3_orders(*market_index).is_err() { mango_client.serum3_create_open_orders(serum3_market.market.name())?; } } diff --git a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs index b50e6514e..0ac3b5ae0 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_all_orders.rs @@ -60,8 +60,7 @@ pub fn serum3_cancel_all_orders(ctx: Context, limit: u8) // Validate open_orders #2 require!( account - .serum3_orders(serum_market.market_index) - .ok_or_else(|| error!(MangoError::SomeError))? + .serum3_orders(serum_market.market_index)? .open_orders == ctx.accounts.open_orders.key(), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/serum3_cancel_order.rs b/programs/mango-v4/src/instructions/serum3_cancel_order.rs index 6b35d8253..7b4af32f5 100644 --- a/programs/mango-v4/src/instructions/serum3_cancel_order.rs +++ b/programs/mango-v4/src/instructions/serum3_cancel_order.rs @@ -69,8 +69,7 @@ pub fn serum3_cancel_order( // Validate open_orders #2 require!( account - .serum3_orders(serum_market.market_index) - .ok_or_else(|| error!(MangoError::SomeError))? + .serum3_orders(serum_market.market_index)? .open_orders == ctx.accounts.open_orders.key(), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs index 0cd170166..7d0cebe91 100644 --- a/programs/mango-v4/src/instructions/serum3_close_open_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_close_open_orders.rs @@ -51,8 +51,7 @@ pub fn serum3_close_open_orders(ctx: Context) -> Result<( // Validate open_orders #2 require!( account - .serum3_orders(serum_market.market_index) - .ok_or_else(|| error!(MangoError::SomeError))? + .serum3_orders(serum_market.market_index)? .open_orders == ctx.accounts.open_orders.key(), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs index 6ae6b40e3..448c44afd 100644 --- a/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs +++ b/programs/mango-v4/src/instructions/serum3_liq_force_cancel_orders.rs @@ -79,8 +79,7 @@ pub fn serum3_liq_force_cancel_orders( // Validate open_orders #2 require!( account - .serum3_orders(serum_market.market_index) - .ok_or_else(|| error!(MangoError::SomeError))? + .serum3_orders(serum_market.market_index)? .open_orders == ctx.accounts.open_orders.key(), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index c5c768fc6..fa29a2b2c 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -207,8 +207,7 @@ pub fn serum3_place_order( // Validate open_orders #2 require!( account - .serum3_orders(serum_market.market_index) - .ok_or_else(|| error!(MangoError::SomeError))? + .serum3_orders(serum_market.market_index)? .open_orders == ctx.accounts.open_orders.key(), MangoError::SomeError diff --git a/programs/mango-v4/src/instructions/serum3_settle_funds.rs b/programs/mango-v4/src/instructions/serum3_settle_funds.rs index 5de906917..6b87ac63b 100644 --- a/programs/mango-v4/src/instructions/serum3_settle_funds.rs +++ b/programs/mango-v4/src/instructions/serum3_settle_funds.rs @@ -85,8 +85,7 @@ pub fn serum3_settle_funds(ctx: Context) -> Result<()> { // Validate open_orders #2 require!( account - .serum3_orders(serum_market.market_index) - .ok_or_else(|| error!(MangoError::SomeError))? + .serum3_orders(serum_market.market_index)? .open_orders == ctx.accounts.open_orders.key(), MangoError::SomeError diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index 07cc7441a..aac8df429 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -126,9 +126,9 @@ impl MangoAccount { padding5: Default::default(), serum3: vec![Serum3Orders::default(); 5], padding6: Default::default(), - perps: vec![PerpPosition::default(); 2], + perps: vec![PerpPosition::default(); 4], padding7: Default::default(), - perp_open_orders: vec![PerpOpenOrder::default(); 2], + perp_open_orders: vec![PerpOpenOrder::default(); 6], } } @@ -182,62 +182,6 @@ impl MangoAccount { } } -#[test] -fn test_serialization_match() { - let mut account = MangoAccount::default_for_tests(); - account.group = Pubkey::new_unique(); - account.owner = Pubkey::new_unique(); - account.name = crate::util::fill_from_str("abcdef").unwrap(); - account.delegate = Pubkey::new_unique(); - account.account_num = 1; - account.being_liquidated = 2; - account.in_health_region = 3; - account.bump = 4; - account.net_deposits = 5; - account.net_settled = 6; - account.health_region_pre_init_health = 7; - account.tokens.resize(8, TokenPosition::default()); - account.tokens[0].token_index = 8; - account.serum3.resize(8, Serum3Orders::default()); - account.perps.resize(8, PerpPosition::default()); - account.perps[0].market_index = 9; - account.perp_open_orders.resize(8, PerpOpenOrder::default()); - - let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap(); - assert_eq!( - 8 + account_bytes.len(), - MangoAccount::space(8, 8, 8, 8).unwrap() - ); - - let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap(); - assert_eq!(account.group, account2.fixed.group); - assert_eq!(account.owner, account2.fixed.owner); - assert_eq!(account.name, account2.fixed.name); - assert_eq!(account.delegate, account2.fixed.delegate); - assert_eq!(account.account_num, account2.fixed.account_num); - assert_eq!(account.being_liquidated, account2.fixed.being_liquidated); - assert_eq!(account.in_health_region, account2.fixed.in_health_region); - assert_eq!(account.bump, account2.fixed.bump); - assert_eq!(account.net_deposits, account2.fixed.net_deposits); - assert_eq!(account.net_settled, account2.fixed.net_settled); - assert_eq!( - account.health_region_pre_init_health, - account2.fixed.health_region_begin_init_health - ); - assert_eq!( - account.tokens[0].token_index, - account2.token_position_by_raw_index(0).token_index - ); - assert_eq!( - account.serum3[0].open_orders, - account2.serum3_orders_by_raw_index(0).open_orders - ); - assert_eq!( - account.perps[0].market_index, - account2.perp_position_by_raw_index(0).market_index - ); -} - // Mango Account fixed part for easy zero copy deserialization #[derive(Copy, Clone)] #[repr(C)] @@ -507,9 +451,10 @@ impl< self.all_token_positions().filter(|token| token.is_active()) } - pub fn serum3_orders(&self, market_index: Serum3MarketIndex) -> Option<&Serum3Orders> { + pub fn serum3_orders(&self, market_index: Serum3MarketIndex) -> Result<&Serum3Orders> { self.all_serum3_orders() .find(|p| p.is_active_for_market(market_index)) + .ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index)) } pub fn serum3_orders_by_raw_index(&self, raw_index: usize) -> &Serum3Orders { @@ -525,9 +470,10 @@ impl< .filter(|serum3_order| serum3_order.is_active()) } - pub fn perp_position(&self, market_index: PerpMarketIndex) -> Option<&PerpPosition> { + pub fn perp_position(&self, market_index: PerpMarketIndex) -> Result<&PerpPosition> { self.all_perp_positions() .find(|p| p.is_active_for_market(market_index)) + .ok_or_else(|| error_msg!("perp position for market index {} not found", market_index)) } pub fn perp_position_by_raw_index(&self, raw_index: usize) -> &PerpPosition { @@ -550,9 +496,10 @@ impl< (0..self.header().perp_oo_count()).map(|i| self.perp_order_by_raw_index(i)) } - pub fn perp_next_order_slot(&self) -> Option { + pub fn perp_next_order_slot(&self) -> Result { self.all_perp_orders() .position(|&oo| oo.order_market == FREE_ORDER_SLOT) + .ok_or_else(|| error_msg!("no free perp order index")) } pub fn perp_find_order_with_client_order_id( @@ -695,7 +642,7 @@ impl< &mut self, market_index: Serum3MarketIndex, ) -> Result<&mut Serum3Orders> { - if self.serum3_orders(market_index).is_some() { + if self.serum3_orders(market_index).is_ok() { return err!(MangoError::Serum3OpenOrdersExistAlready); } @@ -723,11 +670,13 @@ impl< pub fn serum3_orders_mut( &mut self, market_index: Serum3MarketIndex, - ) -> Option<&mut Serum3Orders> { + ) -> Result<&mut Serum3Orders> { let raw_index_opt = self .all_serum3_orders() .position(|p| p.is_active_for_market(market_index)); - raw_index_opt.map(|raw_index| self.serum3_orders_mut_by_raw_index(raw_index)) + raw_index_opt + .map(|raw_index| self.serum3_orders_mut_by_raw_index(raw_index)) + .ok_or_else(|| error_msg!("serum3 orders for market index {} not found", market_index)) } // get mut PerpPosition at raw_index @@ -1058,3 +1007,237 @@ impl< Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_test_account() -> MangoAccountValue { + let bytes = AnchorSerialize::try_to_vec(&MangoAccount::default_for_tests()).unwrap(); + MangoAccountValue::from_bytes(&bytes).unwrap() + } + + #[test] + fn test_serialization_match() { + let mut account = MangoAccount::default_for_tests(); + account.group = Pubkey::new_unique(); + account.owner = Pubkey::new_unique(); + account.name = crate::util::fill_from_str("abcdef").unwrap(); + account.delegate = Pubkey::new_unique(); + account.account_num = 1; + account.being_liquidated = 2; + account.in_health_region = 3; + account.bump = 4; + account.net_deposits = 5; + account.net_settled = 6; + account.health_region_pre_init_health = 7; + account.tokens.resize(8, TokenPosition::default()); + account.tokens[0].token_index = 8; + account.serum3.resize(8, Serum3Orders::default()); + account.perps.resize(8, PerpPosition::default()); + account.perps[0].market_index = 9; + account.perp_open_orders.resize(8, PerpOpenOrder::default()); + + let account_bytes = AnchorSerialize::try_to_vec(&account).unwrap(); + assert_eq!( + 8 + account_bytes.len(), + MangoAccount::space(8, 8, 8, 8).unwrap() + ); + + let account2 = MangoAccountValue::from_bytes(&account_bytes).unwrap(); + assert_eq!(account.group, account2.fixed.group); + assert_eq!(account.owner, account2.fixed.owner); + assert_eq!(account.name, account2.fixed.name); + assert_eq!(account.delegate, account2.fixed.delegate); + assert_eq!(account.account_num, account2.fixed.account_num); + assert_eq!(account.being_liquidated, account2.fixed.being_liquidated); + assert_eq!(account.in_health_region, account2.fixed.in_health_region); + assert_eq!(account.bump, account2.fixed.bump); + assert_eq!(account.net_deposits, account2.fixed.net_deposits); + assert_eq!(account.net_settled, account2.fixed.net_settled); + assert_eq!( + account.health_region_pre_init_health, + account2.fixed.health_region_begin_init_health + ); + assert_eq!( + account.tokens[0].token_index, + account2.token_position_by_raw_index(0).token_index + ); + assert_eq!( + account.serum3[0].open_orders, + account2.serum3_orders_by_raw_index(0).open_orders + ); + assert_eq!( + account.perps[0].market_index, + account2.perp_position_by_raw_index(0).market_index + ); + } + + #[test] + fn test_token_positions() { + let mut account = make_test_account(); + assert!(account.token_position(1).is_err()); + assert!(account.token_position_and_raw_index(2).is_err()); + assert!(account.token_position_mut(3).is_err()); + assert_eq!( + account.token_position_by_raw_index(0).token_index, + TokenIndex::MAX + ); + + { + let (pos, raw, active) = account.ensure_token_position(1).unwrap(); + assert_eq!(raw, 0); + assert_eq!(active, 0); + assert_eq!(pos.token_index, 1); + } + { + let (pos, raw, active) = account.ensure_token_position(7).unwrap(); + assert_eq!(raw, 1); + assert_eq!(active, 1); + assert_eq!(pos.token_index, 7); + } + { + let (pos, raw, active) = account.ensure_token_position(42).unwrap(); + assert_eq!(raw, 2); + assert_eq!(active, 2); + assert_eq!(pos.token_index, 42); + } + + { + account.deactivate_token_position(1); + + let (pos, raw, active) = account.ensure_token_position(42).unwrap(); + assert_eq!(raw, 2); + assert_eq!(active, 1); + assert_eq!(pos.token_index, 42); + + let (pos, raw, active) = account.ensure_token_position(8).unwrap(); + assert_eq!(raw, 1); + assert_eq!(active, 1); + assert_eq!(pos.token_index, 8); + } + + assert_eq!(account.active_token_positions().count(), 3); + account.deactivate_token_position(0); + assert_eq!( + account.token_position_by_raw_index(0).token_index, + TokenIndex::MAX + ); + assert!(account.token_position(1).is_err()); + assert!(account.token_position_mut(1).is_err()); + assert!(account.token_position(8).is_ok()); + assert!(account.token_position(42).is_ok()); + assert_eq!(account.token_position_and_raw_index(42).unwrap().1, 2); + assert_eq!(account.active_token_positions().count(), 2); + + { + let (pos, raw) = account.token_position_mut(42).unwrap(); + assert_eq!(pos.token_index, 42); + assert_eq!(raw, 2); + } + { + let (pos, raw) = account.token_position_mut(8).unwrap(); + assert_eq!(pos.token_index, 8); + assert_eq!(raw, 1); + } + } + + #[test] + fn test_serum3_orders() { + let mut account = make_test_account(); + assert!(account.serum3_orders(1).is_err()); + assert!(account.serum3_orders_mut(3).is_err()); + assert_eq!( + account.serum3_orders_by_raw_index(0).market_index, + Serum3MarketIndex::MAX + ); + + assert_eq!(account.create_serum3_orders(1).unwrap().market_index, 1); + assert_eq!(account.create_serum3_orders(7).unwrap().market_index, 7); + assert_eq!(account.create_serum3_orders(42).unwrap().market_index, 42); + assert!(account.create_serum3_orders(7).is_err()); + assert_eq!(account.active_serum3_orders().count(), 3); + + assert!(account.deactivate_serum3_orders(7).is_ok()); + assert_eq!( + account.serum3_orders_by_raw_index(1).market_index, + Serum3MarketIndex::MAX + ); + assert!(account.create_serum3_orders(8).is_ok()); + assert_eq!(account.serum3_orders_by_raw_index(1).market_index, 8); + + assert_eq!(account.active_serum3_orders().count(), 3); + assert!(account.deactivate_serum3_orders(1).is_ok()); + assert!(account.serum3_orders(1).is_err()); + assert!(account.serum3_orders_mut(1).is_err()); + assert!(account.serum3_orders(8).is_ok()); + assert!(account.serum3_orders(42).is_ok()); + assert_eq!(account.active_serum3_orders().count(), 2); + + assert_eq!(account.serum3_orders_mut(42).unwrap().market_index, 42); + assert_eq!(account.serum3_orders_mut(8).unwrap().market_index, 8); + assert!(account.serum3_orders_mut(7).is_err()); + } + + #[test] + fn test_perp_positions() { + let mut account = make_test_account(); + assert!(account.perp_position(1).is_err()); + //assert!(account.perp_position_mut(3).is_err()); + assert_eq!( + account.perp_position_by_raw_index(0).market_index, + PerpMarketIndex::MAX + ); + + { + let (pos, raw) = account.ensure_perp_position(1).unwrap(); + assert_eq!(raw, 0); + assert_eq!(pos.market_index, 1); + } + { + let (pos, raw) = account.ensure_perp_position(7).unwrap(); + assert_eq!(raw, 1); + assert_eq!(pos.market_index, 7); + } + { + let (pos, raw) = account.ensure_perp_position(42).unwrap(); + assert_eq!(raw, 2); + assert_eq!(pos.market_index, 42); + } + + { + account.deactivate_perp_position(1); + + let (pos, raw) = account.ensure_perp_position(42).unwrap(); + assert_eq!(raw, 2); + assert_eq!(pos.market_index, 42); + + let (pos, raw) = account.ensure_perp_position(8).unwrap(); + assert_eq!(raw, 1); + assert_eq!(pos.market_index, 8); + } + + assert_eq!(account.active_perp_positions().count(), 3); + account.deactivate_perp_position(0); + assert_eq!( + account.perp_position_by_raw_index(0).market_index, + PerpMarketIndex::MAX + ); + assert!(account.perp_position(1).is_err()); + //assert!(account.perp_position_mut(1).is_err()); + assert!(account.perp_position(8).is_ok()); + assert!(account.perp_position(42).is_ok()); + assert_eq!(account.active_perp_positions().count(), 2); + + /*{ + let (pos, raw) = account.perp_position_mut(42).unwrap(); + assert_eq!(pos.perp_index, 42); + assert_eq!(raw, 2); + } + { + let (pos, raw) = account.perp_position_mut(8).unwrap(); + assert_eq!(pos.perp_index, 8); + assert_eq!(raw, 1); + }*/ + } +} diff --git a/programs/mango-v4/src/state/orderbook/book.rs b/programs/mango-v4/src/state/orderbook/book.rs index 4de66b880..17fcd821c 100644 --- a/programs/mango-v4/src/state/orderbook/book.rs +++ b/programs/mango-v4/src/state/orderbook/book.rs @@ -346,9 +346,7 @@ impl<'a> Book<'a> { event_queue.push_back(cast(event)).unwrap(); } - let owner_slot = mango_account - .perp_next_order_slot() - .ok_or_else(|| error!(MangoError::SomeError))?; + let owner_slot = mango_account.perp_next_order_slot()?; let new_order = LeafNode::new( owner_slot as u8, order_id, From 8a2d54cce878e1f02406470f0af0cc5303a2bae8 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 30 Aug 2022 13:46:39 +0200 Subject: [PATCH 40/50] Fix some clippy warnings --- client/src/chain_data.rs | 2 +- client/src/chain_data_fetcher.rs | 13 ++++++------- client/src/context.rs | 5 ++--- keeper/src/taker.rs | 2 +- liquidator/src/main.rs | 4 ++-- programs/mango-v4/src/instructions/flash_loan.rs | 2 +- .../src/instructions/liq_token_bankruptcy.rs | 4 ++-- .../src/instructions/liq_token_with_token.rs | 4 ++-- .../mango-v4/src/instructions/serum3_place_order.rs | 4 ++-- programs/mango-v4/src/state/bank.rs | 7 +++---- programs/mango-v4/src/state/health.rs | 4 ++-- programs/mango-v4/src/state/mango_account.rs | 4 ++-- programs/mango-v4/src/state/orderbook/mod.rs | 4 ++-- programs/mango-v4/src/util.rs | 1 - 14 files changed, 28 insertions(+), 32 deletions(-) diff --git a/client/src/chain_data.rs b/client/src/chain_data.rs index b2580c7e4..1241bd274 100644 --- a/client/src/chain_data.rs +++ b/client/src/chain_data.rs @@ -203,7 +203,7 @@ impl ChainData { .iter() .rev() .find(|w| self.is_account_write_live(w))?; - Some((pubkey.clone(), latest_good_write.clone())) + Some((*pubkey, latest_good_write.clone())) }) .collect() } diff --git a/client/src/chain_data_fetcher.rs b/client/src/chain_data_fetcher.rs index 1def80c58..0749d4837 100644 --- a/client/src/chain_data_fetcher.rs +++ b/client/src/chain_data_fetcher.rs @@ -28,11 +28,10 @@ impl AccountFetcher { &self, address: &Pubkey, ) -> anyhow::Result { - Ok(self + Ok(*self .fetch_raw(address)? .load::() - .with_context(|| format!("loading account {}", address))? - .clone()) + .with_context(|| format!("loading account {}", address))?) } pub fn fetch_mango_account(&self, address: &Pubkey) -> anyhow::Result { @@ -40,12 +39,12 @@ impl AccountFetcher { let data = acc.data(); let disc_bytes = &data[0..8]; - if disc_bytes != &MangoAccount::discriminator() { + if disc_bytes != MangoAccount::discriminator() { anyhow::bail!("not a mango account at {}", address); } - Ok(MangoAccountValue::from_bytes(&data[8..]) - .with_context(|| format!("loading mango account {}", address))?) + MangoAccountValue::from_bytes(&data[8..]) + .with_context(|| format!("loading mango account {}", address)) } // fetches via RPC, stores in ChainData, returns new version @@ -73,7 +72,7 @@ impl AccountFetcher { pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result { let response = self .rpc - .get_account_with_commitment(&address, self.rpc.commitment()) + .get_account_with_commitment(address, self.rpc.commitment()) .with_context(|| format!("refresh account {} via rpc", address))?; let slot = response.context.slot; let account = response diff --git a/client/src/context.rs b/client/src/context.rs index 73e35ec13..6aa12fbf4 100644 --- a/client/src/context.rs +++ b/client/src/context.rs @@ -217,10 +217,9 @@ impl MangoGroupContext { oracles.push(mint_info.oracle); } for affected_token_index in affected_tokens { - if account + if !account .active_token_positions() - .find(|p| p.token_index == affected_token_index) - .is_none() + .any(|p| p.token_index == affected_token_index) { // If there is not yet an active position for the token, we need to pass // the bank/oracle for health check anyway. diff --git a/keeper/src/taker.rs b/keeper/src/taker.rs index b2859e91f..d6919e8a5 100644 --- a/keeper/src/taker.rs +++ b/keeper/src/taker.rs @@ -27,7 +27,7 @@ pub async fn runner( market_name .split('/') .collect::>() - .get(0) + .first() .unwrap(), ) .unwrap(); diff --git a/liquidator/src/main.rs b/liquidator/src/main.rs index 7c1e5741a..76bae3d9e 100644 --- a/liquidator/src/main.rs +++ b/liquidator/src/main.rs @@ -332,13 +332,13 @@ fn liquidate<'a>( config: &liquidate::Config, rebalance_config: &rebalance::Config, ) -> anyhow::Result<()> { - if !liquidate::maybe_liquidate_one(&mango_client, &account_fetcher, accounts, &config) { + if !liquidate::maybe_liquidate_one(mango_client, account_fetcher, accounts, config) { return Ok(()); } let liqor = &mango_client.mango_account_address; if let Err(err) = - rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, &rebalance_config) + rebalance::zero_all_non_quote(mango_client, account_fetcher, liqor, rebalance_config) { log::error!("failed to rebalance liqor: {:?}", err); } diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index d6a4a5181..b71455a84 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -72,7 +72,7 @@ pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>( let token_accounts = &ctx.remaining_accounts[2 * num_loans..3 * num_loans]; let group_ai = &ctx.remaining_accounts[3 * num_loans]; - let group_al = AccountLoader::::try_from(&group_ai)?; + let group_al = AccountLoader::::try_from(group_ai)?; let group = group_al.load()?; let group_seeds = group_seeds!(group); let seeds = [&group_seeds[..]]; diff --git a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs index 7021681f2..75b56ded3 100644 --- a/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs +++ b/programs/mango-v4/src/instructions/liq_token_bankruptcy.rs @@ -107,7 +107,7 @@ pub fn liq_token_bankruptcy( let mut liab_deposit_index = liab_bank.deposit_index; let liab_borrow_index = liab_bank.borrow_index; let (liqee_liab, liqee_raw_token_index) = liqee.token_position_mut(liab_token_index)?; - let initial_liab_native = liqee_liab.native(&liab_bank); + let initial_liab_native = liqee_liab.native(liab_bank); let mut remaining_liab_loss = -initial_liab_native; require_gt!(remaining_liab_loss, I80F48::ZERO); @@ -307,7 +307,7 @@ pub fn liq_token_bankruptcy( mango_group: ctx.accounts.group.key(), liqee: ctx.accounts.liqee.key(), liqor: ctx.accounts.liqor.key(), - liab_token_index: liab_token_index, + liab_token_index, initial_liab_native: initial_liab_native.to_bits(), liab_price: liab_price.to_bits(), insurance_token_index: QUOTE_TOKEN_INDEX, diff --git a/programs/mango-v4/src/instructions/liq_token_with_token.rs b/programs/mango-v4/src/instructions/liq_token_with_token.rs index a01adf5f0..d4205fbb1 100644 --- a/programs/mango-v4/src/instructions/liq_token_with_token.rs +++ b/programs/mango-v4/src/instructions/liq_token_with_token.rs @@ -150,7 +150,7 @@ pub fn liq_token_with_token( let (liqor_liab_active, loan_origination_fee) = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?; let liqor_liab_position_indexed = liqor_liab_position.indexed_position; - let liqee_liab_native_after = liqee_liab_position.native(&liab_bank); + let liqee_liab_native_after = liqee_liab_position.native(liab_bank); let (liqor_asset_position, liqor_asset_raw_index, _) = liqor.ensure_token_position(asset_token_index)?; @@ -161,7 +161,7 @@ pub fn liq_token_with_token( let liqee_asset_active = asset_bank.withdraw_without_fee_with_dusting(liqee_asset_position, asset_transfer)?; let liqee_asset_position_indexed = liqee_asset_position.indexed_position; - let liqee_assets_native_after = liqee_asset_position.native(&asset_bank); + let liqee_assets_native_after = liqee_asset_position.native(asset_bank); // Update the health cache liqee_health_cache.adjust_token_balance( diff --git a/programs/mango-v4/src/instructions/serum3_place_order.rs b/programs/mango-v4/src/instructions/serum3_place_order.rs index fa29a2b2c..e3c60d397 100644 --- a/programs/mango-v4/src/instructions/serum3_place_order.rs +++ b/programs/mango-v4/src/instructions/serum3_place_order.rs @@ -396,9 +396,9 @@ pub fn apply_vault_difference( let needed_change = cm!(I80F48::from(vault_after) - I80F48::from(vault_before)); let (position, _) = account.token_position_mut(bank.token_index)?; - let native_before = position.native(&bank); + let native_before = position.native(bank); bank.change_without_fee(position, needed_change)?; - let native_after = position.native(&bank); + let native_after = position.native(bank); let native_change = cm!(native_after - native_before); let new_borrows = native_change .max(native_after) diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 444c05ae3..d7fbad109 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -276,7 +276,7 @@ impl Bank { let (position_is_active, _) = self.withdraw_internal(position, native_amount, false, !position.is_in_use())?; - return Ok(position_is_active); + Ok(position_is_active) } /// Like `withdraw_without_fee()` but allows dusting of in-use token accounts. @@ -287,9 +287,8 @@ impl Bank { position: &mut TokenPosition, native_amount: I80F48, ) -> Result { - Ok(self - .withdraw_internal(position, native_amount, false, true) - .map(|(not_dusted, _)| not_dusted || position.is_in_use())?) + self.withdraw_internal(position, native_amount, false, true) + .map(|(not_dusted, _)| not_dusted || position.is_in_use()) } /// Withdraws `native_amount` while applying the loan origination fee if a borrow is created. diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index f4b4228c2..ef03cdb82 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -492,7 +492,7 @@ impl PerpInfo { token_infos: &[TokenInfo], ) -> Result { // find the TokenInfos for the market's base and quote tokens - let base_index = find_token_info_index(&token_infos, perp_market.base_token_index)?; + let base_index = find_token_info_index(token_infos, perp_market.base_token_index)?; // TODO: base_index could be unset let base_info = &token_infos[base_index]; @@ -1549,7 +1549,7 @@ mod tests { for (i, testcase) in testcases.iter().enumerate() { println!("checking testcase {}", i); - test_health1_runner(&testcase); + test_health1_runner(testcase); } } diff --git a/programs/mango-v4/src/state/mango_account.rs b/programs/mango-v4/src/state/mango_account.rs index aac8df429..4fc04a7c5 100644 --- a/programs/mango-v4/src/state/mango_account.rs +++ b/programs/mango-v4/src/state/mango_account.rs @@ -652,9 +652,9 @@ impl< market_index: market_index as Serum3MarketIndex, ..Serum3Orders::default() }; - return Ok(self.serum3_orders_mut_by_raw_index(raw_index)); + Ok(self.serum3_orders_mut_by_raw_index(raw_index)) } else { - return err!(MangoError::NoFreeSerum3OpenOrdersIndex); + err!(MangoError::NoFreeSerum3OpenOrdersIndex) } } diff --git a/programs/mango-v4/src/state/orderbook/mod.rs b/programs/mango-v4/src/state/orderbook/mod.rs index 9b3156b3e..d24ed489d 100644 --- a/programs/mango-v4/src/state/orderbook/mod.rs +++ b/programs/mango-v4/src/state/orderbook/mod.rs @@ -327,10 +327,10 @@ mod tests { // simulate event queue processing maker - .execute_perp_maker(market.perp_market_index, &mut market, &fill) + .execute_perp_maker(market.perp_market_index, &mut market, fill) .unwrap(); taker - .execute_perp_taker(market.perp_market_index, &mut market, &fill) + .execute_perp_taker(market.perp_market_index, &mut market, fill) .unwrap(); assert_eq!(market.open_interest, 2 * match_quantity); diff --git a/programs/mango-v4/src/util.rs b/programs/mango-v4/src/util.rs index 112ec6719..2d0aa07ed 100644 --- a/programs/mango-v4/src/util.rs +++ b/programs/mango-v4/src/util.rs @@ -37,5 +37,4 @@ pub fn format_zero_terminated_utf8_bytes( .unwrap() .trim_matches(char::from(0)), ) - .into() } From d0fef4a58684a1359308f7159ff4626355b8db94 Mon Sep 17 00:00:00 2001 From: Christian Kamm Date: Tue, 30 Aug 2022 15:32:59 +0200 Subject: [PATCH 41/50] Test: simplify allow(dead_code) to be per-file --- programs/mango-v4/tests/program_test/serum.rs | 8 ++------ programs/mango-v4/tests/program_test/solana.rs | 13 ++----------- programs/mango-v4/tests/program_test/utils.rs | 6 ++---- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/programs/mango-v4/tests/program_test/serum.rs b/programs/mango-v4/tests/program_test/serum.rs index 5704567ff..a340bbc1f 100644 --- a/programs/mango-v4/tests/program_test/serum.rs +++ b/programs/mango-v4/tests/program_test/serum.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::{mem, sync::Arc}; use bytemuck::from_bytes; @@ -41,7 +43,6 @@ pub struct SerumCookie { } impl SerumCookie { - #[allow(dead_code)] pub fn create_dex_account(&self, unpadded_len: usize) -> (Keypair, Instruction) { let serum_program_id = self.program_id; let key = Keypair::new(); @@ -57,7 +58,6 @@ impl SerumCookie { return (key, create_account_instr); } - #[allow(dead_code)] fn gen_listing_params( &self, _coin_mint: &Pubkey, @@ -94,7 +94,6 @@ impl SerumCookie { return (info, instructions); } - #[allow(dead_code)] pub async fn list_spot_market( &self, coin_mint: &MintCookie, @@ -186,7 +185,6 @@ impl SerumCookie { } } - #[allow(dead_code)] pub async fn consume_spot_events( &self, spot_market_cookie: &SpotMarketCookie, @@ -208,13 +206,11 @@ impl SerumCookie { .unwrap(); } - #[allow(dead_code)] fn strip_dex_padding(data: &[u8]) -> &[u8] { assert!(data.len() >= 12); &data[5..data.len() - 7] } - #[allow(dead_code)] pub async fn load_open_orders(&self, open_orders: Pubkey) -> serum_dex::state::OpenOrders { let data = self.solana.get_account_data(open_orders).await.unwrap(); let slice = Self::strip_dex_padding(&data); diff --git a/programs/mango-v4/tests/program_test/solana.rs b/programs/mango-v4/tests/program_test/solana.rs index ac0195bca..f05115d4f 100644 --- a/programs/mango-v4/tests/program_test/solana.rs +++ b/programs/mango-v4/tests/program_test/solana.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use std::cell::RefCell; use std::sync::{Arc, RwLock}; @@ -23,7 +25,6 @@ pub struct SolanaCookie { } impl SolanaCookie { - #[allow(dead_code)] pub async fn process_transaction( &self, instructions: &[Instruction], @@ -79,7 +80,6 @@ impl SolanaCookie { .unwrap() } - #[allow(dead_code)] pub async fn advance_by_slots(&self, slots: u64) { let clock = self.get_clock().await; self.context @@ -88,8 +88,6 @@ impl SolanaCookie { .unwrap(); } - #[allow(dead_code)] - pub async fn advance_clock(&self) { let mut clock = self.get_clock().await; let old_ts = clock.unix_timestamp; @@ -149,7 +147,6 @@ impl SolanaCookie { key.pubkey() } - #[allow(dead_code)] pub async fn create_token_account(&self, owner: &Pubkey, mint: Pubkey) -> Pubkey { let keypair = Keypair::new(); let rent = self.rent.minimum_balance(spl_token::state::Account::LEN); @@ -178,7 +175,6 @@ impl SolanaCookie { } // Note: Only one table can be created per authority per slot! - #[allow(dead_code)] pub async fn create_address_lookup_table( &self, authority: &Keypair, @@ -196,7 +192,6 @@ impl SolanaCookie { alt_address } - #[allow(dead_code)] pub async fn get_account_data(&self, address: Pubkey) -> Option> { Some( self.context @@ -210,7 +205,6 @@ impl SolanaCookie { ) } - #[allow(dead_code)] pub async fn get_account_opt(&self, address: Pubkey) -> Option { self.context .borrow_mut() @@ -225,17 +219,14 @@ impl SolanaCookie { AccountDeserialize::try_deserialize(&mut data_slice).ok() } - #[allow(dead_code)] pub async fn get_account(&self, address: Pubkey) -> T { self.get_account_opt(address).await.unwrap() } - #[allow(dead_code)] pub async fn token_account_balance(&self, address: Pubkey) -> u64 { self.get_account::(address).await.amount } - #[allow(dead_code)] pub fn program_log(&self) -> Vec { self.last_transaction_log.borrow().clone() } diff --git a/programs/mango-v4/tests/program_test/utils.rs b/programs/mango-v4/tests/program_test/utils.rs index 9a52861fb..c053b6065 100644 --- a/programs/mango-v4/tests/program_test/utils.rs +++ b/programs/mango-v4/tests/program_test/utils.rs @@ -1,15 +1,15 @@ +#![allow(dead_code)] + use bytemuck::{bytes_of, Contiguous}; use solana_program::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; use solana_sdk::signature::Keypair; use std::ops::Deref; -#[allow(dead_code)] pub fn gen_signer_seeds<'a>(nonce: &'a u64, acc_pk: &'a Pubkey) -> [&'a [u8]; 2] { [acc_pk.as_ref(), bytes_of(nonce)] } -#[allow(dead_code)] pub fn gen_signer_key( nonce: u64, acc_pk: &Pubkey, @@ -19,7 +19,6 @@ pub fn gen_signer_key( Ok(Pubkey::create_program_address(&seeds, program_id)?) } -#[allow(dead_code)] pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pubkey, u64) { for i in 0..=u64::MAX_VALUE { if let Ok(pk) = gen_signer_key(i, acc_pk, program_id) { @@ -29,7 +28,6 @@ pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pub panic!("Could not generate signer key"); } -#[allow(dead_code)] pub fn clone_keypair(keypair: &Keypair) -> Keypair { Keypair::from_base58_string(&keypair.to_base58_string()) } From 2fb569cc75d34b625f09603511f6c36461bef16c Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Tue, 30 Aug 2022 17:01:17 +0200 Subject: [PATCH 42/50] make script net agnostic Signed-off-by: microwavedcola1 --- .../{mb-debug-banks.ts => debug-banks.ts} | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) rename ts/client/src/debug-scripts/{mb-debug-banks.ts => debug-banks.ts} (89%) diff --git a/ts/client/src/debug-scripts/mb-debug-banks.ts b/ts/client/src/debug-scripts/debug-banks.ts similarity index 89% rename from ts/client/src/debug-scripts/mb-debug-banks.ts rename to ts/client/src/debug-scripts/debug-banks.ts index 8aa09e7d2..6b0ba32d9 100644 --- a/ts/client/src/debug-scripts/mb-debug-banks.ts +++ b/ts/client/src/debug-scripts/debug-banks.ts @@ -1,31 +1,39 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { coder } from '@project-serum/anchor/dist/cjs/spl/token'; -import { Connection, Keypair } from '@solana/web3.js'; +import { Cluster, Connection, Keypair } from '@solana/web3.js'; import fs from 'fs'; import { I80F48, ZERO_I80F48 } from '../accounts/I80F48'; import { MangoClient } from '../client'; import { MANGO_V4_ID } from '../constants'; import { toUiDecimals } from '../utils'; +const CLUSTER_URL = + process.env.CLUSTER_URL_OVERRIDE || process.env.MB_CLUSTER_URL; +const PAYER_KEYPAIR = + process.env.PAYER_KEYPAIR_OVERRIDE || process.env.MB_PAYER_KEYPAIR; +const GROUP_NUM = Number(process.env.GROUP_NUM || 2); +const CLUSTER: Cluster = + (process.env.CLUSTER_OVERRIDE as Cluster) || 'mainnet-beta'; + async function main() { const options = AnchorProvider.defaultOptions(); - const connection = new Connection(process.env.MB_CLUSTER_URL!, options); + const connection = new Connection(CLUSTER_URL!, options); const admin = Keypair.fromSecretKey( - Buffer.from( - JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')), - ), + Buffer.from(JSON.parse(fs.readFileSync(PAYER_KEYPAIR!, 'utf-8'))), ); const adminWallet = new Wallet(admin); const adminProvider = new AnchorProvider(connection, adminWallet, options); const client = MangoClient.connect( adminProvider, - 'mainnet-beta', - MANGO_V4_ID['mainnet-beta'], + CLUSTER, + MANGO_V4_ID[CLUSTER], + {}, + 'get-program-accounts', ); - const group = await client.getGroupForCreator(admin.publicKey, 2); + const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); console.log(`Group ${group.publicKey.toBase58()}`); const banks = Array.from(group.banksMapByMint.values()).flat(); From c66dd882b61432acff3643fe5ff24fff5682d09e Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Wed, 31 Aug 2022 11:36:44 +0200 Subject: [PATCH 43/50] ts: Additional serum3 support (#196) * get bids and asks for a user on a serum3 market * get orderbook for a market * get max bid or ask that a user can place for a market * simulate health if a bid or ask were to be placed misc: * fix remaining accounts list for health when placing perp bids Signed-off-by: microwavedcola1 format Signed-off-by: microwavedcola1 format Signed-off-by: microwavedcola1 remove testing code Signed-off-by: microwavedcola1 Fixes from review Signed-off-by: microwavedcola1 Fixes from review Signed-off-by: microwavedcola1 script adjustment Signed-off-by: microwavedcola1 comments Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- .gitignore | 2 + Program | 0 ts/client/src/accounts/I80F48.ts | 8 +- ts/client/src/accounts/group.ts | 66 ++- ts/client/src/accounts/healthCache.ts | 419 ++++++++++++++---- ts/client/src/accounts/mangoAccount.ts | 215 +++++++-- ts/client/src/accounts/serum3.ts | 61 ++- ts/client/src/client.ts | 219 +++++---- ts/client/src/debug-scripts/mb-debug-user.ts | 13 +- ts/client/src/scripts/devnet-admin.ts | 360 +++++++++------ .../scripts/devnet-user-2-close-account.ts | 136 ++++++ ts/client/src/scripts/devnet-user-2.ts | 134 ++++++ .../src/scripts/devnet-user-close-account.ts | 2 +- ts/client/src/scripts/devnet-user.ts | 207 +++++++-- ts/client/src/utils.ts | 57 ++- 15 files changed, 1500 insertions(+), 399 deletions(-) create mode 100644 Program create mode 100644 ts/client/src/scripts/devnet-user-2-close-account.ts create mode 100644 ts/client/src/scripts/devnet-user-2.ts diff --git a/.gitignore b/.gitignore index dd40eca29..b3e84ddc6 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ ts/client/**/*.js ts/client/**/*.js.map migrations/*.js migrations/*.js.map + +ts/client/src/scripts/archive/ts.ts \ No newline at end of file diff --git a/Program b/Program new file mode 100644 index 000000000..e69de29bb diff --git a/ts/client/src/accounts/I80F48.ts b/ts/client/src/accounts/I80F48.ts index ec920cccc..9c67c95f5 100644 --- a/ts/client/src/accounts/I80F48.ts +++ b/ts/client/src/accounts/I80F48.ts @@ -224,10 +224,10 @@ export class I80F48 { } /** @internal */ -export const ONE_I80F48 = I80F48.fromString('1'); +export const ONE_I80F48 = I80F48.fromNumber(1); /** @internal */ -export const ZERO_I80F48 = I80F48.fromString('0'); +export const ZERO_I80F48 = I80F48.fromNumber(0); /** @internal */ -export const NEG_ONE_I80F48 = I80F48.fromString('-1'); -export const HUNDRED_I80F48 = I80F48.fromString('100'); +export const NEG_ONE_I80F48 = I80F48.fromNumber(-1); +export const HUNDRED_I80F48 = I80F48.fromNumber(100); export const MAX_I80F48 = new I80F48(I80F48.MAX_BN); diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index e88bd6e4b..a49c82c13 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -1,6 +1,11 @@ import { BorshAccountsCoder } from '@project-serum/anchor'; import { coder } from '@project-serum/anchor/dist/cjs/spl/token'; -import { Market } from '@project-serum/serum'; +import { + getFeeRates, + getFeeTier, + Market, + Orderbook, +} from '@project-serum/serum'; import { parsePriceData, PriceData } from '@pythnetwork/client'; import { PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; @@ -68,8 +73,9 @@ export class Group { public banksMapByName: Map, public banksMapByMint: Map, public banksMapByTokenIndex: Map, - public serum3MarketsMap: Map, + public serum3MarketsMapByExternal: Map, public serum3MarketExternalsMap: Map, + // TODO rethink key public perpMarketsMap: Map, public mintInfosMapByTokenIndex: Map, public mintInfosMapByMint: Map, @@ -77,12 +83,6 @@ export class Group { public vaultAmountsMap: Map, ) {} - public findSerum3Market(marketIndex: number): Serum3Market | undefined { - return Array.from(this.serum3MarketsMap.values()).find( - (serum3Market) => serum3Market.marketIndex === marketIndex, - ); - } - public async reloadAll(client: MangoClient) { let ids: Id | undefined = undefined; @@ -180,14 +180,17 @@ export class Group { serum3Markets = await client.serum3GetMarkets(this); } - this.serum3MarketsMap = new Map( - serum3Markets.map((serum3Market) => [serum3Market.name, serum3Market]), + this.serum3MarketsMapByExternal = new Map( + serum3Markets.map((serum3Market) => [ + serum3Market.serumMarketExternal.toBase58(), + serum3Market, + ]), ); } public async reloadSerum3ExternalMarkets(client: MangoClient, ids?: Id) { const externalMarkets = await Promise.all( - Array.from(this.serum3MarketsMap.values()).map((serum3Market) => + Array.from(this.serum3MarketsMapByExternal.values()).map((serum3Market) => Market.load( client.program.provider.connection, serum3Market.serumMarketExternal, @@ -198,10 +201,12 @@ export class Group { ); this.serum3MarketExternalsMap = new Map( - Array.from(this.serum3MarketsMap.values()).map((serum3Market, index) => [ - serum3Market.name, - externalMarkets[index], - ]), + Array.from(this.serum3MarketsMapByExternal.values()).map( + (serum3Market, index) => [ + serum3Market.serumMarketExternal.toBase58(), + externalMarkets[index], + ], + ), ); } @@ -316,6 +321,37 @@ export class Group { return I80F48.fromNumber(amount); } + public findSerum3Market(marketIndex: number): Serum3Market | undefined { + return Array.from(this.serum3MarketsMapByExternal.values()).find( + (serum3Market) => serum3Market.marketIndex === marketIndex, + ); + } + + public async loadSerum3BidsForMarket( + client: MangoClient, + externalMarketPk: PublicKey, + ): Promise { + return await this.serum3MarketsMapByExternal + .get(externalMarketPk.toBase58()) + .loadBids(client, this); + } + + public async loadSerum3AsksForMarket( + client: MangoClient, + externalMarketPk: PublicKey, + ): Promise { + return await this.serum3MarketsMapByExternal + .get(externalMarketPk.toBase58()) + .loadAsks(client, this); + } + + public getFeeRate(maker = true) { + // TODO: fetch msrm/srm vault balance + const feeTier = getFeeTier(0, 0); + const rates = getFeeRates(feeTier); + return maker ? rates.maker : rates.taker; + } + /** * * @param mintPk diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 55b6e0a7a..1f7eb9187 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -11,6 +11,7 @@ import { ZERO_I80F48, } from './I80F48'; import { HealthType } from './mangoAccount'; +import { Serum3Market, Serum3Side } from './serum3'; // ░░░░ // @@ -37,14 +38,18 @@ import { HealthType } from './mangoAccount'; // warning: this code is copy pasta from rust, keep in sync with health.rs export class HealthCache { - tokenInfos: TokenInfo[]; - serum3Infos: Serum3Info[]; - perpInfos: PerpInfo[]; + constructor( + public tokenInfos: TokenInfo[], + public serum3Infos: Serum3Info[], + public perpInfos: PerpInfo[], + ) {} - constructor(dto: HealthCacheDto) { - this.tokenInfos = dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)); - this.serum3Infos = dto.serum3Infos.map((dto) => new Serum3Info(dto)); - this.perpInfos = dto.perpInfos.map((dto) => new PerpInfo(dto)); + static fromDto(dto) { + return new HealthCache( + dto.tokenInfos.map((dto) => TokenInfo.fromDto(dto)), + dto.serum3Infos.map((dto) => Serum3Info.fromDto(dto)), + dto.perpInfos.map((dto) => new PerpInfo(dto)), + ); } public health(healthType: HealthType): I80F48 { @@ -172,10 +177,54 @@ export class HealthCache { return this.findTokenInfoIndex(bank.tokenIndex); } - private static logHealthCache(debug: string, healthCache: HealthCache) { - console.log(debug); + adjustSerum3Reserved( + // todo change indices to types from numbers + marketIndex: number, + baseTokenIndex: number, + reservedBaseChange: I80F48, + freeBaseChange: I80F48, + quoteTokenIndex: number, + reservedQuoteChange: I80F48, + freeQuoteChange: I80F48, + ) { + const baseEntryIndex = this.findTokenInfoIndex(baseTokenIndex); + const quoteEntryIndex = this.findTokenInfoIndex(quoteTokenIndex); + let reservedAmount = ZERO_I80F48; + + const baseEntry = this.tokenInfos[baseEntryIndex]; + reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice); + + const quoteEntry = this.tokenInfos[quoteEntryIndex]; + reservedAmount = reservedAmount.add( + reservedQuoteChange.mul(quoteEntry.oraclePrice), + ); + + // Apply it to the tokens + baseEntry.serum3MaxReserved = + baseEntry.serum3MaxReserved.add(reservedAmount); + baseEntry.balance = baseEntry.balance.add( + freeBaseChange.mul(baseEntry.oraclePrice), + ); + quoteEntry.serum3MaxReserved = + quoteEntry.serum3MaxReserved.add(reservedAmount); + quoteEntry.balance = quoteEntry.balance.add( + freeQuoteChange.mul(quoteEntry.oraclePrice), + ); + + // Apply it to the serum3 info + const serum3Info = this.serum3Infos.find( + (serum3Info) => serum3Info.marketIndex === marketIndex, + ); + serum3Info.reserved = serum3Info.reserved.add(reservedAmount); + } + + public static logHealthCache(debug: string, healthCache: HealthCache) { + if (debug) console.log(debug); for (const token of healthCache.tokenInfos) { - console.log(`${token.toString()}`); + console.log(` {token.toString()}`); + } + for (const serum3Info of healthCache.serum3Infos) { + console.log(` {serum3Info.toString(healthCache.tokenInfos)}`); } console.log( ` assets ${healthCache.assets( @@ -213,6 +262,118 @@ export class HealthCache { return adjustedCache.healthRatio(healthType); } + simHealthRatioWithSerum3BidChanges( + group: Group, + bidNativeQuoteAmount: I80F48, + serum3Market: Serum3Market, + healthType: HealthType = HealthType.init, + ): I80F48 { + const adjustedCache: HealthCache = _.cloneDeep(this); + const quoteBank = group.banksMapByTokenIndex.get( + serum3Market.quoteTokenIndex, + )[0]; + const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank); + const quote = adjustedCache.tokenInfos[quoteIndex]; + + // Move token balance to reserved funds in open orders, + // essentially simulating a place order + + // Reduce token balance for quote + adjustedCache.tokenInfos[quoteIndex].balance = adjustedCache.tokenInfos[ + quoteIndex + ].balance.sub(bidNativeQuoteAmount.mul(quote.oraclePrice)); + + // Increase reserved in Serum3Info for quote + adjustedCache.adjustSerum3Reserved( + serum3Market.marketIndex, + serum3Market.baseTokenIndex, + ZERO_I80F48, + ZERO_I80F48, + serum3Market.quoteTokenIndex, + bidNativeQuoteAmount, + ZERO_I80F48, + ); + return adjustedCache.healthRatio(healthType); + } + + simHealthRatioWithSerum3AskChanges( + group: Group, + askNativeBaseAmount: I80F48, + serum3Market: Serum3Market, + healthType: HealthType = HealthType.init, + ): I80F48 { + const adjustedCache: HealthCache = _.cloneDeep(this); + const baseBank = group.banksMapByTokenIndex.get( + serum3Market.baseTokenIndex, + )[0]; + const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank); + const base = adjustedCache.tokenInfos[baseIndex]; + + // Move token balance to reserved funds in open orders, + // essentially simulating a place order + + // Reduce token balance for base + adjustedCache.tokenInfos[baseIndex].balance = adjustedCache.tokenInfos[ + baseIndex + ].balance.sub(askNativeBaseAmount.mul(base.oraclePrice)); + + // Increase reserved in Serum3Info for base + adjustedCache.adjustSerum3Reserved( + serum3Market.marketIndex, + serum3Market.baseTokenIndex, + askNativeBaseAmount, + ZERO_I80F48, + serum3Market.quoteTokenIndex, + ZERO_I80F48, + ZERO_I80F48, + ); + return adjustedCache.healthRatio(healthType); + } + + private static binaryApproximationSearch( + left: I80F48, + leftRatio: I80F48, + right: I80F48, + rightRatio: I80F48, + targetRatio: I80F48, + healthRatioAfterActionFn: (I80F48) => I80F48, + ) { + const maxIterations = 40; + // TODO: make relative to health ratio decimals? Might be over engineering + const targetError = I80F48.fromNumber(0.001); + + if ( + (leftRatio.sub(targetRatio).isPos() && + rightRatio.sub(targetRatio).isPos()) || + (leftRatio.sub(targetRatio).isNeg() && + rightRatio.sub(targetRatio).isNeg()) + ) { + throw new Error( + `internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`, + ); + } + + let newAmount; + for (const key of Array(maxIterations).fill(0).keys()) { + newAmount = left.add(right).mul(I80F48.fromNumber(0.5)); + const newAmountRatio = healthRatioAfterActionFn(newAmount); + const error = newAmountRatio.sub(targetRatio); + if (error.isPos() && error.lt(targetError)) { + return newAmount; + } + if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) { + left = newAmount; + } else { + right = newAmount; + rightRatio = newAmountRatio; + } + } + console.error( + `Unable to get targetRatio within ${maxIterations} iterations`, + ); + return newAmount; + } + getMaxSourceForTokenSwap( group: Group, sourceMintPk: PublicKey, @@ -287,54 +448,10 @@ export class HealthCache { .max(ZERO_I80F48); const cache0 = cacheAfterSwap(point0Amount); const point0Ratio = cache0.healthRatio(HealthType.init); - const point0Health = cache0.health(HealthType.init); const cache1 = cacheAfterSwap(point1Amount); const point1Ratio = cache1.healthRatio(HealthType.init); const point1Health = cache1.health(HealthType.init); - function binaryApproximationSearch( - left: I80F48, - leftRatio: I80F48, - right: I80F48, - rightRatio: I80F48, - targetRatio: I80F48, - ) { - const maxIterations = 20; - // TODO: make relative to health ratio decimals? Might be over engineering - const targetError = I80F48.fromString('0.001'); - - if ( - (leftRatio.sub(targetRatio).isPos() && - rightRatio.sub(targetRatio).isPos()) || - (leftRatio.sub(targetRatio).isNeg() && - rightRatio.sub(targetRatio).isNeg()) - ) { - throw new Error( - `internal error: left ${leftRatio.toNumber()} and right ${rightRatio.toNumber()} don't contain the target value ${targetRatio.toNumber()}`, - ); - } - - let newAmount; - for (const key of Array(maxIterations).fill(0).keys()) { - newAmount = left.add(right).mul(I80F48.fromString('0.5')); - const newAmountRatio = healthRatioAfterSwap(newAmount); - const error = newAmountRatio.sub(targetRatio); - if (error.isPos() && error.lt(targetError)) { - return newAmount; - } - if (newAmountRatio.gt(targetRatio) != rightRatio.gt(targetRatio)) { - left = newAmount; - } else { - right = newAmount; - rightRatio = newAmountRatio; - } - } - console.error( - `Unable to get targetRatio within ${maxIterations} iterations`, - ); - return newAmount; - } - let amount: I80F48; if ( @@ -365,30 +482,24 @@ export class HealthCache { const zeroHealthAmount = point1Amount.add( point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)), ); - // console.log(`point1Amount ${point1Amount}`); - // console.log(`point1Health ${point1Health}`); - // console.log(`point1Ratio ${point1Ratio}`); - // console.log(`point0Amount ${point0Amount}`); - // console.log(`point0Health ${point0Health}`); - // console.log(`point0Ratio ${point0Ratio}`); - // console.log(`zeroHealthAmount ${zeroHealthAmount}`); const zeroHealthRatio = healthRatioAfterSwap(zeroHealthAmount); - // console.log(`zeroHealthRatio ${zeroHealthRatio}`); - amount = binaryApproximationSearch( + amount = HealthCache.binaryApproximationSearch( point1Amount, point1Ratio, zeroHealthAmount, zeroHealthRatio, minRatio, + healthRatioAfterSwap, ); } else if (point0Ratio.gte(minRatio)) { // Must be between point0Amount and point1Amount. - amount = binaryApproximationSearch( + amount = HealthCache.binaryApproximationSearch( point0Amount, point0Ratio, point1Amount, point1Ratio, minRatio, + healthRatioAfterSwap, ); } else { throw new Error( @@ -404,6 +515,122 @@ export class HealthCache { ), ); } + + getMaxForSerum3Order( + group: Group, + serum3Market: Serum3Market, + side: Serum3Side, + minRatio: I80F48, + ) { + const baseBank = group.banksMapByTokenIndex.get( + serum3Market.baseTokenIndex, + )[0]; + const quoteBank = group.banksMapByTokenIndex.get( + serum3Market.quoteTokenIndex, + )[0]; + + const healthCacheClone: HealthCache = _.cloneDeep(this); + + const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank); + const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex(quoteBank); + const base = healthCacheClone.tokenInfos[baseIndex]; + const quote = healthCacheClone.tokenInfos[quoteIndex]; + + // Binary search between current health (0 sized new order) and + // an amount to trade which will bring health to 0. + + // Current health and amount i.e. 0 + const initialAmount = ZERO_I80F48; + const initialHealth = this.health(HealthType.init); + const initialRatio = this.healthRatio(HealthType.init); + if (initialRatio.lte(ZERO_I80F48)) { + return ZERO_I80F48; + } + + // Amount which would bring health to 0 + // amount = max(A_deposits, B_borrows) + init_health / (A_liab_weight - B_asset_weight) + // A is what we would be essentially swapping for B + // So when its an ask, then base->quote, + // and when its a bid, then quote->bid + let zeroAmount; + if (side == Serum3Side.ask) { + const quoteBorrows = quote.balance.lt(ZERO_I80F48) + ? quote.balance.abs() + : ZERO_I80F48; + zeroAmount = base.balance + .max(quoteBorrows) + .add( + initialHealth.div( + base + .liabWeight(HealthType.init) + .sub(quote.assetWeight(HealthType.init)), + ), + ); + } else { + const baseBorrows = base.balance.lt(ZERO_I80F48) + ? base.balance.abs() + : ZERO_I80F48; + zeroAmount = quote.balance + .max(baseBorrows) + .add( + initialHealth.div( + quote + .liabWeight(HealthType.init) + .sub(base.assetWeight(HealthType.init)), + ), + ); + } + const cache = cacheAfterPlacingOrder(zeroAmount); + const zeroAmountRatio = cache.healthRatio(HealthType.init); + + function cacheAfterPlacingOrder(amount: I80F48) { + const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); + + side === Serum3Side.ask + ? (adjustedCache.tokenInfos[baseIndex].balance = + adjustedCache.tokenInfos[baseIndex].balance.sub(amount)) + : (adjustedCache.tokenInfos[quoteIndex].balance = + adjustedCache.tokenInfos[quoteIndex].balance.sub(amount)); + + adjustedCache.adjustSerum3Reserved( + serum3Market.marketIndex, + serum3Market.baseTokenIndex, + side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48, + ZERO_I80F48, + serum3Market.quoteTokenIndex, + side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48, + ZERO_I80F48, + ); + + return adjustedCache; + } + + function healthRatioAfterPlacingOrder(amount: I80F48): I80F48 { + return cacheAfterPlacingOrder(amount).healthRatio(HealthType.init); + } + + const amount = HealthCache.binaryApproximationSearch( + initialAmount, + initialRatio, + zeroAmount, + zeroAmountRatio, + minRatio, + healthRatioAfterPlacingOrder, + ); + + // If its a bid then the reserved fund and potential loan is in quote, + // If its a ask then the reserved fund and potential loan is in base, + // also keep some buffer for fees, use taker fees for worst case simulation. + return side === Serum3Side.bid + ? amount + .div(quote.oraclePrice) + .div(ONE_I80F48.add(baseBank.loanOriginationFeeRate)) + .div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false)))) + : amount + .div(base.oraclePrice) + .div(ONE_I80F48.add(quoteBank.loanOriginationFeeRate)) + .div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false)))); + } } export class TokenInfo { @@ -468,20 +695,30 @@ export class TokenInfo { } toString() { - return ` tokenIndex: ${this.tokenIndex}, balance: ${this.balance}`; + return ` tokenIndex: ${this.tokenIndex}, balance: ${ + this.balance + }, serum3MaxReserved: ${ + this.serum3MaxReserved + }, initHealth ${this.healthContribution(HealthType.init)}`; } } export class Serum3Info { - constructor(dto: Serum3InfoDto) { - this.reserved = I80F48.from(dto.reserved); - this.baseIndex = dto.baseIndex; - this.quoteIndex = dto.quoteIndex; - } + constructor( + public reserved: I80F48, + public baseIndex: number, + public quoteIndex: number, + public marketIndex: number, + ) {} - reserved: I80F48; - baseIndex: number; - quoteIndex: number; + static fromDto(dto: Serum3InfoDto) { + return new Serum3Info( + I80F48.from(dto.reserved), + dto.baseIndex, + dto.quoteIndex, + dto.marketIndex, + ); + } healthContribution(healthType: HealthType, tokenInfos: TokenInfo[]): I80F48 { const baseInfo = tokenInfos[this.baseIndex]; @@ -512,7 +749,6 @@ export class Serum3Info { assetPart = maxBalance; liabPart = reserved.sub(maxBalance); } - const assetWeight = tokenInfo.assetWeight(healthType); const liabWeight = tokenInfo.liabWeight(healthType); return assetWeight.mul(assetPart).add(liabWeight.mul(liabPart)); @@ -522,6 +758,14 @@ export class Serum3Info { const reservedAsQuote = computeHealthEffect(quoteInfo); return reservedAsBase.min(reservedAsQuote); } + + toString(tokenInfos: TokenInfo[]) { + return ` marketIndex: ${this.marketIndex}, baseIndex: ${ + this.baseIndex + }, quoteIndex: ${this.quoteIndex}, reserved: ${ + this.reserved + }, initHealth ${this.healthContribution(HealthType.init, tokenInfos)}`; + } } export class PerpInfo { @@ -578,12 +822,39 @@ export class TokenInfoDto { balance: I80F48Dto; // in health-reference-token native units serum3MaxReserved: I80F48Dto; + + constructor( + tokenIndex: number, + maintAssetWeight: I80F48Dto, + initAssetWeight: I80F48Dto, + maintLiabWeight: I80F48Dto, + initLiabWeight: I80F48Dto, + oraclePrice: I80F48Dto, + balance: I80F48Dto, + serum3MaxReserved: I80F48Dto, + ) { + this.tokenIndex = tokenIndex; + this.maintAssetWeight = maintAssetWeight; + this.initAssetWeight = initAssetWeight; + this.maintLiabWeight = maintLiabWeight; + this.initLiabWeight = initLiabWeight; + this.oraclePrice = oraclePrice; + this.balance = balance; + this.serum3MaxReserved = serum3MaxReserved; + } } export class Serum3InfoDto { reserved: I80F48Dto; baseIndex: number; quoteIndex: number; + marketIndex: number; + + constructor(reserved: I80F48Dto, baseIndex: number, quoteIndex: number) { + this.reserved = reserved; + this.baseIndex = baseIndex; + this.quoteIndex = quoteIndex; + } } export class PerpInfoDto { diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 52312a41e..38cb7b52c 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -1,5 +1,6 @@ import { BN } from '@project-serum/anchor'; import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; +import { Order, Orderbook } from '@project-serum/serum/lib/market'; import { PublicKey } from '@solana/web3.js'; import { MangoClient } from '../client'; import { nativeI80F48ToUi, toNative, toUiDecimals } from '../utils'; @@ -7,6 +8,7 @@ import { Bank } from './bank'; import { Group } from './group'; import { HealthCache, HealthCacheDto } from './healthCache'; import { I80F48, I80F48Dto, ONE_I80F48, ZERO_I80F48 } from './I80F48'; +import { Serum3Market, Serum3Side } from './serum3'; export class MangoAccount { public tokens: TokenPosition[]; public serum3: Serum3Orders[]; @@ -89,6 +91,18 @@ export class MangoAccount { return this; } + tokensActive(): TokenPosition[] { + return this.tokens.filter((token) => token.isActive()); + } + + serum3Active(): Serum3Orders[] { + return this.serum3.filter((serum3) => serum3.isActive()); + } + + perpActive(): PerpPosition[] { + return this.perps.filter((perp) => perp.isActive()); + } + findToken(tokenIndex: number): TokenPosition | undefined { return this.tokens.find((ta) => ta.tokenIndex == tokenIndex); } @@ -406,21 +420,184 @@ export class MangoAccount { .toNumber(); } + public async loadSerum3OpenOrdersForMarket( + client: MangoClient, + group: Group, + externalMarketPk: PublicKey, + ): Promise { + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + ); + const serum3OO = this.serum3Active().find( + (s) => s.marketIndex === serum3Market.marketIndex, + ); + if (!serum3OO) { + throw new Error(`No open orders account found for ${externalMarketPk}`); + } + + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + externalMarketPk.toBase58(), + )!; + const [bidsInfo, asksInfo] = + await client.program.provider.connection.getMultipleAccountsInfo([ + serum3MarketExternal.bidsAddress, + serum3MarketExternal.asksAddress, + ]); + const bids = Orderbook.decode(serum3MarketExternal, bidsInfo.data); + const asks = Orderbook.decode(serum3MarketExternal, asksInfo.data); + return [...bids, ...asks].filter((o) => + o.openOrdersAddress.equals(serum3OO.openOrders), + ); + } + /** - * The remaining native quote margin available for given market. * - * TODO: this is a very bad estimation atm. - * It assumes quote asset is always quote, - * it assumes that there are no interaction effects, - * it assumes that there are no existing borrows for either of the tokens in the market. + * @param group + * @param serum3Market + * @returns maximum native quote which can be traded for base token given current health */ - getSerum3MarketMarginAvailable(group: Group, marketName: string): I80F48 { - const initHealth = (this.accountData as MangoAccountData).initHealth; - const serum3Market = group.serum3MarketsMap.get(marketName)!; - const marketAssetWeight = group.getFirstBankByTokenIndex( - serum3Market.baseTokenIndex, - ).initAssetWeight; - return initHealth.div(ONE_I80F48.sub(marketAssetWeight)); + public getMaxQuoteForSerum3Bid( + group: Group, + serum3Market: Serum3Market, + ): I80F48 { + return this.accountData.healthCache.getMaxForSerum3Order( + group, + serum3Market, + Serum3Side.bid, + I80F48.fromNumber(3), + ); + } + + public getMaxQuoteForSerum3BidUi( + group: Group, + externalMarketPk: PublicKey, + ): number { + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + ); + const nativeAmount = this.getMaxQuoteForSerum3Bid(group, serum3Market); + return toUiDecimals( + nativeAmount, + group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex).mintDecimals, + ); + } + + /** + * + * @param group + * @param serum3Market + * @returns maximum native base which can be traded for quote token given current health + */ + public getMaxBaseForSerum3Ask( + group: Group, + serum3Market: Serum3Market, + ): I80F48 { + return this.accountData.healthCache.getMaxForSerum3Order( + group, + serum3Market, + Serum3Side.ask, + I80F48.fromNumber(3), + ); + } + + public getMaxBaseForSerum3AskUi( + group: Group, + externalMarketPk: PublicKey, + ): number { + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + ); + const nativeAmount = this.getMaxBaseForSerum3Ask(group, serum3Market); + return toUiDecimals( + nativeAmount, + group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex).mintDecimals, + ); + } + + /** + * + * @param group + * @param nativeQuoteAmount + * @param serum3Market + * @param healthType + * @returns health ratio after a bid with nativeQuoteAmount is placed + */ + simHealthRatioWithSerum3BidChanges( + group: Group, + nativeQuoteAmount: I80F48, + serum3Market: Serum3Market, + healthType: HealthType = HealthType.init, + ): I80F48 { + return this.accountData.healthCache.simHealthRatioWithSerum3BidChanges( + group, + nativeQuoteAmount, + serum3Market, + healthType, + ); + } + + simHealthRatioWithSerum3BidUiChanges( + group: Group, + uiQuoteAmount: number, + externalMarketPk: PublicKey, + healthType: HealthType = HealthType.init, + ): number { + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + ); + return this.simHealthRatioWithSerum3BidChanges( + group, + toNative( + uiQuoteAmount, + group.getFirstBankByTokenIndex(serum3Market.quoteTokenIndex) + .mintDecimals, + ), + serum3Market, + healthType, + ).toNumber(); + } + + /** + * + * @param group + * @param nativeBaseAmount + * @param serum3Market + * @param healthType + * @returns health ratio after an ask with nativeBaseAmount is placed + */ + simHealthRatioWithSerum3AskChanges( + group: Group, + nativeBaseAmount: I80F48, + serum3Market: Serum3Market, + healthType: HealthType = HealthType.init, + ): I80F48 { + return this.accountData.healthCache.simHealthRatioWithSerum3AskChanges( + group, + nativeBaseAmount, + serum3Market, + healthType, + ); + } + + simHealthRatioWithSerum3AskUiChanges( + group: Group, + uiBaseAmount: number, + externalMarketPk: PublicKey, + healthType: HealthType = HealthType.init, + ): number { + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + ); + return this.simHealthRatioWithSerum3AskChanges( + group, + toNative( + uiBaseAmount, + group.getFirstBankByTokenIndex(serum3Market.baseTokenIndex) + .mintDecimals, + ), + serum3Market, + healthType, + ).toNumber(); } /** @@ -438,18 +615,6 @@ export class MangoAccount { return initHealth.div(ONE_I80F48.sub(marketAssetWeight)); } - tokensActive(): TokenPosition[] { - return this.tokens.filter((token) => token.isActive()); - } - - serum3Active(): Serum3Orders[] { - return this.serum3.filter((serum3) => serum3.isActive()); - } - - perpActive(): PerpPosition[] { - return this.perps.filter((perp) => perp.isActive()); - } - toString(group?: Group): string { let res = 'MangoAccount'; res = res + '\n pk: ' + this.publicKey.toString(); @@ -701,7 +866,7 @@ export class MangoAccountData { tokenAssets: any; }) { return new MangoAccountData( - new HealthCache(event.healthCache), + HealthCache.fromDto(event.healthCache), I80F48.from(event.initHealth), I80F48.from(event.maintHealth), Equity.from(event.equity), diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index cd656d107..03b117fa8 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -1,6 +1,10 @@ import { utf8 } from '@project-serum/anchor/dist/cjs/utils/bytes'; -import { PublicKey } from '@solana/web3.js'; +import { Market, Orderbook } from '@project-serum/serum/lib/market'; +import { Cluster, PublicKey } from '@solana/web3.js'; import BN from 'bn.js'; +import { MangoClient } from '../client'; +import { SERUM3_PROGRAM_ID } from '../constants'; +import { Group } from './group'; export class Serum3Market { public name: string; @@ -44,6 +48,43 @@ export class Serum3Market { ) { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; } + + async loadBids(client: MangoClient, group: Group): Promise { + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + this.serumMarketExternal.toBase58(), + ); + return await serum3MarketExternal.loadBids( + client.program.provider.connection, + ); + } + + async loadAsks(client: MangoClient, group: Group): Promise { + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + this.serumMarketExternal.toBase58(), + ); + return await serum3MarketExternal.loadAsks( + client.program.provider.connection, + ); + } + + async logOb(client: MangoClient, group: Group): Promise { + let res = ``; + res += ` ${this.name} OrderBook`; + let orders = await this?.loadAsks(client, group); + for (const order of orders!.items(true)) { + res += `\n ${order.price.toString().padStart(10)}, ${order.size + .toString() + .padStart(10)}`; + } + res += `\n --------------------------`; + orders = await this?.loadBids(client, group); + for (const order of orders!.items(true)) { + res += `\n ${order.price.toString().padStart(10)}, ${order.size + .toString() + .padStart(10)}`; + } + return res; + } } export class Serum3SelfTradeBehavior { @@ -62,3 +103,21 @@ export class Serum3Side { static bid = { bid: {} }; static ask = { ask: {} }; } + +export async function generateSerum3MarketExternalVaultSignerAddress( + cluster: Cluster, + serum3Market: Serum3Market, + serum3MarketExternal: Market, +): Promise { + return await PublicKey.createProgramAddress( + [ + serum3Market.serumMarketExternal.toBuffer(), + serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike( + Buffer, + 'le', + 8, + ), + ], + SERUM3_PROGRAM_ID[cluster], + ); +} diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 866f44569..ee99f52df 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -1,6 +1,4 @@ import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor'; -import { getFeeRates, getFeeTier } from '@project-serum/serum'; -import { Order } from '@project-serum/serum/lib/market'; import { closeAccount, initializeAccount, @@ -37,6 +35,7 @@ import { import { StubOracle } from './accounts/oracle'; import { OrderType, PerpMarket, Side } from './accounts/perp'; import { + generateSerum3MarketExternalVaultSignerAddress, Serum3Market, Serum3OrderType, Serum3SelfTradeBehavior, @@ -678,9 +677,13 @@ export class MangoClient { mangoAccount: MangoAccount, ): Promise { const healthRemainingAccounts: PublicKey[] = - this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [ - mangoAccount, - ]); + this.buildHealthRemainingAccounts( + AccountRetriever.Fixed, + group, + [mangoAccount], + [], + [], + ); // Use our custom simulate fn in utils/anchor.ts so signing the tx is not required this.program.provider.simulate = simulate; @@ -778,6 +781,7 @@ export class MangoClient { group, [mangoAccount], [bank], + [], ); const transaction = await this.program.methods @@ -852,6 +856,7 @@ export class MangoClient { group, [mangoAccount], [bank], + [], ); const transaction = await this.program.methods @@ -917,9 +922,11 @@ export class MangoClient { public async serum3deregisterMarket( group: Group, - serum3MarketName: string, + externalMarketPk: PublicKey, ): Promise { - const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + )!; return await this.program.methods .serum3DeregisterMarket() @@ -981,7 +988,8 @@ export class MangoClient { mangoAccount: MangoAccount, marketName: string, ): Promise { - const serum3Market: Serum3Market = group.serum3MarketsMap.get(marketName)!; + const serum3Market: Serum3Market = + group.serum3MarketsMapByExternal.get(marketName)!; return await this.program.methods .serum3CreateOpenOrders() @@ -1000,9 +1008,11 @@ export class MangoClient { public async serum3CloseOpenOrders( group: Group, mangoAccount: MangoAccount, - serum3MarketName: string, + externalMarketPk: PublicKey, ): Promise { - const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + )!; const openOrders = mangoAccount.serum3.find( (account) => account.marketIndex === serum3Market.marketIndex, @@ -1026,7 +1036,7 @@ export class MangoClient { public async serum3PlaceOrder( group: Group, mangoAccount: MangoAccount, - serum3MarketName: string, + externalMarketPk: PublicKey, side: Serum3Side, price: number, size: number, @@ -1035,46 +1045,41 @@ export class MangoClient { clientOrderId: number, limit: number, ) { - const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; - + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + )!; if (!mangoAccount.findSerum3Account(serum3Market.marketIndex)) { await this.serum3CreateOpenOrders(group, mangoAccount, 'BTC/USDC'); mangoAccount = await this.getMangoAccount(mangoAccount); } - - const serum3MarketExternal = - group.serum3MarketExternalsMap.get(serum3MarketName)!; - + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + externalMarketPk.toBase58(), + )!; const serum3MarketExternalVaultSigner = - await PublicKey.createProgramAddress( - [ - serum3Market.serumMarketExternal.toBuffer(), - serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike( - Buffer, - 'le', - 8, - ), - ], - SERUM3_PROGRAM_ID[this.cluster], + await generateSerum3MarketExternalVaultSignerAddress( + this.cluster, + serum3Market, + serum3MarketExternal, ); const healthRemainingAccounts: PublicKey[] = - this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [ - mangoAccount, - ]); + this.buildHealthRemainingAccounts( + AccountRetriever.Fixed, + group, + [mangoAccount], + [], + [], + ); const limitPrice = serum3MarketExternal.priceNumberToLots(price); const maxBaseQuantity = serum3MarketExternal.baseSizeNumberToLots(size); - const feeTier = getFeeTier(0, 0 /** TODO: fix msrm/srm balance */); - const rates = getFeeRates(feeTier); - const maxQuoteQuantity = new BN( - serum3MarketExternal.decoded.quoteLotSize.toNumber() * - (1 + rates.taker) /** TODO: fix taker/maker */, - ).mul( - serum3MarketExternal - .baseSizeNumberToLots(size) - .mul(serum3MarketExternal.priceNumberToLots(price)), - ); + const maxQuoteQuantity = serum3MarketExternal.decoded.quoteLotSize + .mul(new BN(1 + group.getFeeRate(orderType === Serum3OrderType.postOnly))) + .mul( + serum3MarketExternal + .baseSizeNumberToLots(size) + .mul(serum3MarketExternal.priceNumberToLots(price)), + ); const payerTokenIndex = (() => { if (side == Serum3Side.bid) { return serum3Market.quoteTokenIndex; @@ -1125,13 +1130,16 @@ export class MangoClient { async serum3CancelAllorders( group: Group, mangoAccount: MangoAccount, - serum3MarketName: string, + externalMarketPk: PublicKey, limit: number, ) { - const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + )!; - const serum3MarketExternal = - group.serum3MarketExternalsMap.get(serum3MarketName)!; + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + externalMarketPk.toBase58(), + )!; return await this.program.methods .serum3CancelAllOrders(limit) @@ -1154,25 +1162,19 @@ export class MangoClient { async serum3SettleFunds( group: Group, mangoAccount: MangoAccount, - serum3MarketName: string, + externalMarketPk: PublicKey, ): Promise { - const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; - - const serum3MarketExternal = - group.serum3MarketExternalsMap.get(serum3MarketName)!; - + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + )!; + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + externalMarketPk.toBase58(), + )!; const serum3MarketExternalVaultSigner = - // TODO: put into a helper method, and remove copy pasta - await PublicKey.createProgramAddress( - [ - serum3Market.serumMarketExternal.toBuffer(), - serum3MarketExternal.decoded.vaultSignerNonce.toArrayLike( - Buffer, - 'le', - 8, - ), - ], - SERUM3_PROGRAM_ID[this.cluster], + await generateSerum3MarketExternalVaultSignerAddress( + this.cluster, + serum3Market, + serum3MarketExternal, ); return await this.program.methods @@ -1204,14 +1206,17 @@ export class MangoClient { async serum3CancelOrder( group: Group, mangoAccount: MangoAccount, - serum3MarketName: string, + externalMarketPk: PublicKey, side: Serum3Side, orderId: BN, ): Promise { - const serum3Market = group.serum3MarketsMap.get(serum3MarketName)!; + const serum3Market = group.serum3MarketsMapByExternal.get( + externalMarketPk.toBase58(), + )!; - const serum3MarketExternal = - group.serum3MarketExternalsMap.get(serum3MarketName)!; + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + externalMarketPk.toBase58(), + )!; return await this.program.methods .serum3CancelOrder(side, orderId) @@ -1230,20 +1235,6 @@ export class MangoClient { .rpc(); } - async getSerum3Orders( - group: Group, - serum3MarketName: string, - ): Promise { - const serum3MarketExternal = - group.serum3MarketExternalsMap.get(serum3MarketName)!; - - // TODO: filter for mango account - return await serum3MarketExternal.loadOrdersForOwner( - this.program.provider.connection, - group.publicKey, - ); - } - /// perps async perpCreateMarket( @@ -1466,9 +1457,13 @@ export class MangoClient { const perpMarket = group.perpMarketsMap.get(perpMarketName)!; const healthRemainingAccounts: PublicKey[] = - this.buildHealthRemainingAccounts(AccountRetriever.Fixed, group, [ - mangoAccount, - ]); + this.buildHealthRemainingAccounts( + AccountRetriever.Fixed, + group, + [mangoAccount], + [], + [perpMarket], + ); const [nativePrice, nativeQuantity] = perpMarket.uiToNativePriceQuantity( price, @@ -1539,6 +1534,7 @@ export class MangoClient { group, [mangoAccount], [inputBank, outputBank], + [], ); const parsedHealthAccounts = healthRemainingAccounts.map( (pk) => @@ -1723,6 +1719,7 @@ export class MangoClient { group, [liqor, liqee], [assetBank, liabBank], + [], ); const parsedHealthAccounts = healthRemainingAccounts.map( @@ -1798,31 +1795,39 @@ export class MangoClient { /// private + // todo make private public buildHealthRemainingAccounts( retriever: AccountRetriever, group: Group, mangoAccounts: MangoAccount[], - banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */, + banks: Bank[], + perpMarkets: PerpMarket[], ): PublicKey[] { if (retriever === AccountRetriever.Fixed) { return this.buildFixedAccountRetrieverHealthAccounts( group, mangoAccounts[0], banks, + perpMarkets, ); } else { return this.buildScanningAccountRetrieverHealthAccounts( group, mangoAccounts, banks, + perpMarkets, ); } } + // todo make private public buildFixedAccountRetrieverHealthAccounts( group: Group, mangoAccount: MangoAccount, - banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */, + // Banks and perpMarkets for whom positions don't exist on mango account, + // but user would potentially open new positions. + banks: Bank[], + perpMarkets: PerpMarket[], ): PublicKey[] { const healthRemainingAccounts: PublicKey[] = []; @@ -1853,11 +1858,7 @@ export class MangoClient { healthRemainingAccounts.push( ...mintInfos.map((mintInfo) => mintInfo.oracle), ); - healthRemainingAccounts.push( - ...mangoAccount.serum3 - .filter((serum3Account) => serum3Account.marketIndex !== 65535) - .map((serum3Account) => serum3Account.openOrders), - ); + healthRemainingAccounts.push( ...mangoAccount.perps .filter((perp) => perp.marketIndex !== 65535) @@ -1868,14 +1869,36 @@ export class MangoClient { )[0].publicKey, ), ); + for (const perpMarket of perpMarkets) { + const alreadyAdded = mangoAccount.perps.find( + (p) => p.marketIndex === perpMarket.perpMarketIndex, + ); + if (!alreadyAdded) { + healthRemainingAccounts.push( + Array.from(group.perpMarketsMap.values()).filter( + (p) => p.perpMarketIndex === perpMarket.perpMarketIndex, + )[0].publicKey, + ); + } + } + + healthRemainingAccounts.push( + ...mangoAccount.serum3 + .filter((serum3Account) => serum3Account.marketIndex !== 65535) + .map((serum3Account) => serum3Account.openOrders), + ); + + // debugHealthAccounts(group, mangoAccount, healthRemainingAccounts); return healthRemainingAccounts; } + // todo make private public buildScanningAccountRetrieverHealthAccounts( group: Group, mangoAccounts: MangoAccount[], - banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */, + banks: Bank[], + perpMarkets: PerpMarket[], ): PublicKey[] { const healthRemainingAccounts: PublicKey[] = []; @@ -1903,6 +1926,7 @@ export class MangoClient { healthRemainingAccounts.push( ...mintInfos.map((mintInfo) => mintInfo.oracle), ); + for (const mangoAccount of mangoAccounts) { healthRemainingAccounts.push( ...mangoAccount.serum3 @@ -1910,6 +1934,7 @@ export class MangoClient { .map((serum3Account) => serum3Account.openOrders), ); } + for (const mangoAccount of mangoAccounts) { healthRemainingAccounts.push( ...mangoAccount.perps @@ -1922,6 +1947,20 @@ export class MangoClient { ), ); } + for (const mangoAccount of mangoAccounts) { + for (const perpMarket of perpMarkets) { + const alreadyAdded = mangoAccount.perps.find( + (p) => p.marketIndex === perpMarket.perpMarketIndex, + ); + if (!alreadyAdded) { + healthRemainingAccounts.push( + Array.from(group.perpMarketsMap.values()).filter( + (p) => p.perpMarketIndex === perpMarket.perpMarketIndex, + )[0].publicKey, + ); + } + } + } return healthRemainingAccounts; } diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index b5e70883d..764adc27a 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -20,10 +20,15 @@ async function debugUser( console.log( 'buildFixedAccountRetrieverHealthAccounts ' + client - .buildFixedAccountRetrieverHealthAccounts(group, mangoAccount, [ - group.banksMapByName.get('BTC')[0], - group.banksMapByName.get('USDC')[0], - ]) + .buildFixedAccountRetrieverHealthAccounts( + group, + mangoAccount, + [ + group.banksMapByName.get('BTC')[0], + group.banksMapByName.get('USDC')[0], + ], + [], + ) .map((pk) => pk.toBase58()) .join(', '), ); diff --git a/ts/client/src/scripts/devnet-admin.ts b/ts/client/src/scripts/devnet-admin.ts index 81f808413..7db068930 100644 --- a/ts/client/src/scripts/devnet-admin.ts +++ b/ts/client/src/scripts/devnet-admin.ts @@ -17,6 +17,8 @@ import { MANGO_V4_ID } from '../constants'; const DEVNET_SERUM3_MARKETS = new Map([ ['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'], ['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'], + ['ETH/USDC', 'BkAraCyL9TTLbeMY3L1VWrPcv32DvSi5QDDQjik1J6Ac'], + ['SRM/USDC', '249LDNPLLL29nRq8kjBTg9hKdXMcZf4vK2UvxszZYcuZ'], ]); const DEVNET_MINTS = new Map([ ['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc @@ -24,12 +26,16 @@ const DEVNET_MINTS = new Map([ ['SOL', 'So11111111111111111111111111111111111111112'], ['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'], ['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'], + ['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'], + ['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'], ]); const DEVNET_ORACLES = new Map([ ['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'], ['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'], ['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'], ['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'], + ['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'], + ['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'], ]); const GROUP_NUM = Number(process.env.GROUP_NUM || 0); @@ -88,19 +94,19 @@ async function main() { 0.1, 0, // tokenIndex 'USDC', - 0.01, - 0.4, - 0.07, - 0.8, - 0.9, - 1.5, + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, 0.0005, - 0.0005, - 0.8, - 0.6, - 1.2, - 1.4, - 0.02, + 1, + 1, + 1, + 1, + 0, ); await group.reloadAll(client); } catch (error) {} @@ -117,19 +123,19 @@ async function main() { 0.1, 1, // tokenIndex 'BTC', - 0.01, - 0.4, - 0.07, - 0.8, + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, 0.9, - 0.88, - 0.0005, - 0.0005, 0.8, - 0.6, + 1.1, 1.2, - 1.4, - 0.02, + 0.05, ); await group.reloadAll(client); } catch (error) { @@ -148,19 +154,19 @@ async function main() { 0.1, 2, // tokenIndex 'SOL', - 0.01, - 0.4, - 0.07, - 0.8, + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, 0.9, - 0.63, - 0.0005, - 0.0005, 0.8, - 0.6, + 1.1, 1.2, - 1.4, - 0.02, + 0.05, ); await group.reloadAll(client); } catch (error) { @@ -198,14 +204,75 @@ async function main() { console.log(error); } - // register token 4 + // register token 7 + console.log(`Registering ETH...`); + const ethDevnetMint = new PublicKey(DEVNET_MINTS.get('ETH')!); + const ethDevnetOracle = new PublicKey(DEVNET_ORACLES.get('ETH')!); + try { + await client.tokenRegister( + group, + ethDevnetMint, + ethDevnetOracle, + 0.1, + 7, // tokenIndex + 'ETH', + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + await group.reloadAll(client); + } catch (error) { + console.log(error); + } + + // register token 5 + console.log(`Registering SRM...`); + const srmDevnetMint = new PublicKey(DEVNET_MINTS.get('SRM')!); + const srmDevnetOracle = new PublicKey(DEVNET_ORACLES.get('SRM')!); + try { + await client.tokenRegister( + group, + srmDevnetMint, + srmDevnetOracle, + 0.1, + 5, // tokenIndex + 'SRM', + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + await group.reloadAll(client); + } catch (error) { + console.log(error); + } + console.log( `Editing group, setting existing admin as fastListingAdmin to be able to add MNGO truslessly...`, ); let sig = await client.groupEdit( group, group.admin, - new PublicKey('Efhak3qj3MiyzgJr3cUUqXXz5wr3oYHt9sPzuqJf9eBN'), + group.admin, undefined, undefined, ); @@ -231,7 +298,7 @@ async function main() { // register serum market console.log(`Registering serum3 market...`); - const serumMarketExternalPk = new PublicKey( + let serumMarketExternalPk = new PublicKey( DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, ); try { @@ -251,7 +318,35 @@ async function main() { group.getFirstBankByMint(btcDevnetMint).tokenIndex, group.getFirstBankByMint(usdcDevnetMint).tokenIndex, ); - console.log(`...registerd serum3 market ${markets[0].publicKey}`); + console.log(`...registered serum3 market ${markets[0].publicKey}`); + + serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('ETH/USDC')!); + try { + await client.serum3RegisterMarket( + group, + serumMarketExternalPk, + group.getFirstBankByMint(ethDevnetMint), + group.getFirstBankByMint(usdcDevnetMint), + 0, + 'ETH/USDC', + ); + } catch (error) { + console.log(error); + } + + serumMarketExternalPk = new PublicKey(DEVNET_SERUM3_MARKETS.get('SRM/USDC')!); + try { + await client.serum3RegisterMarket( + group, + serumMarketExternalPk, + group.getFirstBankByMint(srmDevnetMint), + group.getFirstBankByMint(usdcDevnetMint), + 0, + 'SRM/USDC', + ); + } catch (error) { + console.log(error); + } // register perp market console.log(`Registering perp market...`); @@ -292,114 +387,93 @@ async function main() { // edit // - console.log(`Editing USDC...`); - try { - let sig = await client.tokenEdit( - group, - usdcDevnetMint, - btcDevnetOracle, - 0.1, - undefined, - 0.01, - 0.3, - 0.08, - 0.81, - 0.91, - 0.75, - 0.0007, - 1.7, - 0.9, - 0.7, - 1.3, - 1.5, - 0.04, - ); - console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); - await group.reloadAll(client); - console.log(group.getFirstBankByMint(btcDevnetMint).toString()); - } catch (error) { - throw error; - } - console.log(`Resetting USDC...`); - try { - let sig = await client.tokenEdit( - group, - usdcDevnetMint, - usdcDevnetOracle.publicKey, - 0.1, - undefined, - 0.01, - 0.4, - 0.07, - 0.8, - 0.9, - 1.5, - 0.0005, - 0.0005, - 1.0, - 1.0, - 1.0, - 1.0, - 0.02, - ); - console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); - await group.reloadAll(client); - console.log(group.getFirstBankByMint(usdcDevnetMint).toString()); - } catch (error) { - throw error; - } + if (true) { + console.log(`Editing USDC...`); + try { + let sig = await client.tokenEdit( + group, + usdcDevnetMint, + usdcDevnetOracle.publicKey, + 0.1, + undefined, + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 1, + 1, + 1, + 1, + 0, + ); + console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); + await group.reloadAll(client); + console.log(group.getFirstBankByMint(btcDevnetMint).toString()); + } catch (error) { + throw error; + } - console.log(`Editing perp market...`); - try { - let sig = await client.perpEditMarket( - group, - 'BTC-PERP', - btcDevnetOracle, - 0.2, - 0, - 6, - 0.9, - 0.9, - 1.035, - 1.06, - 0.013, - 0.0003, - 0.1, - 0.07, - 0.07, - 1001, - ); - console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); - await group.reloadAll(client); - console.log(group.perpMarketsMap.get('BTC-PERP')!.toString()); - } catch (error) { - console.log(error); - } - console.log(`Resetting perp market...`); - try { - let sig = await client.perpEditMarket( - group, - 'BTC-PERP', - btcDevnetOracle, - 0.1, - 1, - 6, - 1, - 0.95, - 1.025, - 1.05, - 0.012, - 0.0002, - 0.0, - 0.05, - 0.05, - 100, - ); - console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); - await group.reloadAll(client); - console.log(group.perpMarketsMap.get('BTC-PERP')!.toString()); - } catch (error) { - console.log(error); + console.log(`Editing BTC...`); + try { + let sig = await client.tokenEdit( + group, + usdcDevnetMint, + usdcDevnetOracle.publicKey, + 0.1, + undefined, + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); + await group.reloadAll(client); + console.log(group.getFirstBankByMint(btcDevnetMint).toString()); + } catch (error) { + throw error; + } + + console.log(`Editing SOL...`); + try { + let sig = await client.tokenEdit( + group, + usdcDevnetMint, + usdcDevnetOracle.publicKey, + 0.1, + undefined, + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + console.log(`https://explorer.solana.com/tx/${sig}?cluster=devnet`); + await group.reloadAll(client); + console.log(group.getFirstBankByMint(btcDevnetMint).toString()); + } catch (error) { + throw error; + } } process.exit(); diff --git a/ts/client/src/scripts/devnet-user-2-close-account.ts b/ts/client/src/scripts/devnet-user-2-close-account.ts new file mode 100644 index 000000000..52afc4907 --- /dev/null +++ b/ts/client/src/scripts/devnet-user-2-close-account.ts @@ -0,0 +1,136 @@ +import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { Connection, Keypair } from '@solana/web3.js'; +import fs from 'fs'; +import { Serum3Side } from '../accounts/serum3'; +import { MangoClient } from '../client'; +import { MANGO_V4_ID } from '../constants'; + +// +// script which shows how to close a mango account cleanly i.e. close all active positions, withdraw all tokens, etc. +// + +const GROUP_NUM = Number(process.env.GROUP_NUM || 0); + +// note: either use finalized or expect closing certain things to fail and having to runs scrript multiple times +async function main() { + const options = AnchorProvider.defaultOptions(); + + // note: see note above + // options.commitment = 'finalized'; + + const connection = new Connection( + 'https://mango.devnet.rpcpool.com', + options, + ); + + // user + const user = Keypair.fromSecretKey( + Buffer.from( + JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')), + ), + ); + const userWallet = new Wallet(user); + const userProvider = new AnchorProvider(connection, userWallet, options); + const client = await MangoClient.connect( + userProvider, + 'devnet', + MANGO_V4_ID['devnet'], + {}, + 'get-program-accounts', + ); + console.log(`User ${userWallet.publicKey.toBase58()}`); + + try { + // fetch group + const admin = Keypair.fromSecretKey( + Buffer.from( + JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')), + ), + ); + const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + console.log(`Found group ${group.publicKey.toBase58()}`); + + // fetch account + const mangoAccount = ( + await client.getMangoAccountsForOwner(group, user.publicKey) + )[0]; + console.log(`...found mangoAccount ${mangoAccount.publicKey}`); + console.log(mangoAccount.toString()); + + // close mango account serum3 positions, closing might require cancelling orders and settling + for (const serum3Account of mangoAccount.serum3Active()) { + let orders = await client.getSerum3Orders( + group, + group.findSerum3Market(serum3Account.marketIndex)!.name, + ); + for (const order of orders) { + console.log( + ` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`, + ); + console.log(` - Cancelling order with ${order.orderId}`); + await client.serum3CancelOrder( + group, + mangoAccount, + + 'BTC/USDC', + order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask, + order.orderId, + ); + } + await client.serum3SettleFunds( + group, + mangoAccount, + group.findSerum3Market(serum3Account.marketIndex)!.name, + ); + await client.serum3CloseOpenOrders( + group, + mangoAccount, + group.findSerum3Market(serum3Account.marketIndex)!.name, + ); + } + + // we closed a serum account, this changes the health accounts we are passing in for future ixs + await mangoAccount.reload(client, group); + + // withdraw all tokens + for (const token of mangoAccount.tokensActive()) { + let native = token.balance( + group.getFirstBankByTokenIndex(token.tokenIndex), + ); + + // to avoid rounding issues + if (native.toNumber() < 1) { + continue; + } + let nativeFlooredNumber = Math.floor(native.toNumber()); + console.log( + `withdrawing token ${ + group.getFirstBankByTokenIndex(token.tokenIndex).name + } native amount ${nativeFlooredNumber} `, + ); + + await client.tokenWithdrawNative( + group, + mangoAccount, + group.getFirstBankByTokenIndex(token.tokenIndex).mint, + nativeFlooredNumber - 1 /* see comment in token_withdraw in program */, + false, + ); + } + + // reload and print current positions + await mangoAccount.reload(client, group); + console.log(`...mangoAccount ${mangoAccount.publicKey}`); + console.log(mangoAccount.toString()); + + // close account + console.log(`Close mango account...`); + const res = await client.closeMangoAccount(group, mangoAccount); + } catch (error) { + console.log(error); + } + + process.exit(); +} + +main(); diff --git a/ts/client/src/scripts/devnet-user-2.ts b/ts/client/src/scripts/devnet-user-2.ts new file mode 100644 index 000000000..14b532968 --- /dev/null +++ b/ts/client/src/scripts/devnet-user-2.ts @@ -0,0 +1,134 @@ +import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import fs from 'fs'; +import { MangoClient } from '../client'; +import { MANGO_V4_ID } from '../constants'; + +// +// An example for users based on high level api i.e. the client +// Create +// process.env.USER_KEYPAIR - mango account owner keypair path +// process.env.ADMIN_KEYPAIR - group admin keypair path (useful for automatically finding the group) +// +// This script deposits some tokens, places some serum orders, cancels them, places some perp orders +// + +const DEVNET_MINTS = new Map([ + ['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'], // use devnet usdc + ['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'], + ['SOL', 'So11111111111111111111111111111111111111112'], + ['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'], + ['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'], + ['ETH', 'Cu84KB3tDL6SbFgToHMLYVDJJXdJjenNzSKikeAvzmkA'], + ['SRM', 'AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH'], +]); +const DEVNET_ORACLES = new Map([ + ['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'], + ['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'], + ['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'], + ['MNGO', '8k7F9Xb36oFJsjpCKpsXvg4cgBRoZtwNTc3EzG5Ttd2o'], + ['ETH', 'EdVCmQ9FSPcVe5YySXDPCRmc8aDQLKJ9xvYBMZPie1Vw'], + ['SRM', '992moaMQKs32GKZ9dxi8keyM2bUmbrwBZpK4p2K6X5Vs'], +]); +export const DEVNET_SERUM3_MARKETS = new Map([ + ['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')], + ['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')], +]); + +const GROUP_NUM = Number(process.env.GROUP_NUM || 0); + +async function main() { + const options = AnchorProvider.defaultOptions(); + const connection = new Connection( + 'https://mango.devnet.rpcpool.com', + options, + ); + + const user = Keypair.fromSecretKey( + Buffer.from( + JSON.parse(fs.readFileSync(process.env.USER2_KEYPAIR!, 'utf-8')), + ), + ); + const userWallet = new Wallet(user); + const userProvider = new AnchorProvider(connection, userWallet, options); + const client = await MangoClient.connect( + userProvider, + 'devnet', + MANGO_V4_ID['devnet'], + {}, + 'get-program-accounts', + ); + console.log(`User ${userWallet.publicKey.toBase58()}`); + + // fetch group + const admin = Keypair.fromSecretKey( + Buffer.from( + JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')), + ), + ); + const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + console.log(`${group}`); + + // create + fetch account + console.log(`Creating mangoaccount...`); + const mangoAccount = await client.getOrCreateMangoAccount( + group, + user.publicKey, + ); + console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); + console.log(mangoAccount.toString(group)); + + if (true) { + // deposit and withdraw + + try { + console.log(`...depositing`); + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('USDC')!), + 1000, + ); + await mangoAccount.reload(client, group); + + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('MNGO')!), + 100, + ); + await mangoAccount.reload(client, group); + + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('ETH')!), + 500, + ); + await mangoAccount.reload(client, group); + + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('SRM')!), + 500, + ); + await mangoAccount.reload(client, group); + + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('BTC')!), + 1, + ); + await mangoAccount.reload(client, group); + + console.log(mangoAccount.toString(group)); + } catch (error) { + console.log(error); + } + } + process.exit(); +} + +main(); diff --git a/ts/client/src/scripts/devnet-user-close-account.ts b/ts/client/src/scripts/devnet-user-close-account.ts index a7728065b..6eb7538b0 100644 --- a/ts/client/src/scripts/devnet-user-close-account.ts +++ b/ts/client/src/scripts/devnet-user-close-account.ts @@ -94,7 +94,7 @@ async function main() { // withdraw all tokens for (const token of mangoAccount.tokensActive()) { - let native = token.native( + let native = token.balance( group.getFirstBankByTokenIndex(token.tokenIndex), ); diff --git a/ts/client/src/scripts/devnet-user.ts b/ts/client/src/scripts/devnet-user.ts index 17d2338c3..b450aad96 100644 --- a/ts/client/src/scripts/devnet-user.ts +++ b/ts/client/src/scripts/devnet-user.ts @@ -1,6 +1,7 @@ import { AnchorProvider, Wallet } from '@project-serum/anchor'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import fs from 'fs'; +import { I80F48 } from '../accounts/I80F48'; import { HealthType } from '../accounts/mangoAccount'; import { OrderType, Side } from '../accounts/perp'; import { @@ -28,6 +29,10 @@ const DEVNET_MINTS = new Map([ ['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'], ['MNGO', 'Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC'], ]); +export const DEVNET_SERUM3_MARKETS = new Map([ + ['BTC/USDC', new PublicKey('DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB')], + ['SOL/USDC', new PublicKey('5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR')], +]); const GROUP_NUM = Number(process.env.GROUP_NUM || 0); @@ -61,7 +66,7 @@ async function main() { ), ); const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); - console.log(group.toString()); + console.log(`${group}`); // create + fetch account console.log(`Creating mangoaccount...`); @@ -70,9 +75,11 @@ async function main() { user.publicKey, ); console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`); - console.log(mangoAccount.toString()); + console.log(mangoAccount.toString(group)); - if (true) { + await mangoAccount.reload(client, group); + + if (false) { // set delegate, and change name console.log(`...changing mango account name, and setting a delegate`); const randomKey = new PublicKey( @@ -99,7 +106,8 @@ async function main() { console.log(mangoAccount.toString()); } - if (true) { + if (false) { + // expand account console.log( `...expanding mango account to have serum3 and perp position slots`, ); @@ -107,11 +115,11 @@ async function main() { await mangoAccount.reload(client, group); } - if (true) { + if (false) { // deposit and withdraw try { - console.log(`...depositing 50 USDC`); + console.log(`...depositing 50 USDC, 1 SOL, 1 MNGO`); await client.tokenDeposit( group, mangoAccount, @@ -120,6 +128,22 @@ async function main() { ); await mangoAccount.reload(client, group); + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('SOL')!), + 1, + ); + await mangoAccount.reload(client, group); + + await client.tokenDeposit( + group, + mangoAccount, + new PublicKey(DEVNET_MINTS.get('MNGO')!), + 1, + ); + await mangoAccount.reload(client, group); + console.log(`...withdrawing 1 USDC`); await client.tokenWithdraw( group, @@ -138,21 +162,44 @@ async function main() { 0.0005, ); await mangoAccount.reload(client, group); + + console.log(mangoAccount.toString(group)); } catch (error) { console.log(error); } + } + if (false) { // serum3 + const serum3Market = group.serum3MarketsMapByExternal.get( + DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!, + ); + const serum3MarketExternal = group.serum3MarketExternalsMap.get( + DEVNET_SERUM3_MARKETS.get('BTC/USDC')?.toBase58()!, + ); + const asks = await group.loadSerum3AsksForMarket( + client, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + ); + const lowestAsk = Array.from(asks!)[0]; + const bids = await group.loadSerum3BidsForMarket( + client, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + ); + const highestBid = Array.from(asks!)![0]; + + let price = 20; + let qty = 0.0001; console.log( - `...placing serum3 bid which would not be settled since its relatively low then midprice`, + `...placing serum3 bid which would not be settled since its relatively low then midprice at ${price} for ${qty}`, ); await client.serum3PlaceOrder( group, mangoAccount, - 'BTC/USDC', + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, Serum3Side.bid, - 20, - 0.0001, + price, + qty, Serum3SelfTradeBehavior.decrementTake, Serum3OrderType.limit, Date.now(), @@ -160,15 +207,18 @@ async function main() { ); await mangoAccount.reload(client, group); - console.log(`...placing serum3 bid way above midprice`); + price = lowestAsk.price + lowestAsk.price / 2; + qty = 0.0001; + console.log( + `...placing serum3 bid way above midprice at ${price} for ${qty}`, + ); await client.serum3PlaceOrder( group, mangoAccount, - - 'BTC/USDC', + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, Serum3Side.bid, - 90000, - 0.0001, + price, + qty, Serum3SelfTradeBehavior.decrementTake, Serum3OrderType.limit, Date.now(), @@ -176,15 +226,18 @@ async function main() { ); await mangoAccount.reload(client, group); - console.log(`...placing serum3 ask way below midprice`); + price = highestBid.price - highestBid.price / 2; + qty = 0.0001; + console.log( + `...placing serum3 ask way below midprice at ${price} for ${qty}`, + ); await client.serum3PlaceOrder( group, mangoAccount, - - 'BTC/USDC', + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, Serum3Side.ask, - 30000, - 0.0001, + price, + qty, Serum3SelfTradeBehavior.decrementTake, Serum3OrderType.limit, Date.now(), @@ -192,10 +245,10 @@ async function main() { ); console.log(`...current own orders on OB`); - let orders = await client.getSerum3Orders( + let orders = await mangoAccount.loadSerum3OpenOrdersForMarket( + client, group, - - 'BTC/USDC', + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, ); for (const order of orders) { console.log( @@ -205,18 +258,17 @@ async function main() { await client.serum3CancelOrder( group, mangoAccount, - - 'BTC/USDC', + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask, order.orderId, ); } console.log(`...current own orders on OB`); - orders = await client.getSerum3Orders( + orders = await mangoAccount.loadSerum3OpenOrdersForMarket( + client, group, - - 'BTC/USDC', + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, ); for (const order of orders) { console.log(order); @@ -226,12 +278,19 @@ async function main() { await client.serum3SettleFunds( group, mangoAccount, - - 'BTC/USDC', + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, ); } - if (true) { + if (false) { + // serum3 market + const serum3Market = group.serum3MarketsMapByExternal.get( + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!.toBase58(), + ); + console.log(await serum3Market?.logOb(client, group)); + } + + if (false) { await mangoAccount.reload(client, group); console.log( '...mangoAccount.getEquity() ' + @@ -252,13 +311,13 @@ async function main() { console.log( '...mangoAccount.getAssetsVal() ' + toUiDecimalsForQuote( - mangoAccount.getAssetsVal(HealthType.init).toNumber(), + mangoAccount.getAssetsValue(HealthType.init).toNumber(), ), ); console.log( '...mangoAccount.getLiabsVal() ' + toUiDecimalsForQuote( - mangoAccount.getLiabsVal(HealthType.init).toNumber(), + mangoAccount.getLiabsValue(HealthType.init).toNumber(), ), ); console.log( @@ -272,14 +331,80 @@ async function main() { ).toNumber(), ), ); - console.log( - "...mangoAccount.getSerum3MarketMarginAvailable(group, 'BTC/USDC') " + - toUiDecimalsForQuote( - mangoAccount - .getSerum3MarketMarginAvailable(group, 'BTC/USDC') - .toNumber(), - ), + } + + if (true) { + const asks = await group.loadSerum3AsksForMarket( + client, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, ); + const lowestAsk = Array.from(asks!)[0]; + const bids = await group.loadSerum3BidsForMarket( + client, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + ); + const highestBid = Array.from(asks!)![0]; + + function getMaxSourceForTokenSwapWrapper(src, tgt) { + // console.log(); + console.log( + `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` + + mangoAccount + .getMaxSourceForTokenSwap( + group, + group.banksMapByName.get(src)![0].mint, + group.banksMapByName.get(tgt)![0].mint, + 1, + ) + .div( + I80F48.fromNumber( + Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals), + ), + ) + .toNumber(), + ); + } + for (const srcToken of Array.from(group.banksMapByName.keys())) { + for (const tgtToken of Array.from(group.banksMapByName.keys())) { + getMaxSourceForTokenSwapWrapper(srcToken, tgtToken); + } + } + + const maxQuoteForSerum3BidUi = mangoAccount.getMaxQuoteForSerum3BidUi( + group, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + ); + console.log( + "...mangoAccount.getMaxQuoteForSerum3BidUi(group, 'BTC/USDC') " + + maxQuoteForSerum3BidUi, + ); + + const maxBaseForSerum3AskUi = mangoAccount.getMaxBaseForSerum3AskUi( + group, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + ); + console.log( + "...mangoAccount.getMaxBaseForSerum3AskUi(group, 'BTC/USDC') " + + maxBaseForSerum3AskUi, + ); + + console.log( + `simHealthRatioWithSerum3BidUiChanges ${mangoAccount.simHealthRatioWithSerum3BidUiChanges( + group, + 785, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + )}`, + ); + console.log( + `simHealthRatioWithSerum3AskUiChanges ${mangoAccount.simHealthRatioWithSerum3AskUiChanges( + group, + 0.033, + DEVNET_SERUM3_MARKETS.get('BTC/USDC')!, + )}`, + ); + } + + if (false) { console.log( "...mangoAccount.getPerpMarketMarginAvailable(group, 'BTC-PERP') " + toUiDecimalsForQuote( @@ -290,7 +415,7 @@ async function main() { ); } - if (true) { + if (false) { // perps console.log(`...placing perp bid`); try { diff --git a/ts/client/src/utils.ts b/ts/client/src/utils.ts index 2643d5f95..31a4bd4f9 100644 --- a/ts/client/src/utils.ts +++ b/ts/client/src/utils.ts @@ -9,8 +9,11 @@ import { TransactionInstruction, } from '@solana/web3.js'; import BN from 'bn.js'; -import { QUOTE_DECIMALS } from './accounts/bank'; +import { Bank, QUOTE_DECIMALS } from './accounts/bank'; +import { Group } from './accounts/group'; import { I80F48 } from './accounts/I80F48'; +import { MangoAccount, Serum3Orders } from './accounts/mangoAccount'; +import { PerpMarket } from './accounts/perp'; export const I64_MAX_BN = new BN('9223372036854775807').toTwos(64); @@ -26,6 +29,58 @@ export function debugAccountMetas(ams: AccountMeta[]) { } } +export function debugHealthAccounts( + group: Group, + mangoAccount: MangoAccount, + publicKeys: PublicKey[], +) { + const banks = new Map( + Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [ + banks[0].publicKey.toBase58(), + `${banks[0].name} bank`, + ]), + ); + const oracles = new Map( + Array.from(group.banksMapByName.values()).map((banks: Bank[]) => [ + banks[0].oracle.toBase58(), + `${banks[0].name} oracle`, + ]), + ); + const serum3 = new Map( + mangoAccount.serum3Active().map((serum3: Serum3Orders) => { + return [ + serum3.openOrders.toBase58(), + `${ + Array.from(group.serum3MarketsMapByExternal.values()).find( + (serum3Market) => serum3Market.marketIndex === serum3.marketIndex, + ).name + } spot oo`, + ]; + }), + ); + const perps = new Map( + Array.from(group.perpMarketsMap.values()).map((perpMarket: PerpMarket) => [ + perpMarket.publicKey.toBase58(), + `${perpMarket.name} perp market`, + ]), + ); + + publicKeys.map((pk) => { + if (banks.get(pk.toBase58())) { + console.log(banks.get(pk.toBase58())); + } + if (oracles.get(pk.toBase58())) { + console.log(oracles.get(pk.toBase58())); + } + if (serum3.get(pk.toBase58())) { + console.log(serum3.get(pk.toBase58())); + } + if (perps.get(pk.toBase58())) { + console.log(perps.get(pk.toBase58())); + } + }); +} + export async function findOrCreate( entityName: string, findMethod: (...x: any) => any, From 3cca63c73548a9ec66f46b016c0bd02ccb24dcf0 Mon Sep 17 00:00:00 2001 From: riordanp Date: Wed, 31 Aug 2022 10:37:58 +0100 Subject: [PATCH 44/50] Docker CI Improvements (#195) * Push separate images for binaries to GCR * Add base image version arg to heroku deployment * Test docker build * Try setting mode * Fix tags * switch back to dev Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 Co-authored-by: microwavedcola1 --- .github/workflows/ci-docker-heroku-deploy.yml | 7 ++++- .github/workflows/ci-docker-publish.yml | 26 ++++++++++++++++--- Dockerfile | 6 ++--- keeper/Dockerfile.keeper | 3 ++- liquidator/Dockerfile.liquidator | 3 ++- 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci-docker-heroku-deploy.yml b/.github/workflows/ci-docker-heroku-deploy.yml index 911f669b6..65673fd27 100644 --- a/.github/workflows/ci-docker-heroku-deploy.yml +++ b/.github/workflows/ci-docker-heroku-deploy.yml @@ -11,6 +11,11 @@ on: description: 'Docker Image Name' required: true type: string + imageTag: + description: 'Docker Image Tag' + required: true + type: string + default: 'latest' jobs: deploy: @@ -27,7 +32,7 @@ jobs: - name: Push env: HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }} - run: heroku container:push ${{ inputs.imageName }} -a ${{ inputs.appName }} --recursive + run: heroku container:push ${{ inputs.imageName }} -a ${{ inputs.appName }} --recursive --arg BASE_TAG=${{ inputs.imageTag }} - name: Release env: diff --git a/.github/workflows/ci-docker-publish.yml b/.github/workflows/ci-docker-publish.yml index ddbf396dd..7ce5945bd 100644 --- a/.github/workflows/ci-docker-publish.yml +++ b/.github/workflows/ci-docker-publish.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v2 with: submodules: recursive - + # Use docker buildx - name: Use docker buildx uses: docker/setup-buildx-action@v2 @@ -47,8 +47,8 @@ jobs: username: oauth2accesstoken password: ${{ steps.auth.outputs.access_token }} - # Build and push the image, leveraging layer caching - - name: Build and Push + # Build and push the base image, leveraging layer caching + - name: Build and Push Base Image uses: docker/build-push-action@v2 with: context: . @@ -58,3 +58,23 @@ jobs: us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:latest cache-from: type=registry,ref=us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:buildcache cache-to: type=registry,ref=us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}:buildcache,mode=max + # Build and push the liquidator runtime image + - name: Build and Push Liquidator + uses: docker/build-push-action@v2 + with: + file: liquidator/Dockerfile.liquidator + context: . + push: true + tags: | + us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-liquidator:${{ github.sha }} + us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-liquidator:latest + # Build and push the keeper runtime image + - name: Build and Push Keeper + uses: docker/build-push-action@v2 + with: + file: liquidator/Dockerfile.keeper + context: . + push: true + tags: | + us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-keeper:${{ github.sha }} + us-docker.pkg.dev/${{ env.PROJECT_ID }}/gcr.io/${{ env.IMAGE }}-keeper:latest diff --git a/Dockerfile b/Dockerfile index 27e720e1c..70d397b39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,12 +9,12 @@ COPY ./ . RUN sed -i 's|lib/\*|lib/checked_math|' Cargo.toml # Mount cache for downloaded and compiled dependencies -RUN --mount=type=cache,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \ - --mount=type=cache,target=target \ +RUN --mount=type=cache,mode=0777,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \ + --mount=type=cache,mode=0777,target=target \ cargo build --release --bins # Copy bins out of cache -RUN --mount=type=cache,target=target mkdir .bin && cp target/release/keeper target/release/liquidator .bin/ +RUN --mount=type=cache,mode=0777,target=target mkdir .bin && cp target/release/keeper target/release/liquidator .bin/ FROM debian:bullseye-slim as run RUN apt-get update && apt-get -y install ca-certificates libc6 diff --git a/keeper/Dockerfile.keeper b/keeper/Dockerfile.keeper index 5770c5476..5aee43836 100644 --- a/keeper/Dockerfile.keeper +++ b/keeper/Dockerfile.keeper @@ -1,6 +1,7 @@ # Dockerfile for keeper service in Heroku # heroku container:push keeper -R -a HEROKU_APP_NAME # heroku container:release -a HEROKU_APP_NAME -FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:latest +ARG BASE_TAG=latest +FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG ENTRYPOINT ["keeper"] CMD ["crank"] diff --git a/liquidator/Dockerfile.liquidator b/liquidator/Dockerfile.liquidator index 9935d6b37..c4e15b6b2 100644 --- a/liquidator/Dockerfile.liquidator +++ b/liquidator/Dockerfile.liquidator @@ -1,5 +1,6 @@ # Dockerfile for keeper service in Heroku # heroku container:push keeper -R -a HEROKU_APP_NAME # heroku container:release -a HEROKU_APP_NAME -FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:latest +ARG BASE_TAG=latest +FROM us-docker.pkg.dev/mango-markets/gcr.io/mango-v4:$BASE_TAG CMD ["liquidator"] From 42f22003d80d2b739c9f2290bb12496c81415211 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Wed, 31 Aug 2022 11:40:39 +0200 Subject: [PATCH 45/50] deployment script for vanity (#184) Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/deployment-scripts/mainnet.ts | 290 ++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 ts/client/src/deployment-scripts/mainnet.ts diff --git a/ts/client/src/deployment-scripts/mainnet.ts b/ts/client/src/deployment-scripts/mainnet.ts new file mode 100644 index 000000000..8a86484e2 --- /dev/null +++ b/ts/client/src/deployment-scripts/mainnet.ts @@ -0,0 +1,290 @@ +import { AnchorProvider, Wallet } from '@project-serum/anchor'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import fs from 'fs'; +import { MangoClient } from '../client'; +import { MANGO_V4_ID } from '../constants'; + +const GROUP_NUM = Number(process.env.GROUP_NUM || 0); + +// Reference +// https://explorer.solana.com/ +// https://github.com/blockworks-foundation/mango-client-v3/blob/main/src/ids.json +const MAINNET_MINTS = new Map([ + ['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], + ['USDT', 'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB'], + ['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'], // Wrapped Bitcoin (Sollet) + ['ETH', '7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs'], // Ether (Portal), will be treat as ETH due to higher liquidity + ['soETH', '2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk'], // Wrapped Ethereum (Sollet), will be treated as soETH + ['SOL', 'So11111111111111111111111111111111111111112'], + ['mSOL', 'mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So'], + ['MNGO', 'MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac'], +]); + +// Reference +// https://pyth.network/price-feeds/ +// https://switchboard.xyz/explorer +const MAINNET_ORACLES = new Map([ + ['USDT', '3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL'], + ['BTC', 'GVXRSBjFk6e6J3NbVPXohDJetcTjaeeuykUpbQF8UoMU'], + ['ETH', 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB'], + ['soETH', 'JBu1AL4obBcCMqKBBxhpWCNUt136ijcuMZLFvTP7iWdB'], + ['SOL', 'H6ARHf6YXhGYeQfUzQNGk6rDNnLBQKrenN712K4AQJEG'], + ['mSOL', 'E4v1BBgoso9s64TQvmyownAVJbhbEPGyzA3qn4n46qj9'], + ['MNGO', '79wm3jjcPr6RaNQ4DGvP5KxG1mNd3gEBsg6FsNVFezK4'], +]); + +async function createGroup() { + const admin = Keypair.fromSecretKey( + Buffer.from( + JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')), + ), + ); + + const options = AnchorProvider.defaultOptions(); + const connection = new Connection(process.env.MB_CLUSTER_URL, options); + + const adminWallet = new Wallet(admin); + console.log(`Admin ${adminWallet.publicKey.toBase58()}`); + const adminProvider = new AnchorProvider(connection, adminWallet, options); + const client = await MangoClient.connect( + adminProvider, + 'mainnet-beta', + MANGO_V4_ID['mainnet-beta'], + {}, + 'get-program-accounts', + ); + + console.log(`Creating Group...`); + const insuranceMint = new PublicKey(MAINNET_MINTS.get('USDC')!); + await client.groupCreate( + GROUP_NUM, + true /* with intention */, + 0 /* since spot and perp features are not finished */, + insuranceMint, + ); + const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + console.log(`...registered group ${group.publicKey}`); +} + +async function registerTokens() { + const admin = Keypair.fromSecretKey( + Buffer.from( + JSON.parse(fs.readFileSync(process.env.MB_PAYER_KEYPAIR!, 'utf-8')), + ), + ); + + const options = AnchorProvider.defaultOptions(); + const connection = new Connection(process.env.MB_CLUSTER_URL, options); + + const adminWallet = new Wallet(admin); + console.log(`Admin ${adminWallet.publicKey.toBase58()}`); + const adminProvider = new AnchorProvider(connection, adminWallet, options); + const client = await MangoClient.connect( + adminProvider, + 'mainnet-beta', + MANGO_V4_ID['mainnet-beta'], + {}, + 'get-program-accounts' /* idsjson service doesn't know about this group yet */, + ); + + const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); + + console.log(`Creating USDC stub oracle...`); + const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!); + await client.stubOracleCreate(group, usdcMainnetMint, 1.0); + const usdcMainnetOracle = ( + await client.getStubOracle(group, usdcMainnetMint) + )[0]; + console.log(`...created stub oracle ${usdcMainnetOracle.publicKey}`); + + console.log(`Registering USDC...`); + await client.tokenRegister( + group, + usdcMainnetMint, + usdcMainnetOracle.publicKey, + 0.1, + 0, // insurance vault token should be the first to be registered + 'USDC', + 0.004, // rate parameters are chosen to be the same for all high asset weight tokens, + // hoping that dynamic rate parameter adjustment would be enough to tune their rates to the markets needs + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, // 50 bps + 0.0005, // 5 bps + 1, + 1, + 1, + 1, + 0, + ); + + console.log(`Registering USDT...`); + const usdtMainnetMint = new PublicKey(MAINNET_MINTS.get('USDT')!); + const usdtMainnetOracle = new PublicKey(MAINNET_ORACLES.get('USDT')!); + await client.tokenRegister( + group, + usdtMainnetMint, + usdtMainnetOracle, + 0.1, + 1, + 'USDT', + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.95, + 0.9, + 1.05, + 1.1, + 0.025, // rule of thumb used - half of maintLiabWeight + ); + + console.log(`Registering BTC...`); + const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!); + const btcMainnetOracle = new PublicKey(MAINNET_ORACLES.get('BTC')!); + await client.tokenRegister( + group, + btcMainnetMint, + btcMainnetOracle, + 0.1, + 2, + 'BTC', + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + + console.log(`Registering ETH...`); + const ethMainnetMint = new PublicKey(MAINNET_MINTS.get('ETH')!); + const ethMainnetOracle = new PublicKey(MAINNET_ORACLES.get('ETH')!); + await client.tokenRegister( + group, + ethMainnetMint, + ethMainnetOracle, + 0.1, + 3, + 'ETH', + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + + console.log(`Registering soETH...`); + const soEthMainnetMint = new PublicKey(MAINNET_MINTS.get('soETH')!); + const soEthMainnetOracle = new PublicKey(MAINNET_ORACLES.get('soETH')!); + await client.tokenRegister( + group, + soEthMainnetMint, + soEthMainnetOracle, + 0.1, + 4, + 'soETH', + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + + console.log(`Registering SOL...`); + const solMainnetMint = new PublicKey(MAINNET_MINTS.get('SOL')!); + const solMainnetOracle = new PublicKey(MAINNET_ORACLES.get('SOL')!); + await client.tokenRegister( + group, + solMainnetMint, + solMainnetOracle, + 0.1, + 5, + 'SOL', + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + + console.log(`Registering mSOL...`); + const msolMainnetMint = new PublicKey(MAINNET_MINTS.get('mSOL')!); + const msolMainnetOracle = new PublicKey(MAINNET_ORACLES.get('mSOL')!); + await client.tokenRegister( + group, + msolMainnetMint, + msolMainnetOracle, + 0.1, + 6, + 'mSOL', + 0.004, + 0.7, + 0.1, + 0.85, + 0.2, + 2.0, + 0.005, + 0.0005, + 0.9, + 0.8, + 1.1, + 1.2, + 0.05, + ); + + // log tokens/banks + await group.reloadAll(client); + for (const [bank] of await group.banksMapByMint.values()) { + console.log(`${bank.toString()}`); + } +} + +async function main() { + createGroup(); + registerTokens(); +} + +try { + main(); +} catch (error) { + console.log(error); +} From 4919364d67411b57e51fd8c680d3a6311e726ab9 Mon Sep 17 00:00:00 2001 From: riordanp Date: Wed, 31 Aug 2022 12:52:30 +0100 Subject: [PATCH 46/50] Fix keeper dockerfile path --- .github/workflows/ci-docker-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-docker-publish.yml b/.github/workflows/ci-docker-publish.yml index 7ce5945bd..8a606530c 100644 --- a/.github/workflows/ci-docker-publish.yml +++ b/.github/workflows/ci-docker-publish.yml @@ -72,7 +72,7 @@ jobs: - name: Build and Push Keeper uses: docker/build-push-action@v2 with: - file: liquidator/Dockerfile.keeper + file: keeper/Dockerfile.keeper context: . push: true tags: | From cec0fcab999c5c99979381b11c0640ccdfec80b7 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Wed, 31 Aug 2022 13:55:15 +0200 Subject: [PATCH 47/50] patch script for strictness Signed-off-by: microwavedcola1 --- ts/client/src/debug-scripts/debug-banks.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ts/client/src/debug-scripts/debug-banks.ts b/ts/client/src/debug-scripts/debug-banks.ts index 6b0ba32d9..c14d7a040 100644 --- a/ts/client/src/debug-scripts/debug-banks.ts +++ b/ts/client/src/debug-scripts/debug-banks.ts @@ -59,7 +59,7 @@ async function main() { bank as any ).indexedDepositsByMangoAccounts.add( token.indexedPosition.mul( - banksMapUsingTokenIndex.get(token.tokenIndex).depositIndex, + banksMapUsingTokenIndex.get(token.tokenIndex)!.depositIndex, ), ); } @@ -69,7 +69,7 @@ async function main() { ).indexedBorrowsByMangoAccounts.add( token.indexedPosition .abs() - .mul(banksMapUsingTokenIndex.get(token.tokenIndex).borrowIndex), + .mul(banksMapUsingTokenIndex.get(token.tokenIndex)!.borrowIndex), ); } }), @@ -82,7 +82,7 @@ async function main() { coder() .accounts.decode( 'token', - (await client.program.provider.connection.getAccountInfo(bank.vault)) + (await client.program.provider.connection.getAccountInfo(bank.vault))! .data, ) .amount.toNumber(), @@ -102,7 +102,7 @@ async function main() { `\n ${'bank'.padEnd(40)} ${bank.publicKey}` + `\n ${'vault'.padEnd(40)} ${bank.vault}` + `\n ${'mint'.padEnd(40)} ${bank.mint}` + - `\n ${'price'.padEnd(40)} ${bank.price.toNumber()}` + + `\n ${'price'.padEnd(40)} ${bank.price?.toNumber()}` + `\n ${'uiPrice'.padEnd(40)} ${bank.uiPrice}` + `\n ${'error'.padEnd(40)} ${error}` + `\n ${'collectedFeesNative'.padEnd(40)} ${bank.collectedFeesNative}` + From 0c6cc160231827a3cf658f0c2b363cbeeb4758cb Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Wed, 31 Aug 2022 14:37:45 +0200 Subject: [PATCH 48/50] ts: serum display function for max leverage (#202) Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/accounts/healthCache.ts | 28 +++++----- ts/client/src/accounts/serum3.ts | 65 ++++++++++++++++++++++ ts/client/src/debug-scripts/debug-banks.ts | 1 + 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index bba440676..5ed1daefe 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -278,13 +278,13 @@ export class HealthCache { healthType: HealthType = HealthType.init, ): I80F48 { const adjustedCache: HealthCache = _.cloneDeep(this); - const quoteBanks = group.banksMapByTokenIndex.get( + const quoteBank = group.getFirstBankByTokenIndex( serum3Market.quoteTokenIndex, ); - if (!quoteBanks) { + if (!quoteBank) { throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`); } - const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBanks[0]); + const quoteIndex = adjustedCache.getOrCreateTokenInfoIndex(quoteBank); const quote = adjustedCache.tokenInfos[quoteIndex]; // Move token balance to reserved funds in open orders, @@ -315,13 +315,13 @@ export class HealthCache { healthType: HealthType = HealthType.init, ): I80F48 { const adjustedCache: HealthCache = _.cloneDeep(this); - const baseBanks = group.banksMapByTokenIndex.get( + const baseBank = group.getFirstBankByTokenIndex( serum3Market.baseTokenIndex, ); - if (!baseBanks) { + if (!baseBank) { throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`); } - const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBanks[0]); + const baseIndex = adjustedCache.getOrCreateTokenInfoIndex(baseBank); const base = adjustedCache.tokenInfos[baseIndex]; // Move token balance to reserved funds in open orders, @@ -537,24 +537,24 @@ export class HealthCache { side: Serum3Side, minRatio: I80F48, ) { - const baseBanks = group.banksMapByTokenIndex.get( + const baseBank = group.getFirstBankByTokenIndex( serum3Market.baseTokenIndex, ); - if (!baseBanks) { + if (!baseBank) { throw new Error(`No bank for index ${serum3Market.baseTokenIndex}`); } - const quoteBanks = group.banksMapByTokenIndex.get( + const quoteBank = group.getFirstBankByTokenIndex( serum3Market.quoteTokenIndex, ); - if (!quoteBanks) { + if (!quoteBank) { throw new Error(`No bank for index ${serum3Market.quoteTokenIndex}`); } const healthCacheClone: HealthCache = _.cloneDeep(this); - const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBanks[0]); + const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank); const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex( - quoteBanks[0], + quoteBank, ); const base = healthCacheClone.tokenInfos[baseIndex]; const quote = healthCacheClone.tokenInfos[quoteIndex]; @@ -647,11 +647,11 @@ export class HealthCache { return side === Serum3Side.bid ? amount .div(quote.oraclePrice) - .div(ONE_I80F48.add(baseBanks[0].loanOriginationFeeRate)) + .div(ONE_I80F48.add(baseBank.loanOriginationFeeRate)) .div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false)))) : amount .div(base.oraclePrice) - .div(ONE_I80F48.add(quoteBanks[0].loanOriginationFeeRate)) + .div(ONE_I80F48.add(quoteBank.loanOriginationFeeRate)) .div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false)))); } } diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index 8fc0e08b5..242983e7f 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -5,6 +5,7 @@ import BN from 'bn.js'; import { MangoClient } from '../client'; import { SERUM3_PROGRAM_ID } from '../constants'; import { Group } from './group'; +import { MAX_I80F48, ONE_I80F48, ZERO_I80F48 } from './I80F48'; export class Serum3Market { public name: string; @@ -49,6 +50,70 @@ export class Serum3Market { this.name = utf8.decode(new Uint8Array(name)).split('\x00')[0]; } + /** + * + * @param group + * @returns maximum leverage one can bid on this market, this is only for display purposes, + * also see getMaxQuoteForSerum3BidUi and getMaxBaseForSerum3AskUi + */ + maxBidLeverage(group: Group): number { + const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex); + if (!baseBank) { + throw new Error( + `bank for base token with index ${this.baseTokenIndex} not found`, + ); + } + + const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex); + if (!quoteBank) { + throw new Error( + `bank for quote token with index ${this.quoteTokenIndex} not found`, + ); + } + + if ( + quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48) + ) { + return MAX_I80F48.toNumber(); + } + + return ONE_I80F48.div( + quoteBank.initLiabWeight.sub(baseBank.initAssetWeight), + ).toNumber(); + } + + /** + * + * @param group + * @returns maximum leverage one can ask on this market, this is only for display purposes, + * also see getMaxQuoteForSerum3BidUi and getMaxBaseForSerum3AskUi + */ + maxAskLeverage(group: Group): number { + const baseBank = group.getFirstBankByTokenIndex(this.baseTokenIndex); + if (!baseBank) { + throw new Error( + `bank for base token with index ${this.baseTokenIndex} not found`, + ); + } + + const quoteBank = group.getFirstBankByTokenIndex(this.quoteTokenIndex); + if (!quoteBank) { + throw new Error( + `bank for quote token with index ${this.quoteTokenIndex} not found`, + ); + } + + if ( + baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48) + ) { + return MAX_I80F48.toNumber(); + } + + return ONE_I80F48.div( + baseBank.initLiabWeight.sub(quoteBank.initAssetWeight), + ).toNumber(); + } + public async loadBids(client: MangoClient, group: Group): Promise { const serum3MarketExternal = group.serum3MarketExternalsMap.get( this.serumMarketExternal.toBase58(), diff --git a/ts/client/src/debug-scripts/debug-banks.ts b/ts/client/src/debug-scripts/debug-banks.ts index c14d7a040..746b5d9e6 100644 --- a/ts/client/src/debug-scripts/debug-banks.ts +++ b/ts/client/src/debug-scripts/debug-banks.ts @@ -35,6 +35,7 @@ async function main() { const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM); console.log(`Group ${group.publicKey.toBase58()}`); + console.log(`${group.toString()}`); const banks = Array.from(group.banksMapByMint.values()).flat(); const banksMapUsingTokenIndex = new Map( From 5f62f155491cf91bb0ad4f5ed7c908cd6c55b515 Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Thu, 1 Sep 2022 09:48:23 +0200 Subject: [PATCH 49/50] mc/i80F48 in-place cleanups (#203) * ts: use in place I80F48 wherever applicable Signed-off-by: microwavedcola1 * ts: dont overwrite I80F48 constants Signed-off-by: microwavedcola1 * Fix script Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- ts/client/src/accounts/I80F48.ts | 30 +++- ts/client/src/accounts/bank.ts | 10 +- ts/client/src/accounts/group.ts | 2 +- ts/client/src/accounts/healthCache.ts | 179 +++++++++---------- ts/client/src/accounts/mangoAccount.ts | 38 ++-- ts/client/src/accounts/serum3.ts | 20 +-- ts/client/src/debug-scripts/debug-banks.ts | 4 +- ts/client/src/debug-scripts/mb-debug-user.ts | 37 ++-- 8 files changed, 158 insertions(+), 162 deletions(-) diff --git a/ts/client/src/accounts/I80F48.ts b/ts/client/src/accounts/I80F48.ts index 9c67c95f5..978a98848 100644 --- a/ts/client/src/accounts/I80F48.ts +++ b/ts/client/src/accounts/I80F48.ts @@ -197,16 +197,16 @@ export class I80F48 { return this.data.cmp(x.getData()); } neg(): I80F48 { - return this.mul(NEG_ONE_I80F48); + return this.mul(_NEG_ONE_I80F48); } isPos(): boolean { - return this.gt(ZERO_I80F48); + return this.gt(_ZERO_I80F48); } isNeg(): boolean { return this.data.isNeg(); } isZero(): boolean { - return this.eq(ZERO_I80F48); + return this.eq(_ZERO_I80F48); } min(x: I80F48): I80F48 { return this.lte(x) ? this : x; @@ -224,10 +224,22 @@ export class I80F48 { } /** @internal */ -export const ONE_I80F48 = I80F48.fromNumber(1); +const _ZERO_I80F48 = I80F48.fromNumber(0); /** @internal */ -export const ZERO_I80F48 = I80F48.fromNumber(0); -/** @internal */ -export const NEG_ONE_I80F48 = I80F48.fromNumber(-1); -export const HUNDRED_I80F48 = I80F48.fromNumber(100); -export const MAX_I80F48 = new I80F48(I80F48.MAX_BN); +const _NEG_ONE_I80F48 = I80F48.fromNumber(-1); + +export function ONE_I80F48(): I80F48 { + return I80F48.fromNumber(1); +} + +export function ZERO_I80F48(): I80F48 { + return I80F48.fromNumber(0); +} + +export function HUNDRED_I80F48(): I80F48 { + return I80F48.fromNumber(100); +} + +export function MAX_I80F48(): I80F48 { + return new I80F48(I80F48.MAX_BN); +} diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index 8a081c2ed..ef1c4078b 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -286,8 +286,8 @@ export class Bank { const totalBorrows = this.nativeBorrows(); const totalDeposits = this.nativeDeposits(); - if (totalDeposits.eq(ZERO_I80F48) && totalBorrows.eq(ZERO_I80F48)) { - return ZERO_I80F48; + if (totalDeposits.isZero() && totalBorrows.isZero()) { + return ZERO_I80F48(); } if (totalDeposits.lte(totalBorrows)) { return this.maxRate; @@ -329,9 +329,9 @@ export class Bank { const totalBorrows = this.nativeBorrows(); const totalDeposits = this.nativeDeposits(); - if (totalDeposits.eq(ZERO_I80F48) && totalBorrows.eq(ZERO_I80F48)) { - return ZERO_I80F48; - } else if (totalDeposits.eq(ZERO_I80F48)) { + if (totalDeposits.isZero() && totalBorrows.isZero()) { + return ZERO_I80F48(); + } else if (totalDeposits.isZero()) { return this.maxRate; } diff --git a/ts/client/src/accounts/group.ts b/ts/client/src/accounts/group.ts index fc1e6ad64..a04910b96 100644 --- a/ts/client/src/accounts/group.ts +++ b/ts/client/src/accounts/group.ts @@ -242,7 +242,7 @@ export class Group { for (const [index, price] of prices.entries()) { for (const bank of banks[index]) { if (bank.name === 'USDC') { - bank.price = ONE_I80F48; + bank.price = ONE_I80F48(); bank.uiPrice = 1; } else { // TODO: Implement switchboard oracle type diff --git a/ts/client/src/accounts/healthCache.ts b/ts/client/src/accounts/healthCache.ts index 5ed1daefe..8139d9b65 100644 --- a/ts/client/src/accounts/healthCache.ts +++ b/ts/client/src/accounts/healthCache.ts @@ -53,31 +53,31 @@ export class HealthCache { } public health(healthType: HealthType): I80F48 { - let health = ZERO_I80F48; + const health = ZERO_I80F48(); for (const tokenInfo of this.tokenInfos) { const contrib = tokenInfo.healthContribution(healthType); - health = health.add(contrib); + health.iadd(contrib); } for (const serum3Info of this.serum3Infos) { const contrib = serum3Info.healthContribution( healthType, this.tokenInfos, ); - health = health.add(contrib); + health.iadd(contrib); } for (const perpInfo of this.perpInfos) { const contrib = perpInfo.healthContribution(healthType); - health = health.add(contrib); + health.iadd(contrib); } return health; } public assets(healthType: HealthType): I80F48 { - let assets = ZERO_I80F48; + const assets = ZERO_I80F48(); for (const tokenInfo of this.tokenInfos) { const contrib = tokenInfo.healthContribution(healthType); if (contrib.isPos()) { - assets = assets.add(contrib); + assets.iadd(contrib); } } for (const serum3Info of this.serum3Infos) { @@ -86,24 +86,24 @@ export class HealthCache { this.tokenInfos, ); if (contrib.isPos()) { - assets = assets.add(contrib); + assets.iadd(contrib); } } for (const perpInfo of this.perpInfos) { const contrib = perpInfo.healthContribution(healthType); if (contrib.isPos()) { - assets = assets.add(contrib); + assets.iadd(contrib); } } return assets; } public liabs(healthType: HealthType): I80F48 { - let liabs = ZERO_I80F48; + const liabs = ZERO_I80F48(); for (const tokenInfo of this.tokenInfos) { const contrib = tokenInfo.healthContribution(healthType); if (contrib.isNeg()) { - liabs = liabs.sub(contrib); + liabs.isub(contrib); } } for (const serum3Info of this.serum3Infos) { @@ -112,28 +112,28 @@ export class HealthCache { this.tokenInfos, ); if (contrib.isNeg()) { - liabs = liabs.sub(contrib); + liabs.isub(contrib); } } for (const perpInfo of this.perpInfos) { const contrib = perpInfo.healthContribution(healthType); if (contrib.isNeg()) { - liabs = liabs.sub(contrib); + liabs.isub(contrib); } } return liabs; } public healthRatio(healthType: HealthType): I80F48 { - let assets = ZERO_I80F48; - let liabs = ZERO_I80F48; + const assets = ZERO_I80F48(); + const liabs = ZERO_I80F48(); for (const tokenInfo of this.tokenInfos) { const contrib = tokenInfo.healthContribution(healthType); if (contrib.isPos()) { - assets = assets.add(contrib); + assets.iadd(contrib); } else { - liabs = liabs.sub(contrib); + liabs.isub(contrib); } } for (const serum3Info of this.serum3Infos) { @@ -142,24 +142,24 @@ export class HealthCache { this.tokenInfos, ); if (contrib.isPos()) { - assets = assets.add(contrib); + assets.iadd(contrib); } else { - liabs = liabs.sub(contrib); + liabs.isub(contrib); } } for (const perpInfo of this.perpInfos) { const contrib = perpInfo.healthContribution(healthType); if (contrib.isPos()) { - assets = assets.add(contrib); + assets.iadd(contrib); } else { - liabs = liabs.sub(contrib); + liabs.isub(contrib); } } if (liabs.isPos()) { - return HUNDRED_I80F48.mul(assets.sub(liabs).div(liabs)); + return HUNDRED_I80F48().mul(assets.sub(liabs).div(liabs)); } else { - return MAX_I80F48; + return MAX_I80F48(); } } @@ -189,27 +189,18 @@ export class HealthCache { ) { const baseEntryIndex = this.findTokenInfoIndex(baseTokenIndex); const quoteEntryIndex = this.findTokenInfoIndex(quoteTokenIndex); - let reservedAmount = ZERO_I80F48; const baseEntry = this.tokenInfos[baseEntryIndex]; - reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice); + const reservedAmount = reservedBaseChange.mul(baseEntry.oraclePrice); const quoteEntry = this.tokenInfos[quoteEntryIndex]; - reservedAmount = reservedAmount.add( - reservedQuoteChange.mul(quoteEntry.oraclePrice), - ); + reservedAmount.iadd(reservedQuoteChange.mul(quoteEntry.oraclePrice)); // Apply it to the tokens - baseEntry.serum3MaxReserved = - baseEntry.serum3MaxReserved.add(reservedAmount); - baseEntry.balance = baseEntry.balance.add( - freeBaseChange.mul(baseEntry.oraclePrice), - ); - quoteEntry.serum3MaxReserved = - quoteEntry.serum3MaxReserved.add(reservedAmount); - quoteEntry.balance = quoteEntry.balance.add( - freeQuoteChange.mul(quoteEntry.oraclePrice), - ); + baseEntry.serum3MaxReserved.iadd(reservedAmount); + baseEntry.balance.iadd(freeBaseChange.mul(baseEntry.oraclePrice)); + quoteEntry.serum3MaxReserved.iadd(reservedAmount); + quoteEntry.balance.iadd(freeQuoteChange.mul(quoteEntry.oraclePrice)); // Apply it to the serum3 info const serum3Info = this.serum3Infos.find( @@ -263,9 +254,9 @@ export class HealthCache { throw new Error( `Oracle price not loaded for ${change.mintPk.toString()}`, ); - adjustedCache.tokenInfos[changeIndex].balance = adjustedCache.tokenInfos[ - changeIndex - ].balance.add(change.nativeTokenAmount.mul(bank.price)); + adjustedCache.tokenInfos[changeIndex].balance.iadd( + change.nativeTokenAmount.mul(bank.price), + ); } // HealthCache.logHealthCache('afterChange', adjustedCache); return adjustedCache.healthRatio(healthType); @@ -291,19 +282,19 @@ export class HealthCache { // essentially simulating a place order // Reduce token balance for quote - adjustedCache.tokenInfos[quoteIndex].balance = adjustedCache.tokenInfos[ - quoteIndex - ].balance.sub(bidNativeQuoteAmount.mul(quote.oraclePrice)); + adjustedCache.tokenInfos[quoteIndex].balance.isub( + bidNativeQuoteAmount.mul(quote.oraclePrice), + ); // Increase reserved in Serum3Info for quote adjustedCache.adjustSerum3Reserved( serum3Market.marketIndex, serum3Market.baseTokenIndex, - ZERO_I80F48, - ZERO_I80F48, + ZERO_I80F48(), + ZERO_I80F48(), serum3Market.quoteTokenIndex, bidNativeQuoteAmount, - ZERO_I80F48, + ZERO_I80F48(), ); return adjustedCache.healthRatio(healthType); } @@ -328,19 +319,19 @@ export class HealthCache { // essentially simulating a place order // Reduce token balance for base - adjustedCache.tokenInfos[baseIndex].balance = adjustedCache.tokenInfos[ - baseIndex - ].balance.sub(askNativeBaseAmount.mul(base.oraclePrice)); + adjustedCache.tokenInfos[baseIndex].balance.isub( + askNativeBaseAmount.mul(base.oraclePrice), + ); // Increase reserved in Serum3Info for base adjustedCache.adjustSerum3Reserved( serum3Market.marketIndex, serum3Market.baseTokenIndex, askNativeBaseAmount, - ZERO_I80F48, + ZERO_I80F48(), serum3Market.quoteTokenIndex, - ZERO_I80F48, - ZERO_I80F48, + ZERO_I80F48(), + ZERO_I80F48(), ); return adjustedCache.healthRatio(healthType); } @@ -399,20 +390,20 @@ export class HealthCache { const targetBank: Bank = group.getFirstBankByMint(targetMintPk); if (sourceMintPk.equals(targetMintPk)) { - return ZERO_I80F48; + return ZERO_I80F48(); } - if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48)) { - return ZERO_I80F48; + if (!sourceBank.price || sourceBank.price.lte(ZERO_I80F48())) { + return ZERO_I80F48(); } if ( sourceBank.initLiabWeight .sub(targetBank.initAssetWeight) .abs() - .lte(ZERO_I80F48) + .lte(ZERO_I80F48()) ) { - return ZERO_I80F48; + return ZERO_I80F48(); } // The health_ratio is a nonlinear based on swap amount. @@ -424,8 +415,8 @@ export class HealthCache { // - be careful about finding the minRatio point: the function isn't convex const initialRatio = this.healthRatio(HealthType.init); - if (initialRatio.lte(ZERO_I80F48)) { - return ZERO_I80F48; + if (initialRatio.lte(ZERO_I80F48())) { + return ZERO_I80F48(); } const healthCacheClone: HealthCache = _.cloneDeep(this); @@ -443,10 +434,8 @@ export class HealthCache { function cacheAfterSwap(amount: I80F48) { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); // HealthCache.logHealthCache('beforeSwap', adjustedCache); - adjustedCache.tokenInfos[sourceIndex].balance = - adjustedCache.tokenInfos[sourceIndex].balance.sub(amount); - adjustedCache.tokenInfos[targetIndex].balance = - adjustedCache.tokenInfos[targetIndex].balance.add(amount); + adjustedCache.tokenInfos[sourceIndex].balance.isub(amount); + adjustedCache.tokenInfos[targetIndex].balance.iadd(amount); // HealthCache.logHealthCache('afterSwap', adjustedCache); return adjustedCache; } @@ -457,10 +446,10 @@ export class HealthCache { const point0Amount = source.balance .min(target.balance.neg()) - .max(ZERO_I80F48); + .max(ZERO_I80F48()); const point1Amount = source.balance .max(target.balance.neg()) - .max(ZERO_I80F48); + .max(ZERO_I80F48()); const cache0 = cacheAfterSwap(point0Amount); const point0Ratio = cache0.healthRatio(HealthType.init); const cache1 = cacheAfterSwap(point1Amount); @@ -484,15 +473,15 @@ export class HealthCache { } else if (point1Ratio.gt(initialRatio)) { amount = point1Amount; } else { - amount = ZERO_I80F48; + amount = ZERO_I80F48(); } } else if (point1Ratio.gte(minRatio)) { // If point1Ratio is still bigger than minRatio, the target amount must be >point1Amount // search to the right of point1Amount: but how far? // At point1, source.balance < 0 and target.balance > 0, so use a simple estimation for // zero health: health - source_liab_weight * a + target_asset_weight * a = 0. - if (point1Health.lte(ZERO_I80F48)) { - return ZERO_I80F48; + if (point1Health.lte(ZERO_I80F48())) { + return ZERO_I80F48(); } const zeroHealthAmount = point1Amount.add( point1Health.div(source.initLiabWeight.sub(target.initAssetWeight)), @@ -525,7 +514,7 @@ export class HealthCache { return amount .div(source.oraclePrice) .div( - ONE_I80F48.add( + ONE_I80F48().add( group.getFirstBankByMint(sourceMintPk).loanOriginationFeeRate, ), ); @@ -553,9 +542,7 @@ export class HealthCache { const healthCacheClone: HealthCache = _.cloneDeep(this); const baseIndex = healthCacheClone.getOrCreateTokenInfoIndex(baseBank); - const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex( - quoteBank, - ); + const quoteIndex = healthCacheClone.getOrCreateTokenInfoIndex(quoteBank); const base = healthCacheClone.tokenInfos[baseIndex]; const quote = healthCacheClone.tokenInfos[quoteIndex]; @@ -563,11 +550,11 @@ export class HealthCache { // an amount to trade which will bring health to 0. // Current health and amount i.e. 0 - const initialAmount = ZERO_I80F48; + const initialAmount = ZERO_I80F48(); const initialHealth = this.health(HealthType.init); const initialRatio = this.healthRatio(HealthType.init); - if (initialRatio.lte(ZERO_I80F48)) { - return ZERO_I80F48; + if (initialRatio.lte(ZERO_I80F48())) { + return ZERO_I80F48(); } // Amount which would bring health to 0 @@ -577,9 +564,9 @@ export class HealthCache { // and when its a bid, then quote->bid let zeroAmount; if (side == Serum3Side.ask) { - const quoteBorrows = quote.balance.lt(ZERO_I80F48) + const quoteBorrows = quote.balance.lt(ZERO_I80F48()) ? quote.balance.abs() - : ZERO_I80F48; + : ZERO_I80F48(); zeroAmount = base.balance .max(quoteBorrows) .add( @@ -590,9 +577,9 @@ export class HealthCache { ), ); } else { - const baseBorrows = base.balance.lt(ZERO_I80F48) + const baseBorrows = base.balance.lt(ZERO_I80F48()) ? base.balance.abs() - : ZERO_I80F48; + : ZERO_I80F48(); zeroAmount = quote.balance .max(baseBorrows) .add( @@ -610,19 +597,17 @@ export class HealthCache { const adjustedCache: HealthCache = _.cloneDeep(healthCacheClone); side === Serum3Side.ask - ? (adjustedCache.tokenInfos[baseIndex].balance = - adjustedCache.tokenInfos[baseIndex].balance.sub(amount)) - : (adjustedCache.tokenInfos[quoteIndex].balance = - adjustedCache.tokenInfos[quoteIndex].balance.sub(amount)); + ? adjustedCache.tokenInfos[baseIndex].balance.isub(amount) + : adjustedCache.tokenInfos[quoteIndex].balance.isub(amount); adjustedCache.adjustSerum3Reserved( serum3Market.marketIndex, serum3Market.baseTokenIndex, - side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48, - ZERO_I80F48, + side === Serum3Side.ask ? amount.div(base.oraclePrice) : ZERO_I80F48(), + ZERO_I80F48(), serum3Market.quoteTokenIndex, - side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48, - ZERO_I80F48, + side === Serum3Side.bid ? amount.div(quote.oraclePrice) : ZERO_I80F48(), + ZERO_I80F48(), ); return adjustedCache; @@ -647,12 +632,12 @@ export class HealthCache { return side === Serum3Side.bid ? amount .div(quote.oraclePrice) - .div(ONE_I80F48.add(baseBank.loanOriginationFeeRate)) - .div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false)))) + .div(ONE_I80F48().add(baseBank.loanOriginationFeeRate)) + .div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false)))) : amount .div(base.oraclePrice) - .div(ONE_I80F48.add(quoteBank.loanOriginationFeeRate)) - .div(ONE_I80F48.add(I80F48.fromNumber(group.getFeeRate(false)))); + .div(ONE_I80F48().add(quoteBank.loanOriginationFeeRate)) + .div(ONE_I80F48().add(I80F48.fromNumber(group.getFeeRate(false)))); } } @@ -696,8 +681,8 @@ export class TokenInfo { bank.maintLiabWeight, bank.initLiabWeight, bank.price, - ZERO_I80F48, - ZERO_I80F48, + ZERO_I80F48(), + ZERO_I80F48(), ); } @@ -753,7 +738,7 @@ export class Serum3Info { const reserved = this.reserved; if (reserved.isZero()) { - return ZERO_I80F48; + return ZERO_I80F48(); } // How much the health would increase if the reserved balance were applied to the passed @@ -768,9 +753,9 @@ export class Serum3Info { let assetPart, liabPart; if (maxBalance.gte(reserved)) { assetPart = reserved; - liabPart = ZERO_I80F48; + liabPart = ZERO_I80F48(); } else if (maxBalance.isNeg()) { - assetPart = ZERO_I80F48; + assetPart = ZERO_I80F48(); liabPart = reserved; } else { assetPart = maxBalance; @@ -829,7 +814,7 @@ export class PerpInfo { // FUTURE: Allow v3-style "reliable" markets where we can return // `self.quote + weight * self.base` here - return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48); + return this.quote.add(weight.mul(this.base)).min(ZERO_I80F48()); } } diff --git a/ts/client/src/accounts/mangoAccount.ts b/ts/client/src/accounts/mangoAccount.ts index 21b96eab6..cd2fbec18 100644 --- a/ts/client/src/accounts/mangoAccount.ts +++ b/ts/client/src/accounts/mangoAccount.ts @@ -125,7 +125,7 @@ export class MangoAccount { */ getTokenBalance(bank: Bank): I80F48 { const tp = this.findToken(bank.tokenIndex); - return tp ? tp.balance(bank) : ZERO_I80F48; + return tp ? tp.balance(bank) : ZERO_I80F48(); } /** @@ -135,7 +135,7 @@ export class MangoAccount { */ getTokenDeposits(bank: Bank): I80F48 { const tp = this.findToken(bank.tokenIndex); - return tp ? tp.deposits(bank) : ZERO_I80F48; + return tp ? tp.deposits(bank) : ZERO_I80F48(); } /** @@ -145,7 +145,7 @@ export class MangoAccount { */ getTokenBorrows(bank: Bank): I80F48 { const tp = this.findToken(bank.tokenIndex); - return tp ? tp.borrows(bank) : ZERO_I80F48; + return tp ? tp.borrows(bank) : ZERO_I80F48(); } /** @@ -222,7 +222,7 @@ export class MangoAccount { const equity = this.accountData.equity; const total_equity = equity.tokens.reduce( (a, b) => a.add(b.value), - ZERO_I80F48, + ZERO_I80F48(), ); return total_equity; } @@ -268,8 +268,8 @@ export class MangoAccount { // Case 1: // Cannot withdraw if init health is below 0 - if (initHealth.lte(ZERO_I80F48)) { - return ZERO_I80F48; + if (initHealth.lte(ZERO_I80F48())) { + return ZERO_I80F48(); } // Deposits need special treatment since they would neither count towards liabilities @@ -277,12 +277,12 @@ export class MangoAccount { const tp = this.findToken(tokenBank.tokenIndex); if (!tokenBank.price) return undefined; - const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48; - let existingPositionHealthContrib = ZERO_I80F48; - if (existingTokenDeposits.gt(ZERO_I80F48)) { - existingPositionHealthContrib = existingTokenDeposits - .mul(tokenBank.price) - .mul(tokenBank.initAssetWeight); + const existingTokenDeposits = tp ? tp.deposits(tokenBank) : ZERO_I80F48(); + const existingPositionHealthContrib = ZERO_I80F48(); + if (existingTokenDeposits.gt(ZERO_I80F48())) { + existingTokenDeposits + .imul(tokenBank.price) + .imul(tokenBank.initAssetWeight); } // Case 2: token deposits have higher contribution than initHealth, @@ -309,7 +309,7 @@ export class MangoAccount { .div(tokenBank.initLiabWeight) .div(tokenBank.price); const maxBorrowNativeWithoutFees = maxBorrowNative.div( - ONE_I80F48.add(tokenBank.loanOriginationFeeRate), + ONE_I80F48().add(tokenBank.loanOriginationFeeRate), ); // console.log(`initHealth ${initHealth}`); // console.log( @@ -357,7 +357,7 @@ export class MangoAccount { group, sourceMintPk, targetMintPk, - ONE_I80F48, // target 1% health + ONE_I80F48(), // target 1% health ) .mul(I80F48.fromNumber(slippageAndFeesFactor)); } @@ -690,7 +690,7 @@ export class MangoAccount { const initHealth = this.accountData.initHealth; const perpMarket = group.perpMarketsMap.get(marketName)!; const marketAssetWeight = perpMarket.initAssetWeight; - return initHealth.div(ONE_I80F48.sub(marketAssetWeight)); + return initHealth.div(ONE_I80F48().sub(marketAssetWeight)); } toString(group?: Group): string { @@ -768,8 +768,8 @@ export class TokenPosition { * @returns native deposits, 0 if position has borrows */ public deposits(bank: Bank): I80F48 { - if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48)) { - return ZERO_I80F48; + if (this.indexedPosition && this.indexedPosition.lt(ZERO_I80F48())) { + return ZERO_I80F48(); } return this.balance(bank); } @@ -780,8 +780,8 @@ export class TokenPosition { * @returns native borrows, 0 if position has deposits */ public borrows(bank: Bank): I80F48 { - if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48)) { - return ZERO_I80F48; + if (this.indexedPosition && this.indexedPosition.gt(ZERO_I80F48())) { + return ZERO_I80F48(); } return this.balance(bank).abs(); } diff --git a/ts/client/src/accounts/serum3.ts b/ts/client/src/accounts/serum3.ts index 242983e7f..bb07fd09d 100644 --- a/ts/client/src/accounts/serum3.ts +++ b/ts/client/src/accounts/serum3.ts @@ -72,14 +72,14 @@ export class Serum3Market { } if ( - quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48) + quoteBank.initLiabWeight.sub(baseBank.initAssetWeight).lte(ZERO_I80F48()) ) { - return MAX_I80F48.toNumber(); + return MAX_I80F48().toNumber(); } - return ONE_I80F48.div( - quoteBank.initLiabWeight.sub(baseBank.initAssetWeight), - ).toNumber(); + return ONE_I80F48() + .div(quoteBank.initLiabWeight.sub(baseBank.initAssetWeight)) + .toNumber(); } /** @@ -104,14 +104,14 @@ export class Serum3Market { } if ( - baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48) + baseBank.initLiabWeight.sub(quoteBank.initAssetWeight).lte(ZERO_I80F48()) ) { - return MAX_I80F48.toNumber(); + return MAX_I80F48().toNumber(); } - return ONE_I80F48.div( - baseBank.initLiabWeight.sub(quoteBank.initAssetWeight), - ).toNumber(); + return ONE_I80F48() + .div(baseBank.initLiabWeight.sub(quoteBank.initAssetWeight)) + .toNumber(); } public async loadBids(client: MangoClient, group: Group): Promise { diff --git a/ts/client/src/debug-scripts/debug-banks.ts b/ts/client/src/debug-scripts/debug-banks.ts index 746b5d9e6..9c46369f1 100644 --- a/ts/client/src/debug-scripts/debug-banks.ts +++ b/ts/client/src/debug-scripts/debug-banks.ts @@ -40,8 +40,8 @@ async function main() { const banks = Array.from(group.banksMapByMint.values()).flat(); const banksMapUsingTokenIndex = new Map( banks.map((bank) => { - (bank as any).indexedDepositsByMangoAccounts = ZERO_I80F48; - (bank as any).indexedBorrowsByMangoAccounts = ZERO_I80F48; + (bank as any).indexedDepositsByMangoAccounts = ZERO_I80F48(); + (bank as any).indexedBorrowsByMangoAccounts = ZERO_I80F48(); return [bank.tokenIndex, bank]; }), ); diff --git a/ts/client/src/debug-scripts/mb-debug-user.ts b/ts/client/src/debug-scripts/mb-debug-user.ts index 764adc27a..50c2da8cf 100644 --- a/ts/client/src/debug-scripts/mb-debug-user.ts +++ b/ts/client/src/debug-scripts/mb-debug-user.ts @@ -24,8 +24,8 @@ async function debugUser( group, mangoAccount, [ - group.banksMapByName.get('BTC')[0], - group.banksMapByName.get('USDC')[0], + group.banksMapByName.get('BTC')![0], + group.banksMapByName.get('USDC')![0], ], [], ) @@ -34,15 +34,15 @@ async function debugUser( ); console.log( 'mangoAccount.getEquity() ' + - toUiDecimalsForQuote(mangoAccount.getEquity().toNumber()), + toUiDecimalsForQuote(mangoAccount.getEquity()!.toNumber()), ); console.log( 'mangoAccount.getHealth(HealthType.init) ' + - toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init).toNumber()), + toUiDecimalsForQuote(mangoAccount.getHealth(HealthType.init)!.toNumber()), ); console.log( 'mangoAccount.getHealthRatio(HealthType.init) ' + - mangoAccount.getHealthRatio(HealthType.init).toNumber(), + mangoAccount.getHealthRatio(HealthType.init)!.toNumber(), ); console.log( 'mangoAccount.getHealthRatioUi(HealthType.init) ' + @@ -50,7 +50,7 @@ async function debugUser( ); console.log( 'mangoAccount.getHealthRatio(HealthType.maint) ' + - mangoAccount.getHealthRatio(HealthType.maint).toNumber(), + mangoAccount.getHealthRatio(HealthType.maint)!.toNumber(), ); console.log( 'mangoAccount.getHealthRatioUi(HealthType.maint) ' + @@ -58,18 +58,18 @@ async function debugUser( ); console.log( 'mangoAccount.getCollateralValue() ' + - toUiDecimalsForQuote(mangoAccount.getCollateralValue().toNumber()), + toUiDecimalsForQuote(mangoAccount.getCollateralValue()!.toNumber()), ); console.log( 'mangoAccount.getAssetsValue() ' + toUiDecimalsForQuote( - mangoAccount.getAssetsValue(HealthType.init).toNumber(), + mangoAccount.getAssetsValue(HealthType.init)!.toNumber(), ), ); console.log( 'mangoAccount.getLiabsValue() ' + toUiDecimalsForQuote( - mangoAccount.getLiabsValue(HealthType.init).toNumber(), + mangoAccount.getLiabsValue(HealthType.init)!.toNumber(), ), ); @@ -78,7 +78,7 @@ async function debugUser( `mangoAccount.getMaxWithdrawWithBorrowForTokenUi(group, ${token}) ` + mangoAccount.getMaxWithdrawWithBorrowForTokenUi( group, - group.banksMapByName.get(token)[0].mint, + group.banksMapByName.get(token)![0].mint, ), ); } @@ -94,29 +94,28 @@ async function debugUser( } for (const srcToken of Array.from(group.banksMapByName.keys())) { simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} 1 `, { - mintPk: group.banksMapByName.get(srcToken)[0].mint, + mintPk: group.banksMapByName.get(srcToken)![0].mint, uiTokenAmount: 1, }); simHealthRatioWithTokenPositionChangesWrapper(`${srcToken} -1 `, { - mintPk: group.banksMapByName.get(srcToken)[0].mint, + mintPk: group.banksMapByName.get(srcToken)![0].mint, uiTokenAmount: -1, }); } function getMaxSourceForTokenSwapWrapper(src, tgt) { - // console.log(); console.log( `getMaxSourceForTokenSwap ${src.padEnd(4)} ${tgt.padEnd(4)} ` + mangoAccount .getMaxSourceForTokenSwap( group, - group.banksMapByName.get(src)[0].mint, - group.banksMapByName.get(tgt)[0].mint, + group.banksMapByName.get(src)![0].mint, + group.banksMapByName.get(tgt)![0].mint, 1, - ) + )! .div( I80F48.fromNumber( - Math.pow(10, group.banksMapByName.get(src)[0].mintDecimals), + Math.pow(10, group.banksMapByName.get(src)![0].mintDecimals), ), ) .toNumber(), @@ -153,8 +152,8 @@ async function main() { const group = await client.getGroupForCreator(admin.publicKey, 2); for (const keypair of [ - process.env.MB_PAYER_KEYPAIR, - process.env.MB_USER2_KEYPAIR, + process.env.MB_PAYER_KEYPAIR!, + process.env.MB_USER2_KEYPAIR!, ]) { console.log(); const user = Keypair.fromSecretKey( From b30b2f88705854cdd3a5811ae2344ce358fb9a2d Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Thu, 1 Sep 2022 09:48:38 +0200 Subject: [PATCH 50/50] program: serum3 should work when group has version 1, perps and multiple banks should be moved to version 2 or beyond (#205) Signed-off-by: microwavedcola1 Signed-off-by: microwavedcola1 --- programs/mango-v4/src/state/group.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/mango-v4/src/state/group.rs b/programs/mango-v4/src/state/group.rs index b24e92648..8aaabbcf8 100644 --- a/programs/mango-v4/src/state/group.rs +++ b/programs/mango-v4/src/state/group.rs @@ -44,7 +44,7 @@ impl Group { } pub fn multiple_banks_supported(&self) -> bool { - self.is_testing() || self.version > 0 + self.is_testing() || self.version > 1 } pub fn serum3_supported(&self) -> bool { @@ -52,7 +52,7 @@ impl Group { } pub fn perps_supported(&self) -> bool { - self.is_testing() || self.version > 0 + self.is_testing() || self.version > 1 } }