From 9b6077411818740505a1471afc1b0a031d5162ac Mon Sep 17 00:00:00 2001 From: 0xripleys <105607696+0xripleys@users.noreply.github.com> Date: Thu, 17 Nov 2022 13:23:31 -0500 Subject: [PATCH] 0xripleys manual liquidate (#112) * kinda working cli liquidation * dynamically create atas if they don't exist * adding compute budget request * fix clippy * liquidate scripts * withdraw ctokens command * add redeem reserve collateral function --- Cargo.lock | 1 + token-lending/cli/Cargo.toml | 1 + token-lending/cli/scripts/liquidate.sh | 30 ++ token-lending/cli/scripts/withdraw.sh | 25 ++ token-lending/cli/src/lending_state.rs | 135 +++++++++ token-lending/cli/src/main.rs | 386 ++++++++++++++++++++++++- 6 files changed, 577 insertions(+), 1 deletion(-) create mode 100755 token-lending/cli/scripts/liquidate.sh create mode 100755 token-lending/cli/scripts/withdraw.sh create mode 100644 token-lending/cli/src/lending_state.rs diff --git a/Cargo.lock b/Cargo.lock index 164ce182de1..89f5a3ebcfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3369,6 +3369,7 @@ dependencies = [ "solana-program", "solana-sdk", "solend-program", + "spl-associated-token-account", "spl-token", ] diff --git a/token-lending/cli/Cargo.toml b/token-lending/cli/Cargo.toml index 86820b71856..881fd51c858 100644 --- a/token-lending/cli/Cargo.toml +++ b/token-lending/cli/Cargo.toml @@ -18,6 +18,7 @@ solana-sdk = "=1.9.18" solana-program = "=1.9.18" solend-program = { path="../program", features = [ "no-entrypoint" ] } spl-token = { version = "3.2.0", features=["no-entrypoint"] } +spl-associated-token-account = "1.0.3" [[bin]] name = "solend-program" diff --git a/token-lending/cli/scripts/liquidate.sh b/token-lending/cli/scripts/liquidate.sh new file mode 100755 index 00000000000..44b9fd27654 --- /dev/null +++ b/token-lending/cli/scripts/liquidate.sh @@ -0,0 +1,30 @@ +set -ex + +ETH_RESERVE=CPDiKagfozERtJ33p7HHhEfJERjvfk1VAjMXAFLrvrKP +SOL_RESERVE=8PbodeaosQP19SjYFx855UMqWxH2HynZLdBXmsrbac36 +STSOL_RESERVE=5sjkv6HD8wycocJ4tC4U36HHbvgcXYqcyiPRUkncnwWs +MSOL_RESERVE=CCpirWrgNuBVLdkP2haxLTbD6XqEgaYuVXixbbpxUB6 +USDC_RESERVE=BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw +USDT_RESERVE=8K9WC8xoh2rtQNY7iEGXtPvfbDCi563SdWhCAhuMP2xE +BTC_RESERVE=GYzjMCXTDue12eUGKKWAqtF5jcBYNmewr6Db6LaguEaX +RAY_RESERVE=9n2exoMQwMTzfw6NFoFFujxYPndWVLtKREJePssrKb36 +SLND_RESERVE=CviGNzD2C9ZCMmjDt5DKCce5cLV4Emrcm3NFvwudBFKA + +USDC_ATA=Bqn9qMjFEHRNS4wBRVAs3Uc52Dr3vm2AXJ5GaMkepBiQ +USDT_ATA=2bEeupwb9eC5R9LjCCrfetPm5yLwdGVLYng6XhNtue9H +BTC_ATA=A6Fu8DtnUqeYpzUMbZnnDpUFE5URnNUd8toZzcNBMkJ4 + +OBLIGATION_PUBKEY=HLRd6Dn4RUs4XbVzYhdp6UswQMCJTqWc9PgJ6VxvsyXu +# OBLIGATION_PUBKEY=3ErCznFWTRmhZE8C1mAQCkcneqcZQedB5ACqAwbbWUAP +REPAY_RESERVE=$USDC_RESERVE +WITHDRAW_RESERVE=$SOL_RESERVE +LIQUIDITY_AMOUNT=1000000000 +SOURCE_LIQUIDITY=$USDC_ATA + + +cargo run liquidate-obligation \ + --obligation $OBLIGATION_PUBKEY \ + --repay-reserve $REPAY_RESERVE \ + --withdraw-reserve $WITHDRAW_RESERVE \ + --liquidity-amount $LIQUIDITY_AMOUNT \ + --source-liquidity $SOURCE_LIQUIDITY diff --git a/token-lending/cli/scripts/withdraw.sh b/token-lending/cli/scripts/withdraw.sh new file mode 100755 index 00000000000..ccd4e3a3577 --- /dev/null +++ b/token-lending/cli/scripts/withdraw.sh @@ -0,0 +1,25 @@ +set -ex + +ETH_RESERVE=CPDiKagfozERtJ33p7HHhEfJERjvfk1VAjMXAFLrvrKP +SOL_RESERVE=8PbodeaosQP19SjYFx855UMqWxH2HynZLdBXmsrbac36 +STSOL_RESERVE=5sjkv6HD8wycocJ4tC4U36HHbvgcXYqcyiPRUkncnwWs +MSOL_RESERVE=CCpirWrgNuBVLdkP2haxLTbD6XqEgaYuVXixbbpxUB6 +USDC_RESERVE=BgxfHJDzm44T7XG68MYKx7YisTjZu73tVovyZSjJMpmw +USDT_RESERVE=8K9WC8xoh2rtQNY7iEGXtPvfbDCi563SdWhCAhuMP2xE +BTC_RESERVE=GYzjMCXTDue12eUGKKWAqtF5jcBYNmewr6Db6LaguEaX +RAY_RESERVE=9n2exoMQwMTzfw6NFoFFujxYPndWVLtKREJePssrKb36 +SLND_RESERVE=CviGNzD2C9ZCMmjDt5DKCce5cLV4Emrcm3NFvwudBFKA + +USDC_ATA=Bqn9qMjFEHRNS4wBRVAs3Uc52Dr3vm2AXJ5GaMkepBiQ +USDT_ATA=2bEeupwb9eC5R9LjCCrfetPm5yLwdGVLYng6XhNtue9H +BTC_ATA=A6Fu8DtnUqeYpzUMbZnnDpUFE5URnNUd8toZzcNBMkJ4 + +OBLIGATION_PUBKEY=9XQ18M6VvB9X9QVXxj1bCvKifAW2RWDePEfBwi6fsLhq +WITHDRAW_RESERVE=$SOL_RESERVE +COLLATERAL_AMOUNT=1000 + + +cargo run withdraw-collateral \ + --obligation $OBLIGATION_PUBKEY \ + --withdraw-reserve $WITHDRAW_RESERVE \ + --withdraw-amount $COLLATERAL_AMOUNT \ diff --git a/token-lending/cli/src/lending_state.rs b/token-lending/cli/src/lending_state.rs new file mode 100644 index 00000000000..2ab8b5dae9d --- /dev/null +++ b/token-lending/cli/src/lending_state.rs @@ -0,0 +1,135 @@ +use solana_program::instruction::Instruction; +use solend_program::instruction::{ + refresh_obligation, refresh_reserve, withdraw_obligation_collateral, +}; +use solend_program::state::{Obligation, Reserve}; + +use solana_client::rpc_client::RpcClient; +use solana_program::program_pack::Pack; +use solana_program::pubkey::Pubkey; +use spl_associated_token_account::get_associated_token_address; +use std::collections::HashSet; + +pub struct SolendState { + lending_program_id: Pubkey, + obligation_pubkey: Pubkey, + obligation: Obligation, + reserves: Vec<(Pubkey, Reserve)>, +} + +impl SolendState { + pub fn new( + lending_program_id: Pubkey, + obligation_pubkey: Pubkey, + rpc_client: &RpcClient, + ) -> Self { + let obligation = { + let data = rpc_client.get_account(&obligation_pubkey).unwrap(); + Obligation::unpack(&data.data).unwrap() + }; + + // get reserve pubkeys + let reserve_pubkeys: Vec = { + let mut r = HashSet::new(); + r.extend(obligation.deposits.iter().map(|d| d.deposit_reserve)); + r.extend(obligation.borrows.iter().map(|b| b.borrow_reserve)); + r.into_iter().collect() + }; + + // get reserve accounts + let reserves: Vec<(Pubkey, Reserve)> = rpc_client + .get_multiple_accounts(&reserve_pubkeys) + .unwrap() + .into_iter() + .zip(reserve_pubkeys.iter()) + .map(|(account, pubkey)| (*pubkey, Reserve::unpack(&account.unwrap().data).unwrap())) + .collect(); + + assert!(reserve_pubkeys.len() == reserves.len()); + + Self { + lending_program_id, + obligation_pubkey, + obligation, + reserves, + } + } + + pub fn find_reserve_by_key(&self, pubkey: Pubkey) -> Option<&Reserve> { + self.reserves.iter().find_map( + |(p, reserve)| { + if pubkey == *p { + Some(reserve) + } else { + None + } + }, + ) + } + + fn get_refresh_instructions(&self) -> Vec { + let mut instructions = Vec::new(); + instructions.extend(self.reserves.iter().map(|(pubkey, reserve)| { + refresh_reserve( + self.lending_program_id, + *pubkey, + reserve.liquidity.pyth_oracle_pubkey, + reserve.liquidity.switchboard_oracle_pubkey, + ) + })); + + let reserve_pubkeys: Vec = { + let mut r = Vec::new(); + r.extend(self.obligation.deposits.iter().map(|d| d.deposit_reserve)); + r.extend(self.obligation.borrows.iter().map(|b| b.borrow_reserve)); + r + }; + + // refresh obligation + instructions.push(refresh_obligation( + self.lending_program_id, + self.obligation_pubkey, + reserve_pubkeys, + )); + + instructions + } + + /// withdraw obligation ctokens to owner's ata + pub fn withdraw( + &self, + withdraw_reserve_pubkey: &Pubkey, + collateral_amount: u64, + ) -> Vec { + let mut instructions = self.get_refresh_instructions(); + + // find repay, withdraw reserve states + let withdraw_reserve = self + .reserves + .iter() + .find_map(|(pubkey, reserve)| { + if withdraw_reserve_pubkey == pubkey { + Some(reserve) + } else { + None + } + }) + .unwrap(); + + instructions.push(withdraw_obligation_collateral( + self.lending_program_id, + collateral_amount, + withdraw_reserve.collateral.supply_pubkey, + get_associated_token_address( + &self.obligation.owner, + &withdraw_reserve.collateral.mint_pubkey, + ), + *withdraw_reserve_pubkey, + self.obligation_pubkey, + withdraw_reserve.lending_market, + self.obligation.owner, + )); + + instructions + } +} diff --git a/token-lending/cli/src/main.rs b/token-lending/cli/src/main.rs index 07ae02db3c0..df6a0c661b9 100644 --- a/token-lending/cli/src/main.rs +++ b/token-lending/cli/src/main.rs @@ -1,3 +1,16 @@ +use lending_state::SolendState; +use solana_client::rpc_config::RpcSendTransactionConfig; +use solana_sdk::{commitment_config::CommitmentLevel, compute_budget::ComputeBudgetInstruction}; +use solend_program::{ + instruction::{ + liquidate_obligation_and_redeem_reserve_collateral, redeem_reserve_collateral, + refresh_obligation, refresh_reserve, + }, + state::Obligation, +}; + +mod lending_state; + use { clap::{ crate_description, crate_name, crate_version, value_t, App, AppSettings, Arg, ArgMatches, @@ -35,6 +48,8 @@ use { system_instruction::create_account, }; +use spl_associated_token_account::{create_associated_token_account, get_associated_token_address}; + struct Config { rpc_client: RpcClient, fee_payer: Box, @@ -194,6 +209,101 @@ fn main() { .help("Currency market prices are quoted in"), ), ) + .subcommand( + SubCommand::with_name("liquidate-obligation") + .about("Liquidate Obligation and redeem reserve collateral") + // @TODO: use is_valid_signer + .arg( + Arg::with_name("obligation") + .long("obligation") + .value_name("OBLIGATION_PUBKEY") + .takes_value(true) + .required(true) + .help("obligation pubkey"), + ) + .arg( + Arg::with_name("repay-reserve") + .long("repay-reserve") + .value_name("RESERVE_PUBKEY") + .takes_value(true) + .required(true) + .help("repay reserve"), + ) + .arg( + Arg::with_name("source-liquidity") + .long("source-liquidity") + .value_name("PUBKEY") + .takes_value(true) + .required(true) + .help("Token account that repays the obligation's debt"), + ) + .arg( + Arg::with_name("withdraw-reserve") + .long("withdraw-reserve") + .value_name("RESERVE_PUBKEY") + .takes_value(true) + .required(true) + .help("withdraw reserve"), + ) + .arg( + Arg::with_name("liquidity-amount") + .long("liquidity-amount") + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .help("amount of tokens to repay"), + ) + ) + .subcommand( + SubCommand::with_name("withdraw-collateral") + .about("Withdraw obligation collateral") + // @TODO: use is_valid_signer + .arg( + Arg::with_name("obligation") + .long("obligation") + .value_name("OBLIGATION_PUBKEY") + .takes_value(true) + .required(true) + .help("obligation pubkey"), + ) + .arg( + Arg::with_name("withdraw-reserve") + .long("withdraw-reserve") + .value_name("RESERVE_PUBKEY") + .takes_value(true) + .required(true) + .help("reserve that you want to withdraw ctokens from"), + ) + .arg( + Arg::with_name("collateral-amount") + .long("withdraw-amount") + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .help("amount of ctokens to withdraw"), + ) + ) + .subcommand( + SubCommand::with_name("redeem-collateral") + .about("Redeem ctokens for tokens") + // @TODO: use is_valid_signer + .arg( + Arg::with_name("redeem-reserve") + .long("redeem-reserve") + .value_name("RESERVE_PUBKEY") + .takes_value(true) + .required(true) + .help("reserve pubkey"), + ) + .arg( + Arg::with_name("collateral-amount") + .long("redeem-amount") + .value_name("AMOUNT") + .takes_value(true) + .required(true) + .help("amount of ctokens to redeem"), + ) + ) .subcommand( SubCommand::with_name("add-reserve") .about("Add a reserve to a lending market") @@ -660,6 +770,35 @@ fn main() { switchboard_oracle_program_id, ) } + ("liquidate-obligation", Some(arg_matches)) => { + let obligation = pubkey_of(arg_matches, "obligation").unwrap(); + let repay_reserve = pubkey_of(arg_matches, "repay-reserve").unwrap(); + let source_liquidity = pubkey_of(arg_matches, "source-liquidity").unwrap(); + let withdraw_reserve = pubkey_of(arg_matches, "withdraw-reserve").unwrap(); + let liquidity_amount = value_of(arg_matches, "liquidity-amount").unwrap(); + + command_liquidate_obligation( + &config, + obligation, + repay_reserve, + source_liquidity, + withdraw_reserve, + liquidity_amount, + ) + } + ("withdraw-collateral", Some(arg_matches)) => { + let obligation = pubkey_of(arg_matches, "obligation").unwrap(); + let withdraw_reserve = pubkey_of(arg_matches, "withdraw-reserve").unwrap(); + let collateral_amount = value_of(arg_matches, "collateral-amount").unwrap(); + + command_withdraw_collateral(&config, obligation, withdraw_reserve, collateral_amount) + } + ("redeem-collateral", Some(arg_matches)) => { + let redeem_reserve = pubkey_of(arg_matches, "redeem-reserve").unwrap(); + let collateral_amount = value_of(arg_matches, "collateral-amount").unwrap(); + + command_redeem_collateral(&config, &redeem_reserve, collateral_amount) + } ("add-reserve", Some(arg_matches)) => { let lending_market_owner_keypair = keypair_of(arg_matches, "lending_market_owner").unwrap(); @@ -875,6 +1014,215 @@ fn command_create_lending_market( Ok(()) } +#[allow(clippy::too_many_arguments)] +fn command_redeem_collateral( + config: &Config, + redeem_reserve_pubkey: &Pubkey, + collateral_amount: u64, +) -> CommandResult { + let redeem_reserve = { + let data = config + .rpc_client + .get_account(redeem_reserve_pubkey) + .unwrap(); + Reserve::unpack(&data.data).unwrap() + }; + + let source_ata = + get_or_create_associated_token_address(config, &redeem_reserve.collateral.mint_pubkey); + let dest_ata = + get_or_create_associated_token_address(config, &redeem_reserve.liquidity.mint_pubkey); + + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref()], + Message::new_with_blockhash( + &[redeem_reserve_collateral( + config.lending_program_id, + collateral_amount, + source_ata, + dest_ata, + *redeem_reserve_pubkey, + redeem_reserve.collateral.mint_pubkey, + redeem_reserve.liquidity.supply_pubkey, + redeem_reserve.lending_market, + config.fee_payer.pubkey(), + )], + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ), + recent_blockhash, + ); + + send_transaction(config, transaction)?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn command_withdraw_collateral( + config: &Config, + obligation_pubkey: Pubkey, + withdraw_reserve_pubkey: Pubkey, + collateral_amount: u64, +) -> CommandResult { + let solend_state = SolendState::new( + config.lending_program_id, + obligation_pubkey, + &config.rpc_client, + ); + + let withdraw_reserve = solend_state + .find_reserve_by_key(withdraw_reserve_pubkey) + .unwrap(); + + // make atas + get_or_create_associated_token_address(config, &withdraw_reserve.collateral.mint_pubkey); + + let instructions = solend_state.withdraw(&withdraw_reserve_pubkey, collateral_amount); + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref()], + Message::new_with_blockhash( + &instructions, + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ), + recent_blockhash, + ); + + send_transaction(config, transaction)?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn command_liquidate_obligation( + config: &Config, + obligation_pubkey: Pubkey, + repay_reserve_pubkey: Pubkey, + source_liquidity_pubkey: Pubkey, + withdraw_reserve_pubkey: Pubkey, + liquidity_amount: u64, +) -> CommandResult { + let obligation_state = { + let data = config.rpc_client.get_account(&obligation_pubkey)?; + Obligation::unpack(&data.data)? + }; + + // get reserve pubkeys + let reserve_pubkeys = { + let mut r = Vec::new(); + r.extend(obligation_state.deposits.iter().map(|d| d.deposit_reserve)); + r.extend(obligation_state.borrows.iter().map(|b| b.borrow_reserve)); + r + }; + + // get reserve accounts + let reserves: Vec<(Pubkey, Reserve)> = config + .rpc_client + .get_multiple_accounts(&reserve_pubkeys)? + .into_iter() + .zip(reserve_pubkeys.iter()) + .map(|(account, pubkey)| (*pubkey, Reserve::unpack(&account.unwrap().data).unwrap())) + .collect(); + + assert!(reserve_pubkeys.len() == reserves.len()); + + // find repay, withdraw reserve states + let withdraw_reserve_state = reserves + .iter() + .find_map(|(pubkey, reserve)| { + if withdraw_reserve_pubkey == *pubkey { + Some(reserve) + } else { + None + } + }) + .unwrap(); + let repay_reserve_state = reserves + .iter() + .find_map(|(pubkey, reserve)| { + if repay_reserve_pubkey == *pubkey { + Some(reserve) + } else { + None + } + }) + .unwrap(); + + // make sure atas exist. if they don't, create them. + let required_mints = [ + withdraw_reserve_state.collateral.mint_pubkey, + withdraw_reserve_state.liquidity.mint_pubkey, + ]; + + for mint in required_mints { + get_or_create_associated_token_address(config, &mint); + } + + let destination_collateral_pubkey = get_associated_token_address( + &config.fee_payer.pubkey(), + &withdraw_reserve_state.collateral.mint_pubkey, + ); + let destination_liquidity_pubkey = get_associated_token_address( + &config.fee_payer.pubkey(), + &withdraw_reserve_state.liquidity.mint_pubkey, + ); + + let mut instructions = vec![ComputeBudgetInstruction::request_units(300_000, 30101)]; + + // refresh all reserves + instructions.extend(reserves.iter().map(|(pubkey, reserve)| { + refresh_reserve( + config.lending_program_id, + *pubkey, + reserve.liquidity.pyth_oracle_pubkey, + reserve.liquidity.switchboard_oracle_pubkey, + ) + })); + + // refresh obligation + instructions.push(refresh_obligation( + config.lending_program_id, + obligation_pubkey, + reserve_pubkeys, + )); + + instructions.push(liquidate_obligation_and_redeem_reserve_collateral( + config.lending_program_id, + liquidity_amount, + source_liquidity_pubkey, + destination_collateral_pubkey, + destination_liquidity_pubkey, + repay_reserve_pubkey, + repay_reserve_state.liquidity.supply_pubkey, + withdraw_reserve_pubkey, + withdraw_reserve_state.collateral.mint_pubkey, + withdraw_reserve_state.collateral.supply_pubkey, + withdraw_reserve_state.liquidity.supply_pubkey, + withdraw_reserve_state.config.fee_receiver, + obligation_pubkey, + obligation_state.lending_market, + config.fee_payer.pubkey(), + )); + + let recent_blockhash = config.rpc_client.get_latest_blockhash()?; + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref()], + Message::new_with_blockhash( + &instructions, + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ), + recent_blockhash, + ); + + send_transaction(config, transaction)?; + + Ok(()) +} + #[allow(clippy::too_many_arguments)] fn command_add_reserve( config: &mut Config, @@ -1378,7 +1726,16 @@ fn send_transaction( } else { let signature = config .rpc_client - .send_and_confirm_transaction_with_spinner(&transaction)?; + .send_and_confirm_transaction_with_spinner_and_config( + &transaction, + CommitmentConfig::confirmed(), + RpcSendTransactionConfig { + preflight_commitment: Some(CommitmentLevel::Processed), + skip_preflight: true, + encoding: None, + max_retries: None, + }, + )?; println!("Signature: {}", signature); } Ok(()) @@ -1399,3 +1756,30 @@ fn quote_currency_of(matches: &ArgMatches<'_>, name: &str) -> Option<[u8; 32]> { None } } + +fn get_or_create_associated_token_address(config: &Config, mint: &Pubkey) -> Pubkey { + let ata = get_associated_token_address(&config.fee_payer.pubkey(), mint); + + if config.rpc_client.get_account(&ata).is_err() { + println!("Creating ATA for mint {:?}", mint); + + let recent_blockhash = config.rpc_client.get_latest_blockhash().unwrap(); + let transaction = Transaction::new( + &vec![config.fee_payer.as_ref()], + Message::new_with_blockhash( + &[create_associated_token_account( + &config.fee_payer.pubkey(), + &config.fee_payer.pubkey(), + mint, + )], + Some(&config.fee_payer.pubkey()), + &recent_blockhash, + ), + recent_blockhash, + ); + + send_transaction(config, transaction).unwrap(); + } + + ata +}