Skip to content

Commit

Permalink
feat: close balance ix
Browse files Browse the repository at this point in the history
  • Loading branch information
J committed Nov 7, 2023
1 parent 1ace78e commit 1529657
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 11 deletions.
11 changes: 6 additions & 5 deletions programs/marginfi/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ pub enum MarginfiError {
LendingAccountBalanceSlotsFull,
#[msg("Bank already exists")] // 6012
BankAlreadyExists,
// 6013
#[msg("Illegal post liquidation state, account is either not unhealthy or liquidation was too big")]
#[msg("Illegal liquidation")] // 6013
IllegalLiquidation,
#[msg("Account is not bankrupt")] // 6014
AccountNotBankrupt,
Expand Down Expand Up @@ -65,8 +64,8 @@ pub enum MarginfiError {
#[msg("Bank borrow cap exceeded")] // 6029
BankLiabilityCapacityExceeded,
#[msg("Invalid Price")] // 6030
InvalidPrice, // 6031
#[msg("Account can have only one liablity when account is under isolated risk")]
InvalidPrice,
#[msg("Account can have only one liablity when account is under isolated risk")] // 6031
IsolatedAccountIllegalState, // 6032
#[msg("Emissions already setup")]
EmissionsAlreadySetup,
Expand All @@ -80,8 +79,10 @@ pub enum MarginfiError {
EmissionsUpdateError,
#[msg("Account disabled")] // 6037
AccountDisabled,
#[msg("Account can't temporarily open 3 balances, please close a balance first")]
#[msg("Account can't temporarily open new balances, please close a balance first")]
AccountTempActiveBalanceLimitExceeded,
#[msg("Illegal balance state")] // 6038
IllegalBalanceState,
}

impl From<MarginfiError> for ProgramError {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use anchor_lang::prelude::*;

use crate::{
check,
prelude::*,
state::{
marginfi_account::{BankAccountWrapper, MarginfiAccount, DISABLED_FLAG},
marginfi_group::Bank,
},
};

pub fn lending_account_close_balance(ctx: Context<LendingAccountCloseBalance>) -> MarginfiResult {
let LendingAccountCloseBalance {
marginfi_account,
bank: bank_loader,
..
} = ctx.accounts;

let mut marginfi_account = marginfi_account.load_mut()?;
let mut bank = bank_loader.load_mut()?;

check!(
!marginfi_account.get_flag(DISABLED_FLAG),
MarginfiError::AccountDisabled
);

bank.accrue_interest(
Clock::get()?.unix_timestamp,
#[cfg(not(feature = "client"))]
bank_loader.key(),
)?;

let mut bank_account = BankAccountWrapper::find(
&bank_loader.key(),
&mut bank,
&mut marginfi_account.lending_account,
)?;

bank_account.close_balance()?;

Ok(())
}

#[derive(Accounts)]
pub struct LendingAccountCloseBalance<'info> {
pub marginfi_group: AccountLoader<'info, MarginfiGroup>,

#[account(
mut,
constraint = marginfi_account.load()?.group == marginfi_group.key(),
)]
pub marginfi_account: AccountLoader<'info, MarginfiAccount>,

#[account(
address = marginfi_account.load()?.authority,
)]
pub signer: Signer<'info>,

#[account(
mut,
constraint = bank.load()?.group == marginfi_group.key(),
)]
pub bank: AccountLoader<'info, Bank>,
}
2 changes: 2 additions & 0 deletions programs/marginfi/src/instructions/marginfi_account/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod borrow;
mod close_balance;
mod deposit;
mod emissions;
mod initialize;
Expand All @@ -7,6 +8,7 @@ mod repay;
mod withdraw;

pub use borrow::*;
pub use close_balance::*;
pub use deposit::*;
pub use emissions::*;
pub use initialize::*;
Expand Down
6 changes: 6 additions & 0 deletions programs/marginfi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ pub mod marginfi {
marginfi_account::lending_account_borrow(ctx, amount)
}

