support pyth oracle to deprecate flux aggregator (#33)

* add pyth client

* add pyth oracle and instruction to switch

* implement cli for oracle switch

* fix common build

* fix build for cli

* fix devnet url

* rename arg
This commit is contained in:
Maximilian Schneider 2022-08-02 16:59:07 +02:00 committed by GitHub
parent 141b10577b
commit 234d8e2682
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2080 additions and 2136 deletions

3123
cli/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,14 +16,15 @@ clap = "3.0.0-beta.2"
solana-client = "^1.6.4"
solana-cli = "^1.6.4"
solana-sdk = "^1.6.4"
mango = { version = "*", path = "../program", features=["no-entrypoint"] }
spl-token = { version = "^3.1.0", features=["no-entrypoint"] }
serde_json = "1.0.60"
chrono = "*"
common = { version = "*", path = "../common" }
serum_dex = { version = "^0.2", git = "https://github.com/blockworks-foundation/serum-dex.git", features=["no-entrypoint", "program"] }
flux-aggregator = { version = "^0.1", git = "https://github.com/blockworks-foundation/solana-flux-aggregator.git", features=["program", "no-entrypoint"] }
arrayref = "^0.3.6"
fixed = { version = "^1.7.0" }
common = { version = "*", path = "../common" }
mango = { version = "*", path = "../program", features=["no-entrypoint"] }
spl-token = { version = "^3.0.0", features=["no-entrypoint"] }
serum_dex = { version = "0.4.0", rev="3104f424ee38a415418a1cdef67970771f832857", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false, features = ["no-entrypoint", "program"] }
flux-aggregator = { version = "^0.1", rev="ca6706d05218acc84d164ed5149fac7612d3aa2b", git = "https://github.com/blockworks-foundation/solana-flux-aggregator.git", features=["program", "no-entrypoint"] }
pyth-client = {version = ">=0.5.0", features = ["no-entrypoint"]}

View File

@ -17,7 +17,7 @@ use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::program_pack::Pack;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Signer};
use mango::instruction::{init_mango_group, init_margin_account, withdraw, borrow, deposit, settle_borrow, change_borrow_limit};
use mango::instruction::{init_mango_group, init_margin_account, withdraw, borrow, deposit, settle_borrow, change_borrow_limit, switch_oracles};
#[derive(Clap, Debug)]
pub struct Opts {
@ -136,7 +136,19 @@ pub enum Command {
token_symbol: String,
#[clap(long)]
borrow_limit: f64
}
},
SwitchOracle {
#[clap(long, short)]
payer: String, // assumes for now payer is same as admin
#[clap(long, short)]
ids_path: String,
#[clap(long)]
mango_group_name: String,
#[clap(long)]
pair_name: String,
#[clap(long)]
oracle: String,
},
}
impl Opts {
@ -681,6 +693,48 @@ pub fn start(opts: Opts) -> Result<()> {
let signers = vec![&payer];
send_instructions(&client, instructions, signers, &payer.pubkey())?;
}
Command::SwitchOracle {
payer,
ids_path,
mango_group_name,
pair_name,
oracle,
} => {
println!("SwitchOracle");
let payer = read_keypair_file(payer.as_str())?;
let ids: Value = serde_json::from_reader(File::open(&ids_path)?)?;
let cluster_name = opts.cluster.name();
let cluster_ids = &ids[cluster_name];
let cids = ClusterIds::load(cluster_ids);
let mgids = cids.mango_groups[&mango_group_name].clone();
// replace old oracle with new one when assembling instruction
let old_oracle_pk = &cids.oracles[&pair_name];
let new_oracle_pks: Vec<Pubkey> = mgids
.oracle_pks
.iter()
.map(|o| {
if o == old_oracle_pk {
Pubkey::from_str(oracle.as_str()).unwrap()
} else {
*o
}
})
.collect();
let instruction = switch_oracles(
&cids.mango_program_id,
&mgids.mango_group_pk,
&payer.pubkey(),
&new_oracle_pks.as_slice(),
)?;
let instructions = vec![instruction];
let signers = vec![&payer];
send_instructions(&client, instructions, signers, &payer.pubkey())?;
// update ids
// let oracle_pks = ids[cluster_name][mango_group_name].get_mut("oracle_pks").unwrap();
// oracle_pks = new_oracle_pks.iter().map(|pk| pk.to_string()).collect() as Vec<String>;
// let f = File::create(ids_path.as_str()).unwrap();
// serde_json::to_writer_pretty(&f, &ids).unwrap();
}
}
Ok(())
}

