Merge pull request #147 from blockworks-foundation/ckamm/liq-and-rebalance

Liquidate and buy/sell to keep only quote
This commit is contained in:
Christian Kamm 2022-08-10 17:09:16 +02:00 committed by GitHub
commit fbdc5ee6b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1404 additions and 230 deletions

1
Cargo.lock generated
View File

@ -1276,6 +1276,7 @@ dependencies = [
"solana-account-decoder", "solana-account-decoder",
"solana-client", "solana-client",
"solana-sdk", "solana-sdk",
"spl-associated-token-account",
"thiserror", "thiserror",
"tokio", "tokio",
] ]

View File

@ -176,7 +176,13 @@ fn main() -> Result<(), anyhow::Error> {
let input_mint = client::pubkey_from_cli(&cmd.input_mint); let input_mint = client::pubkey_from_cli(&cmd.input_mint);
let output_mint = client::pubkey_from_cli(&cmd.output_mint); let output_mint = client::pubkey_from_cli(&cmd.output_mint);
let client = MangoClient::new_for_existing_account(client, account, owner)?; let client = MangoClient::new_for_existing_account(client, account, owner)?;
let txsig = client.jupiter_swap(input_mint, output_mint, cmd.amount, cmd.slippage)?; let txsig = client.jupiter_swap(
input_mint,
output_mint,
cmd.amount,
cmd.slippage,
client::JupiterSwapMode::ExactIn,
)?;
println!("{}", txsig); println!("{}", txsig);
} }
Command::GroupAddress { creator, num } => { Command::GroupAddress { creator, num } => {

View File

@ -21,6 +21,7 @@ shellexpand = "2.1.0"
solana-account-decoder = "~1.10.29" solana-account-decoder = "~1.10.29"
solana-client = "~1.10.29" solana-client = "~1.10.29"
solana-sdk = "~1.10.29" solana-sdk = "~1.10.29"
spl-associated-token-account = "1.0.3"
thiserror = "1.0.31" thiserror = "1.0.31"
log = "0.4" log = "0.4"
reqwest = "0.11.11" reqwest = "0.11.11"

View File

@ -1,4 +1,6 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{Duration, Instant};
use crate::chain_data::*; use crate::chain_data::*;
@ -11,7 +13,9 @@ use anyhow::Context;
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
use solana_sdk::account::{AccountSharedData, ReadableAccount}; use solana_sdk::account::{AccountSharedData, ReadableAccount};
use solana_sdk::clock::Slot;
use solana_sdk::pubkey::Pubkey; use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Signature;
pub struct AccountFetcher { pub struct AccountFetcher {
pub chain_data: Arc<RwLock<ChainData>>, pub chain_data: Arc<RwLock<ChainData>>,
@ -66,11 +70,12 @@ impl AccountFetcher {
.clone()) .clone())
} }
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<()> { pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
let response = self let response = self
.rpc .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))?; .with_context(|| format!("refresh account {} via rpc", address))?;
let slot = response.context.slot;
let account = response let account = response
.value .value
.ok_or(anchor_client::ClientError::AccountNotFound) .ok_or(anchor_client::ClientError::AccountNotFound)
@ -85,6 +90,43 @@ impl AccountFetcher {
}, },
); );
Ok(slot)
}
/// Return the maximum slot reported for the processing of the signatures
pub fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
let statuses = self.rpc.get_signature_statuses(signatures)?.value;
Ok(statuses
.iter()
.map(|status_opt| status_opt.as_ref().map(|status| status.slot).unwrap_or(0))
.max()
.unwrap_or(0))
}
/// Return success once all addresses have data >= min_slot
pub fn refresh_accounts_via_rpc_until_slot(
&self,
addresses: &[Pubkey],
min_slot: Slot,
timeout: Duration,
) -> anyhow::Result<()> {
let start = Instant::now();
for address in addresses {
loop {
if start.elapsed() > timeout {
anyhow::bail!(
"timeout while waiting for data for {} that's newer than slot {}",
address,
min_slot
);
}
let data_slot = self.refresh_account_via_rpc(address)?;
if data_slot >= min_slot {
break;
}
thread::sleep(Duration::from_millis(500));
}
}
Ok(()) Ok(())
} }
} }

View File

