Fix bug in closing mango account (#559)

* reafactor code for collecting health accounts, fix bug where bank oracle was skipped while closing account

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* Fixes from review

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

---------

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2023-04-24 14:48:53 +02:00 committed by GitHub
parent fe8d1a63bd
commit 1bf1a8deb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 88 additions and 195 deletions

View File

@ -20,6 +20,8 @@ import {
TransactionSignature, TransactionSignature,
} from '@solana/web3.js'; } from '@solana/web3.js';
import bs58 from 'bs58'; import bs58 from 'bs58';
import cloneDeep from 'lodash/cloneDeep';
import uniq from 'lodash/uniq';
import { Bank, MintInfo, TokenIndex } from './accounts/bank'; import { Bank, MintInfo, TokenIndex } from './accounts/bank';
import { Group } from './accounts/group'; import { Group } from './accounts/group';
import { import {
@ -39,6 +41,7 @@ import {
PerpOrderType, PerpOrderType,
} from './accounts/perp'; } from './accounts/perp';
import { import {
MarketIndex,
Serum3Market, Serum3Market,
Serum3OrderType, Serum3OrderType,
Serum3SelfTradeBehavior, Serum3SelfTradeBehavior,
@ -719,13 +722,7 @@ export class MangoClient {
mangoAccount: MangoAccount, mangoAccount: MangoAccount,
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(group, [mangoAccount], [], []);
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[],
);
const ix = await this.program.methods const ix = await this.program.methods
.computeAccountData() .computeAccountData()
@ -939,67 +936,59 @@ export class MangoClient {
group: Group, group: Group,
mangoAccount: MangoAccount, mangoAccount: MangoAccount,
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
// Work on a deep cloned mango account, since we would deactivating positions
// before deactivation reaches on-chain state in order to simplify building a fresh list
// of healthRemainingAccounts to each subsequent ix
const clonedMangoAccount = cloneDeep(mangoAccount);
const instructions: TransactionInstruction[] = []; const instructions: TransactionInstruction[] = [];
const healthAccountsToExclude: PublicKey[] = [];
for (const serum3Account of mangoAccount.serum3Active()) { for (const serum3Account of clonedMangoAccount.serum3Active()) {
const serum3Market = group.serum3MarketsMapByMarketIndex.get( const serum3Market = group.serum3MarketsMapByMarketIndex.get(
serum3Account.marketIndex, serum3Account.marketIndex,
)!; )!;
const closeOOIx = await this.serum3CloseOpenOrdersIx( const closeOOIx = await this.serum3CloseOpenOrdersIx(
group, group,
mangoAccount, clonedMangoAccount,
serum3Market.serumMarketExternal, serum3Market.serumMarketExternal,
); );
healthAccountsToExclude.push(serum3Account.openOrders);
instructions.push(closeOOIx); instructions.push(closeOOIx);
serum3Account.marketIndex =
Serum3Orders.Serum3MarketIndexUnset as MarketIndex;
} }
for (const perp of mangoAccount.perpActive()) { for (const pp of clonedMangoAccount.perpActive()) {
const perpMarketIndex = perp.marketIndex; const perpMarketIndex = pp.marketIndex;
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const deactivatingPositionIx = await this.perpDeactivatePositionIx( const deactivatingPositionIx = await this.perpDeactivatePositionIx(
group, group,
mangoAccount, clonedMangoAccount,
perpMarketIndex, perpMarketIndex,
); );
healthAccountsToExclude.push(perpMarket.publicKey, perpMarket.oracle);
instructions.push(deactivatingPositionIx); instructions.push(deactivatingPositionIx);
pp.marketIndex = PerpPosition.PerpMarketIndexUnset as PerpMarketIndex;
} }
for (const index in mangoAccount.tokensActive()) { for (const tp of clonedMangoAccount.tokensActive()) {
const indexNum = Number(index); const bank = group.getFirstBankByTokenIndex(tp.tokenIndex);
const accountsToExclude = [...healthAccountsToExclude];
const token = mangoAccount.tokensActive()[indexNum];
const bank = group.getFirstBankByTokenIndex(token.tokenIndex);
//to withdraw from all token accounts we need to exclude previous tokens pubkeys
//used to build health remaining accounts
if (indexNum !== 0) {
for (let i = indexNum; i--; i >= 0) {
const prevToken = mangoAccount.tokensActive()[i];
const prevBank = group.getFirstBankByTokenIndex(prevToken.tokenIndex);
accountsToExclude.push(prevBank.publicKey, prevBank.oracle);
}
}
const withdrawIx = await this.tokenWithdrawNativeIx( const withdrawIx = await this.tokenWithdrawNativeIx(
group, group,
mangoAccount, clonedMangoAccount,
bank.mint, bank.mint,
U64_MAX_BN, U64_MAX_BN,
false, false,
[...accountsToExclude],
); );
instructions.push(...withdrawIx); instructions.push(...withdrawIx);
tp.tokenIndex = TokenPosition.TokenIndexUnset as TokenIndex;
} }
const closeIx = await this.program.methods const closeIx = await this.program.methods
.accountClose(false) .accountClose(false)
.accounts({ .accounts({
group: group.publicKey, group: group.publicKey,
account: mangoAccount.publicKey, account: clonedMangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey, owner: (this.program.provider as AnchorProvider).wallet.publicKey,
solDestination: mangoAccount.owner, solDestination: clonedMangoAccount.owner,
}) })
.instruction(); .instruction();
instructions.push(closeIx); instructions.push(closeIx);
@ -1105,13 +1094,7 @@ export class MangoClient {
} }
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(group, [mangoAccount], [bank], []);
AccountRetriever.Fixed,
group,
[mangoAccount],
[bank],
[],
);
const ix = await this.program.methods const ix = await this.program.methods
.tokenDeposit(new BN(nativeAmount), reduceOnly) .tokenDeposit(new BN(nativeAmount), reduceOnly)
@ -1165,7 +1148,6 @@ export class MangoClient {
mintPk: PublicKey, mintPk: PublicKey,
nativeAmount: BN, nativeAmount: BN,
allowBorrow: boolean, allowBorrow: boolean,
healthAccountsToExclude: PublicKey[] = [],
): Promise<TransactionInstruction[]> { ): Promise<TransactionInstruction[]> {
const bank = group.getFirstBankByMint(mintPk); const bank = group.getFirstBankByMint(mintPk);
@ -1195,13 +1177,7 @@ export class MangoClient {
} }
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(group, [mangoAccount], [bank], [], []);
AccountRetriever.Fixed,
group,
[mangoAccount],
[bank],
[],
);
const ix = await this.program.methods const ix = await this.program.methods
.tokenWithdraw(new BN(nativeAmount), allowBorrow) .tokenWithdraw(new BN(nativeAmount), allowBorrow)
@ -1215,14 +1191,7 @@ export class MangoClient {
tokenAccount: tokenAccountPk, tokenAccount: tokenAccountPk,
}) })
.remainingAccounts( .remainingAccounts(
healthRemainingAccounts healthRemainingAccounts.map(
.filter(
(accounts) =>
!healthAccountsToExclude.find((accountsToExclude) =>
accounts.equals(accountsToExclude),
),
)
.map(
(pk) => (pk) =>
({ ({
pubkey: pk, pubkey: pk,
@ -1242,7 +1211,6 @@ export class MangoClient {
mintPk: PublicKey, mintPk: PublicKey,
nativeAmount: BN, nativeAmount: BN,
allowBorrow: boolean, allowBorrow: boolean,
healthAccountsToExclude: PublicKey[] = [],
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
const ixs = await this.tokenWithdrawNativeIx( const ixs = await this.tokenWithdrawNativeIx(
group, group,
@ -1250,7 +1218,6 @@ export class MangoClient {
mintPk, mintPk,
nativeAmount, nativeAmount,
allowBorrow, allowBorrow,
healthAccountsToExclude,
); );
return await this.sendAndConfirmTransactionForGroup(group, ixs); return await this.sendAndConfirmTransactionForGroup(group, ixs);
} }
@ -1494,7 +1461,6 @@ export class MangoClient {
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group, group,
[mangoAccount], [mangoAccount],
banks, banks,
@ -2073,13 +2039,7 @@ export class MangoClient {
): Promise<TransactionInstruction> { ): Promise<TransactionInstruction> {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(group, [mangoAccount], [], []);
AccountRetriever.Fixed,
group,
[mangoAccount],
[],
[],
);
return await this.program.methods return await this.program.methods
.perpDeactivatePosition() .perpDeactivatePosition()
.accounts({ .accounts({
@ -2162,7 +2122,6 @@ export class MangoClient {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group, group,
[mangoAccount], [mangoAccount],
// Settlement token bank, because a position for it may be created // Settlement token bank, because a position for it may be created
@ -2254,7 +2213,6 @@ export class MangoClient {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group, group,
[mangoAccount], [mangoAccount],
// Settlement token bank, because a position for it may be created // Settlement token bank, because a position for it may be created
@ -2422,7 +2380,6 @@ export class MangoClient {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Scanning,
group, group,
[profitableAccount, unprofitableAccount], [profitableAccount, unprofitableAccount],
[group.getFirstBankForPerpSettlement()], [group.getFirstBankForPerpSettlement()],
@ -2477,7 +2434,6 @@ export class MangoClient {
const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex); const perpMarket = group.getPerpMarketByMarketIndex(perpMarketIndex);
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group, group,
[account], // Account must be unprofitable [account], // Account must be unprofitable
[group.getFirstBankForPerpSettlement()], [group.getFirstBankForPerpSettlement()],
@ -2614,7 +2570,6 @@ export class MangoClient {
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group, group,
[mangoAccount], [mangoAccount],
[inputBank, outputBank], [inputBank, outputBank],
@ -2808,7 +2763,6 @@ export class MangoClient {
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Scanning,
group, group,
[liqor, liqee], [liqor, liqee],
[assetBank, liabBank], [assetBank, liabBank],
@ -2886,7 +2840,6 @@ export class MangoClient {
): Promise<TransactionInstruction> { ): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group, group,
[account], [account],
[...banks], [...banks],
@ -2920,7 +2873,6 @@ export class MangoClient {
): Promise<TransactionInstruction> { ): Promise<TransactionInstruction> {
const healthRemainingAccounts: PublicKey[] = const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts( this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group, group,
[account], [account],
[...banks], [...banks],
@ -2979,44 +2931,39 @@ export class MangoClient {
); );
} }
public buildHealthRemainingAccounts( /**
retriever: AccountRetriever, * Builds health remaining accounts.
*
* For single mango account it builds a list of PublicKeys
* which is compatbile with Fixed account retriever.
*
* For multiple mango accounts it uses same logic as for fixed
* but packing all banks, then perp markets, and then serum oo accounts, which
* should always be compatible with Scanning account retriever.
*
* @param group
* @param mangoAccounts
* @param banks - banks in which new positions might be opened
* @param perpMarkets - markets in which new positions might be opened
* @param openOrdersForMarket - markets in which new positions might be opened
* @returns
*/
buildHealthRemainingAccounts(
group: Group, group: Group,
mangoAccounts: MangoAccount[], mangoAccounts: MangoAccount[],
// Banks and markets for whom positions don't exist on mango account,
// but user would potentially open new positions.
banks: Bank[] = [], banks: Bank[] = [],
perpMarkets: PerpMarket[] = [], perpMarkets: PerpMarket[] = [],
openOrdersForMarket: [Serum3Market, PublicKey][] = [], openOrdersForMarket: [Serum3Market, PublicKey][] = [],
): PublicKey[] {
if (retriever === AccountRetriever.Fixed) {
return this.buildFixedAccountRetrieverHealthAccounts(
group,
mangoAccounts[0],
banks,
perpMarkets,
openOrdersForMarket,
);
} else {
return this.buildScanningAccountRetrieverHealthAccounts(
group,
mangoAccounts,
banks,
perpMarkets,
);
}
}
private buildFixedAccountRetrieverHealthAccounts(
group: Group,
mangoAccount: MangoAccount,
// Banks and perpMarkets for whom positions don't exist on mango account,
// but user would potentially open new positions.
banks: Bank[],
perpMarkets: PerpMarket[],
openOrdersForMarket: [Serum3Market, PublicKey][],
): PublicKey[] { ): PublicKey[] {
const healthRemainingAccounts: PublicKey[] = []; const healthRemainingAccounts: PublicKey[] = [];
const tokenPositionIndices = mangoAccount.tokens.map((t) => t.tokenIndex); const tokenPositionIndices = uniq(
mangoAccounts
.map((mangoAccount) => mangoAccount.tokens.map((t) => t.tokenIndex))
.flat(),
);
for (const bank of banks) { for (const bank of banks) {
const tokenPositionExists = const tokenPositionExists =
tokenPositionIndices.indexOf(bank.tokenIndex) > -1; tokenPositionIndices.indexOf(bank.tokenIndex) > -1;
@ -3039,55 +2986,66 @@ export class MangoClient {
...mintInfos.map((mintInfo) => mintInfo.oracle), ...mintInfos.map((mintInfo) => mintInfo.oracle),
); );
// insert any extra perp markets in the free perp position slots // Insert any extra perp markets in the free perp position slots
const perpPositionIndices = mangoAccount.perps.map((p) => p.marketIndex); const perpPositionsMarketIndices = uniq(
mangoAccounts
.map((mangoAccount) => mangoAccount.perps.map((p) => p.marketIndex))
.flat(),
);
for (const perpMarket of perpMarkets) { for (const perpMarket of perpMarkets) {
const perpPositionExists = const perpPositionExists =
perpPositionIndices.indexOf(perpMarket.perpMarketIndex) > -1; perpPositionsMarketIndices.indexOf(perpMarket.perpMarketIndex) > -1;
if (!perpPositionExists) { if (!perpPositionExists) {
const inactivePerpPosition = perpPositionIndices.findIndex( const inactivePerpPosition = perpPositionsMarketIndices.findIndex(
(perpIdx) => perpIdx === PerpPosition.PerpMarketIndexUnset, (perpIdx) => perpIdx === PerpPosition.PerpMarketIndexUnset,
); );
if (inactivePerpPosition != -1) { if (inactivePerpPosition != -1) {
perpPositionIndices[inactivePerpPosition] = perpPositionsMarketIndices[inactivePerpPosition] =
perpMarket.perpMarketIndex; perpMarket.perpMarketIndex;
} }
} }
} }
const allPerpMarkets = perpPositionsMarketIndices
const allPerpMarkets = perpPositionIndices .filter(
.filter((perpIdx) => perpIdx !== PerpPosition.PerpMarketIndexUnset) (perpMarktIndex) =>
perpMarktIndex !== PerpPosition.PerpMarketIndexUnset,
)
.map((perpIdx) => group.getPerpMarketByMarketIndex(perpIdx)!); .map((perpIdx) => group.getPerpMarketByMarketIndex(perpIdx)!);
healthRemainingAccounts.push( healthRemainingAccounts.push(
...allPerpMarkets.map((perp) => perp.publicKey), ...allPerpMarkets.map((perp) => perp.publicKey),
); );
healthRemainingAccounts.push(...allPerpMarkets.map((perp) => perp.oracle)); healthRemainingAccounts.push(...allPerpMarkets.map((perp) => perp.oracle));
// insert any extra open orders accounts in the cooresponding free serum market slot // Insert any extra open orders accounts in the cooresponding free serum market slot
const serumPositionIndices = mangoAccount.serum3.map((s) => ({ const serumPositionMarketIndices = mangoAccounts
.map((mangoAccount) =>
mangoAccount.serum3.map((s) => ({
marketIndex: s.marketIndex, marketIndex: s.marketIndex,
openOrders: s.openOrders, openOrders: s.openOrders,
})); })),
)
.flat();
for (const [serum3Market, openOrderPk] of openOrdersForMarket) { for (const [serum3Market, openOrderPk] of openOrdersForMarket) {
const ooPositionExists = const ooPositionExists =
serumPositionIndices.findIndex( serumPositionMarketIndices.findIndex(
(i) => i.marketIndex === serum3Market.marketIndex, (i) => i.marketIndex === serum3Market.marketIndex,
) > -1; ) > -1;
if (!ooPositionExists) { if (!ooPositionExists) {
const inactiveSerumPosition = serumPositionIndices.findIndex( const inactiveSerumPosition = serumPositionMarketIndices.findIndex(
(serumPos) => (serumPos) =>
serumPos.marketIndex === Serum3Orders.Serum3MarketIndexUnset, serumPos.marketIndex === Serum3Orders.Serum3MarketIndexUnset,
); );
if (inactiveSerumPosition != -1) { if (inactiveSerumPosition != -1) {
serumPositionIndices[inactiveSerumPosition].marketIndex = serumPositionMarketIndices[inactiveSerumPosition].marketIndex =
serum3Market.marketIndex; serum3Market.marketIndex;
serumPositionIndices[inactiveSerumPosition].openOrders = openOrderPk; serumPositionMarketIndices[inactiveSerumPosition].openOrders =
openOrderPk;
} }
} }
} }
healthRemainingAccounts.push( healthRemainingAccounts.push(
...serumPositionIndices ...serumPositionMarketIndices
.filter( .filter(
(serumPosition) => (serumPosition) =>
serumPosition.marketIndex !== Serum3Orders.Serum3MarketIndexUnset, serumPosition.marketIndex !== Serum3Orders.Serum3MarketIndexUnset,
@ -3095,71 +3053,6 @@ export class MangoClient {
.map((serumPosition) => serumPosition.openOrders), .map((serumPosition) => serumPosition.openOrders),
); );
// debugHealthAccounts(group, mangoAccount, healthRemainingAccounts);
return healthRemainingAccounts;
}
private buildScanningAccountRetrieverHealthAccounts(
group: Group,
mangoAccounts: MangoAccount[],
banks: Bank[],
perpMarkets: PerpMarket[],
): PublicKey[] {
const healthRemainingAccounts: PublicKey[] = [];
let tokenIndices: TokenIndex[] = [];
for (const mangoAccount of mangoAccounts) {
tokenIndices.push(
...mangoAccount.tokens
.filter((token) => token.tokenIndex !== 65535)
.map((token) => token.tokenIndex),
);
}
tokenIndices = [...new Set(tokenIndices)];
if (banks?.length) {
for (const bank of banks) {
tokenIndices.push(bank.tokenIndex);
}
}
const mintInfos = [...new Set(tokenIndices)].map(
(tokenIndex) => group.mintInfosMapByTokenIndex.get(tokenIndex)!,
);
healthRemainingAccounts.push(
...mintInfos.map((mintInfo) => mintInfo.firstBank()),
);
healthRemainingAccounts.push(
...mintInfos.map((mintInfo) => mintInfo.oracle),
);
const perpIndices: PerpMarketIndex[] = [];
for (const mangoAccount of mangoAccounts) {
perpIndices.push(
...mangoAccount.perps
.filter((perp) => perp.marketIndex !== 65535)
.map((perp) => perp.marketIndex),
);
}
perpIndices.push(...perpMarkets.map((perp) => perp.perpMarketIndex));
const allPerpMarkets = [...new Set(perpIndices)].map(
(marketIndex) => group.findPerpMarket(marketIndex)!,
);
// Add perp accounts
healthRemainingAccounts.push(...allPerpMarkets.map((p) => p.publicKey));
// Add oracle for each perp
healthRemainingAccounts.push(...allPerpMarkets.map((p) => p.oracle));
for (const mangoAccount of mangoAccounts) {
healthRemainingAccounts.push(
...mangoAccount.serum3
.filter((serum3Account) => serum3Account.marketIndex !== 65535)
.map((serum3Account) => serum3Account.openOrders),
);
}
return healthRemainingAccounts; return healthRemainingAccounts;
} }

View File

@ -43,7 +43,7 @@ export async function sendTransaction(
if ( if (
typeof payer.signTransaction === 'function' && typeof payer.signTransaction === 'function' &&
!(payer instanceof NodeWallet) !(payer instanceof NodeWallet || payer.constructor.name == 'NodeWallet')
) { ) {
vtx = (await payer.signTransaction( vtx = (await payer.signTransaction(
vtx as any, vtx as any,