Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/specs/juplend/jlr03_multiDeposit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ describe("jlr03: JupLend multi-deposit + health pulse (bankrun)", () => {
return value;
};

// Note: All prices in this test are for assets, so the get a confidence discount
// Note: All prices in this test are for assets, so they get a confidence discount
const adjustedOraclePriceForMint = (mint: PublicKey): number => {
const confAdj = 1 - ORACLE_CONF_INTERVAL * CONF_INTERVAL_MULTIPLE;
if (mint.equals(ecosystem.usdcMint.publicKey))
Expand Down
9 changes: 1 addition & 8 deletions tests/specs/juplend/jlr04_withdraw.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
VersionedTransaction,
} from "@solana/web3.js";
import { assert } from "chai";
import BigNumber from "bignumber.js";
import {
BanksTransactionMeta,
BanksTransactionResultWithMeta,
Expand All @@ -36,6 +35,7 @@ import {
assertI80F48Approx,
assertI80F48Equal,
getTokenBalance,
i80ToBn,
} from "../../utils/genericTests";
import { deriveLiquidityVaultAuthority } from "../../utils/pdas";
import { deriveJuplendPoolKeys } from "../../utils/juplend/juplend-pdas";
Expand Down Expand Up @@ -178,13 +178,6 @@ describe("jlr04: JupLend withdraws (bankrun)", () => {
activeMarginfiAccountPk = marginfiAccount;
};

const i80ToBn = (value: any): BN =>
new BN(
wrappedI80F48toBigNumber(value)
.integerValue(BigNumber.ROUND_FLOOR)
.toFixed(0),
);

const previewSharesForDeposit = (
assets: BN,
liquidityExchangePrice: BN,
Expand Down
112 changes: 111 additions & 1 deletion tests/specs/limits/m01_accountLimits.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
depositIx,
liquidateIx,
withdrawIx,
closeBalanceIx,
} from "../../utils/user-instructions";
import { bigNumberToWrappedI80F48 } from "@mrgnlabs/mrgn-common";
import {
Expand All @@ -35,7 +36,12 @@ import {
refreshPullOraclesBankrun,
refreshSwitchboardPullOracleBankrun,
} from "../../utils/bankrun-oracles";
import { assertI80F48Approx, assertKeyDefault } from "../../utils/genericTests";
import {
assertBankrunTxFailed,
assertI80F48Approx,
assertKeyDefault,
i80ToBn,
} from "../../utils/genericTests";

const startingSeed: number = 199;

Expand Down Expand Up @@ -506,5 +512,109 @@ ORACLE_MODES.forEach((oracleMode, oracleModeIndex) => {
);
await processBankrunTransaction(bankrunContext, tx, [user.wallet]);
});

// Draining a balance without `withdraw_all` leaves it active with zero shares, so the slot
// stays held. `withdraw_all` can't recover it (it needs a positive asset amount), leaving
// `close_balance` as the only way out.
//
// Runs last, and drains banks[0]: the other banks back user 0's borrows, so emptying them
// would trip `IllegalUtilizationRatio`.
it("(admin) Withdraws all WITHOUT withdraw_all true - effectively blocks the account: cannot withdraw_all after this, only close_balance recovers the slot", async () => {
const user = groupAdmin;
const userAccount = user.accounts.get(USER_ACCOUNT_THROWAWAY);
const before = await bankrunProgram.account.marginfiAccount.fetch(
userAccount
);

const remainingAccounts: PublicKey[][] = [];
const bankAccs = await Promise.all(
banks.map(async (pubkey) => {
const account = await bankrunProgram.account.bank.fetch(pubkey);
return { pubkey, account };
})
);
for (let i = 0; i < MAX_BALANCES; i++) {
if ("fixed" in bankAccs[i].account.config.oracleSetup) {
remainingAccounts.push([banks[i]]);
} else {
remainingAccounts.push([banks[i], oracles.pythPullLst.publicKey]);
}
}
const remaining = composeRemainingAccounts(remainingAccounts);

assert.equal(
before.lendingAccount.balances.filter((b) => b.active !== 0).length,
MAX_BALANCES
);

// Drain banks[0] without the withdraw_all flag.
const balance = before.lendingAccount.balances.find((b) =>
b.bankPk.equals(banks[0])
);
const bank = await bankrunProgram.account.bank.fetch(banks[0]);
const drainTx = new Transaction().add(
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),
await withdrawIx(user.mrgnBankrunProgram, {
marginfiAccount: userAccount,
bank: banks[0],
tokenAccount: user.lstAlphaAccount,
remaining,
amount: i80ToBn(balance.assetShares).mul(
i80ToBn(bank.assetShareValue)
),
withdrawAll: false,
})
);
await processBankrunTransaction(bankrunContext, drainTx, [user.wallet]);

// Emptied, but the slot is still held.
const drained = (
await bankrunProgram.account.marginfiAccount.fetch(userAccount)
).lendingAccount.balances.find((b) => b.bankPk.equals(banks[0]));
assert.notEqual(drained.active, 0, "slot should still be occupied");
assert.equal(i80ToBn(drained.assetShares).toString(), "0");

// withdraw_all can't recover it: there is no asset left to withdraw.
const withdrawAllTx = new Transaction().add(
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),
await withdrawIx(user.mrgnBankrunProgram, {
marginfiAccount: userAccount,
bank: banks[0],
tokenAccount: user.lstAlphaAccount,
remaining,
amount: new BN(0),
withdrawAll: true,
})
);
const failed = await processBankrunTransaction(
bankrunContext,
withdrawAllTx,
[user.wallet],
true
);
assertBankrunTxFailed(failed, 6023); // NoAssetFound

// close_balance is the only way out, and it frees the slot.
const closeTx = new Transaction().add(
await closeBalanceIx(user.mrgnBankrunProgram, {
marginfiAccount: userAccount,
bank: banks[0],
})
);
await processBankrunTransaction(bankrunContext, closeTx, [user.wallet]);

const after = await bankrunProgram.account.marginfiAccount.fetch(
userAccount
);
assert.equal(
after.lendingAccount.balances.find((b) => b.bankPk.equals(banks[0])),
undefined,
"close_balance should have released the slot"
);
assert.equal(
after.lendingAccount.balances.filter((b) => b.active !== 0).length,
MAX_BALANCES - 1
);
});
});
});
6 changes: 6 additions & 0 deletions tests/utils/genericTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,3 +440,9 @@ export function parseMarginfiEvents(
}
return events;
}

export function i80ToBn(value: any): BN {
return new BN(
wrappedI80F48toBigNumber(value).integerValue(BigNumber.ROUND_FLOOR).toFixed(0)
);
}
22 changes: 22 additions & 0 deletions tests/utils/user-instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,28 @@ export const withdrawIx = (
return ix;
};

export type CloseBalanceIxArgs = {
marginfiAccount: PublicKey;
bank: PublicKey;
};

/**
* Close an empty balance, freeing its slot on the account.
* * `authority` - MarginfiAccount's authority must sign
*/
export const closeBalanceIx = (
program: Program<Marginfi>,
args: CloseBalanceIxArgs,
) => {
return program.methods
.lendingAccountCloseBalance()
.accounts({
marginfiAccount: args.marginfiAccount,
bank: args.bank,
})
.instruction();
};

export type RepayIxArgs = {
marginfiAccount: PublicKey;
bank: PublicKey;
Expand Down
Loading