@ -395,6 +395,57 @@ impl MangoClient {
.map_err(prettify_client_error) .map_err(prettify_client_error)
} }
pub fn token_withdraw(
&self,
mint: Pubkey,
amount: u64,
allow_borrow: bool,
) -> anyhow::Result<Signature> {
let token = self.context.token_by_mint(&mint)?;
let token_index = token.token_index;
let mint_info = token.mint_info;
let health_check_metas =
self.derive_health_check_remaining_account_metas(vec![token_index], false)?;
self.program()
.request()
.instruction(create_associated_token_account_idempotent(
&self.owner(),
&self.owner(),
&mint,
))
.instruction(Instruction {
program_id: mango_v4::id(),
accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenWithdraw {
group: self.group(),
account: self.mango_account_address,
owner: self.owner(),
bank: mint_info.first_bank(),
vault: mint_info.first_vault(),
token_account: get_associated_token_address(
&self.owner(),
&mint_info.mint,
),
token_program: Token::id(),
},
None,
);
ams.extend(health_check_metas.into_iter());
ams
},
data: anchor_lang::InstructionData::data(&mango_v4::instruction::TokenWithdraw {
amount,
allow_borrow,
}),
})
.signer(&self.owner)
.send()
.map_err(prettify_client_error)
}
pub fn get_oracle_price( pub fn get_oracle_price(
&self, &self,
token_name: &str, token_name: &str,
@ -837,33 +888,50 @@ impl MangoClient {
&self, &self,
input_mint: Pubkey, input_mint: Pubkey,
output_mint: Pubkey, output_mint: Pubkey,
source_amount: u64, amount: u64,
slippage: f64, slippage: f64,
swap_mode: JupiterSwapMode,
) -> anyhow::Result<Signature> { ) -> anyhow::Result<Signature> {
self.invoke(self.jupiter_swap_async(input_mint, output_mint, source_amount, slippage)) self.invoke(self.jupiter_swap_async(input_mint, output_mint, amount, slippage, swap_mode))
} }
// Not actually fully async, since it uses the blocking RPC client to send the actual tx pub fn jupiter_route(
pub async fn jupiter_swap_async(
&self, &self,
input_mint: Pubkey, input_mint: Pubkey,
output_mint: Pubkey, output_mint: Pubkey,
source_amount: u64, amount: u64,
slippage: f64, slippage: f64,
) -> anyhow::Result<Signature> { swap_mode: JupiterSwapMode,
let source_token = self.context.token_by_mint(&input_mint)?; ) -> anyhow::Result<jupiter::QueryRoute> {
let target_token = self.context.token_by_mint(&output_mint)?; self.invoke(self.jupiter_route_async(input_mint, output_mint, amount, slippage, swap_mode))
}
pub async fn jupiter_route_async(
&self,
input_mint: Pubkey,
output_mint: Pubkey,
amount: u64,
slippage: f64,
swap_mode: JupiterSwapMode,
) -> anyhow::Result<jupiter::QueryRoute> {
let quote = self let quote = self
.http_client .http_client
.get("https://quote-api.jup.ag/v1/quote") .get("https://quote-api.jup.ag/v1/quote")
.query(&[ .query(&[
("inputMint", input_mint.to_string()), ("inputMint", input_mint.to_string()),
("outputMint", output_mint.to_string()), ("outputMint", output_mint.to_string()),
("amount", format!("{}", source_amount)), ("amount", format!("{}", amount)),
("onlyDirectRoutes", "true".into()), ("onlyDirectRoutes", "true".into()),
("filterTopNResult", "10".into()), ("filterTopNResult", "10".into()),
("slippage", format!("{}", slippage)), ("slippage", format!("{}", slippage)),
(
"swapMode",
match swap_mode {
JupiterSwapMode::ExactIn => "ExactIn",
JupiterSwapMode::ExactOut => "ExactOut",
}
.into(),
),
]) ])
.send() .send()
.await .await
@ -889,6 +957,23 @@ impl MangoClient {
) )
})?; })?;
Ok(route.clone())
}
pub async fn jupiter_swap_async(
&self,
input_mint: Pubkey,
output_mint: Pubkey,
amount: u64,
slippage: f64,
swap_mode: JupiterSwapMode,
) -> anyhow::Result<Signature> {
let source_token = self.context.token_by_mint(&input_mint)?;
let target_token = self.context.token_by_mint(&output_mint)?;
let route = self
.jupiter_route_async(input_mint, output_mint, amount, slippage, swap_mode)
.await?;
let swap = self let swap = self
.http_client .http_client
.post("https://quote-api.jup.ag/v1/swap") .post("https://quote-api.jup.ag/v1/swap")
@ -919,19 +1004,12 @@ impl MangoClient {
.context("base64 decoding jupiter transaction")?, .context("base64 decoding jupiter transaction")?,
) )
.context("parsing jupiter transaction")?; .context("parsing jupiter transaction")?;
let ata_program = anchor_spl::associated_token::ID;
let token_program = anchor_spl::token::ID;
let is_setup_ix = |k: Pubkey| -> bool { k == ata_program || k == token_program };
let jup_ixs = deserialize_instructions(&jup_tx.message) let jup_ixs = deserialize_instructions(&jup_tx.message)
.into_iter() .into_iter()
// TODO: possibly creating associated token accounts if they don't exist yet is good?! .filter(|ix| !is_setup_ix(ix.program_id))
// we could squeeze the FlashLoan instructions in the middle:
// - beginning AToken...
// - FlashLoanBegin
// - other JUP ix
// - FlashLoanEnd
// - ending AToken
.filter(|ix| {
ix.program_id
!= Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap()
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let bank_ams = [ let bank_ams = [
@ -962,7 +1040,14 @@ impl MangoClient {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let loan_amounts = vec![source_amount, 0u64]; let loan_amounts = vec![
match swap_mode {
JupiterSwapMode::ExactIn => amount,
// in amount + slippage
JupiterSwapMode::ExactOut => route.other_amount_threshold,
},
0u64,
];
// This relies on the fact that health account banks will be identical to the first_bank above! // This relies on the fact that health account banks will be identical to the first_bank above!
let health_ams = self let health_ams = self
@ -973,7 +1058,19 @@ impl MangoClient {
.context("building health accounts")?; .context("building health accounts")?;
let program = self.program(); let program = self.program();
let mut builder = program.request().instruction(Instruction { let mut builder = program.request();
builder = builder.instruction(create_associated_token_account_idempotent(
&self.owner.pubkey(),
&self.owner.pubkey(),
&source_token.mint_info.mint,
));
builder = builder.instruction(create_associated_token_account_idempotent(
&self.owner.pubkey(),
&self.owner.pubkey(),
&target_token.mint_info.mint,
));
builder = builder.instruction(Instruction {
program_id: mango_v4::id(), program_id: mango_v4::id(),
accounts: { accounts: {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas( let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
@ -994,7 +1091,7 @@ impl MangoClient {
}), }),
}); });
for ix in jup_ixs { for ix in jup_ixs {
builder = builder.instruction(ix); builder = builder.instruction(ix.clone());
} }
builder = builder.instruction(Instruction { builder = builder.instruction(Instruction {
program_id: mango_v4::id(), program_id: mango_v4::id(),
@ -1094,6 +1191,12 @@ pub fn prettify_client_error(err: anchor_client::ClientError) -> anyhow::Error {
err.into() err.into()
} }
#[derive(Clone, Copy)]
pub enum JupiterSwapMode {
ExactIn,
ExactOut,
}
pub fn keypair_from_cli(keypair: &str) -> Keypair { pub fn keypair_from_cli(keypair: &str) -> Keypair {
let maybe_keypair = keypair::read_keypair(&mut keypair.as_bytes()); let maybe_keypair = keypair::read_keypair(&mut keypair.as_bytes());
match maybe_keypair { match maybe_keypair {
@ -1128,3 +1231,17 @@ fn to_writable_account_meta(pubkey: Pubkey) -> AccountMeta {
is_signer: false, is_signer: false,
} }
} }
// FUTURE: use spl_associated_token_account::instruction::create_associated_token_account_idempotent
// Right now anchor depends on an earlier version of this package...
fn create_associated_token_account_idempotent(
funder: &Pubkey,
owner: &Pubkey,
mint: &Pubkey,
) -> Instruction {
let mut instr = spl_associated_token_account::instruction::create_associated_token_account(
funder, owner, mint,
);
instr.data = vec![0x1]; // CreateIdempotent
instr
}

View File

@ -15,7 +15,7 @@ pub struct QueryRoute {
pub out_amount: u64, pub out_amount: u64,
pub amount: u64, pub amount: u64,
pub other_amount_threshold: u64, pub other_amount_threshold: u64,
pub out_amount_with_slippage: u64, pub out_amount_with_slippage: Option<u64>,
pub swap_mode: String, pub swap_mode: String,
pub price_impact_pct: f64, pub price_impact_pct: f64,
pub market_infos: Vec<QueryMarketInfo>, pub market_infos: Vec<QueryMarketInfo>,
@ -33,7 +33,7 @@ pub struct QueryMarketInfo {
pub lp_fee: QueryFee, pub lp_fee: QueryFee,
pub platform_fee: QueryFee, pub platform_fee: QueryFee,
pub not_enough_liquidity: bool, pub not_enough_liquidity: bool,
pub price_impact_pct: f64, pub price_impact_pct: Option<f64>,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
@ -41,7 +41,7 @@ pub struct QueryMarketInfo {
pub struct QueryFee { pub struct QueryFee {
pub amount: u64, pub amount: u64,
pub mint: String, pub mint: String,
pub pct: f64, pub pct: Option<f64>,
} }
#[derive(Deserialize, Serialize, Debug, Clone)] #[derive(Deserialize, Serialize, Debug, Clone)]
@ -49,6 +49,7 @@ pub struct QueryFee {
pub struct SwapRequest { pub struct SwapRequest {
pub route: QueryRoute, pub route: QueryRoute,
pub user_public_key: String, pub user_public_key: String,
#[serde(rename = "wrapUnwrapSOL")]
pub wrap_unwrap_sol: bool, pub wrap_unwrap_sol: bool,
} }

View File

@ -1,4 +1,15 @@
use solana_sdk::signature::Keypair; use solana_client::{
client_error::Result as ClientResult, rpc_client::RpcClient, rpc_request::RpcError,
};
use solana_sdk::transaction::Transaction;
use solana_sdk::{
clock::Slot,
commitment_config::CommitmentConfig,
signature::{Keypair, Signature},
transaction::uses_durable_nonce,
};
use std::{thread, time};
// #[allow(dead_code)] // #[allow(dead_code)]
// pub fn retry<T>(request: impl Fn() -> Result<T, anchor_client::ClientError>) -> anyhow::Result<T> { // pub fn retry<T>(request: impl Fn() -> Result<T, anchor_client::ClientError>) -> anyhow::Result<T> {
@ -24,3 +35,64 @@ impl MyClone for Keypair {
Self::from_bytes(&self.to_bytes()).unwrap() Self::from_bytes(&self.to_bytes()).unwrap()
} }
} }
/// A copy of RpcClient::send_and_confirm_transaction that returns the slot the
/// transaction confirmed in.
pub fn send_and_confirm_transaction(
rpc_client: &RpcClient,
transaction: &Transaction,
) -> ClientResult<(Signature, Slot)> {
const SEND_RETRIES: usize = 1;
const GET_STATUS_RETRIES: usize = usize::MAX;
'sending: for _ in 0..SEND_RETRIES {
let signature = rpc_client.send_transaction(transaction)?;
let recent_blockhash = if uses_durable_nonce(transaction).is_some() {
let (recent_blockhash, ..) =
rpc_client.get_latest_blockhash_with_commitment(CommitmentConfig::processed())?;
recent_blockhash
} else {
transaction.message.recent_blockhash
};
for status_retry in 0..GET_STATUS_RETRIES {
let response = rpc_client.get_signature_statuses(&[signature])?.value;
match response[0]
.clone()
.filter(|result| result.satisfies_commitment(rpc_client.commitment()))
{
Some(tx_status) => {
return if let Some(e) = tx_status.err {
Err(e.into())
} else {
Ok((signature, tx_status.slot))
};
}
None => {
if !rpc_client
.is_blockhash_valid(&recent_blockhash, CommitmentConfig::processed())?
{
// Block hash is not found by some reason
break 'sending;
} else if cfg!(not(test))
// Ignore sleep at last step.
&& status_retry < GET_STATUS_RETRIES
{
// Retry twice a second
thread::sleep(time::Duration::from_millis(500));
continue;
}
}
}
}
}
Err(RpcError::ForUser(
"unable to confirm transaction. \
This can happen in situations such as transaction expiration \
and insufficient fee-payer funds"
.to_string(),
)
.into())
}

View File

@ -1,13 +1,20 @@
use std::time::Duration;
use crate::account_shared_data::KeyedAccountSharedData; use crate::account_shared_data::KeyedAccountSharedData;
use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext}; use client::{chain_data, AccountFetcher, MangoClient, MangoClientError, MangoGroupContext};
use mango_v4::state::{ use mango_v4::state::{
new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType, new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType,
MangoAccountValue, TokenIndex, MangoAccountValue, TokenIndex, QUOTE_TOKEN_INDEX,
}; };
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey}; use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
pub struct Config {
pub min_health_ratio: f64,
pub refresh_timeout: Duration,
}
pub fn new_health_cache_( pub fn new_health_cache_(
context: &MangoGroupContext, context: &MangoGroupContext,
account_fetcher: &chain_data::AccountFetcher, account_fetcher: &chain_data::AccountFetcher,
@ -36,14 +43,68 @@ pub fn new_health_cache_(
new_health_cache(&account.borrow(), &retriever).context("make health cache") new_health_cache(&account.borrow(), &retriever).context("make health cache")
} }
pub fn jupiter_market_can_buy(
mango_client: &MangoClient,
token: TokenIndex,
quote_token: TokenIndex,
) -> bool {
if token == quote_token {
return true;
}
let token_mint = mango_client.context.token(token).mint_info.mint;
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
// Consider a market alive if we can swap $10 worth at 1% slippage
// TODO: configurable
// TODO: cache this, no need to recheck often
let quote_amount = 10_000_000u64;
let slippage = 1.0;
mango_client
.jupiter_route(
quote_token_mint,
token_mint,
quote_amount,
slippage,
client::JupiterSwapMode::ExactIn,
)
.is_ok()
}
pub fn jupiter_market_can_sell(
mango_client: &MangoClient,
token: TokenIndex,
quote_token: TokenIndex,
) -> bool {
if token == quote_token {
return true;
}
let token_mint = mango_client.context.token(token).mint_info.mint;
let quote_token_mint = mango_client.context.token(quote_token).mint_info.mint;
// Consider a market alive if we can swap $10 worth at 1% slippage
// TODO: configurable
// TODO: cache this, no need to recheck often
let quote_amount = 10_000_000u64;
let slippage = 1.0;
mango_client
.jupiter_route(
token_mint,
quote_token_mint,
quote_amount,
slippage,
client::JupiterSwapMode::ExactOut,
)
.is_ok()
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn process_account( pub fn maybe_liquidate_account(
mango_client: &MangoClient, mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher, account_fetcher: &chain_data::AccountFetcher,
pubkey: &Pubkey, pubkey: &Pubkey,
) -> anyhow::Result<()> { config: &Config,
// TODO: configurable ) -> anyhow::Result<bool> {
let min_health_ratio = I80F48::from_num(50.0f64); let min_health_ratio = I80F48::from_num(config.min_health_ratio);
let quote_token_index = 0; let quote_token_index = 0;
let account = account_fetcher.fetch_mango_account(pubkey)?; let account = account_fetcher.fetch_mango_account(pubkey)?;
@ -52,7 +113,7 @@ pub fn process_account(
.health(HealthType::Maint); .health(HealthType::Maint);
if maint_health >= 0 && !account.is_bankrupt() { if maint_health >= 0 && !account.is_bankrupt() {
return Ok(()); return Ok(false);
} }
log::trace!( log::trace!(
@ -85,11 +146,11 @@ pub fn process_account(
)?; )?;
Ok(( Ok((
token_position.token_index, token_position.token_index,
bank, price,
token_position.native(&bank) * price, token_position.native(&bank) * price,
)) ))
}) })
.collect::<anyhow::Result<Vec<(TokenIndex, Bank, I80F48)>>>()?; .collect::<anyhow::Result<Vec<(TokenIndex, I80F48, I80F48)>>>()?;
tokens.sort_by(|a, b| a.2.cmp(&b.2)); tokens.sort_by(|a, b| a.2.cmp(&b.2));
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> { let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
@ -111,31 +172,69 @@ pub fn process_account(
}; };
// try liquidating // try liquidating
if account.is_bankrupt() { let txsig = if account.is_bankrupt() {
if tokens.is_empty() { if tokens.is_empty() {
anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey); anyhow::bail!("mango account {}, is bankrupt has no active tokens", pubkey);
} }
let (liab_token_index, _liab_bank, _liab_price) = tokens.first().unwrap(); let liab_token_index = tokens
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
let max_liab_transfer = get_max_liab_transfer(*liab_token_index, quote_token_index)?; let max_liab_transfer = get_max_liab_transfer(liab_token_index, quote_token_index)?;
let sig = mango_client let sig = mango_client
.liq_token_bankruptcy((pubkey, &account), *liab_token_index, max_liab_transfer) .liq_token_bankruptcy((pubkey, &account), liab_token_index, max_liab_transfer)
.context("sending liq_token_bankruptcy")?; .context("sending liq_token_bankruptcy")?;
log::info!( log::info!(
"Liquidated bankruptcy for {}..., maint_health was {}, tx sig {:?}", "Liquidated bankruptcy for {}, maint_health was {}, tx sig {:?}",
&pubkey.to_string()[..3], pubkey,
maint_health, maint_health,
sig sig
); );
sig
} else if maint_health.is_negative() { } else if maint_health.is_negative() {
if tokens.len() < 2 { let asset_token_index = tokens
anyhow::bail!("mango account {}, has less than 2 active tokens", pubkey); .iter()
} .rev()
let (asset_token_index, _asset_bank, _asset_price) = tokens.last().unwrap(); .find(|(asset_token_index, _asset_price, asset_usdc_equivalent)| {
let (liab_token_index, _liab_bank, _liab_price) = tokens.first().unwrap(); asset_usdc_equivalent.is_positive()
&& jupiter_market_can_sell(mango_client, *asset_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no asset tokens that are sellable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
let liab_token_index = tokens
.iter()
.find(|(liab_token_index, _liab_price, liab_usdc_equivalent)| {
liab_usdc_equivalent.is_negative()
&& jupiter_market_can_buy(mango_client, *liab_token_index, QUOTE_TOKEN_INDEX)
})
.ok_or_else(|| {
anyhow::anyhow!(
"mango account {}, has no liab tokens that are purchasable for USDC: {:?}",
pubkey,
tokens
)
})?
.0;
let max_liab_transfer = get_max_liab_transfer(*liab_token_index, *asset_token_index) let max_liab_transfer = get_max_liab_transfer(liab_token_index, asset_token_index)
.context("getting max_liab_transfer")?; .context("getting max_liab_transfer")?;
// //
@ -146,29 +245,43 @@ pub fn process_account(
let sig = mango_client let sig = mango_client
.liq_token_with_token( .liq_token_with_token(
(pubkey, &account), (pubkey, &account),
*asset_token_index, asset_token_index,
*liab_token_index, liab_token_index,
max_liab_transfer, max_liab_transfer,
) )
.context("sending liq_token_with_token")?; .context("sending liq_token_with_token")?;
log::info!( log::info!(
"Liquidated token with token for {}..., maint_health was {}, tx sig {:?}", "Liquidated token with token for {}, maint_health was {}, tx sig {:?}",
&pubkey.to_string()[..3], pubkey,
maint_health, maint_health,
sig sig
); );
sig
} else {
return Ok(false);
};
let slot = account_fetcher.transaction_max_slot(&[txsig])?;
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
&[*pubkey, mango_client.mango_account_address],
slot,
config.refresh_timeout,
) {
log::info!("could not refresh after liquidation: {}", e);
} }
Ok(())
Ok(true)
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn process_accounts<'a>( pub fn maybe_liquidate_one<'a>(
mango_client: &MangoClient, mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher, account_fetcher: &chain_data::AccountFetcher,
accounts: impl Iterator<Item = &'a Pubkey>, accounts: impl Iterator<Item = &'a Pubkey>,
) -> anyhow::Result<()> { config: &Config,
) -> bool {
for pubkey in accounts { for pubkey in accounts {
match process_account(mango_client, account_fetcher, pubkey) { match maybe_liquidate_account(mango_client, account_fetcher, pubkey, config) {
Err(err) => { Err(err) => {
// Not all errors need to be raised to the user's attention. // Not all errors need to be raised to the user's attention.
let mut log_level = log::Level::Error; let mut log_level = log::Level::Error;
@ -185,9 +298,10 @@ pub fn process_accounts<'a>(
}; };
log::log!(log_level, "liquidating account {}: {:?}", pubkey, err); log::log!(log_level, "liquidating account {}: {:?}", pubkey, err);
} }
Ok(true) => return true,
_ => {} _ => {}
}; };
} }
Ok(()) false
} }

View File

@ -15,6 +15,7 @@ use std::collections::HashSet;
pub mod account_shared_data; pub mod account_shared_data;
pub mod liquidate; pub mod liquidate;
pub mod metrics; pub mod metrics;
pub mod rebalance;
pub mod snapshot_source; pub mod snapshot_source;
pub mod util; pub mod util;
pub mod websocket_source; pub mod websocket_source;
@ -67,13 +68,20 @@ struct Cli {
#[clap(long, env, default_value = "300")] #[clap(long, env, default_value = "300")]
snapshot_interval_secs: u64, snapshot_interval_secs: u64,
// how many getMultipleAccounts requests to send in parallel /// how many getMultipleAccounts requests to send in parallel
#[clap(long, env, default_value = "10")] #[clap(long, env, default_value = "10")]
parallel_rpc_requests: usize, parallel_rpc_requests: usize,
// typically 100 is the max number for getMultipleAccounts /// typically 100 is the max number of accounts getMultipleAccounts will retrieve at once
#[clap(long, env, default_value = "100")] #[clap(long, env, default_value = "100")]
get_multiple_accounts_count: usize, get_multiple_accounts_count: usize,
/// liquidator health ratio should not fall below this value
#[clap(long, env, default_value = "50")]
min_health_ratio: f64,
#[clap(long, env, default_value = "1")]
rebalance_slippage: f64,
} }
pub fn encode_address(addr: &Pubkey) -> String { pub fn encode_address(addr: &Pubkey) -> String {
@ -114,8 +122,7 @@ async fn main() -> anyhow::Result<()> {
let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?; let group_context = MangoGroupContext::new_from_rpc(mango_group, cluster.clone(), commitment)?;
// TODO: this is all oracles, not just pyth! let mango_oracles = group_context
let mango_pyth_oracles = group_context
.tokens .tokens
.values() .values()
.map(|value| value.mint_info.oracle) .map(|value| value.mint_info.oracle)
@ -145,7 +152,7 @@ async fn main() -> anyhow::Result<()> {
serum_program: cli.serum_program, serum_program: cli.serum_program,
open_orders_authority: mango_group, open_orders_authority: mango_group,
}, },
mango_pyth_oracles.clone(), mango_oracles.clone(),
websocket_sender, websocket_sender,
); );
@ -169,7 +176,7 @@ async fn main() -> anyhow::Result<()> {
snapshot_interval: std::time::Duration::from_secs(cli.snapshot_interval_secs), snapshot_interval: std::time::Duration::from_secs(cli.snapshot_interval_secs),
min_slot: first_websocket_slot + 10, min_slot: first_websocket_slot + 10,
}, },
mango_pyth_oracles, mango_oracles,
snapshot_sender, snapshot_sender,
); );
@ -183,14 +190,6 @@ async fn main() -> anyhow::Result<()> {
let mut oracles = HashSet::<Pubkey>::new(); let mut oracles = HashSet::<Pubkey>::new();
let mut perp_markets = HashMap::<PerpMarketIndex, Pubkey>::new(); let mut perp_markets = HashMap::<PerpMarketIndex, Pubkey>::new();
// List of accounts that are potentially liquidatable.
//
// Used to send a different message for newly liqudatable accounts and
// accounts that are still liquidatable but not fresh anymore.
//
// This should actually be done per connected websocket client, and not globally.
let _current_candidates = HashSet::<Pubkey>::new();
// Is the first snapshot done? Only start checking account health when it is. // Is the first snapshot done? Only start checking account health when it is.
let mut one_snapshot_done = false; let mut one_snapshot_done = false;
@ -211,6 +210,19 @@ async fn main() -> anyhow::Result<()> {
)?) )?)
}; };
let liq_config = liquidate::Config {
min_health_ratio: cli.min_health_ratio,
// TODO: config
refresh_timeout: Duration::from_secs(30),
};
let mut rebalance_interval = tokio::time::interval(Duration::from_secs(5));
let rebalance_config = rebalance::Config {
slippage: cli.rebalance_slippage,
// TODO: config
refresh_timeout: Duration::from_secs(30),
};
info!("main loop"); info!("main loop");
loop { loop {
tokio::select! { tokio::select! {
@ -238,14 +250,13 @@ async fn main() -> anyhow::Result<()> {
continue; continue;
} }
if let Err(err) = liquidate::process_accounts( liquidate(
&mango_client, &mango_client,
&account_fetcher, &account_fetcher,
std::iter::once(&account_write.pubkey), std::iter::once(&account_write.pubkey),
&liq_config,
) { &rebalance_config,
warn!("could not process account {}: {:?}", account_write.pubkey, err); )?;
}
} }
if is_mango_bank(&account_write.account, &mango_program, &mango_group).is_some() || oracles.contains(&account_write.pubkey) { if is_mango_bank(&account_write.account, &mango_program, &mango_group).is_some() || oracles.contains(&account_write.pubkey) {
@ -261,21 +272,13 @@ async fn main() -> anyhow::Result<()> {
log::debug!("change to oracle {}", &account_write.pubkey); log::debug!("change to oracle {}", &account_write.pubkey);
} }
// check health of all accounts liquidate(
// &mango_client,
// TODO: This could be done asynchronously by calling &account_fetcher,
// let accounts = chain_data.accounts_snapshot(); mango_accounts.iter(),
// and then working with the snapshot of the data &liq_config,
// &rebalance_config,
// However, this currently takes like 50ms for me in release builds, )?;
// so optimizing much seems unnecessary.
if let Err(err) = liquidate::process_accounts(
&mango_client,
&account_fetcher,
mango_accounts.iter(),
) {
warn!("could not process accounts: {:?}", err);
}
} }
} }
}, },
@ -302,19 +305,46 @@ async fn main() -> anyhow::Result<()> {
snapshot_source::update_chain_data(&mut chain_data.write().unwrap(), message); snapshot_source::update_chain_data(&mut chain_data.write().unwrap(), message);
one_snapshot_done = true; one_snapshot_done = true;
// trigger a full health check liquidate(
if let Err(err) = liquidate::process_accounts( &mango_client,
&mango_client, &account_fetcher,
&account_fetcher, mango_accounts.iter(),
mango_accounts.iter(), &liq_config,
) { &rebalance_config,
warn!("could not process accounts: {:?}", err); )?;
}
}, },
_ = rebalance_interval.tick() => {
if one_snapshot_done {
if let Err(err) = rebalance::zero_all_non_quote(&mango_client, &account_fetcher, &cli.liqor_mango_account, &rebalance_config) {
log::error!("failed to rebalance liqor: {:?}", err);
}
}
}
} }
} }
} }
fn liquidate<'a>(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
accounts: impl Iterator<Item = &'a Pubkey>,
config: &liquidate::Config,
rebalance_config: &rebalance::Config,
) -> anyhow::Result<()> {
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)
{
log::error!("failed to rebalance liqor: {:?}", err);
}
Ok(())
}
fn start_chain_data_metrics(chain: Arc<RwLock<chain_data::ChainData>>, metrics: &metrics::Metrics) { fn start_chain_data_metrics(chain: Arc<RwLock<chain_data::ChainData>>, metrics: &metrics::Metrics) {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(5)); let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));