pub fn lending_account_close_balance(
ctx: Context<LendingAccountCloseBalance>,
) -> MarginfiResult {
marginfi_account::lending_account_close_balance(ctx)
}

pub fn lending_account_withdraw_emissions(
ctx: Context<LendingAccountWithdrawEmissions>,
) -> MarginfiResult {
Expand Down
42 changes: 37 additions & 5 deletions programs/marginfi/src/state/marginfi_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,8 @@ impl<'a, 'b> RiskEngine<'a, 'b> {

check!(
account_health <= I80F48::ZERO,
MarginfiError::IllegalLiquidation
MarginfiError::IllegalLiquidation,
"Account not unhealthy"
);

Ok(account_health)
Expand Down Expand Up @@ -438,12 +439,14 @@ impl<'a, 'b> RiskEngine<'a, 'b> {
liability_bank_balance
.is_empty(BalanceSide::Liabilities)
.not(),
MarginfiError::IllegalLiquidation
MarginfiError::IllegalLiquidation,
"Liability payoff too severe"
);

check!(
liability_bank_balance.is_empty(BalanceSide::Assets),
MarginfiError::IllegalLiquidation
MarginfiError::IllegalLiquidation,
"Liability payoff too severe"
);

let (assets, liabs) =
Expand All @@ -453,7 +456,8 @@ impl<'a, 'b> RiskEngine<'a, 'b> {

check!(
account_health <= I80F48::ZERO,
MarginfiError::IllegalLiquidation
MarginfiError::IllegalLiquidation,
"Liquidation too severe"
);

msg!(
Expand All @@ -466,7 +470,8 @@ impl<'a, 'b> RiskEngine<'a, 'b> {

check!(
account_health > pre_liquidation_health,
MarginfiError::IllegalLiquidation
MarginfiError::IllegalLiquidation,
"Post liquidation health worse"
);

Ok(account_health)
Expand Down Expand Up @@ -848,6 +853,33 @@ impl<'a> BankAccountWrapper<'a> {
.ok_or_else(math_error!())?)
}

pub fn close_balance(&mut self) -> MarginfiResult<()> {
self.claim_emissions(Clock::get()?.unix_timestamp as u64)?;

let balance = &mut self.balance;
let bank = &mut self.bank;

let current_liability_amount =
bank.get_liability_amount(balance.liability_shares.into())?;
let current_asset_amount = bank.get_asset_amount(balance.asset_shares.into())?;

check!(
current_liability_amount.is_zero_with_tolerance(ZERO_AMOUNT_THRESHOLD),
MarginfiError::IllegalBalanceState,
"Balance has existing debt"
);

check!(
current_asset_amount.is_zero_with_tolerance(ZERO_AMOUNT_THRESHOLD),
MarginfiError::IllegalBalanceState,
"Balance has existing assets"
);

balance.close()?;

Ok(())
}

// ------------ Internal accounting logic

fn increase_balance_internal(
Expand Down
102 changes: 102 additions & 0 deletions programs/marginfi/tests/marginfi_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1733,3 +1733,105 @@ async fn emissions_test_2() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test]
async fn lending_account_close_balance() -> anyhow::Result<()> {
let test_f = TestFixture::new(Some(TestSettings::all_banks_payer_not_admin())).await;

let usdc_bank = test_f.get_bank(&BankMint::USDC);
let sol_eq_bank = test_f.get_bank(&BankMint::SolEquivalent);
let sol_bank = test_f.get_bank(&BankMint::SOL);

// Fund SOL lender
let lender_mfi_account_f = test_f.create_marginfi_account().await;
let lender_token_account_sol = test_f
.sol_equivalent_mint
.create_token_account_and_mint_to(1_000)
.await;
lender_mfi_account_f
.try_bank_deposit(lender_token_account_sol.key, sol_eq_bank, 1_000)
.await?;

let lender_token_account_sol = test_f
.sol_mint
.create_token_account_and_mint_to(1_000)
.await;
lender_mfi_account_f
.try_bank_deposit(lender_token_account_sol.key, sol_bank, 1_000)
.await?;

let res = lender_mfi_account_f.try_balance_close(sol_bank).await;

assert!(res.is_err());
assert_custom_error!(res.unwrap_err(), MarginfiError::IllegalBalanceState);

// Fund SOL borrower
let borrower_mfi_account_f = test_f.create_marginfi_account().await;
let borrower_token_account_f_usdc = test_f
.usdc_mint
.create_token_account_and_mint_to(1_000)
.await;
let borrower_token_account_f_sol = test_f
.sol_mint
.create_token_account_and_mint_to(1_000)
.await;
let borrower_token_account_f_sol_eq = test_f
.sol_equivalent_mint
.create_token_account_and_mint_to(1_000)
.await;
borrower_mfi_account_f
.try_bank_deposit(borrower_token_account_f_usdc.key, usdc_bank, 1_000)
.await?;

// Borrow SOL EQ
let res = borrower_mfi_account_f
.try_bank_borrow(borrower_token_account_f_sol_eq.key, sol_eq_bank, 0.01)
.await;

assert!(res.is_ok());

// Borrow SOL
let res = borrower_mfi_account_f
.try_bank_borrow(borrower_token_account_f_sol.key, sol_bank, 0.01)
.await;

assert!(res.is_ok());

// This issue is not that bad, because the user can still borrow other assets (isolated liab < empty threshold)
let res = borrower_mfi_account_f.try_balance_close(sol_bank).await;
assert!(res.is_err());
assert_custom_error!(res.unwrap_err(), MarginfiError::IllegalBalanceState);

// Let a second go b
{
let mut ctx = test_f.context.borrow_mut();
let mut clock: Clock = ctx.banks_client.get_sysvar().await?;
// Advance clock by 1 second
clock.unix_timestamp += 1;
ctx.set_sysvar(&clock);
}

// Repay isolated SOL EQ borrow successfully
let res = borrower_mfi_account_f
.try_bank_repay(
borrower_token_account_f_sol_eq.key,
sol_eq_bank,
0.01,
Some(false),
)
.await;
assert!(res.is_ok());

// Liability share in balance is smaller than 0.0001, so repay all should fail
let res = borrower_mfi_account_f
.try_bank_repay(borrower_token_account_f_sol_eq.key, sol_eq_bank, 1, Some(true))
.await;
assert!(res.is_err());
assert_custom_error!(res.unwrap_err(), MarginfiError::NoLiabilityFound);

// This issue is not that bad, because the user can still borrow other assets (isolated liab < empty threshold)
let res = borrower_mfi_account_f.try_balance_close(sol_eq_bank).await;
assert!(res.is_ok());

Ok(())
}
2 changes: 1 addition & 1 deletion scripts/test.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash
anchor build --program-name marginfi
RUST_LOG=error cargo test-sbf --features=test -- --test-threads=1
RUST_LOG=error cargo test-sbf --features=test -- --skip marginfi_account_liquidation_success_many_balances --test-threads=1
31 changes: 31 additions & 0 deletions test-utils/src/marginfi_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,37 @@ impl MarginfiAccountFixture {
Ok(())
}

pub async fn try_balance_close(
&self,
bank: &BankFixture,
) -> anyhow::Result<(), BanksClientError> {
let marginfi_account = self.load().await;
let mut ctx = self.ctx.borrow_mut();

let ix = Instruction {
program_id: marginfi::id(),
accounts: marginfi::accounts::LendingAccountCloseBalance {
marginfi_group: marginfi_account.group,
marginfi_account: self.key,
signer: ctx.payer.pubkey(),
bank: bank.key,
}
.to_account_metas(Some(true)),
data: marginfi::instruction::LendingAccountCloseBalance.data(),
};

let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&ctx.payer.pubkey().clone()),
&[&ctx.payer],
ctx.last_blockhash,
);

ctx.banks_client.process_transaction(tx).await?;

Ok(())
}

pub async fn try_liquidate<T: Into<f64>>(
&self,
liquidatee: &MarginfiAccountFixture,
Expand Down

0 comments on commit 1529657

Please sign in to comment.