2
common/Cargo.lock generated
View File

@ -414,7 +414,7 @@ dependencies = [
"bs58 0.4.0",
"bytemuck",
"ed25519-dalek",
"rand 0.8.4",
"rand 0.7.3",
"serde_json",
"solana-client",
"solana-sdk",

View File

@ -10,11 +10,11 @@ solana-sdk = "^1.7.10"
spl-token = { version = "^3.2.0", features=["no-entrypoint"] }
anyhow = "^1.0.43"
solana-client = "^1.7.10"
rand = "0.8.4"
rand = "^0.7.0"
serde_json = "^1.0.66"
bs58 = "0.4.0"
bincode = "^1.3.1"
bytemuck = "^1.7.2"
tiny-bip39 = "0.8.0"
tiny-hderive = "0.3.0"
ed25519-dalek = "1.0.0-pre.4"
ed25519-dalek = "^1.0.0"

View File

@ -57,7 +57,7 @@ impl std::fmt::Display for Cluster {
impl Cluster {
pub fn url(&self) -> &'static str {
match self {
Cluster::Devnet => "https://devnet.solana.com",
Cluster::Devnet => "https://api.devnet.solana.com",
Cluster::Testnet => "https://testnet.solana.com",
// Cluster::Mainnet => "https://api.stakeconomy.com",
Cluster::Mainnet => "https://api.mainnet-beta.solana.com",

873
program/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,6 @@ test-bpf = []
[dependencies]
solana-program = "^1.6.4"
spl-token = { version = "^3.0.0", features=["no-entrypoint"] }
byteorder = "^1.3.4"
arrayref = "^0.3.6"
num_enum = "^0.5.1"
@ -22,13 +21,15 @@ static_assertions = "^1.1.0"
thiserror = "^1.0.24"
serde = "^1.0.118"
bincode = "^1.3.1"
serum_dex = { version = "0.4.0", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false, features = ["no-entrypoint", "program"] }
num-derive = "^0.3.3"
flux-aggregator = { version = "^0.1", git = "https://github.com/blockworks-foundation/solana-flux-aggregator.git", features=["program", "no-entrypoint"] }
fixed = { version = "^1.7.0", features=["serde"] }
fixed-macro = "^1.1.1"
spl-token = { version = "^3.0.0", features=["no-entrypoint"] }
serum_dex = { version = "0.4.0", rev="3104f424ee38a415418a1cdef67970771f832857", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false, features = ["no-entrypoint", "program"] }
flux-aggregator = { version = "^0.1", rev="ca6706d05218acc84d164ed5149fac7612d3aa2b", git = "https://github.com/blockworks-foundation/solana-flux-aggregator.git", features=["program", "no-entrypoint"] }
pyth-client = {version = ">=0.5.0", features = ["no-entrypoint"]}
[dev-dependencies]
solana-sdk = "^1.6.4"
solana-program-test = "^1.6.4"

View File

@ -67,6 +67,8 @@ pub enum MangoErrorCode {
FeeDiscountFunctionality,
#[error("MangoErrorCode::Deprecated")]
Deprecated,
#[error("MangoErrorCode::OracleOffline")]
OracleOffline,
#[error("MangoErrorCode::Default Check the source code for more info")]
Default = u32::MAX_VALUE,
@ -92,6 +94,14 @@ impl From<serum_dex::error::DexError> for MangoError {
}
}
impl From<pyth_client::PythError> for MangoError {
fn from(pyth_e: pyth_client::PythError) -> Self {
let prog_e: ProgramError = pyth_e.into();
prog_e.into()
}
}
#[inline]
pub fn check_assert(
cond: bool,

View File

@ -36,7 +36,7 @@ pub enum MangoInstruction {
/// 7+2*NUM_TOKENS..7+2*NUM_TOKENS+NUM_MARKETS `[]`
/// spot_market_accs - MarketState account from serum dex for each of the spot markets
/// 7+2*NUM_TOKENS+NUM_MARKETS..7+2*NUM_TOKENS+2*NUM_MARKETS `[]`
/// oracle_accs - Solana Flux Aggregator accounts corresponding to each trading pair
/// oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
InitMangoGroup {
signer_nonce: u64,
maint_coll_ratio: U64F64,
@ -83,7 +83,7 @@ pub enum MangoInstruction {
/// 7. `[]` clock_acc - Clock sysvar account
/// 8..8+NUM_MARKETS `[]` open_orders_accs - open orders for each of the spot market
/// 8+NUM_MARKETS..8+2*NUM_MARKETS `[]`
/// oracle_accs - flux aggregator feed accounts
/// oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
Withdraw {
quantity: u64
},
@ -98,7 +98,7 @@ pub enum MangoInstruction {
/// 3. `[]` clock_acc - Clock sysvar account
/// 4..4+NUM_MARKETS `[]` open_orders_accs - open orders for each of the spot market
/// 4+NUM_MARKETS..4+2*NUM_MARKETS `[]`
/// oracle_accs - flux aggregator feed accounts
/// oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
Borrow {
token_index: usize,
quantity: u64
@ -128,7 +128,7 @@ pub enum MangoInstruction {
/// 4. `[]` clock_acc - Clock sysvar account
/// 5..5+NUM_MARKETS `[]` open_orders_accs - open orders for each of the spot market
/// 5+NUM_MARKETS..5+2*NUM_MARKETS `[]`
/// oracle_accs - flux aggregator feed accounts
/// oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
/// 5+2*NUM_MARKETS..5+2*NUM_MARKETS+NUM_TOKENS `[writable]`
/// vault_accs - MangoGroup vaults
/// 5+2*NUM_MARKETS+NUM_TOKENS..5+2*NUM_MARKETS+2*NUM_TOKENS `[writable]`
@ -197,7 +197,7 @@ pub enum MangoInstruction {
/// 16. `[writable]` srm_vault_acc - MangoGroup's srm_vault used for fee reduction
/// 17..17+NUM_MARKETS `[writable]` open_orders_accs - open orders for each of the spot market
/// 17+NUM_MARKETS..17+2*NUM_MARKETS `[]`
/// oracle_accs - flux aggregator feed accounts
/// oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
PlaceOrder {
order: serum_dex::instruction::NewOrderInstructionV3
},
@ -297,7 +297,7 @@ pub enum MangoInstruction {
/// 18. `[]` dex_signer_acc - signer for serum dex MarketState
/// 19..19+NUM_MARKETS `[writable]` open_orders_accs - open orders for each of the spot market
/// 19+NUM_MARKETS..19+2*NUM_MARKETS `[]`
/// oracle_accs - flux aggregator feed accounts
/// oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
PlaceAndSettle {
order: serum_dex::instruction::NewOrderInstructionV3
},
@ -324,7 +324,7 @@ pub enum MangoInstruction {
/// 15. `[]` clock_acc - Clock sysvar account
/// 16..16+NUM_MARKETS `[writable]` open_orders_accs - open orders for each of the spot market
/// 16+NUM_MARKETS..16+2*NUM_MARKETS `[]`
/// oracle_accs - flux aggregator feed accounts
/// oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
ForceCancelOrders {
/// Max orders to cancel -- could be useful to lower this if running into compute limits
/// Recommended: 5
@ -347,7 +347,7 @@ pub enum MangoInstruction {
/// 9. `[]` clock_acc - Clock sysvar account
/// 10..10+NUM_MARKETS `[]` open_orders_accs - open orders for each of the spot market
/// 10+NUM_MARKETS..10+2*NUM_MARKETS `[]`
/// oracle_accs - flux aggregator feed accounts
/// oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
PartialLiquidate {
/// Quantity of the token being deposited to repay borrows
max_deposit: u64
@ -356,7 +356,13 @@ pub enum MangoInstruction {
AddMarginAccountInfo {
info: [u8; INFO_LEN]
}
},
/// Allows to switch the oracles to a new set
/// 0. `[writable]` mango_group_acc - the data account to store mango group state vars
/// 1. `[signer]` admin_acc - admin key that created the group
/// 2..2+NUM_MARKETS `[]` oracle_accs - Pyth Price / Solana Flux Aggregator accounts corresponding to each trading pair
SwitchOracles
}
@ -508,6 +514,9 @@ impl MangoInstruction {
info: *info
}
}
18 => {
MangoInstruction::SwitchOracles
}
_ => { return None; }
})
}
@ -1233,4 +1242,26 @@ pub fn add_margin_account_info(
accounts,
data
})
}
pub fn switch_oracles(
program_id: &Pubkey,
mango_group_pk: &Pubkey,
admin_pk: &Pubkey,
oracle_pks: &[Pubkey],
) -> Result<Instruction, ProgramError> {
let mut accounts = vec![
AccountMeta::new(*mango_group_pk, false),
AccountMeta::new_readonly(*admin_pk, true),
];
accounts.extend(oracle_pks.iter().map(
|pk| AccountMeta::new_readonly(*pk, false))
);
let instr = MangoInstruction::SwitchOracles {};
let data = instr.pack();
Ok(Instruction {
program_id: *program_id,
accounts,
data
})
}

View File

@ -1,11 +1,13 @@
use std::cmp;
use std::cmp::min;
use std::mem::size_of;
use std::ops::Neg;
use arrayref::{array_ref, array_refs};
use fixed::types::U64F64;
use fixed_macro::types::U64F64;
use flux_aggregator::borsh_state::InitBorshState;
use pyth_client::PriceStatus;
use serum_dex::matching::Side;
use serum_dex::state::ToAlignedBytes;
use solana_program::account_info::AccountInfo;
@ -1375,6 +1377,52 @@ impl Processor {
margin_account.info = info;
Ok(())
}
#[inline(never)]
fn switch_oracles(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> MangoResult<()> {
const NUM_FIXED: usize = 2;
let accounts = array_ref![accounts, 0, NUM_FIXED + NUM_MARKETS];
let (fixed_accs, oracle_accs) = array_refs![accounts, NUM_FIXED, NUM_MARKETS];
let [
mango_group_acc,
admin_acc,
] = fixed_accs;
let mut mango_group = MangoGroup::load_mut_checked(
mango_group_acc, program_id)?;
check_eq!(admin_acc.key, &mango_group.admin, MangoErrorCode::InvalidGroupOwner)?;
check!(admin_acc.is_signer, MangoErrorCode::SignerNecessary)?;
for i in 0..NUM_MARKETS {
mango_group.oracles[i] = *oracle_accs[i].key;
// determine oracle type
let borrowed = oracle_accs[i].data.borrow();
let magic = u32::from_le_bytes(*array_ref![borrowed, 0, 4]);
// read oracle decimals
let decimals = if magic == pyth_client::MAGIC {
// detected pyth oracle
let price_account = pyth_client::load_price(&borrowed)?;
// usually expo is -8, verify anyways that it's within bounds
check!(price_account.expo <= 0, MangoErrorCode::Default);
check!(price_account.expo >= -255, MangoErrorCode::Default);
price_account.expo.neg() as u8
} else {
// fall back to legacy flux aggregator
let oracle = flux_aggregator::state::Aggregator::load_initialized(&oracle_accs[i])?;
oracle.config.decimals
};
mango_group.oracle_decimals[i] = decimals;
}
Ok(())
}
pub fn process(
program_id: &Pubkey,
accounts: &[AccountInfo],
@ -1492,6 +1540,10 @@ impl Processor {
msg!("Mango: AddMarginAccountInfo");
Self::add_margin_account_info(program_id, accounts, info)?;
}
MangoInstruction::SwitchOracles => {
msg!("Mango: SwitchOracles");
Self::switch_oracles(program_id, accounts)?;
}
}
Ok(())
}
@ -1671,16 +1723,27 @@ pub fn get_prices(
for i in 0..NUM_MARKETS {
check_eq_default!(&mango_group.oracles[i], oracle_accs[i].key)?;
// TODO store this info in MangoGroup, first make sure it cannot be changed by solink
let base_adj = U64F64::from_num(10u64.pow(mango_group.mint_decimals[i] as u32));
let quote_adj = U64F64::from_num(
10u64.pow(quote_decimals.checked_sub(mango_group.oracle_decimals[i]).unwrap() as u32)
);
let answer = flux_aggregator::read_median(&oracle_accs[i])?; // this is in USD cents
// determine oracle type
let borrowed = oracle_accs[i].data.borrow();
let magic = u32::from_le_bytes(*array_ref![borrowed, 0, 4]);
let value = U64F64::from_num(answer.median);
// read oracle value
let value = if magic == pyth_client::MAGIC {
// detected pyth oracle
let price_account = pyth_client::load_price(&borrowed)?;
check_eq!(price_account.get_current_price_status(), PriceStatus::Trading, MangoErrorCode::OracleOffline);
U64F64::from_num(price_account.agg.price)
} else {
// fall back to legacy flux aggregator
let answer = flux_aggregator::read_median(&oracle_accs[i])?; // this is in USD cents
U64F64::from_num(answer.median)
};
let base_adj = U64F64::from_num(10u64.pow(mango_group.mint_decimals[i] as u32));
prices[i] = quote_adj
.checked_div(base_adj).unwrap()
.checked_mul(value).unwrap();