201
liquidator/src/rebalance.rs Normal file
View File

@ -0,0 +1,201 @@
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 {fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
use std::{collections::HashMap, time::Duration};
pub struct Config {
pub slippage: f64,
pub refresh_timeout: Duration,
}
#[derive(Debug)]
struct TokenState {
price: I80F48,
native_position: I80F48,
}
impl TokenState {
fn new_position(
token: &TokenContext,
position: &TokenPosition,
account_fetcher: &chain_data::AccountFetcher,
) -> anyhow::Result<Self> {
let bank = Self::bank(token, account_fetcher)?;
Ok(Self {
price: Self::fetch_price(token, &bank, account_fetcher)?,
native_position: position.native(&bank),
})
}
fn bank(
token: &TokenContext,
account_fetcher: &chain_data::AccountFetcher,
) -> anyhow::Result<Bank> {
account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())
}
fn fetch_price(
token: &TokenContext,
bank: &Bank,
account_fetcher: &chain_data::AccountFetcher,
) -> anyhow::Result<I80F48> {
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,
)
.map_err_anyhow()
}
}
#[allow(clippy::too_many_arguments)]
pub fn zero_all_non_quote(
mango_client: &MangoClient,
account_fetcher: &chain_data::AccountFetcher,
mango_account_address: &Pubkey,
config: &Config,
) -> anyhow::Result<()> {
log::trace!("checking for rebalance: {}", mango_account_address);
// TODO: configurable?
let quote_token = mango_client.context.token(QUOTE_TOKEN_INDEX);
let account = account_fetcher.fetch_mango_account(mango_account_address)?;
let tokens = account
.token_iter_active()
.map(|token_position| {
let token = mango_client.context.token(token_position.token_index);
Ok((
token.token_index,
TokenState::new_position(token, token_position, account_fetcher)?,
))
})
.collect::<anyhow::Result<HashMap<TokenIndex, TokenState>>>()?;
log::trace!("account tokens: {:?}", tokens);
// Function to refresh the mango account after the txsig confirmed. Returns false on timeout.
let refresh_mango_account =
|account_fetcher: &chain_data::AccountFetcher, txsig| -> anyhow::Result<bool> {
let max_slot = account_fetcher.transaction_max_slot(&[txsig])?;
if let Err(e) = account_fetcher.refresh_accounts_via_rpc_until_slot(
&[*mango_account_address],
max_slot,
config.refresh_timeout,
) {
// If we don't get fresh data, maybe the tx landed on a fork?
// Rebalance is technically still ok.
log::info!("could not refresh account data: {}", e);
return Ok(false);
}
Ok(true)
};
for (token_index, token_state) in tokens {
let token = mango_client.context.token(token_index);
if token_index == quote_token.token_index {
continue;
}
let token_mint = token.mint_info.mint;
let quote_mint = quote_token.mint_info.mint;
// It's not always possible to bring the native balance to 0 through swaps:
// Consider a price <1. You need to sell a bunch of tokens to get 1 USDC native and
// similarly will get multiple tokens when buying.
// Imagine SOL at 0.04 USDC-native per SOL-native: Any amounts below 25 SOL-native
// would not be worth a single USDC-native.
//
// To avoid errors, we consider all amounts below 2 * (1/oracle) dust and don't try
// to sell them. Instead they will be withdrawn at the end.
// Purchases will aim to purchase slightly more than is needed, such that we can
// again withdraw the dust at the end.
let dust_threshold = I80F48::from(2) / token_state.price;
let mut amount = token_state.native_position;
if amount > dust_threshold {
// Sell
let txsig = mango_client.jupiter_swap(
token_mint,
quote_mint,
amount.to_num::<u64>(),
config.slippage,
client::JupiterSwapMode::ExactIn,
)?;
log::info!(
"sold {} {} for {} in tx {}",
token.native_to_ui(amount),
token.name,
quote_token.name,
txsig
);
if !refresh_mango_account(account_fetcher, txsig)? {
return Ok(());
}
let bank = TokenState::bank(token, account_fetcher)?;
amount = mango_client
.mango_account()?
.token_get(token_index)
.map(|(position, _)| position.native(&bank))
.unwrap_or(I80F48::ZERO);
} else if token_state.native_position < 0 {
// Buy
let buy_amount = (-token_state.native_position).ceil()
+ (dust_threshold - I80F48::ONE).max(I80F48::ZERO);
let txsig = mango_client.jupiter_swap(
quote_mint,
token_mint,
buy_amount.to_num::<u64>(),
config.slippage,
client::JupiterSwapMode::ExactOut,
)?;
log::info!(
"bought {} {} for {} in tx {}",
token.native_to_ui(buy_amount),
token.name,
quote_token.name,
txsig
);
if !refresh_mango_account(account_fetcher, txsig)? {
return Ok(());
}
let bank = TokenState::bank(token, account_fetcher)?;
amount = mango_client
.mango_account()?
.token_get(token_index)
.map(|(position, _)| position.native(&bank))
.unwrap_or(I80F48::ZERO);
}
// Any remainder that could not be sold just gets withdrawn to ensure the
// TokenPosition is freed up
if amount > 0 && amount <= dust_threshold {
// TODO: fix to false once program updated to fix allow_borrow bug
let allow_borrow = true;
let txsig =
mango_client.token_withdraw(token_mint, amount.to_num::<u64>(), allow_borrow)?;
log::info!(
"withdrew {} {} to liqor wallet in {}",
token.native_to_ui(amount),
token.name,
txsig
);
if !refresh_mango_account(account_fetcher, txsig)? {
return Ok(());
}
} else {
anyhow::bail!(
"unexpected {} position after rebalance swap: {} native",
token.name,
amount
);
}
}
Ok(())
}

View File

@ -86,7 +86,7 @@ pub struct Config {
async fn feed_snapshots( async fn feed_snapshots(
config: &Config, config: &Config,
mango_pyth_oracles: Vec<Pubkey>, mango_oracles: Vec<Pubkey>,
sender: &async_channel::Sender<AccountSnapshot>, sender: &async_channel::Sender<AccountSnapshot>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let rpc_client = http::connect_with_options::<AccountsDataClient>(&config.rpc_http_url, true) let rpc_client = http::connect_with_options::<AccountsDataClient>(&config.rpc_http_url, true)
@ -128,7 +128,7 @@ async fn feed_snapshots(
let results: Vec<( let results: Vec<(
Vec<Pubkey>, Vec<Pubkey>,
Result<Response<Vec<Option<UiAccount>>>, jsonrpc_core_client::RpcError>, Result<Response<Vec<Option<UiAccount>>>, jsonrpc_core_client::RpcError>,
)> = stream::iter(mango_pyth_oracles) )> = stream::iter(mango_oracles)
.chunks(config.get_multiple_accounts_count) .chunks(config.get_multiple_accounts_count)
.map(|keys| { .map(|keys| {
let rpc_client = &rpc_client; let rpc_client = &rpc_client;
@ -207,7 +207,7 @@ async fn feed_snapshots(
pub fn start( pub fn start(
config: Config, config: Config,
mango_pyth_oracles: Vec<Pubkey>, mango_oracles: Vec<Pubkey>,
sender: async_channel::Sender<AccountSnapshot>, sender: async_channel::Sender<AccountSnapshot>,
) { ) {
let mut poll_wait_first_snapshot = time::interval(time::Duration::from_secs(2)); let mut poll_wait_first_snapshot = time::interval(time::Duration::from_secs(2));
@ -239,7 +239,7 @@ pub fn start(
loop { loop {
interval_between_snapshots.tick().await; interval_between_snapshots.tick().await;
if let Err(err) = feed_snapshots(&config, mango_pyth_oracles.clone(), &sender).await { if let Err(err) = feed_snapshots(&config, mango_oracles.clone(), &sender).await {
warn!("snapshot error: {:?}", err); warn!("snapshot error: {:?}", err);
} else { } else {
info!("snapshot success"); info!("snapshot success");

View File

@ -57,7 +57,7 @@ pub struct Config {
async fn feed_data( async fn feed_data(
config: &Config, config: &Config,
mango_pyth_oracles: Vec<Pubkey>, mango_oracles: Vec<Pubkey>,
sender: async_channel::Sender<Message>, sender: async_channel::Sender<Message>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let connect = ws::try_connect::<RpcSolPubSubClient>(&config.rpc_ws_url).map_err_anyhow()?; let connect = ws::try_connect::<RpcSolPubSubClient>(&config.rpc_ws_url).map_err_anyhow()?;
@ -99,10 +99,9 @@ async fn feed_data(
Some(all_accounts_config.clone()), Some(all_accounts_config.clone()),
) )
.map_err_anyhow()?; .map_err_anyhow()?;
// TODO: mango_pyth_oracles should not contain stub mango_pyth_oracles, since they already sub'ed with mango_sub let mut mango_oracles_sub_map = StreamMap::new();
let mut mango_pyth_oracles_sub_map = StreamMap::new(); for oracle in mango_oracles.into_iter() {
for oracle in mango_pyth_oracles.into_iter() { mango_oracles_sub_map.insert(
mango_pyth_oracles_sub_map.insert(
oracle, oracle,
client client
.account_subscribe( .account_subscribe(
@ -136,13 +135,13 @@ async fn feed_data(
return Ok(()); return Ok(());
} }
}, },
message = mango_pyth_oracles_sub_map.next() => { message = mango_oracles_sub_map.next() => {
if let Some(data) = message { if let Some(data) = message {
let response = data.1.map_err_anyhow()?; let response = data.1.map_err_anyhow()?;
let response = solana_client::rpc_response::Response{ context: RpcResponseContext{ slot: response.context.slot, api_version: None }, value: RpcKeyedAccount{ pubkey: data.0.to_string(), account: response.value} } ; let response = solana_client::rpc_response::Response{ context: RpcResponseContext{ slot: response.context.slot, api_version: None }, value: RpcKeyedAccount{ pubkey: data.0.to_string(), account: response.value} } ;
sender.send(Message::Account(AccountUpdate::from_rpc(response)?)).await.expect("sending must succeed"); sender.send(Message::Account(AccountUpdate::from_rpc(response)?)).await.expect("sending must succeed");
} else { } else {
warn!("pyth stream closed"); warn!("oracle stream closed");
return Ok(()); return Ok(());
} }
}, },
@ -171,16 +170,12 @@ async fn feed_data(
} }
} }
pub fn start( pub fn start(config: Config, mango_oracles: Vec<Pubkey>, sender: async_channel::Sender<Message>) {
config: Config,
mango_pyth_oracles: Vec<Pubkey>,
sender: async_channel::Sender<Message>,
) {
tokio::spawn(async move { tokio::spawn(async move {
// if the websocket disconnects, we get no data in a while etc, reconnect and try again // if the websocket disconnects, we get no data in a while etc, reconnect and try again
loop { loop {
info!("connecting to solana websocket streams"); info!("connecting to solana websocket streams");
let out = feed_data(&config, mango_pyth_oracles.clone(), sender.clone()); let out = feed_data(&config, mango_oracles.clone(), sender.clone());
let _ = out.await; let _ = out.await;
} }
}); });

View File

@ -85,8 +85,8 @@ pub fn liq_token_with_token(
// The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position // The main complication here is that we can't keep the liqee_asset_position and liqee_liab_position
// borrows alive at the same time. Possibly adding get_mut_pair() would be helpful. // borrows alive at the same time. Possibly adding get_mut_pair() would be helpful.
let (liqee_asset_position, liqee_asset_raw_index) = liqee.token_get(asset_token_index)?; let (liqee_asset_position, liqee_asset_raw_index) = liqee.token_get(asset_token_index)?;
let liqee_assets_native = liqee_asset_position.native(asset_bank); let liqee_asset_native = liqee_asset_position.native(asset_bank);
require!(liqee_assets_native.is_positive(), MangoError::SomeError); require!(liqee_asset_native.is_positive(), MangoError::SomeError);
let (liqee_liab_position, liqee_liab_raw_index) = liqee.token_get(liab_token_index)?; let (liqee_liab_position, liqee_liab_raw_index) = liqee.token_get(liab_token_index)?;
let liqee_liab_native = liqee_liab_position.native(liab_bank); let liqee_liab_native = liqee_liab_position.native(liab_bank);
@ -115,7 +115,7 @@ pub fn liq_token_with_token(
/ (liab_price * init_liab_weight - init_asset_weight * liab_price_adjusted)); / (liab_price * init_liab_weight - init_asset_weight * liab_price_adjusted));
// How much liab can we get at most for the asset balance? // How much liab can we get at most for the asset balance?
let liab_possible = cm!(liqee_assets_native * asset_price / liab_price_adjusted); let liab_possible = cm!(liqee_asset_native * asset_price / liab_price_adjusted);
// The amount of liab native tokens we will transfer // The amount of liab native tokens we will transfer
let liab_transfer = min( let liab_transfer = min(
@ -135,6 +135,7 @@ pub fn liq_token_with_token(
liqor.token_get_mut_or_create(liab_token_index)?; liqor.token_get_mut_or_create(liab_token_index)?;
let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?; let liqor_liab_active = liab_bank.withdraw_with_fee(liqor_liab_position, liab_transfer)?;
let liqor_liab_position_indexed = liqor_liab_position.indexed_position; let liqor_liab_position_indexed = liqor_liab_position.indexed_position;
let liqee_liab_native_after = liqee_liab_position.native(&liab_bank);
let (liqor_asset_position, liqor_asset_raw_index, _) = let (liqor_asset_position, liqor_asset_raw_index, _) =
liqor.token_get_mut_or_create(asset_token_index)?; liqor.token_get_mut_or_create(asset_token_index)?;
@ -145,10 +146,17 @@ pub fn liq_token_with_token(
let liqee_asset_active = let liqee_asset_active =
asset_bank.withdraw_without_fee(liqee_asset_position, asset_transfer)?; asset_bank.withdraw_without_fee(liqee_asset_position, asset_transfer)?;
let liqee_asset_position_indexed = liqee_asset_position.indexed_position; let liqee_asset_position_indexed = liqee_asset_position.indexed_position;
let liqee_assets_native_after = liqee_asset_position.native(&asset_bank);
// Update the health cache // Update the health cache
liqee_health_cache.adjust_token_balance(liab_token_index, liab_transfer)?; liqee_health_cache.adjust_token_balance(
liqee_health_cache.adjust_token_balance(asset_token_index, -asset_transfer)?; liab_token_index,
cm!(liqee_liab_native_after - liqee_liab_native),
)?;
liqee_health_cache.adjust_token_balance(
asset_token_index,
cm!(liqee_assets_native_after - liqee_asset_native),
)?;
msg!( msg!(
"liquidated {} liab for {} asset", "liquidated {} liab for {} asset",

View File

@ -505,5 +505,73 @@ async fn test_liq_tokens_with_token() -> Result<(), TransportError> {
assert!(!liqee.being_liquidated()); assert!(!liqee.being_liquidated());
assert!(!liqee.is_bankrupt()); assert!(!liqee.is_bankrupt());
//
// TEST: bankruptcy when collateral is dusted
//
// Setup: make collateral really valueable, remove nearly all of it
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: collateral_token1.mint.pubkey,
payer,
price: "100000.0",
},
)
.await
.unwrap();
send_tx(
solana,
TokenWithdrawInstruction {
amount: (account_position(solana, account, collateral_token1.bank).await) as u64 - 1,
allow_borrow: false,
account,
owner,
token_account: payer_mint_accounts[2],
bank_index: 0,
},
)
.await
.unwrap();
// Setup: reduce collateral value to trigger liquidatability
// We have -93 borrows, so -93*2*1.4 = -260.4 health from that
// And 1-2 collateral, so max 2*0.6*X health; say X=150 for max 180 health
send_tx(
solana,
StubOracleSetInstruction {
group,
admin,
mint: collateral_token1.mint.pubkey,
payer,
price: "150.0",
},
)
.await
.unwrap();
send_tx(
solana,
LiqTokenWithTokenInstruction {
liqee: account,
liqor: vault_account,
liqor_owner: owner,
asset_token_index: collateral_token1.index,
liab_token_index: borrow_token1.index,
max_liab_transfer: I80F48::from_num(10001.0),
asset_bank_index: 0,
liab_bank_index: 0,
},
)
.await
.unwrap();
// Liqee's remaining collateral got dusted, only borrows remain: bankrupt
let liqee = get_mango_account(solana, account).await;
assert_eq!(liqee.token_iter_active().count(), 1);
assert!(liqee.is_bankrupt());
assert!(liqee.being_liquidated());
Ok(()) Ok(())
} }

View File

@ -128,10 +128,7 @@ export class MangoClient {
return group; return group;
} }
public async getGroupForCreator( public async getGroupsForCreator(creatorPk: PublicKey): Promise<Group[]> {
creatorPk: PublicKey,
groupNum?: number,
): Promise<Group> {
const filters: MemcmpFilter[] = [ const filters: MemcmpFilter[] = [
{ {
memcmp: { memcmp: {
@ -141,20 +138,25 @@ export class MangoClient {
}, },
]; ];
if (groupNum !== undefined) { return (await this.program.account.group.all(filters)).map((tuple) =>
const bbuf = Buffer.alloc(4); Group.from(tuple.publicKey, tuple.account),
bbuf.writeUInt32LE(groupNum);
filters.push({
memcmp: {
bytes: bs58.encode(bbuf),
offset: 40,
},
});
}
const groups = (await this.program.account.group.all(filters)).map(
(tuple) => Group.from(tuple.publicKey, tuple.account),
); );
}
public async getGroupForCreator(
creatorPk: PublicKey,
groupNum?: number,
): Promise<Group> {
const groups = (await this.getGroupsForCreator(creatorPk)).filter(
(group) => {
if (groupNum !== undefined) {
return group.groupNum == groupNum;
} else {
return true;
}
},
);
await groups[0].reloadAll(this); await groups[0].reloadAll(this);
return groups[0]; return groups[0];
} }
@ -472,12 +474,35 @@ export class MangoClient {
accountNumber?: number, accountNumber?: number,
name?: string, name?: string,
): Promise<MangoAccount> { ): Promise<MangoAccount> {
let mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk); // TODO: this function discards accountSize and name when the account exists already!
if (mangoAccounts.length === 0) { // TODO: this function always creates accounts for this.program.owner, and not
await this.createMangoAccount(group, accountNumber, name); // ownerPk! It needs to get passed a keypair, and we need to add
mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk); // createMangoAccountForOwner
if (accountNumber === undefined) {
// Get any MangoAccount
// TODO: should probably sort by accountNum for deterministic output!
let mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk);
if (mangoAccounts.length === 0) {
await this.createMangoAccount(group, accountNumber, name);
mangoAccounts = await this.getMangoAccountsForOwner(group, ownerPk);
}
return mangoAccounts[0];
} else {
let account = await this.getMangoAccountForOwner(
group,
ownerPk,
accountNumber,
);
if (account === undefined) {
await this.createMangoAccount(group, accountNumber, name);
account = await this.getMangoAccountForOwner(
group,
ownerPk,
accountNumber,
);
}
return account;
} }
return mangoAccounts[0];
} }
public async createMangoAccount( public async createMangoAccount(
@ -537,6 +562,16 @@ export class MangoClient {
); );
} }
public async getMangoAccountForOwner(
group: Group,
ownerPk: PublicKey,
accountNumber: number,
): Promise<MangoAccount> {
return (await this.getMangoAccountsForOwner(group, ownerPk)).find(
(a) => a.accountNum == accountNumber,
);
}
public async getMangoAccountsForOwner( public async getMangoAccountsForOwner(
group: Group, group: Group,
ownerPk: PublicKey, ownerPk: PublicKey,
@ -623,6 +658,22 @@ export class MangoClient {
amount: number, amount: number,
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
const bank = group.banksMap.get(tokenName)!; const bank = group.banksMap.get(tokenName)!;
const nativeAmount = toNativeDecimals(amount, bank.mintDecimals).toNumber();
return await this.tokenDepositNative(
group,
mangoAccount,
tokenName,
nativeAmount,
);
}
public async tokenDepositNative(
group: Group,
mangoAccount: MangoAccount,
tokenName: string,
nativeAmount: number,
) {
const bank = group.banksMap.get(tokenName)!;
const tokenAccountPk = await getAssociatedTokenAddress( const tokenAccountPk = await getAssociatedTokenAddress(
bank.mint, bank.mint,
@ -635,7 +686,7 @@ export class MangoClient {
const additionalSigners: Signer[] = []; const additionalSigners: Signer[] = [];
if (bank.mint.equals(WRAPPED_SOL_MINT)) { if (bank.mint.equals(WRAPPED_SOL_MINT)) {
wrappedSolAccount = new Keypair(); wrappedSolAccount = new Keypair();
const lamports = Math.round(amount * LAMPORTS_PER_SOL) + 1e7; const lamports = nativeAmount + 1e7;
preInstructions = [ preInstructions = [
SystemProgram.createAccount({ SystemProgram.createAccount({
@ -670,7 +721,7 @@ export class MangoClient {
); );
return await this.program.methods return await this.program.methods
.tokenDeposit(toNativeDecimals(amount, bank.mintDecimals)) .tokenDeposit(new BN(nativeAmount))
.accounts({ .accounts({
group: group.publicKey, group: group.publicKey,
account: mangoAccount.publicKey, account: mangoAccount.publicKey,
@ -699,45 +750,14 @@ export class MangoClient {
allowBorrow: boolean, allowBorrow: boolean,
): Promise<TransactionSignature> { ): Promise<TransactionSignature> {
const bank = group.banksMap.get(tokenName)!; const bank = group.banksMap.get(tokenName)!;
const nativeAmount = toNativeDecimals(amount, bank.mintDecimals).toNumber();
const tokenAccountPk = await getAssociatedTokenAddress( return await this.tokenWithdrawNative(
bank.mint, group,
mangoAccount.owner, mangoAccount,
tokenName,
nativeAmount,
allowBorrow,
); );
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(
AccountRetriever.Fixed,
group,
[mangoAccount],
[bank],
);
return await this.program.methods
.tokenWithdraw(toNativeDecimals(amount, bank.mintDecimals), allowBorrow)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
bank: bank.publicKey,
vault: bank.vault,
tokenAccount: tokenAccountPk,
owner: mangoAccount.owner,
})
.remainingAccounts(
healthRemainingAccounts.map(
(pk) =>
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
),
)
.preInstructions([
// ensure withdraws don't fail with missing ATAs
await createAssociatedTokenAccountIdempotentInstruction(
mangoAccount.owner,
mangoAccount.owner,
bank.mint,
),
])
.rpc({ skipPreflight: true });
} }
public async tokenWithdrawNative( public async tokenWithdrawNative(
@ -778,6 +798,14 @@ export class MangoClient {
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta), ({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
), ),
) )
.preInstructions([
// ensure withdraws don't fail with missing ATAs
await createAssociatedTokenAccountIdempotentInstruction(
mangoAccount.owner,
mangoAccount.owner,
bank.mint,
),
])
.rpc({ skipPreflight: true }); .rpc({ skipPreflight: true });
} }

View File

@ -0,0 +1,29 @@
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { BN, BorshCoder } from '@project-serum/anchor';
import { MangoClient } from '../client';
import { MANGO_V4_ID } from '../constants';
import { IDL } from '../mango_v4';
async function main() {
const coder = new BorshCoder(IDL);
const event = coder.events.decode(process.argv[2]);
console.log(
JSON.stringify(
event,
function (key, value) {
const orig_value = this[key]; // value is already processed
if (orig_value instanceof BN) {
return orig_value.toString();
}
return value;
},
' ',
),
);
process.exit();
}
main();

View File

@ -8,11 +8,8 @@ import { MANGO_V4_ID } from '../constants';
// example script to close accounts - banks, markets, group etc. which require admin to be the signer // example script to close accounts - banks, markets, group etc. which require admin to be the signer
// //
const MAINNET_MINTS = new Map([ // Use to close only a specific group by number. Use "all" to close all groups.
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], const GROUP_NUM = process.env.GROUP_NUM;
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['SOL', 'So11111111111111111111111111111111111111112'],
]);
async function main() { async function main() {
const options = AnchorProvider.defaultOptions(); const options = AnchorProvider.defaultOptions();
@ -32,49 +29,58 @@ async function main() {
MANGO_V4_ID['mainnet-beta'], MANGO_V4_ID['mainnet-beta'],
); );
const group = await client.getGroupForCreator(admin.publicKey); const groups = await (async () => {
console.log(`Group ${group.publicKey}`); if (GROUP_NUM === 'all') {
return await client.getGroupsForCreator(admin.publicKey);
} else {
return [
await client.getGroupForCreator(admin.publicKey, Number(GROUP_NUM)),
];
}
})();
for (const group of groups) {
console.log(`Group ${group.publicKey}`);
let sig; let sig;
// close stub oracle // close stub oracles
const usdcMainnetBetaMint = new PublicKey(MAINNET_MINTS.get('USDC')!); const stubOracles = await client.getStubOracle(group);
const usdcMainnetBetaOracle = ( for (const stubOracle of stubOracles) {
await client.getStubOracle(group, usdcMainnetBetaMint) sig = await client.stubOracleClose(group, stubOracle.publicKey);
)[0]; console.log(
sig = await client.stubOracleClose(group, usdcMainnetBetaOracle.publicKey); `Closed stub oracle ${stubOracle.publicKey}, sig https://explorer.solana.com/tx/${sig}`,
console.log( );
`Closed USDC stub oracle, sig https://explorer.solana.com/tx/${sig}`, }
);
// close all bank // close all banks
for (const bank of group.banksMap.values()) { for (const bank of group.banksMap.values()) {
sig = await client.tokenDeregister(group, bank.name); sig = await client.tokenDeregister(group, bank.name);
console.log( console.log(
`Removed token ${bank.name}, sig https://explorer.solana.com/tx/${sig}`, `Removed token ${bank.name}, sig https://explorer.solana.com/tx/${sig}`,
); );
}
// deregister all serum markets
for (const market of group.serum3MarketsMap.values()) {
sig = await client.serum3deregisterMarket(group, market.name);
console.log(
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// close all perp markets
for (const market of group.perpMarketsMap.values()) {
sig = await client.perpCloseMarket(group, market.name);
console.log(
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// finally, close the group
sig = await client.groupClose(group);
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
} }
// deregister all serum markets
for (const market of group.serum3MarketsMap.values()) {
sig = await client.serum3deregisterMarket(group, market.name);
console.log(
`Deregistered serum market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// close all perp markets
for (const market of group.perpMarketsMap.values()) {
sig = await client.perpCloseMarket(group, market.name);
console.log(
`Closed perp market ${market.name}, sig https://explorer.solana.com/tx/${sig}`,
);
}
// finally, close the group
sig = await client.groupClose(group);
console.log(`Closed group, sig https://explorer.solana.com/tx/${sig}`);
process.exit(); process.exit();
} }

View File

@ -0,0 +1,175 @@
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';
//
// Script which depoys a new mango group, and registers 3 tokens
// with stub oracles
//
// default to group 1, to not conflict with the normal group
const GROUP_NUM = Number(process.env.GROUP_NUM || 1);
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['SOL', 'So11111111111111111111111111111111111111112'],
]);
const STUB_PRICES = new Map([
['USDC', 1.0],
['BTC', 20000.0], // btc and usdc both have 6 decimals
['SOL', 0.04], // sol has 9 decimals, equivalent to $40 per SOL
]);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
),
),
);
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'],
);
// group
console.log(`Creating Group...`);
try {
const insuranceMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
await client.groupCreate(GROUP_NUM, true, 0, insuranceMint);
} catch (error) {
console.log(error);
}
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(`...registered group ${group.publicKey}`);
// stub oracles
let oracles = new Map();
for (let [name, mint] of MAINNET_MINTS) {
console.log(`Creating stub oracle for ${name}...`);
const mintPk = new PublicKey(mint);
try {
const price = STUB_PRICES.get(name);
await client.stubOracleCreate(group, mintPk, price);
} catch (error) {
console.log(error);
}
const oracle = (await client.getStubOracle(group, mintPk))[0];
console.log(`...created stub oracle ${oracle.publicKey}`);
oracles.set(name, oracle.publicKey);
}
// register token 1
console.log(`Registering BTC...`);
const btcMainnetMint = new PublicKey(MAINNET_MINTS.get('BTC')!);
const btcMainnetOracle = oracles.get('BTC');
try {
await client.tokenRegister(
group,
btcMainnetMint,
btcMainnetOracle,
0.1,
1,
'BTC',
0.01,
0.4,
0.07,
0.7,
0.88,
1.5,
0.0,
0.0001,
0.9,
0.8,
1.1,
1.2,
0.05,
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
}
// register token 0
console.log(`Registering USDC...`);
const usdcMainnetMint = new PublicKey(MAINNET_MINTS.get('USDC')!);
const usdcMainnetOracle = oracles.get('USDC');
try {
await client.tokenRegister(
group,
usdcMainnetMint,
usdcMainnetOracle,
0.1,
0,
'USDC',
0.01,
0.4,
0.07,
0.8,
0.9,
1.5,
0.0,
0.0001,
1,
1,
1,
1,
0,
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
}
// register token 2
console.log(`Registering SOL...`);
const solMainnetMint = new PublicKey(MAINNET_MINTS.get('SOL')!);
const solMainnetOracle = oracles.get('SOL');
try {
await client.tokenRegister(
group,
solMainnetMint,
solMainnetOracle,
0.1,
2, // tokenIndex
'SOL',
0.01,
0.4,
0.07,
0.8,
0.9,
1.5,
0.0,
0.0001,
0.9,
0.8,
1.1,
1.2,
0.05,
);
await group.reloadAll(client);
} catch (error) {
console.log(error);
}
// log tokens/banks
for (const bank of await group.banksMap.values()) {
console.log(`${bank.toString()}`);
}
process.exit();
}
main();

View File

@ -0,0 +1,68 @@
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';
//
// This script deposits some tokens, so other liquidation scripts can borrow.
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 1);
const ACCOUNT_NUM = Number(process.env.ACCOUNT_NUM || 0);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
),
),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
// create + fetch account
console.log(`Creating mangoaccount...`);
const mangoAccount = await client.getOrCreateMangoAccount(
group,
admin.publicKey,
ACCOUNT_NUM,
);
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
console.log(mangoAccount.toString());
// deposit
try {
console.log(`...depositing 10 USDC`);
await client.tokenDeposit(group, mangoAccount, 'USDC', 10);
await mangoAccount.reload(client, group);
console.log(`...depositing 0.0004 BTC`);
await client.tokenDeposit(group, mangoAccount, 'BTC', 0.0004);
await mangoAccount.reload(client, group);
console.log(`...depositing 0.25 SOL`);
await client.tokenDeposit(group, mangoAccount, 'SOL', 0.25);
await mangoAccount.reload(client, group);
} catch (error) {
console.log(error);
}
process.exit();
}
main();

View File

@ -0,0 +1,110 @@
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';
//
// This script creates liquidation candidates
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 1);
// native prices
const PRICES = {
BTC: 20000.0,
SOL: 0.04,
USDC: 1,
};
const MAINNET_MINTS = new Map([
['USDC', 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
['BTC', '9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E'],
['SOL', 'So11111111111111111111111111111111111111112'],
]);
const SCENARIOS: [string, string, number, string, number][] = [
['LIQTEST, LIQOR', 'USDC', 1000000, 'USDC', 0],
['LIQTEST, A: USDC, L: SOL', 'USDC', 1000 * PRICES.SOL, 'SOL', 920],
['LIQTEST, A: SOL, L: USDC', 'SOL', 1000, 'USDC', 920 * PRICES.SOL],
// TODO: needs the fix on liq+dust to go live
//['LIQTEST, A: BTC, L: SOL', 'BTC', 20, 'SOL', 18 * PRICES.BTC / PRICES.SOL],
];
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
),
),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
// fetch group
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
const accounts = await client.getMangoAccountsForOwner(
group,
admin.publicKey,
);
let maxAccountNum = Math.max(...accounts.map((a) => a.accountNum));
for (const scenario of SCENARIOS) {
const [name, assetName, assetAmount, liabName, liabAmount] = scenario;
// create account
console.log(`Creating mangoaccount...`);
let mangoAccount = await client.getOrCreateMangoAccount(
group,
admin.publicKey,
maxAccountNum + 1,
);
maxAccountNum = maxAccountNum + 1;
console.log(
`...created mangoAccount ${mangoAccount.publicKey} for ${name}`,
);
await client.tokenDepositNative(
group,
mangoAccount,
assetName,
assetAmount,
);
await mangoAccount.reload(client, group);
if (liabAmount > 0) {
// temporarily drop the borrowed token value, so the borrow goes through
const oracle = group.banksMap.get(liabName).oracle;
try {
await client.stubOracleSet(group, oracle, PRICES[liabName] / 2);
await client.tokenWithdrawNative(
group,
mangoAccount,
liabName,
liabAmount,
true,
);
} finally {
// restore the oracle
await client.stubOracleSet(group, oracle, PRICES[liabName]);
}
}
}
process.exit();
}
main();

View File

@ -0,0 +1,102 @@
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';
//
// This script tries to withdraw all positive balances for all accounts
// by MANGO_MAINNET_PAYER_KEYPAIR in the group.
//
const GROUP_NUM = Number(process.env.GROUP_NUM || 1);
async function main() {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(process.env.CLUSTER_URL, options);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
),
),
);
const userWallet = new Wallet(admin);
const userProvider = new AnchorProvider(connection, userWallet, options);
const client = await MangoClient.connect(
userProvider,
'mainnet-beta',
MANGO_V4_ID['mainnet-beta'],
);
console.log(`User ${userWallet.publicKey.toBase58()}`);
const group = await client.getGroupForCreator(admin.publicKey, GROUP_NUM);
console.log(group.toString());
let accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
for (let account of accounts) {
console.log(`settling borrows on account: ${account}`);
// first, settle all borrows
for (let token of account.tokensActive()) {
const bank = group.findBank(token.tokenIndex);
const amount = token.native(bank).toNumber();
if (amount < 0) {
try {
await client.tokenDepositNative(
group,
account,
bank.name,
Math.ceil(-amount),
);
await account.reload(client, group);
} catch (error) {
console.log(
`failed to deposit ${bank.name} into ${account.publicKey}: ${error}`,
);
}
}
}
}
accounts = await client.getMangoAccountsForOwner(group, admin.publicKey);
for (let account of accounts) {
console.log(`withdrawing deposits of account: ${account}`);
// withdraw all funds
for (let token of account.tokensActive()) {
const bank = group.findBank(token.tokenIndex);
const amount = token.native(bank).toNumber();
if (amount > 0) {
try {
const allowBorrow = true; // TODO: set this to false once the withdraw amount ___<___ nativePosition bug is fixed
await client.tokenWithdrawNative(
group,
account,
bank.name,
amount,
allowBorrow,
);
await account.reload(client, group);
} catch (error) {
console.log(
`failed to withdraw ${bank.name} from ${account.publicKey}: ${error}`,
);
}
}
}
// close account
try {
console.log(`closing account: ${account}`);
await client.closeMangoAccount(group, account);
} catch (error) {
console.log(`failed to close ${account.publicKey}: ${error}`);
}
}
process.exit();
}
main();