Compare commits
77 Commits
Author | SHA1 | Date |
---|---|---|
Maximilian Schneider | 0d67dc1361 | |
Maximilian Schneider | 30e2bb4fec | |
Maximilian Schneider | eeddeb011c | |
Maximilian Schneider | 9a5a50efb7 | |
Maximilian Schneider | 1cd9a9dd3f | |
Maximilian Schneider | 663d909d38 | |
Maximilian Schneider | a96083d1f9 | |
Maximilian Schneider | 234d8e2682 | |
dd | 141b10577b | |
dafyddd | fcfc26dba9 | |
armaniferrante | 63d9eab005 | |
dafyddd | 5556f483f9 | |
dd | 8d49dc63ff | |
dd | 7a9af58186 | |
dafyddd | 02ca658620 | |
dd | f4b60d7946 | |
dd | df14fb62a2 | |
dd | 5b3078cda1 | |
dd | 328756de3e | |
dd | 4d79e88ff6 | |
dd | 1c09e227d9 | |
dd | 9f62096058 | |
dd | a5c18a6ccc | |
dd | e4fe8e7084 | |
dd | c685014017 | |
dd | 4acc2dc206 | |
dd | ec5c1862c3 | |
dd | d4fe5c879e | |
dd | 43e5e83f13 | |
dd | 690ad95c99 | |
dd | 2dc272b1cf | |
Nicholas Clarke | ae1b688cf8 | |
Nicholas Clarke | 874076dcf5 | |
Nicholas Clarke | 5bfffa1f8d | |
Riordan Panayides | e86e43268b | |
Riordan Panayides | 7d895dcf66 | |
Riordan Panayides | ef0c45dc36 | |
Riordan Panayides | 61790d6850 | |
Riordan Panayides | 81afd8bca7 | |
Riordan Panayides | 85f64e9af9 | |
Riordan Panayides | 8a76b9809d | |
Riordan Panayides | 2468a83232 | |
Riordan Panayides | 7de3e4d65c | |
Ralfs | 0b7c6481fc | |
Ralfs | 0e0741ee4e | |
Ralfs | 64c16ba0c6 | |
Nicholas Clarke | 8af143c35a | |
dafyddd | 708c215b50 | |
dd | e2483baba2 | |
dd | 0aebcc3756 | |
dd | d3d7c254b3 | |
dd | a96eff6ed0 | |
dd | 532aa92c9b | |
Riordan Panayides | 0dbe98fefe | |
Riordan Panayides | cbc527f587 | |
kevlubkcm | 5d28cef78c | |
Riordan Panayides | 1ff2ec9e5c | |
Maximilian Schneider | fc67807f3c | |
kevlubkcm | 971a8df0b8 | |
kevlubkcm | b4e360f037 | |
dd | 734e9500d7 | |
Maximilian Schneider | b7b85ab75c | |
kevlubkcm | ef9eb55f18 | |
dd | 2fc5289c21 | |
dd | 9a10391005 | |
dd | d5f8e96efd | |
dd | 2b7e619f48 | |
dd | 55883ef873 | |
dd | 565b43c3be | |
dd | 48095b30bd | |
dd | 884fe87660 | |
dd | 476b84a5eb | |
dd | 16f9cad69f | |
dd | 369e96c3d5 | |
dafyddd | 896a3c66ad | |
dd | a28d3923d9 | |
dd | 1fb1cc5eba |
|
@ -0,0 +1,21 @@
|
|||
dist: bionic
|
||||
language: rust
|
||||
rust:
|
||||
- 1.51.0
|
||||
cache: cargo
|
||||
os: linux
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libudev-dev
|
||||
- libssl-dev
|
||||
before_script:
|
||||
- sh -c "$(curl -sSfL https://release.solana.com/v1.6.4/install)"
|
||||
- export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH"
|
||||
- cd program
|
||||
jobs:
|
||||
include:
|
||||
- name: Run Tests
|
||||
script:
|
||||
- cargo test
|
||||
- cargo test-bpf
|
|
@ -0,0 +1,11 @@
|
|||
anchor_version = "0.13.2"
|
||||
|
||||
[workspace]
|
||||
members = ["program"]
|
||||
|
||||
[provider]
|
||||
cluster = "mainnet"
|
||||
wallet = "~/.config/solana/id.json"
|
||||
|
||||
[programs.mainnet]
|
||||
mango = "5fNfvyp5czQVX77yoACa3JJVEhdRaWjPuazuWgjhTqEH"
|
21
README.md
21
README.md
|
@ -22,14 +22,20 @@ and/or here: https://docs.solana.com/cli to get set up with node and rust as wel
|
|||
sudo apt-get install -y pkg-config build-essential python3-pip jq
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source $HOME/.cargo/env
|
||||
rustup override set 1.58
|
||||
rustup component add rustfmt
|
||||
rustup default nightly
|
||||
rustup component add rust-src
|
||||
```
|
||||
|
||||
Note that currently rust version at least 1.50 is needed. Check rust version and upgrade if necessary with
|
||||
```
|
||||
rustc --version
|
||||
rustup update
|
||||
```
|
||||
|
||||
### get mango
|
||||
```
|
||||
VERSION=v1.6.4
|
||||
VERSION=v1.9.1
|
||||
sh -c "$(curl -sSfL https://release.solana.com/$VERSION/install)"
|
||||
sudo apt-get install -y libssl-dev libudev-dev
|
||||
cargo install spl-token-cli
|
||||
|
@ -63,4 +69,13 @@ cd cli
|
|||
```
|
||||
|
||||
### deploy mainnet
|
||||
Rework devnet_deploy.sh and use cli/mainnet.env to deploy to mainnet
|
||||
Rework devnet_deploy.sh and use cli/mainnet.env to deploy to mainnet
|
||||
|
||||
|
||||
### run tests
|
||||
Regression and integration tests are in progress. To run them
|
||||
```
|
||||
cd program
|
||||
cargo test # run non-solana VM tests (none at the moment but would include simple unit tests in the future)
|
||||
cargo test-bpf # run tests that use the solana VM (ie the smart contract tests)
|
||||
```
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
version = "2.0.0"
|
||||
authors = ["blockworks"]
|
||||
edition = "2018"
|
||||
|
||||
|
@ -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/project-serum/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" }
|
||||
|
||||
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"]}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
CLUSTER=devnet
|
||||
CLUSTER_URL=https://devnet.solana.com
|
||||
CLUSTER_URL=https://api.devnet.solana.com
|
||||
if [ $# -eq 0 ]
|
||||
then
|
||||
KEYPAIR=~/.config/solana/id.json
|
||||
|
@ -14,6 +14,8 @@ ETH=$(cat $IDS_PATH | jq '.devnet.symbols|.["ETH"]' -r)
|
|||
USDC=$(cat $IDS_PATH | jq '.devnet.symbols|.["USDC"]' -r)
|
||||
WUSDT=$(cat $IDS_PATH | jq '.devnet.symbols|.["WUSDT"]' -r)
|
||||
USDT=$(cat $IDS_PATH | jq '.devnet.symbols|.["USDT"]' -r)
|
||||
SOL=$(cat $IDS_PATH | jq '.devnet.symbols|.["SOL"]' -r)
|
||||
SRM=$(cat $IDS_PATH | jq '.devnet.symbols|.["SRM"]' -r)
|
||||
|
||||
MANGO_PROGRAM_ID=$(cat $IDS_PATH | jq '."devnet".mango_program_id' -r)
|
||||
DEX_PROGRAM_ID=$(cat $IDS_PATH | jq '."devnet".dex_program_id' -r)
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
# devnet
|
||||
if [ $# -eq 0 ]
|
||||
then
|
||||
KEYPAIR=~/.config/solana/id.json
|
||||
KEYPAIR=~/.config/solana/devnet.json
|
||||
else
|
||||
KEYPAIR=$1
|
||||
fi
|
||||
|
||||
# deploy mango program and new mango group
|
||||
source ~/mango/cli/devnet.env $KEYPAIR
|
||||
solana config set --url $DEVNET_URL
|
||||
solana config set --url $CLUSTER_URL
|
||||
|
||||
cd ~/mango
|
||||
pushd program
|
||||
|
@ -20,15 +20,17 @@ cargo build-bpf --features devnet --bpf-out-dir target/devnet
|
|||
# this will give a separate program id for devnet
|
||||
#solana-keygen new --outfile target/devnet/mango-dev.json
|
||||
#MANGO_PROGRAM_ID="$(solana program deploy target/devnet/mango.so --program-id target/devnet/mango-dev.json | jq .programId -r)"
|
||||
MANGO_PROGRAM_ID="$(solana program deploy target/devnet/mango.so --program-id $MANGO_PROGRAM_ID --output json-compact | jq .programId -r)"
|
||||
#MANGO_PROGRAM_ID="$(solana program deploy target/devnet/mango.so --program-id $MANGO_PROGRAM_ID --keypair $KEYPAIR --output json-compact | jq .programId -r)"
|
||||
solana program deploy target/devnet/mango.so --program-id $MANGO_PROGRAM_ID --keypair $KEYPAIR --output json-compact
|
||||
popd
|
||||
cd cli
|
||||
|
||||
CLUSTER=devnet
|
||||
TOKENS="BTC ETH USDT"
|
||||
MANGO_GROUP_NAME=BTC_ETH_USDT
|
||||
BORROW_LIMITS="1.0 20.0 50000.0"
|
||||
TOKENS="BTC ETH SOL SRM USDC"
|
||||
BORROW_LIMITS="0.0 0.0 0.0 0.0 0.0"
|
||||
|
||||
# This will deploy the BTC_ETH_USDT mango group and automatically update the ids.json in mango client
|
||||
# Make sure IDS_PATH is set correctly in mango/cli/devnet.env, or set it again before running this
|
||||
cargo run -- $CLUSTER init-mango-group \
|
||||
--payer $KEYPAIR \
|
||||
--ids-path $IDS_PATH \
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
1. First build and deploy serum dex to devnet (if you just want to use already deployed then skip this step)
|
||||
2. Go to blockworks-foundation/solana-flux-aggregator, Add the token pairs you want into config/setup.dev.json
|
||||
3. Run `yarn solink setup config/setup.dev.json`
|
||||
4. Make sure the feeds in solana flux aggregator can feed new tokens
|
||||
* Make sure the supported exchanges in feeds.ts have the tokens you want, if not write the feed
|
||||
5. Add the oracle pubkeys found in deploy.dev.json into mango-client-ts/src/ids.json devnet.oracles
|
||||
6. Add the token mints for your new tokens to ids.json devnet.symbols
|
||||
7. Amend devnet.env and add your new new symbols
|
||||
8. List the new markets. For example:
|
||||
|
||||
```
|
||||
source ~/mango/cli/devnet.env
|
||||
cd ~/blockworks-foundation/serum-dex/
|
||||
cargo run -- $CLUSTER list-market $KEYPAIR $DEX_PROGRAM_ID --coin-mint $BTC --pc-mint $USDT
|
||||
cargo run -- $CLUSTER list-market $KEYPAIR $DEX_PROGRAM_ID --coin-mint $ETH --pc-mint $USDT
|
||||
```
|
||||
|
||||
9. Add the MarketState pubkeys to ids.json devnet.spot_markets
|
||||
10. go to blockworks-foundation/liquidator/crank.sh and add support for your new markets
|
||||
11. run crank.sh to run the cranks, for example
|
||||
```
|
||||
source crank.sh $KEYPAIR btc usdt
|
||||
source crank.sh $KEYPAIR eth usdt
|
||||
```
|
||||
12. Deploy new mango group for example:
|
||||
```
|
||||
CLUSTER=devnet
|
||||
KEYPAIR=~/.config/solana/id.json
|
||||
IDS_PATH=~/mango-client-ts/src/ids.json
|
||||
TOKENS="BTC ETH USDT"
|
||||
MANGO_GROUP_NAME=BTC_ETH_USDT
|
||||
BORROW_LIMITS="1.0 20.0 50000.0"
|
||||
|
||||
cargo run -- $CLUSTER init-mango-group \
|
||||
--payer $KEYPAIR \
|
||||
--ids-path $IDS_PATH \
|
||||
--tokens $TOKENS \
|
||||
--mango-program-id $MANGO_PROGRAM_ID \
|
||||
--borrow-limits $BORROW_LIMITS
|
||||
```
|
||||
|
||||
13. For mainnet, it's recommended that you first do this on devnet and then rework it for mainnet
|
|
@ -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 {
|
||||
|
@ -270,21 +282,32 @@ pub fn start(opts: Opts) -> Result<()> {
|
|||
|token| (token.clone(), get_symbol_pk(symbols, token.as_str()).to_string())
|
||||
).collect();
|
||||
|
||||
// Create vaults owned by mango program id
|
||||
// Create vaults owned by mango program id and check if SRM in mango group
|
||||
let srm_mint_pk = get_symbol_pk(symbols, "SRM");
|
||||
let mut srm_in_mango_group_index: Option<usize> = None;
|
||||
let mut vault_pks = vec![];
|
||||
for i in 0..mint_pks.len() {
|
||||
println!("Creating vault for: {}", &tokens[i]);
|
||||
let vault_pk = create_token_account(
|
||||
&client, &mint_pks[i], &signer_key, &payer
|
||||
)?.pubkey();
|
||||
if &mint_pks[i] == &srm_mint_pk {
|
||||
srm_in_mango_group_index = Some(i);
|
||||
}
|
||||
vault_pks.push(vault_pk);
|
||||
}
|
||||
|
||||
let srm_mint_pk = get_symbol_pk(symbols, "SRM");
|
||||
println!("Creating vault for: SRM");
|
||||
let srm_vault_pk = create_token_account(
|
||||
&client, &srm_mint_pk, &signer_key, &payer
|
||||
)?.pubkey();
|
||||
// If SRM is in mango group don't create a new vault for it, instead use existing
|
||||
let srm_vault_pk: Pubkey;
|
||||
if srm_in_mango_group_index.is_some() {
|
||||
println!("Reusing existing vault for: SRM");
|
||||
srm_vault_pk = vault_pks[srm_in_mango_group_index.unwrap()];
|
||||
} else {
|
||||
println!("Creating vault for: SRM");
|
||||
srm_vault_pk = create_token_account(
|
||||
&client, &srm_mint_pk, &signer_key, &payer
|
||||
)?.pubkey();
|
||||
};
|
||||
|
||||
println!("set up spot markets");
|
||||
// Find corresponding spot markets
|
||||
|
@ -297,9 +320,10 @@ pub fn start(opts: Opts) -> Result<()> {
|
|||
for i in 0..(tokens.len() - 1) {
|
||||
let base_symbol = tokens[i].as_str();
|
||||
let market_symbol = format!("{}/{}", base_symbol, quote_symbol);
|
||||
println!("{}", market_symbol);
|
||||
spot_market_pks.push(get_symbol_pk(spot_markets, market_symbol.as_str()));
|
||||
oracle_pks.push(get_symbol_pk(oracles, market_symbol.as_str()));
|
||||
spot_market_symbols.insert(market_symbol.clone(), spot_markets[market_symbol.as_str()].as_str().unwrap().to_string());
|
||||
spot_market_symbols.insert(market_symbol.clone(), spot_markets[market_symbol.as_str()].as_str().expect("spot market not found").to_string());
|
||||
}
|
||||
|
||||
println!("borrow limits");
|
||||
|
@ -669,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(())
|
||||
}
|
||||
|
@ -678,7 +744,7 @@ fn get_pk(json: &Value, name: &str) -> Pubkey {
|
|||
}
|
||||
|
||||
fn get_symbol_pk(symbols: &Value, symbol: &str) -> Pubkey {
|
||||
Pubkey::from_str(symbols[symbol].as_str().unwrap()).unwrap()
|
||||
Pubkey::from_str(symbols[symbol].as_str().expect("get_symbol_pk symbol not found")).unwrap()
|
||||
}
|
||||
|
||||
fn get_vec_pks(value: &Value) -> Vec<Pubkey> {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,20 +1,20 @@
|
|||
[package]
|
||||
name = "common"
|
||||
version = "0.1.0"
|
||||
version = "2.0.0"
|
||||
authors = ["blockworks"]
|
||||
edition = "2018"
|
||||
|
||||
|
||||
[dependencies]
|
||||
solana-sdk = { version = "1.5.11", features=["default"] }
|
||||
spl-token = { version = "3.0.0", features=["no-entrypoint"] }
|
||||
anyhow = "1.0.35"
|
||||
solana-client = "1.5.11"
|
||||
rand = "0.7.3"
|
||||
serde_json = "1.0.60"
|
||||
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.7.0"
|
||||
serde_json = "^1.0.66"
|
||||
bs58 = "0.4.0"
|
||||
bincode = "1.3.1"
|
||||
bytemuck = "1.4.1"
|
||||
tiny-bip39 = "0.7.3"
|
||||
tiny-hderive = "0.2.1"
|
||||
ed25519-dalek = "1.0.0-pre.4"
|
||||
bincode = "^1.3.1"
|
||||
bytemuck = "^1.7.2"
|
||||
tiny-bip39 = "0.8.0"
|
||||
tiny-hderive = "0.3.0"
|
||||
ed25519-dalek = "^1.0.0"
|
||||
|
|
|
@ -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",
|
||||
|
@ -265,6 +265,7 @@ pub fn send_txn(client: &RpcClient, txn: &Transaction, _simulate: bool) -> Resul
|
|||
// }
|
||||
//
|
||||
// )?)
|
||||
|
||||
let txid = client.send_transaction_with_config(txn, RpcSendTransactionConfig {
|
||||
skip_preflight: true,
|
||||
..RpcSendTransactionConfig::default()
|
||||
|
@ -280,7 +281,8 @@ pub fn send_txn(client: &RpcClient, txn: &Transaction, _simulate: bool) -> Resul
|
|||
println!("Confirming txid: {}", txid.to_string());
|
||||
client.confirm_transaction(&txid)?;
|
||||
Ok(txid)
|
||||
//
|
||||
|
||||
|
||||
// Ok(client.send_and_confirm_transaction_with_spinner_and_config(
|
||||
// txn,
|
||||
// CommitmentConfig::confirmed(),
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,16 +1,16 @@
|
|||
[package]
|
||||
name = "mango"
|
||||
version = "0.2.4"
|
||||
version = "2.2.1"
|
||||
authors = ["blockworks"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
devnet = []
|
||||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "^1.6.4"
|
||||
spl-token = { version = "^3.0.0", features=["no-entrypoint"] }
|
||||
solana-program = "=1.9.13"
|
||||
byteorder = "^1.3.4"
|
||||
arrayref = "^0.3.6"
|
||||
num_enum = "^0.5.1"
|
||||
|
@ -21,13 +21,24 @@ static_assertions = "^1.1.0"
|
|||
thiserror = "^1.0.24"
|
||||
serde = "^1.0.118"
|
||||
bincode = "^1.3.1"
|
||||
serum_dex = { version = "^0.2", git = "https://github.com/blockworks-foundation/serum-dex.git", 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"]}
|
||||
|
||||
switchboard-v2 = "=0.1.11"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-sdk = "=1.9.13"
|
||||
solana-program-test = "=1.9.13"
|
||||
blake3 = "=1.2.0"
|
||||
h2="=0.3.18"
|
||||
thread_local="=1.0.1"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -6,7 +6,7 @@ use crate::processor::Processor;
|
|||
|
||||
|
||||
entrypoint!(process_instruction);
|
||||
fn process_instruction(
|
||||
pub fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
instruction_data: &[u8],
|
||||
|
|
|
@ -63,6 +63,12 @@ pub enum MangoErrorCode {
|
|||
InvalidMangoVault,
|
||||
#[error("MangoErrorCode::BeingLiquidated The margin account has restricted functionality while being liquidated")]
|
||||
BeingLiquidated,
|
||||
#[error("MangoErrorCode::FeeDiscountFunctionality SRM is already part of MangoGroup. Deposit and withdraw SRM functionality disabled.")]
|
||||
FeeDiscountFunctionality,
|
||||
#[error("MangoErrorCode::Deprecated")]
|
||||
Deprecated,
|
||||
#[error("MangoErrorCode::OracleOffline")]
|
||||
OracleOffline,
|
||||
|
||||
#[error("MangoErrorCode::Default Check the source code for more info")]
|
||||
Default = u32::MAX_VALUE,
|
||||
|
@ -88,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,
|
||||
|
|
|
@ -10,7 +10,7 @@ use solana_program::instruction::{AccountMeta, Instruction};
|
|||
use solana_program::program_error::ProgramError;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
|
||||
use crate::state::NUM_TOKENS;
|
||||
use crate::state::{NUM_TOKENS, INFO_LEN};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
|
@ -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,12 +347,22 @@ 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
|
||||
},
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
@ -498,6 +508,15 @@ impl MangoInstruction {
|
|||
max_deposit: u64::from_le_bytes(*max_deposit)
|
||||
}
|
||||
}
|
||||
17 => {
|
||||
let info = array_ref![data, 0, INFO_LEN];
|
||||
MangoInstruction::AddMarginAccountInfo {
|
||||
info: *info
|
||||
}
|
||||
}
|
||||
18 => {
|
||||
MangoInstruction::SwitchOracles
|
||||
}
|
||||
_ => { return None; }
|
||||
})
|
||||
}
|
||||
|
@ -1202,3 +1221,47 @@ pub fn partial_liquidate(
|
|||
data
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_margin_account_info(
|
||||
program_id: &Pubkey,
|
||||
mango_group_pk: &Pubkey,
|
||||
margin_account_pk: &Pubkey,
|
||||
owner_pk: &Pubkey,
|
||||
info: [u8; INFO_LEN]
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*mango_group_pk, false),
|
||||
AccountMeta::new(*margin_account_pk, false),
|
||||
AccountMeta::new_readonly(*owner_pk, true),
|
||||
];
|
||||
|
||||
let instr = MangoInstruction::AddMarginAccountInfo { info };
|
||||
let data = instr.pack();
|
||||
Ok(Instruction {
|
||||
program_id: *program_id,
|
||||
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
|
||||
})
|
||||
}
|
|
@ -1,11 +1,15 @@
|
|||
use std::cell::Ref;
|
||||
use std::cmp;
|
||||
use std::cmp::min;
|
||||
use std::convert::TryInto;
|
||||
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;
|
||||
|
@ -19,10 +23,11 @@ use solana_program::pubkey::Pubkey;
|
|||
use solana_program::rent::Rent;
|
||||
use solana_program::sysvar::Sysvar;
|
||||
use spl_token::state::{Account, Mint};
|
||||
use switchboard_v2::AggregatorAccountData;
|
||||
|
||||
use crate::error::{check_assert, MangoError, MangoErrorCode, MangoResult, SourceFileId};
|
||||
use crate::instruction::MangoInstruction;
|
||||
use crate::state::{AccountFlag, check_open_orders, DUST_THRESHOLD, load_asks_mut, load_bids_mut, load_market_state, load_open_orders, Loadable, MangoGroup, MangoIndex, MangoSrmAccount, MarginAccount, NUM_MARKETS, NUM_TOKENS, ONE_U64F64, PARTIAL_LIQ_INCENTIVE, ZERO_U64F64};
|
||||
use crate::state::{AccountFlag, check_open_orders, DUST_THRESHOLD, load_asks_mut, load_bids_mut, load_market_state, load_open_orders, Loadable, MangoGroup, MangoIndex, MangoSrmAccount, MarginAccount, NUM_MARKETS, NUM_TOKENS, ONE_U64F64, PARTIAL_LIQ_INCENTIVE, ZERO_U64F64, INFO_LEN};
|
||||
use crate::utils::{gen_signer_key, gen_signer_seeds};
|
||||
|
||||
macro_rules! check_default {
|
||||
|
@ -56,7 +61,7 @@ macro_rules! throw_err {
|
|||
}
|
||||
}
|
||||
|
||||
mod srm_token {
|
||||
pub mod srm_token {
|
||||
use solana_program::declare_id;
|
||||
|
||||
#[cfg(feature = "devnet")]
|
||||
|
@ -229,7 +234,6 @@ impl Processor {
|
|||
let clock = Clock::from_account_info(clock_acc)?;
|
||||
mango_group.update_indexes(&clock)?;
|
||||
|
||||
check_eq!(&margin_account.owner, owner_acc.key, MangoErrorCode::InvalidMarginAccountOwner)?;
|
||||
|
||||
let token_index = mango_group.get_token_index_with_vault(vault_acc.key).unwrap();
|
||||
check_eq_default!(&mango_group.vaults[token_index], vault_acc.key)?;
|
||||
|
@ -252,6 +256,7 @@ impl Processor {
|
|||
|
||||
let deposit: U64F64 = U64F64::from_num(quantity) / mango_group.indexes[token_index].deposit;
|
||||
checked_add_deposit(&mut mango_group, &mut margin_account, token_index, deposit)?;
|
||||
settle_borrow_full_unchecked(&mut mango_group, &mut margin_account, token_index)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -426,123 +431,11 @@ impl Processor {
|
|||
|
||||
#[inline(never)]
|
||||
fn liquidate(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
deposit_quantities: [u64; NUM_TOKENS]
|
||||
_program_id: &Pubkey,
|
||||
_accounts: &[AccountInfo],
|
||||
_deposit_quantities: [u64; NUM_TOKENS]
|
||||
) -> MangoResult<()> {
|
||||
const NUM_FIXED: usize = 5;
|
||||
let accounts = array_ref![accounts, 0, NUM_FIXED + 2 * NUM_MARKETS + 2 * NUM_TOKENS];
|
||||
let (
|
||||
fixed_accs,
|
||||
open_orders_accs,
|
||||
oracle_accs,
|
||||
vault_accs,
|
||||
liqor_token_account_accs,
|
||||
) = array_refs![accounts, NUM_FIXED, NUM_MARKETS, NUM_MARKETS, NUM_TOKENS, NUM_TOKENS];
|
||||
|
||||
let [
|
||||
mango_group_acc,
|
||||
liqor_acc,
|
||||
liqee_margin_account_acc,
|
||||
token_prog_acc,
|
||||
clock_acc
|
||||
] = fixed_accs;
|
||||
|
||||
check_default!(liqor_acc.is_signer)?;
|
||||
let mut mango_group = MangoGroup::load_mut_checked(
|
||||
mango_group_acc, program_id
|
||||
)?;
|
||||
let mut liqee_margin_account = MarginAccount::load_mut_checked(
|
||||
program_id, liqee_margin_account_acc, mango_group_acc.key
|
||||
)?;
|
||||
let clock = Clock::from_account_info(clock_acc)?;
|
||||
mango_group.update_indexes(&clock)?;
|
||||
|
||||
for i in 0..NUM_MARKETS {
|
||||
check_eq_default!(open_orders_accs[i].key, &liqee_margin_account.open_orders[i])?;
|
||||
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
|
||||
}
|
||||
|
||||
let prices = get_prices(&mango_group, oracle_accs)?;
|
||||
let coll_ratio = liqee_margin_account.get_collateral_ratio(
|
||||
&mango_group, &prices, open_orders_accs
|
||||
)?;
|
||||
|
||||
// No liquidations if account above maint collateral ratio
|
||||
check!(coll_ratio < mango_group.maint_coll_ratio, MangoErrorCode::NotLiquidatable)?;
|
||||
|
||||
// Settle borrows to see if it gets us above maint
|
||||
for i in 0..NUM_TOKENS {
|
||||
let native_borrow = liqee_margin_account.get_native_borrow(&mango_group.indexes[i], i);
|
||||
settle_borrow_unchecked(&mut mango_group, &mut liqee_margin_account, i, native_borrow)?;
|
||||
}
|
||||
let coll_ratio = liqee_margin_account.get_collateral_ratio(
|
||||
&mango_group, &prices, open_orders_accs
|
||||
)?;
|
||||
if coll_ratio >= mango_group.maint_coll_ratio { // if account not liquidatable after settle borrow, then return
|
||||
return Ok(())
|
||||
}
|
||||
|
||||
// TODO liquidator may forcefully SettleFunds and SettleBorrow on account with less than maint
|
||||
|
||||
if coll_ratio < ONE_U64F64 {
|
||||
let liabs = liqee_margin_account.get_total_liabs(&mango_group)?;
|
||||
let liabs_val = liqee_margin_account.get_liabs_val(&mango_group, &prices)?;
|
||||
let assets_val = liqee_margin_account.get_assets_val(&mango_group, &prices, open_orders_accs)?;
|
||||
|
||||
// reduction_val = amount of quote currency value to reduce liabilities by to get coll_ratio = 1.01
|
||||
let reduction_val = liabs_val
|
||||
.checked_sub(assets_val / LIQ_MIN_COLL_RATIO).unwrap();
|
||||
|
||||
for i in 0..NUM_TOKENS {
|
||||
let proportion = U64F64::from_num(liabs[i])
|
||||
.checked_div(liabs_val).unwrap();
|
||||
|
||||
let token_reduce = proportion.checked_mul(reduction_val).unwrap();
|
||||
socialize_loss(&mut mango_group, &mut liqee_margin_account, i, token_reduce)?;
|
||||
// TODO this will reduce deposits of liqee as well which could put actual value below; way to fix is to SettleBorrow first
|
||||
// TODO Can socialize loss cause more liquidations? Perhaps other accounts then go below threshold
|
||||
// TODO what happens if unable to socialize loss? If not enough deposits in currency
|
||||
}
|
||||
}
|
||||
|
||||
// Pull deposits from liqor's token wallets
|
||||
check_eq_default!(token_prog_acc.key, &spl_token::id())?;
|
||||
for i in 0..NUM_TOKENS {
|
||||
let quantity = deposit_quantities[i];
|
||||
if quantity == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let vault_acc: &AccountInfo = &vault_accs[i];
|
||||
check_eq_default!(&mango_group.vaults[i], vault_acc.key)?;
|
||||
let token_account_acc: &AccountInfo = &liqor_token_account_accs[i];
|
||||
let deposit_instruction = spl_token::instruction::transfer(
|
||||
&spl_token::id(),
|
||||
token_account_acc.key,
|
||||
vault_acc.key,
|
||||
&liqor_acc.key, &[], quantity
|
||||
)?;
|
||||
let deposit_accs = [
|
||||
token_account_acc.clone(),
|
||||
vault_acc.clone(),
|
||||
liqor_acc.clone(),
|
||||
token_prog_acc.clone()
|
||||
];
|
||||
|
||||
solana_program::program::invoke_signed(&deposit_instruction, &deposit_accs, &[])?;
|
||||
let deposit: U64F64 = U64F64::from_num(quantity) / mango_group.indexes[i].deposit;
|
||||
checked_add_deposit(&mut mango_group, &mut liqee_margin_account, i, deposit)?;
|
||||
}
|
||||
|
||||
// Check to make sure liqor's deposits brought account above init_coll_ratio
|
||||
let coll_ratio = liqee_margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
|
||||
check_default!(coll_ratio >= mango_group.init_coll_ratio)?;
|
||||
|
||||
// If all deposits are good, transfer ownership of margin account to liqor
|
||||
liqee_margin_account.owner = *liqor_acc.key;
|
||||
|
||||
Ok(())
|
||||
throw_err!(MangoErrorCode::Deprecated)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
|
@ -568,6 +461,9 @@ impl Processor {
|
|||
|
||||
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
|
||||
|
||||
// Check if SRM is part of the MangoGroup, if so throw err
|
||||
check!(mango_group.get_token_index(&srm_token::ID).is_none(), MangoErrorCode::FeeDiscountFunctionality)?;
|
||||
|
||||
// if MangoSrmAccount is empty, initialize it
|
||||
check_eq_default!(mango_srm_account_acc.data_len(), size_of::<MangoSrmAccount>())?;
|
||||
let mut mango_srm_account = MangoSrmAccount::load_mut(mango_srm_account_acc)?;
|
||||
|
@ -629,6 +525,10 @@ impl Processor {
|
|||
] = accounts;
|
||||
|
||||
let mut mango_group = MangoGroup::load_mut_checked(mango_group_acc, program_id)?;
|
||||
|
||||
// Check if SRM is part of the MangoGroup, if so throw err
|
||||
check!(mango_group.get_token_index(&srm_token::ID).is_none(), MangoErrorCode::FeeDiscountFunctionality)?;
|
||||
|
||||
let mut mango_srm_account = MangoSrmAccount::load_mut_checked(
|
||||
program_id, mango_srm_account_acc, mango_group_acc.key)?;
|
||||
|
||||
|
@ -894,7 +794,7 @@ impl Processor {
|
|||
|
||||
let (pre_base, pre_quote) = {
|
||||
let open_orders = load_open_orders(open_orders_acc)?;
|
||||
(open_orders.native_coin_free, open_orders.native_pc_free)
|
||||
(open_orders.native_coin_free, open_orders.native_pc_free + open_orders.referrer_rebates_accrued)
|
||||
};
|
||||
|
||||
if pre_base == 0 && pre_quote == 0 {
|
||||
|
@ -918,7 +818,7 @@ impl Processor {
|
|||
|
||||
let (post_base, post_quote) = {
|
||||
let open_orders = load_open_orders(open_orders_acc)?;
|
||||
(open_orders.native_coin_free, open_orders.native_pc_free)
|
||||
(open_orders.native_coin_free, open_orders.native_pc_free + open_orders.referrer_rebates_accrued)
|
||||
};
|
||||
|
||||
check_default!(post_base <= pre_base)?;
|
||||
|
@ -1269,7 +1169,7 @@ impl Processor {
|
|||
|
||||
let (pre_base, pre_quote) = {
|
||||
let open_orders = load_open_orders(open_orders_acc)?;
|
||||
(open_orders.native_coin_free, open_orders.native_pc_free)
|
||||
(open_orders.native_coin_free, open_orders.native_pc_free + open_orders.referrer_rebates_accrued)
|
||||
};
|
||||
|
||||
if pre_base == 0 && pre_quote == 0 {
|
||||
|
@ -1282,7 +1182,7 @@ impl Processor {
|
|||
|
||||
let (post_base, post_quote) = {
|
||||
let open_orders = load_open_orders(open_orders_acc)?;
|
||||
(open_orders.native_coin_free, open_orders.native_pc_free)
|
||||
(open_orders.native_coin_free, open_orders.native_pc_free + open_orders.referrer_rebates_accrued)
|
||||
};
|
||||
|
||||
check_default!(post_base <= pre_base)?;
|
||||
|
@ -1322,7 +1222,7 @@ impl Processor {
|
|||
out_vault_acc,
|
||||
signer_acc,
|
||||
token_prog_acc,
|
||||
clock_acc,
|
||||
_clock_acc,
|
||||
] = fixed_accs;
|
||||
check!(token_prog_acc.key == &spl_token::ID, MangoErrorCode::InvalidProgramId)?;
|
||||
check!(liqor_acc.is_signer, MangoErrorCode::SignerNecessary)?;
|
||||
|
@ -1350,11 +1250,26 @@ impl Processor {
|
|||
check_open_orders(&open_orders_accs[i], &mango_group.signer_key)?;
|
||||
}
|
||||
|
||||
let clock = Clock::from_account_info(clock_acc)?;
|
||||
mango_group.update_indexes(&clock)?;
|
||||
// TODO - add a check to make sure indexes were updated in last hour
|
||||
// if not updated, then update indexes and return without continuing
|
||||
// there is not enough compute to continue
|
||||
// code is written below but needs to be tested on devnet first
|
||||
|
||||
// let clock = Clock::from_account_info(clock_acc)?;
|
||||
// let now_ts = clock.unix_timestamp as u64;
|
||||
// for i in 0..NUM_TOKENS {
|
||||
// if now_ts > mango_group.indexes[i].last_update + 3600 {
|
||||
// msg!("Invalid indexes");
|
||||
// mango_group.update_indexes(&clock)?;
|
||||
// return Ok(());
|
||||
// }
|
||||
// }
|
||||
|
||||
let prices = get_prices(&mango_group, oracle_accs)?;
|
||||
let coll_ratio = liqee_margin_account.get_collateral_ratio(
|
||||
&mango_group, &prices, open_orders_accs)?;
|
||||
let start_assets = liqee_margin_account.get_assets(&mango_group, open_orders_accs)?;
|
||||
let start_liabs = liqee_margin_account.get_liabs(&mango_group)?;
|
||||
let coll_ratio = liqee_margin_account.coll_ratio_from_assets_liabs(
|
||||
&prices, &start_assets, &start_liabs)?;
|
||||
|
||||
// Only allow liquidations on accounts already being liquidated and below init or accounts below maint
|
||||
if liqee_margin_account.being_liquidated {
|
||||
|
@ -1378,10 +1293,12 @@ impl Processor {
|
|||
if liqee_margin_account.being_liquidated {
|
||||
if coll_ratio >= mango_group.init_coll_ratio {
|
||||
// TODO make sure liquidator knows why tx was success but he didn't receive any funds
|
||||
msg!("Account above init_coll_ratio after settling borrows");
|
||||
liqee_margin_account.being_liquidated = false;
|
||||
return Ok(());
|
||||
}
|
||||
} else if coll_ratio >= mango_group.maint_coll_ratio {
|
||||
msg!("Account above maint_coll_ratio after settling borrows");
|
||||
return Ok(());
|
||||
} else {
|
||||
liqee_margin_account.being_liquidated = true;
|
||||
|
@ -1399,29 +1316,117 @@ impl Processor {
|
|||
invoke_transfer(token_prog_acc, out_vault_acc, liqor_out_token_acc, signer_acc,
|
||||
&[&signers_seeds], out_quantity)?;
|
||||
|
||||
|
||||
// Check if account valid now
|
||||
let coll_ratio = liqee_margin_account.get_collateral_ratio(&mango_group, &prices, open_orders_accs)?;
|
||||
let end_assets = liqee_margin_account.get_assets(&mango_group, open_orders_accs)?;
|
||||
let end_liabs = liqee_margin_account.get_liabs(&mango_group)?;
|
||||
let coll_ratio = liqee_margin_account.coll_ratio_from_assets_liabs(
|
||||
&prices, &end_assets, &end_liabs)?;
|
||||
let mut total_deposits = [ZERO_U64F64; NUM_TOKENS];
|
||||
|
||||
let mut socialized_losses = false;
|
||||
if coll_ratio >= mango_group.init_coll_ratio {
|
||||
// set margin account to no longer being liquidated
|
||||
liqee_margin_account.being_liquidated = false;
|
||||
} else {
|
||||
// if all asset vals is dust (less than 1 cent?) socialize loss on lenders
|
||||
let assets_val = liqee_margin_account.get_assets_val(&mango_group, &prices, open_orders_accs)?;
|
||||
|
||||
if assets_val < DUST_THRESHOLD {
|
||||
for i in 0..NUM_TOKENS {
|
||||
let native_borrow = liqee_margin_account.borrows[i] * mango_group.indexes[i].borrow;
|
||||
let native_borrow: U64F64 = end_liabs[i];
|
||||
let total_deposits_native: U64F64 = mango_group.total_deposits[i] * mango_group.indexes[i].deposit;
|
||||
|
||||
total_deposits[i] = total_deposits_native;
|
||||
|
||||
if native_borrow > 0 {
|
||||
socialized_losses = true;
|
||||
socialize_loss(
|
||||
&mut mango_group,
|
||||
&mut liqee_margin_account,
|
||||
i,
|
||||
native_borrow)?;
|
||||
native_borrow,
|
||||
total_deposits_native
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note total_deposits is only logged with reasonable values if assets_val < DUST_THRESHOLD
|
||||
log_liquidation_details(&start_assets, &start_liabs, &end_assets, &end_liabs, &prices, socialized_losses, &total_deposits);
|
||||
// TODO do I need to check total deposits and total borrows?
|
||||
// TODO log deposit indexes before and after liquidation as a way to measure socialize of losses
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
fn add_margin_account_info(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
info: [u8; INFO_LEN]
|
||||
) -> MangoResult<()> {
|
||||
const NUM_FIXED: usize = 3;
|
||||
let accounts = array_ref![accounts, 0, NUM_FIXED];
|
||||
let [
|
||||
mango_group_acc,
|
||||
margin_account_acc,
|
||||
owner_acc,
|
||||
] = accounts;
|
||||
|
||||
let mut margin_account = MarginAccount::load_mut_checked(
|
||||
program_id, margin_account_acc, mango_group_acc.key)?;
|
||||
check_eq!(owner_acc.key, &margin_account.owner, MangoErrorCode::InvalidMarginAccountOwner)?;
|
||||
check!(owner_acc.is_signer, MangoErrorCode::SignerNecessary)?;
|
||||
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 magic4 = u32::from_le_bytes(*array_ref![borrowed, 0, 4]);
|
||||
let magic8 = *array_ref![borrowed, 0, 8];
|
||||
|
||||
// read oracle decimals
|
||||
let decimals = if magic4 == 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 if magic8 == SWITCHBOARD_MAGIC {
|
||||
// detected switchboard oracle, which uses an internal decimal type, so no corretion needed
|
||||
0
|
||||
} 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(())
|
||||
}
|
||||
|
||||
|
@ -1536,11 +1541,53 @@ impl Processor {
|
|||
msg!("Mango: PartialLiquidate");
|
||||
Self::partial_liquidate(program_id, accounts, max_deposit)?;
|
||||
}
|
||||
MangoInstruction::AddMarginAccountInfo {
|
||||
info
|
||||
} => {
|
||||
msg!("Mango: AddMarginAccountInfo");
|
||||
Self::add_margin_account_info(program_id, accounts, info)?;
|
||||
}
|
||||
MangoInstruction::SwitchOracles => {
|
||||
msg!("Mango: SwitchOracles");
|
||||
Self::switch_oracles(program_id, accounts)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn log_liquidation_details(
|
||||
start_assets: &[U64F64; NUM_TOKENS],
|
||||
start_liabs: &[U64F64; NUM_TOKENS],
|
||||
end_assets: &[U64F64; NUM_TOKENS],
|
||||
end_liabs: &[U64F64; NUM_TOKENS],
|
||||
prices: &[U64F64; NUM_TOKENS],
|
||||
socialized_losses: bool,
|
||||
total_deposits: &[U64F64; NUM_TOKENS]
|
||||
) {
|
||||
let mut prices_f64 = [0_f64; NUM_TOKENS];
|
||||
let mut start_assets_u64 = [0u64; NUM_TOKENS];
|
||||
let mut start_liabs_u64 = [0u64; NUM_TOKENS];
|
||||
let mut end_assets_u64 = [0u64; NUM_TOKENS];
|
||||
let mut end_liabs_u64 = [0u64; NUM_TOKENS];
|
||||
let mut total_deposits_u64 = [0u64; NUM_TOKENS];
|
||||
for i in 0..NUM_TOKENS {
|
||||
prices_f64[i] = prices[i].to_num::<f64>();
|
||||
start_assets_u64[i] = start_assets[i].to_num();
|
||||
start_liabs_u64[i] = start_liabs[i].to_num();
|
||||
end_assets_u64[i] = end_assets[i].to_num();
|
||||
end_liabs_u64[i] = end_liabs[i].to_num();
|
||||
total_deposits_u64[i] = total_deposits[i].to_num();
|
||||
}
|
||||
|
||||
msg!("liquidation details: {{ \
|
||||
\"start\": {{ \"assets\": {:?}, \"liabs\": {:?} }}, \
|
||||
\"end\": {{ \"assets\": {:?}, \"liabs\": {:?} }}, \
|
||||
\"prices\": {:?}, \
|
||||
\"socialized_losses\": {}, \
|
||||
\"total_deposits\": {:?} \
|
||||
}}", start_assets_u64, start_liabs_u64, end_assets_u64, end_liabs_u64, prices_f64, socialized_losses, total_deposits_u64);
|
||||
}
|
||||
|
||||
fn settle_borrow_unchecked(
|
||||
mango_group: &mut MangoGroup,
|
||||
|
@ -1548,21 +1595,23 @@ fn settle_borrow_unchecked(
|
|||
token_index: usize,
|
||||
quantity: u64
|
||||
) -> MangoResult<()> {
|
||||
let index: &MangoIndex = &mango_group.indexes[token_index];
|
||||
let deposit_index = mango_group.indexes[token_index].deposit;
|
||||
let borrow_index = mango_group.indexes[token_index].borrow;
|
||||
let native_borrow: U64F64 = margin_account.borrows[token_index] * borrow_index;
|
||||
let native_deposit: U64F64 = margin_account.deposits[token_index] * deposit_index;
|
||||
let quantity = U64F64::from_num(quantity);
|
||||
|
||||
let native_borrow = margin_account.get_native_borrow(index, token_index);
|
||||
let native_deposit = margin_account.get_native_deposit(index, token_index);
|
||||
|
||||
let quantity = cmp::min(cmp::min(quantity, native_borrow), native_deposit);
|
||||
|
||||
let borr_settle = U64F64::from_num(quantity) / index.borrow;
|
||||
let dep_settle = U64F64::from_num(quantity) / index.deposit;
|
||||
|
||||
checked_sub_deposit(mango_group, margin_account, token_index, dep_settle)?;
|
||||
checked_sub_borrow(mango_group, margin_account, token_index, borr_settle)?;
|
||||
let quantity = min(quantity, native_deposit);
|
||||
if quantity >= native_borrow { // Reduce borrows to 0 to prevent rounding related dust
|
||||
// NOTE: native_borrow / index.borrow is same as margin_account.borrows[token_index]
|
||||
checked_sub_deposit(mango_group, margin_account, token_index, native_borrow / deposit_index)?;
|
||||
checked_sub_borrow(mango_group, margin_account, token_index, margin_account.borrows[token_index])?;
|
||||
} else {
|
||||
checked_sub_deposit(mango_group, margin_account, token_index, quantity / deposit_index)?;
|
||||
checked_sub_borrow(mango_group, margin_account, token_index, quantity / borrow_index)?;
|
||||
}
|
||||
|
||||
// No need to check collateralization ratio or deposits/borrows validity
|
||||
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
@ -1595,7 +1644,8 @@ fn socialize_loss(
|
|||
mango_group: &mut MangoGroup,
|
||||
margin_account: &mut MarginAccount,
|
||||
token_index: usize,
|
||||
reduce_quantity_native: U64F64
|
||||
reduce_quantity_native: U64F64,
|
||||
total_deposits_native: U64F64
|
||||
) -> MangoResult<()> {
|
||||
|
||||
// reduce borrow for this margin_account by appropriate amount
|
||||
|
@ -1605,8 +1655,7 @@ fn socialize_loss(
|
|||
let quantity: U64F64 = reduce_quantity_native / mango_group.indexes[token_index].borrow;
|
||||
checked_sub_borrow(mango_group, margin_account, token_index, quantity)?;
|
||||
|
||||
let total_deposits = U64F64::from_num(mango_group.get_total_native_deposit(token_index));
|
||||
let percentage_loss = reduce_quantity_native.checked_div(total_deposits).unwrap();
|
||||
let percentage_loss = reduce_quantity_native.checked_div(total_deposits_native).unwrap();
|
||||
let index: &mut MangoIndex = &mut mango_group.indexes[token_index];
|
||||
index.deposit = index.deposit
|
||||
.checked_sub(percentage_loss.checked_mul(index.deposit).unwrap()).unwrap();
|
||||
|
@ -1631,7 +1680,17 @@ fn checked_sub_borrow(
|
|||
quantity: U64F64
|
||||
) -> MangoResult<()> {
|
||||
margin_account.checked_sub_borrow(token_index, quantity)?;
|
||||
mango_group.checked_sub_borrow(token_index, quantity)
|
||||
mango_group.checked_sub_borrow(token_index, quantity)?;
|
||||
|
||||
let mut has_borrows = false;
|
||||
for i in 0..NUM_TOKENS {
|
||||
if margin_account.borrows[i] > 0 {
|
||||
has_borrows = true;
|
||||
}
|
||||
}
|
||||
margin_account.has_borrows = has_borrows;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn checked_add_deposit(
|
||||
|
@ -1651,30 +1710,62 @@ fn checked_add_borrow(
|
|||
quantity: U64F64
|
||||
) -> MangoResult<()> {
|
||||
margin_account.checked_add_borrow(token_index, quantity)?;
|
||||
mango_group.checked_add_borrow(token_index, quantity)
|
||||
mango_group.checked_add_borrow(token_index, quantity)?;
|
||||
|
||||
if !margin_account.has_borrows && quantity > 0 {
|
||||
margin_account.has_borrows = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SWITCHBOARD_MAGIC: [u8;8] = [217, 230, 65, 101, 201, 162, 27, 125];
|
||||
|
||||
pub fn get_prices(
|
||||
mango_group: &MangoGroup,
|
||||
oracle_accs: &[AccountInfo]
|
||||
) -> MangoResult<[U64F64; NUM_TOKENS]> {
|
||||
let mut prices = [ZERO_U64F64; NUM_TOKENS];
|
||||
prices[NUM_MARKETS] = ONE_U64F64; // quote currency is 1
|
||||
let quote_decimals: u8 = mango_group.mint_decimals[NUM_MARKETS];
|
||||
let quote_decimals: i16 = mango_group.mint_decimals[NUM_MARKETS].into();
|
||||
|
||||
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 quote_adj = U64F64::from_num(
|
||||
10u64.pow(quote_decimals.checked_sub(mango_group.oracle_decimals[i]).unwrap() as u32)
|
||||
);
|
||||
let base_adj = U64F64::from_num(10u64.checked_pow(mango_group.mint_decimals[i] as u32).unwrap());
|
||||
let quote_dec_adj = quote_decimals.checked_sub(mango_group.oracle_decimals[i].into()).unwrap();
|
||||
let quote_adj = if quote_dec_adj >= 0 {
|
||||
U64F64::from_num(10u64.checked_pow(quote_dec_adj as u32).unwrap())
|
||||
} else {
|
||||
U64F64::ONE.checked_div(U64F64::from_num(10u64.checked_pow(-quote_dec_adj as u32).unwrap())).unwrap()
|
||||
};
|
||||
|
||||
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 magic4 = u32::from_le_bytes(*array_ref![borrowed, 0, 4]);
|
||||
let magic8 = *array_ref![borrowed, 0, 8];
|
||||
|
||||
let value = U64F64::from_num(answer.median);
|
||||
// read oracle value
|
||||
let value = if magic4 == 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 if magic8 == SWITCHBOARD_MAGIC {
|
||||
// detected switchboard oracle
|
||||
let feed: Ref<'_, AggregatorAccountData> = Ref::map(borrowed, |data| bytemuck::from_bytes(&data[8..]));
|
||||
let price: f64 = if let Ok(d) = feed.get_result() {
|
||||
d.try_into().unwrap()
|
||||
} else {
|
||||
throw_err!(MangoErrorCode::OracleOffline)?
|
||||
};
|
||||
U64F64::from_num(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();
|
||||
|
@ -1709,6 +1800,7 @@ fn invoke_settle_funds<'a>(
|
|||
AccountMeta::new(*quote_vault_acc.key, false),
|
||||
AccountMeta::new_readonly(*dex_signer_acc.key, false),
|
||||
AccountMeta::new_readonly(*token_prog_acc.key, false),
|
||||
AccountMeta::new(*quote_vault_acc.key, false),
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -1722,7 +1814,8 @@ fn invoke_settle_funds<'a>(
|
|||
base_vault_acc.clone(),
|
||||
quote_vault_acc.clone(),
|
||||
dex_signer_acc.clone(),
|
||||
token_prog_acc.clone()
|
||||
token_prog_acc.clone(),
|
||||
quote_vault_acc.clone(),
|
||||
];
|
||||
solana_program::program::invoke_signed(&instruction, &account_infos, signers_seeds)
|
||||
}
|
||||
|
@ -1887,7 +1980,7 @@ fn get_in_out_quantities(
|
|||
liqor_max_in: u64
|
||||
) -> MangoResult<(u64, u64)> {
|
||||
let deficit_val = margin_account.get_partial_liq_deficit(&mango_group, &prices, open_orders_accs)? + ONE_U64F64;
|
||||
let out_avail: U64F64 = margin_account.deposits[out_token_index] * mango_group.indexes[out_token_index].deposit;
|
||||
let out_avail: U64F64 = margin_account.deposits[out_token_index].checked_mul(mango_group.indexes[out_token_index].deposit).unwrap();
|
||||
let out_avail_val = out_avail * prices[out_token_index];
|
||||
|
||||
// liq incentive is max of 1/2 the dist between
|
||||
|
@ -1898,7 +1991,8 @@ fn get_in_out_quantities(
|
|||
|
||||
// we know prices are not 0; if they are this will error;
|
||||
let max_in: U64F64 = max_in_val / prices[in_token_index];
|
||||
let native_borrow = margin_account.borrows[in_token_index] * mango_group.indexes[in_token_index].borrow;
|
||||
let native_borrow = margin_account.borrows[in_token_index].checked_mul(
|
||||
mango_group.indexes[in_token_index].borrow).unwrap();
|
||||
|
||||
// Can only deposit as much there is borrows to offset in in_token
|
||||
let in_quantity = min(min(max_in, native_borrow), U64F64::from_num(liqor_max_in));
|
||||
|
@ -1908,7 +2002,7 @@ fn get_in_out_quantities(
|
|||
checked_sub_borrow(mango_group, margin_account, in_token_index, deposit)?;
|
||||
|
||||
// Withdraw incentive funds to liqor
|
||||
let in_val: U64F64 = in_quantity * prices[in_token_index];
|
||||
let in_val: U64F64 = in_quantity.checked_mul(prices[in_token_index]).unwrap();
|
||||
let out_val: U64F64 = in_val * PARTIAL_LIQ_INCENTIVE;
|
||||
let out_quantity: U64F64 = out_val / prices[out_token_index];
|
||||
|
||||
|
@ -1920,4 +2014,3 @@ fn get_in_out_quantities(
|
|||
|
||||
Ok((in_quantity.checked_ceil().unwrap().to_num(), out_quantity.checked_floor().unwrap().to_num()))
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use fixed_macro::types::U64F64;
|
|||
use crate::error::{check_assert, MangoResult, SourceFileId, MangoErrorCode, MangoError};
|
||||
|
||||
/// Initially launching with BTC/USDT, ETH/USDT
|
||||
pub const NUM_TOKENS: usize = 3;
|
||||
pub const NUM_TOKENS: usize = 5;
|
||||
pub const NUM_MARKETS: usize = NUM_TOKENS - 1;
|
||||
pub const MANGO_GROUP_PADDING: usize = 8 - (NUM_TOKENS + NUM_MARKETS) % 8;
|
||||
pub const MINUTE: u64 = 60;
|
||||
|
@ -24,12 +24,16 @@ pub const HOUR: u64 = 3600;
|
|||
pub const DAY: u64 = 86400;
|
||||
pub const YEAR: U64F64 = U64F64!(31536000);
|
||||
const OPTIMAL_UTIL: U64F64 = U64F64!(0.7);
|
||||
const OPTIMAL_R: U64F64 = U64F64!(3.17097919837645865e-09); // 10% APY -> 0.1 / YEAR
|
||||
const MAX_R: U64F64 = U64F64!(3.17097919837645865e-08); // max 100% APY -> 1 / YEAR
|
||||
const OPTIMAL_R: U64F64 = U64F64!(1.9025875190258751902587e-09); // 6% APR -> 0.06 / YEAR
|
||||
const MAX_R: U64F64 = U64F64!(4.7564687975646879756468e-08); // max 150% APR -> 2 / YEAR
|
||||
|
||||
pub const ONE_U64F64: U64F64 = U64F64!(1);
|
||||
pub const ZERO_U64F64: U64F64 = U64F64!(0);
|
||||
pub const PARTIAL_LIQ_INCENTIVE: U64F64 = U64F64!(1.05);
|
||||
pub const DUST_THRESHOLD: U64F64 = U64F64!(0.01); // TODO make this part of MangoGroup state
|
||||
pub const DUST_THRESHOLD: U64F64 = U64F64!(1); // TODO make this part of MangoGroup state
|
||||
pub const EPSILON: U64F64 = U64F64!(1.0e-17);
|
||||
pub const INFO_LEN: usize = 32;
|
||||
|
||||
|
||||
macro_rules! check_default {
|
||||
($cond:expr) => {
|
||||
|
@ -172,13 +176,13 @@ impl MangoGroup {
|
|||
/// interest is in units per second (e.g. 0.01 => 1% interest per second)
|
||||
pub fn get_interest_rate(&self, token_index: usize) -> U64F64 {
|
||||
let index: &MangoIndex = &self.indexes[token_index];
|
||||
let native_deposits = index.deposit * self.total_deposits[token_index];
|
||||
let native_borrows = index.borrow * self.total_borrows[token_index];
|
||||
let native_deposits = index.deposit.checked_mul(self.total_deposits[token_index]).unwrap();
|
||||
let native_borrows = index.borrow.checked_mul(self.total_borrows[token_index]).unwrap();
|
||||
if native_deposits <= native_borrows { // if deps == 0, this is always true
|
||||
return MAX_R; // kind of an error state
|
||||
}
|
||||
|
||||
let utilization = native_borrows / native_deposits;
|
||||
let utilization = native_borrows.checked_div(native_deposits).unwrap();
|
||||
if utilization > OPTIMAL_UTIL {
|
||||
let extra_util = utilization - OPTIMAL_UTIL;
|
||||
let slope = (MAX_R - OPTIMAL_R) / (ONE_U64F64 - OPTIMAL_UTIL);
|
||||
|
@ -188,6 +192,8 @@ impl MangoGroup {
|
|||
slope * utilization
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn update_indexes(&mut self, clock: &Clock) -> MangoResult<()> {
|
||||
// TODO verify what happens if total_deposits < total_borrows
|
||||
// TODO verify what happens if total_deposits == 0 && total_borrows > 0
|
||||
|
@ -204,10 +210,10 @@ impl MangoGroup {
|
|||
continue;
|
||||
}
|
||||
|
||||
let native_deposits: U64F64 = self.total_deposits[i] * index.deposit;
|
||||
let native_borrows: U64F64 = self.total_borrows[i] * index.borrow;
|
||||
let epsilon = U64F64::from_bits(1u128) * 100;
|
||||
check_default!(native_borrows <= native_deposits + epsilon)?; // to account for rounding errors
|
||||
// don't need to check here because this check already happens in get interest rate
|
||||
let native_deposits: U64F64 = self.total_deposits[i].checked_mul(index.deposit).unwrap();
|
||||
let native_borrows: U64F64 = self.total_borrows[i].checked_mul(index.borrow).unwrap();
|
||||
check_default!(native_borrows <= native_deposits + EPSILON)?; // to account for rounding errors
|
||||
|
||||
let utilization = native_borrows.checked_div(native_deposits).unwrap();
|
||||
let borrow_interest = interest_rate
|
||||
|
@ -229,26 +235,33 @@ impl MangoGroup {
|
|||
pub fn has_valid_deposits_borrows(&self, token_i: usize) -> bool {
|
||||
self.get_total_native_deposit(token_i) >= self.get_total_native_borrow(token_i)
|
||||
}
|
||||
|
||||
pub fn get_total_native_borrow(&self, token_i: usize) -> u64 {
|
||||
let native: U64F64 = self.total_borrows[token_i] * self.indexes[token_i].borrow;
|
||||
native.checked_ceil().unwrap().to_num() // rounds toward +inf
|
||||
}
|
||||
|
||||
pub fn get_total_native_deposit(&self, token_i: usize) -> u64 {
|
||||
let native: U64F64 = self.total_deposits[token_i] * self.indexes[token_i].deposit;
|
||||
native.checked_floor().unwrap().to_num() // rounds toward -inf
|
||||
}
|
||||
|
||||
pub fn get_market_index(&self, spot_market_pk: &Pubkey) -> Option<usize> {
|
||||
self.spot_markets.iter().position(|market| market == spot_market_pk)
|
||||
}
|
||||
|
||||
pub fn checked_add_borrow(&mut self, token_i: usize, v: U64F64) -> MangoResult<()> {
|
||||
Ok(self.total_borrows[token_i] = self.total_borrows[token_i].checked_add(v).ok_or(throw!())?)
|
||||
}
|
||||
|
||||
pub fn checked_sub_borrow(&mut self, token_i: usize, v: U64F64) -> MangoResult<()> {
|
||||
Ok(self.total_borrows[token_i] = self.total_borrows[token_i].checked_sub(v).ok_or(throw!())?)
|
||||
}
|
||||
|
||||
pub fn checked_add_deposit(&mut self, token_i: usize, v: U64F64) -> MangoResult<()> {
|
||||
Ok(self.total_deposits[token_i] = self.total_deposits[token_i].checked_add(v).ok_or(throw!())?)
|
||||
}
|
||||
|
||||
pub fn checked_sub_deposit(&mut self, token_i: usize, v: U64F64) -> MangoResult<()> {
|
||||
Ok(self.total_deposits[token_i] = self.total_deposits[token_i].checked_sub(v).ok_or(throw!())?)
|
||||
}
|
||||
|
@ -270,10 +283,10 @@ pub struct MarginAccount {
|
|||
pub borrows: [U64F64; NUM_TOKENS], // multiply by current index to get actual value
|
||||
|
||||
pub open_orders: [Pubkey; NUM_MARKETS], // owned by Mango
|
||||
|
||||
pub being_liquidated: bool,
|
||||
pub padding: [u8; 7] // padding to make compatible with previous MarginAccount size
|
||||
// TODO add has_borrows field for easy memcmp fetching
|
||||
pub has_borrows: bool, // does the account have any open borrows? set by checked_add_borrow and checked_sub_borrow
|
||||
pub info: [u8; INFO_LEN],
|
||||
pub padding: [u8; 38] // padding for future expansion
|
||||
}
|
||||
impl_loadable!(MarginAccount);
|
||||
|
||||
|
@ -337,43 +350,70 @@ impl MarginAccount {
|
|||
if liabs == ZERO_U64F64 {
|
||||
Ok(U64F64::MAX)
|
||||
} else {
|
||||
Ok(assets / liabs)
|
||||
Ok(assets.checked_div( liabs).unwrap())
|
||||
}
|
||||
}
|
||||
pub fn get_total_assets(
|
||||
|
||||
pub fn coll_ratio_from_assets_liabs(
|
||||
&self,
|
||||
prices: &[U64F64; NUM_TOKENS],
|
||||
assets: &[U64F64; NUM_TOKENS],
|
||||
liabs: &[U64F64; NUM_TOKENS]
|
||||
) -> MangoResult<U64F64> {
|
||||
let mut assets_val: U64F64 = ZERO_U64F64;
|
||||
let mut liabs_val: U64F64 = ZERO_U64F64;
|
||||
for i in 0..NUM_TOKENS {
|
||||
liabs_val = liabs[i].checked_mul(prices[i]).unwrap().checked_add(liabs_val).unwrap();
|
||||
assets_val = assets[i].checked_mul(prices[i]).unwrap().checked_add(assets_val).unwrap();
|
||||
}
|
||||
|
||||
if liabs_val == ZERO_U64F64 {
|
||||
Ok(U64F64::MAX)
|
||||
} else {
|
||||
Ok(assets_val.checked_div(liabs_val).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_assets(
|
||||
&self,
|
||||
mango_group: &MangoGroup,
|
||||
open_orders_accs: &[AccountInfo; NUM_MARKETS]
|
||||
) -> MangoResult<[u64; NUM_TOKENS]> {
|
||||
let mut assets = [0u64; NUM_TOKENS];
|
||||
) -> MangoResult<[U64F64; NUM_TOKENS]> {
|
||||
let mut assets = [ZERO_U64F64; NUM_TOKENS];
|
||||
|
||||
for i in 0..NUM_TOKENS {
|
||||
assets[i] = self.get_native_deposit(&mango_group.indexes[i], i)
|
||||
assets[i] = mango_group.indexes[i].deposit.checked_mul(self.deposits[i]).unwrap()
|
||||
.checked_add(assets[i]).unwrap();
|
||||
}
|
||||
|
||||
for i in 0..NUM_MARKETS {
|
||||
if *open_orders_accs[i].key == Pubkey::default() {
|
||||
continue;
|
||||
}
|
||||
let open_orders = load_open_orders(&open_orders_accs[i])?;
|
||||
|
||||
assets[i] = open_orders.native_coin_total.checked_add(assets[i]).unwrap();
|
||||
assets[NUM_TOKENS-1] = open_orders.native_pc_total.checked_add(assets[NUM_TOKENS-1]).unwrap();
|
||||
assets[i] = U64F64::from_num(open_orders.native_coin_total).checked_add(assets[i]).unwrap();
|
||||
assets[NUM_TOKENS-1] = U64F64::from_num(open_orders.native_pc_total + open_orders.referrer_rebates_accrued).checked_add(assets[NUM_TOKENS-1]).unwrap();
|
||||
}
|
||||
Ok(assets)
|
||||
}
|
||||
|
||||
pub fn get_total_liabs(
|
||||
|
||||
pub fn get_liabs(
|
||||
&self,
|
||||
mango_group: &MangoGroup
|
||||
) -> MangoResult<[u64; NUM_TOKENS]> {
|
||||
let mut liabs = [0u64; NUM_TOKENS];
|
||||
for i in 0.. NUM_TOKENS {
|
||||
liabs[i] = self.get_native_borrow(&mango_group.indexes[i], i);
|
||||
mango_group: &MangoGroup,
|
||||
) -> MangoResult<[U64F64; NUM_TOKENS]> {
|
||||
let mut liabs = [ZERO_U64F64; NUM_TOKENS];
|
||||
|
||||
for i in 0..NUM_TOKENS {
|
||||
liabs[i] = mango_group.indexes[i].borrow.checked_mul(self.borrows[i]).unwrap()
|
||||
.checked_add(liabs[i]).unwrap();
|
||||
}
|
||||
|
||||
Ok(liabs)
|
||||
}
|
||||
|
||||
|
||||
pub fn get_assets_val(
|
||||
&self,
|
||||
mango_group: &MangoGroup,
|
||||
|
@ -392,9 +432,8 @@ impl MarginAccount {
|
|||
let open_orders = load_open_orders(&open_orders_accs[i])?;
|
||||
assets = U64F64::from_num(open_orders.native_coin_total)
|
||||
.checked_mul(prices[i]).unwrap()
|
||||
.checked_add(U64F64::from_num(open_orders.native_pc_total)).unwrap()
|
||||
.checked_add(U64F64::from_num(open_orders.native_pc_total + open_orders.referrer_rebates_accrued)).unwrap()
|
||||
.checked_add(assets).unwrap();
|
||||
|
||||
}
|
||||
for i in 0..NUM_TOKENS { // add up the value in margin account deposits and positions
|
||||
let index: &MangoIndex = &mango_group.indexes[i];
|
||||
|
@ -406,6 +445,8 @@ impl MarginAccount {
|
|||
Ok(assets)
|
||||
|
||||
}
|
||||
|
||||
|
||||
pub fn get_liabs_val(
|
||||
&self,
|
||||
mango_group: &MangoGroup,
|
||||
|
@ -420,6 +461,7 @@ impl MarginAccount {
|
|||
Ok(liabs)
|
||||
}
|
||||
/// Return amount of quote currency to deposit to get account above init_coll_ratio
|
||||
|
||||
pub fn get_collateral_deficit(
|
||||
&self,
|
||||
mango_group: &MangoGroup,
|
||||
|
@ -436,6 +478,7 @@ impl MarginAccount {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_partial_liq_deficit(
|
||||
&self,
|
||||
mango_group: &MangoGroup,
|
||||
|
@ -451,23 +494,29 @@ impl MarginAccount {
|
|||
// TODO make this checked
|
||||
Ok((liabs * mango_group.init_coll_ratio - assets) / (mango_group.init_coll_ratio - PARTIAL_LIQ_INCENTIVE))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub fn get_native_borrow(&self, index: &MangoIndex, token_i: usize) -> u64 {
|
||||
(self.borrows[token_i] * index.borrow).to_num()
|
||||
}
|
||||
|
||||
pub fn get_native_deposit(&self, index: &MangoIndex, token_i: usize) -> u64 {
|
||||
(self.deposits[token_i] * index.deposit).to_num()
|
||||
}
|
||||
|
||||
pub fn checked_add_borrow(&mut self, token_i: usize, v: U64F64) -> MangoResult<()> {
|
||||
Ok(self.borrows[token_i] = self.borrows[token_i].checked_add(v).ok_or(throw!())?)
|
||||
Err(throw!())
|
||||
// Ok(self.borrows[token_i] = self.borrows[token_i].checked_add(v).ok_or(throw!())?)
|
||||
}
|
||||
|
||||
pub fn checked_sub_borrow(&mut self, token_i: usize, v: U64F64) -> MangoResult<()> {
|
||||
Ok(self.borrows[token_i] = self.borrows[token_i].checked_sub(v).ok_or(throw!())?)
|
||||
}
|
||||
|
||||
pub fn checked_add_deposit(&mut self, token_i: usize, v: U64F64) -> MangoResult<()> {
|
||||
Ok(self.deposits[token_i] = self.deposits[token_i].checked_add(v).ok_or(throw!())?)
|
||||
}
|
||||
|
||||
pub fn checked_sub_deposit(&mut self, token_i: usize, v: U64F64) -> MangoResult<()> {
|
||||
Ok(self.deposits[token_i] = self.deposits[token_i].checked_sub(v).ok_or(throw!())?)
|
||||
}
|
||||
|
@ -663,6 +712,7 @@ pub fn load_open_orders<'a>(
|
|||
Ok(Ref::map(strip_dex_padding(acc)?, from_bytes))
|
||||
}
|
||||
|
||||
|
||||
pub fn check_open_orders(
|
||||
acc: &AccountInfo,
|
||||
owner: &Pubkey
|
||||
|
|
|
@ -0,0 +1,411 @@
|
|||
#![cfg(feature="test-bpf")]
|
||||
|
||||
use std::convert::TryInto;
|
||||
use std::mem::size_of;
|
||||
|
||||
use bytemuck::{bytes_of, Contiguous};
|
||||
use fixed::types::U64F64;
|
||||
use flux_aggregator::borsh_state::BorshState;
|
||||
use flux_aggregator::borsh_utils;
|
||||
use flux_aggregator::state::{Aggregator, AggregatorConfig, Answer};
|
||||
use safe_transmute::{self, to_bytes::transmute_one_to_bytes};
|
||||
use serum_dex::state::{AccountFlag, MarketState, ToAlignedBytes};
|
||||
use solana_program::program_option::COption;
|
||||
use solana_program::program_pack::Pack;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use solana_program::pubkey::PubkeyError;
|
||||
use solana_program_test::{BanksClient, ProgramTest};
|
||||
use solana_sdk::{
|
||||
account::Account,
|
||||
account_info::IntoAccountInfo,
|
||||
instruction::Instruction,
|
||||
signature::{Keypair, Signer}
|
||||
};
|
||||
use spl_token::state::{Account as Token, AccountState, Mint};
|
||||
|
||||
use mango::instruction::init_mango_group;
|
||||
use mango::processor::srm_token;
|
||||
use mango::state::MangoGroup;
|
||||
|
||||
pub const PRICE_BTC: u64 = 50000;
|
||||
pub const PRICE_ETH: u64 = 2000;
|
||||
pub const PRICE_SOL: u64 = 30;
|
||||
pub const PRICE_SRM: u64 = 5;
|
||||
pub const PRICE_RAY: u64 = 5;
|
||||
|
||||
pub fn gen_signer_seeds<'a>(nonce: &'a u64, acc_pk: &'a Pubkey) -> [&'a [u8]; 2] {
|
||||
[acc_pk.as_ref(), bytes_of(nonce)]
|
||||
}
|
||||
|
||||
fn gen_signer_key(
|
||||
nonce: u64,
|
||||
acc_pk: &Pubkey,
|
||||
program_id: &Pubkey,
|
||||
) -> Result<Pubkey, PubkeyError> {
|
||||
let seeds = gen_signer_seeds(&nonce, acc_pk);
|
||||
Pubkey::create_program_address(&seeds, program_id)
|
||||
}
|
||||
|
||||
fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pubkey, u64) {
|
||||
|
||||
for i in 0..=u64::MAX_VALUE {
|
||||
if let Ok(pk) = gen_signer_key(i, acc_pk, program_id) {
|
||||
return (pk, i);
|
||||
}
|
||||
}
|
||||
panic!("Could not generate signer key");
|
||||
|
||||
}
|
||||
|
||||
trait AddPacked {
|
||||
fn add_packable_account<T: Pack>(
|
||||
&mut self,
|
||||
pubkey: Pubkey,
|
||||
amount: u64,
|
||||
data: &T,
|
||||
owner: &Pubkey,
|
||||
);
|
||||
}
|
||||
|
||||
impl AddPacked for ProgramTest {
|
||||
fn add_packable_account<T: Pack>(
|
||||
&mut self,
|
||||
pubkey: Pubkey,
|
||||
amount: u64,
|
||||
data: &T,
|
||||
owner: &Pubkey,
|
||||
) {
|
||||
let mut account = Account::new(amount, T::get_packed_len(), owner);
|
||||
data.pack_into_slice(&mut account.data);
|
||||
self.add_account(pubkey, account);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct TestMint {
|
||||
pub pubkey: Pubkey,
|
||||
pub authority: Keypair,
|
||||
pub decimals: u8,
|
||||
}
|
||||
|
||||
|
||||
pub fn add_mint(test: &mut ProgramTest, decimals: u8) -> TestMint {
|
||||
let authority = Keypair::new();
|
||||
let pubkey = Pubkey::new_unique();
|
||||
test.add_packable_account(
|
||||
pubkey,
|
||||
u32::MAX as u64,
|
||||
&Mint {
|
||||
is_initialized: true,
|
||||
mint_authority: COption::Some(authority.pubkey()),
|
||||
decimals,
|
||||
..Mint::default()
|
||||
},
|
||||
&spl_token::id(),
|
||||
);
|
||||
TestMint {
|
||||
pubkey,
|
||||
authority,
|
||||
decimals,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_mint_srm(test: &mut ProgramTest) -> TestMint {
|
||||
let authority = Keypair::new();
|
||||
let pubkey = srm_token::ID;
|
||||
let decimals = 6;
|
||||
test.add_packable_account(
|
||||
pubkey,
|
||||
u32::MAX as u64,
|
||||
&Mint {
|
||||
is_initialized: true,
|
||||
mint_authority: COption::Some(authority.pubkey()),
|
||||
decimals,
|
||||
..Mint::default()
|
||||
},
|
||||
&spl_token::id(),
|
||||
);
|
||||
TestMint {
|
||||
pubkey,
|
||||
authority,
|
||||
decimals,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TestDex {
|
||||
pub pubkey: Pubkey,
|
||||
}
|
||||
|
||||
pub fn add_dex_empty(test: &mut ProgramTest, base_mint: Pubkey, quote_mint: Pubkey, dex_prog_id: Pubkey) -> TestDex {
|
||||
let pubkey = Pubkey::new_unique();
|
||||
let mut acc = Account::new(u32::MAX as u64, 0, &dex_prog_id);
|
||||
let ms = MarketState {
|
||||
account_flags: (AccountFlag::Initialized | AccountFlag::Market).bits(),
|
||||
own_address: pubkey.to_aligned_bytes(),
|
||||
vault_signer_nonce: 0,
|
||||
coin_mint: base_mint.to_aligned_bytes(),
|
||||
pc_mint: quote_mint.to_aligned_bytes(),
|
||||
|
||||
coin_vault: Pubkey::new_unique().to_aligned_bytes(),
|
||||
coin_deposits_total: 0,
|
||||
coin_fees_accrued: 0,
|
||||
|
||||
pc_vault: Pubkey::new_unique().to_aligned_bytes(),
|
||||
pc_deposits_total: 0,
|
||||
pc_fees_accrued: 0,
|
||||
pc_dust_threshold: 0,
|
||||
|
||||
req_q: Pubkey::new_unique().to_aligned_bytes(),
|
||||
event_q: Pubkey::new_unique().to_aligned_bytes(),
|
||||
bids: Pubkey::new_unique().to_aligned_bytes(),
|
||||
asks: Pubkey::new_unique().to_aligned_bytes(),
|
||||
|
||||
coin_lot_size: 1,
|
||||
pc_lot_size: 1,
|
||||
|
||||
fee_rate_bps: 1,
|
||||
referrer_rebates_accrued: 0,
|
||||
};
|
||||
let head: &[u8; 5] = b"serum";
|
||||
let tail: &[u8; 7] = b"padding";
|
||||
let data = transmute_one_to_bytes(&ms);
|
||||
let mut accdata = vec![];
|
||||
accdata.extend(head);
|
||||
accdata.extend(data);
|
||||
accdata.extend(tail);
|
||||
acc.data = accdata;
|
||||
|
||||
test.add_account(pubkey, acc);
|
||||
TestDex { pubkey }
|
||||
}
|
||||
|
||||
pub struct TestTokenAccount {
|
||||
pub pubkey: Pubkey,
|
||||
}
|
||||
|
||||
pub fn add_token_account(test: &mut ProgramTest, owner: Pubkey, mint: Pubkey, initial_balance: u64) -> TestTokenAccount {
|
||||
let pubkey = Pubkey::new_unique();
|
||||
test.add_packable_account(
|
||||
pubkey,
|
||||
u32::MAX as u64,
|
||||
&Token {
|
||||
mint: mint,
|
||||
owner: owner,
|
||||
amount: initial_balance,
|
||||
state: AccountState::Initialized,
|
||||
..Token::default()
|
||||
},
|
||||
&spl_token::id(),
|
||||
);
|
||||
TestTokenAccount { pubkey }
|
||||
}
|
||||
|
||||
pub struct TestAggregator {
|
||||
pub name: String,
|
||||
pub pubkey: Pubkey,
|
||||
pub price: u64,
|
||||
}
|
||||
|
||||
pub fn add_aggregator(test: &mut ProgramTest, name: &str, decimals: u8, price: u64, owner: &Pubkey) -> TestAggregator {
|
||||
let pubkey = Pubkey::new_unique();
|
||||
|
||||
let mut description = [0u8; 32];
|
||||
let size = name.len().min(description.len());
|
||||
description[0..size].copy_from_slice(&name.as_bytes()[0..size]);
|
||||
|
||||
let aggregator = Aggregator {
|
||||
config: AggregatorConfig {
|
||||
description,
|
||||
decimals,
|
||||
..AggregatorConfig::default()
|
||||
},
|
||||
is_initialized: true,
|
||||
answer: Answer {
|
||||
median: price,
|
||||
created_at: 1, // set to > 0 to initialize
|
||||
..Answer::default()
|
||||
},
|
||||
..Aggregator::default()
|
||||
};
|
||||
|
||||
let mut account = Account::new(
|
||||
u32::MAX as u64,
|
||||
borsh_utils::get_packed_len::<Aggregator>(),
|
||||
&owner,
|
||||
);
|
||||
let account_info = (&pubkey, false, &mut account).into_account_info();
|
||||
aggregator.save(&account_info).unwrap();
|
||||
test.add_account(pubkey, account);
|
||||
|
||||
TestAggregator {
|
||||
name: name.to_string(),
|
||||
pubkey,
|
||||
price,
|
||||
}
|
||||
}
|
||||
|
||||
// Holds all of the dependencies for a MangoGroup
|
||||
pub struct TestMangoGroup {
|
||||
pub program_id: Pubkey,
|
||||
pub mango_group_pk: Pubkey,
|
||||
pub signer_pk: Pubkey,
|
||||
pub signer_nonce: u64,
|
||||
|
||||
// Mints and Vaults must ordered with base assets first, quote asset last
|
||||
// They must be ordered in the same way
|
||||
pub mints: Vec<TestMint>,
|
||||
pub vaults: Vec<TestTokenAccount>,
|
||||
|
||||
pub srm_mint: TestMint,
|
||||
pub srm_vault: TestTokenAccount,
|
||||
|
||||
pub dex_prog_id: Pubkey,
|
||||
// Dexes and Oracles must be sorted in the same way as the first n-1 mints
|
||||
// mints[x] / mints[-1]
|
||||
pub dexes: Vec<TestDex>,
|
||||
pub oracles: Vec<TestAggregator>,
|
||||
|
||||
pub borrow_limits: Vec<u64>,
|
||||
}
|
||||
|
||||
|
||||
// This should probably go into the main code at some point when we remove the hard-coded market sizes
|
||||
fn to_fixed_array<T, const N: usize>(v: Vec<T>) -> [T; N] {
|
||||
v.try_into().unwrap_or_else(|v: Vec<T>| panic!("Expected a Vec of length {} but it was {}", N, v.len()))
|
||||
}
|
||||
|
||||
impl TestMangoGroup {
|
||||
pub fn init_mango_group(&self, payer: &Pubkey) -> Instruction {
|
||||
init_mango_group(
|
||||
&self.program_id,
|
||||
&self.mango_group_pk,
|
||||
&self.signer_pk,
|
||||
&self.dex_prog_id,
|
||||
&self.srm_vault.pubkey,
|
||||
payer,
|
||||
self.mints.iter().map(|m| m.pubkey).collect::<Vec<Pubkey>>().as_slice(),
|
||||
self.vaults.iter().map(|m| m.pubkey).collect::<Vec<Pubkey>>().as_slice(),
|
||||
self.dexes.iter().map(|m| m.pubkey).collect::<Vec<Pubkey>>().as_slice(),
|
||||
self.oracles.iter().map(|m| m.pubkey).collect::<Vec<Pubkey>>().as_slice(),
|
||||
self.signer_nonce,
|
||||
U64F64::from_num(1.1),
|
||||
U64F64::from_num(1.2),
|
||||
to_fixed_array(self.borrow_limits.clone()),
|
||||
).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_mango_group_prodlike(test: &mut ProgramTest, program_id: Pubkey) -> TestMangoGroup {
|
||||
let mango_group_pk = Pubkey::new_unique();
|
||||
let (signer_pk, signer_nonce) = create_signer_key_and_nonce(&program_id, &mango_group_pk);
|
||||
test.add_account(mango_group_pk, Account::new(u32::MAX as u64, size_of::<MangoGroup>(), &program_id));
|
||||
|
||||
let btc_mint = add_mint(test, 6);
|
||||
let eth_mint = add_mint(test, 6);
|
||||
let sol_mint = add_mint(test, 6);
|
||||
let srm_mint = add_mint_srm(test);
|
||||
let srm_mint_token = add_mint_srm(test);
|
||||
let usdt_mint = add_mint(test, 6);
|
||||
|
||||
let btc_vault = add_token_account(test, signer_pk, btc_mint.pubkey, 0);
|
||||
let eth_vault = add_token_account(test, signer_pk, eth_mint.pubkey, 0);
|
||||
let sol_vault = add_token_account(test, signer_pk, sol_mint.pubkey, 0);
|
||||
let srm_vault = add_token_account(test, signer_pk, srm_mint.pubkey, 0);
|
||||
let srm_vault_token = add_token_account(test, signer_pk, srm_mint.pubkey, 0);
|
||||
let usdt_vault = add_token_account(test, signer_pk, usdt_mint.pubkey, 0);
|
||||
|
||||
let dex_prog_id = Pubkey::new_unique();
|
||||
let btc_usdt_dex = add_dex_empty(test, btc_mint.pubkey, usdt_mint.pubkey, dex_prog_id);
|
||||
let eth_usdt_dex = add_dex_empty(test, eth_mint.pubkey, usdt_mint.pubkey, dex_prog_id);
|
||||
let sol_usdt_dex = add_dex_empty(test, sol_mint.pubkey, usdt_mint.pubkey, dex_prog_id);
|
||||
let srm_usdt_dex = add_dex_empty(test, srm_mint.pubkey, usdt_mint.pubkey, dex_prog_id);
|
||||
|
||||
let unit = 10u64.pow(6);
|
||||
let btc_usdt = add_aggregator(test, "BTC:USDT", 6, PRICE_BTC * unit, &program_id);
|
||||
let eth_usdt = add_aggregator(test, "ETH:USDT", 6, PRICE_ETH * unit, &program_id);
|
||||
let sol_usdt = add_aggregator(test, "SOL:USDT", 6, PRICE_SOL * unit, &program_id);
|
||||
let srm_usdt = add_aggregator(test, "SRM:USDT", 6, PRICE_SRM * unit, &program_id);
|
||||
|
||||
let mints = vec![btc_mint, eth_mint, sol_mint, srm_mint_token, usdt_mint];
|
||||
let vaults = vec![btc_vault, eth_vault, sol_vault, srm_vault_token, usdt_vault];
|
||||
let dexes = vec![btc_usdt_dex, eth_usdt_dex, sol_usdt_dex, srm_usdt_dex];
|
||||
let oracles = vec![btc_usdt, eth_usdt, sol_usdt, srm_usdt];
|
||||
let borrow_limits = vec![100, 100, 100, 100, 100];
|
||||
|
||||
TestMangoGroup {
|
||||
program_id,
|
||||
mango_group_pk,
|
||||
signer_pk,
|
||||
signer_nonce,
|
||||
mints,
|
||||
vaults,
|
||||
srm_mint,
|
||||
srm_vault,
|
||||
dex_prog_id,
|
||||
dexes,
|
||||
oracles,
|
||||
borrow_limits,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_mango_group_nosrm(test: &mut ProgramTest, program_id: Pubkey) -> TestMangoGroup {
|
||||
let mango_group_pk = Pubkey::new_unique();
|
||||
let (signer_pk, signer_nonce) = create_signer_key_and_nonce(&program_id, &mango_group_pk);
|
||||
test.add_account(mango_group_pk, Account::new(u32::MAX as u64, size_of::<MangoGroup>(), &program_id));
|
||||
|
||||
let btc_mint = add_mint(test, 6);
|
||||
let eth_mint = add_mint(test, 6);
|
||||
let sol_mint = add_mint(test, 6);
|
||||
let srm_mint = add_mint_srm(test);
|
||||
let ray_mint = add_mint(test, 6);
|
||||
let usdt_mint = add_mint(test, 6);
|
||||
|
||||
let btc_vault = add_token_account(test, signer_pk, btc_mint.pubkey, 0);
|
||||
let eth_vault = add_token_account(test, signer_pk, eth_mint.pubkey, 0);
|
||||
let sol_vault = add_token_account(test, signer_pk, sol_mint.pubkey, 0);
|
||||
let srm_vault = add_token_account(test, signer_pk, srm_mint.pubkey, 0);
|
||||
let ray_vault = add_token_account(test, signer_pk, ray_mint.pubkey, 0);
|
||||
let usdt_vault = add_token_account(test, signer_pk, usdt_mint.pubkey, 0);
|
||||
|
||||
let dex_prog_id = Pubkey::new_unique();
|
||||
let btc_usdt_dex = add_dex_empty(test, btc_mint.pubkey, usdt_mint.pubkey, dex_prog_id);
|
||||
let eth_usdt_dex = add_dex_empty(test, eth_mint.pubkey, usdt_mint.pubkey, dex_prog_id);
|
||||
let sol_usdt_dex = add_dex_empty(test, sol_mint.pubkey, usdt_mint.pubkey, dex_prog_id);
|
||||
let ray_usdt_dex = add_dex_empty(test, ray_mint.pubkey, usdt_mint.pubkey, dex_prog_id);
|
||||
|
||||
let unit = 10u64.pow(6);
|
||||
let btc_usdt = add_aggregator(test, "BTC:USDT", 6, PRICE_BTC * unit, &program_id);
|
||||
let eth_usdt = add_aggregator(test, "ETH:USDT", 6, PRICE_ETH * unit, &program_id);
|
||||
let sol_usdt = add_aggregator(test, "SOL:USDT", 6, PRICE_SOL * unit, &program_id);
|
||||
let ray_usdt = add_aggregator(test, "RAY:USDT", 6, PRICE_RAY * unit, &program_id);
|
||||
|
||||
let mints = vec![btc_mint, eth_mint, sol_mint, ray_mint, usdt_mint];
|
||||
let vaults = vec![btc_vault, eth_vault, sol_vault, ray_vault, usdt_vault];
|
||||
let dexes = vec![btc_usdt_dex, eth_usdt_dex, sol_usdt_dex, ray_usdt_dex];
|
||||
let oracles = vec![btc_usdt, eth_usdt, sol_usdt, ray_usdt];
|
||||
let borrow_limits = vec![100, 100, 100, 100, 100];
|
||||
|
||||
TestMangoGroup {
|
||||
program_id,
|
||||
mango_group_pk,
|
||||
signer_pk,
|
||||
signer_nonce,
|
||||
mints,
|
||||
vaults,
|
||||
srm_mint,
|
||||
srm_vault,
|
||||
dex_prog_id,
|
||||
dexes,
|
||||
oracles,
|
||||
borrow_limits,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)] // Compiler complains about this even tho it is used
|
||||
pub async fn get_token_balance(banks_client: &mut BanksClient, pubkey: Pubkey) -> u64 {
|
||||
let token: Account = banks_client.get_account(pubkey).await.unwrap().unwrap();
|
||||
|
||||
spl_token::state::Account::unpack(&token.data[..])
|
||||
.unwrap()
|
||||
.amount
|
||||
}
|
|
@ -0,0 +1,315 @@
|
|||
// Tests related to borrowing on a MangoGroup
|
||||
#![cfg(feature="test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use std::mem::size_of;
|
||||
use helpers::*;
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
signature::{Signer, Keypair},
|
||||
transaction::Transaction,
|
||||
account::Account,
|
||||
};
|
||||
use mango::{
|
||||
entrypoint::process_instruction,
|
||||
instruction::{deposit, borrow, init_margin_account},
|
||||
state::MarginAccount,
|
||||
state::MangoGroup,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_borrow_succeeds() {
|
||||
// Test that the borrow instruction succeeds and the expected side effects occurr
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"mango",
|
||||
program_id,
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let deposit_token_index = 0;
|
||||
let borrow_token_index = 1;
|
||||
let initial_amount = 2;
|
||||
let deposit_amount = 1;
|
||||
// 5x leverage
|
||||
let borrow_amount = (deposit_amount * PRICE_BTC * 5) / PRICE_ETH;
|
||||
|
||||
let mango_group = add_mango_group_prodlike(&mut test, program_id);
|
||||
let mango_group_pk = mango_group.mango_group_pk;
|
||||
|
||||
let user = Keypair::new();
|
||||
test.add_account(user.pubkey(), Account::new(u32::MAX as u64, 0, &user.pubkey()));
|
||||
|
||||
let user_account = add_token_account(
|
||||
&mut test,
|
||||
user.pubkey(),
|
||||
mango_group.mints[deposit_token_index].pubkey,
|
||||
initial_amount,
|
||||
);
|
||||
|
||||
let margin_account_pk = Pubkey::new_unique();
|
||||
test.add_account(margin_account_pk, Account::new(u32::MAX as u64, size_of::<MarginAccount>(), &program_id));
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
// setup mango group and make a deposit
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey()),
|
||||
init_margin_account(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
).unwrap(),
|
||||
deposit(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
&user_account.pubkey,
|
||||
&mango_group.vaults[deposit_token_index].pubkey,
|
||||
deposit_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction succeeded
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
}
|
||||
|
||||
// make a borrow
|
||||
{
|
||||
let mut margin_account = banks_client
|
||||
.get_account(margin_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut margin_account).into();
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
borrow(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
&margin_account.open_orders,
|
||||
mango_group.oracles.iter().map(|m| m.pubkey).collect::<Vec<Pubkey>>().as_slice(),
|
||||
borrow_token_index,
|
||||
borrow_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction succeeded
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let mut margin_account = banks_client
|
||||
.get_account(margin_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut margin_account).into();
|
||||
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
// Test expected borrow is in margin account
|
||||
assert_eq!(margin_account.borrows[borrow_token_index], borrow_amount);
|
||||
|
||||
//Test has_borrows flag is set correctly
|
||||
assert_eq!(margin_account.has_borrows, true);
|
||||
|
||||
let mut mango_group = banks_client
|
||||
.get_account(mango_group_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&mango_group_pk, &mut mango_group).into();
|
||||
|
||||
let mango_group = MangoGroup::load_mut_checked(
|
||||
&account_info,
|
||||
&program_id,
|
||||
)
|
||||
.unwrap();
|
||||
// Test expected borrow is added to total in mango group
|
||||
assert_eq!(mango_group.total_borrows[borrow_token_index], borrow_amount);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_borrow_fails_overleveraged() {
|
||||
// Test that the deposit instruction fails when a user exceeds their leverage limit
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"mango",
|
||||
program_id,
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let deposit_token_index = 0;
|
||||
let borrow_token_index = 1;
|
||||
let initial_amount = 2;
|
||||
let deposit_amount = 1;
|
||||
// try to go for 6x leverage
|
||||
let borrow_amount = (deposit_amount * PRICE_BTC * 6) / PRICE_ETH;
|
||||
|
||||
let mango_group = add_mango_group_prodlike(&mut test, program_id);
|
||||
let mango_group_pk = mango_group.mango_group_pk;
|
||||
|
||||
let user = Keypair::new();
|
||||
test.add_account(user.pubkey(), Account::new(u32::MAX as u64, 0, &user.pubkey()));
|
||||
|
||||
let user_account = add_token_account(
|
||||
&mut test,
|
||||
user.pubkey(),
|
||||
mango_group.mints[deposit_token_index].pubkey,
|
||||
initial_amount,
|
||||
);
|
||||
|
||||
let margin_account_pk = Pubkey::new_unique();
|
||||
test.add_account(margin_account_pk, Account::new(u32::MAX as u64, size_of::<MarginAccount>(), &program_id));
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
// setup mango group and make a deposit
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey()),
|
||||
init_margin_account(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
).unwrap(),
|
||||
deposit(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
&user_account.pubkey,
|
||||
&mango_group.vaults[deposit_token_index].pubkey,
|
||||
deposit_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction succeeded
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
}
|
||||
|
||||
// make a borrow
|
||||
{
|
||||
let mut margin_account = banks_client
|
||||
.get_account(margin_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut margin_account).into();
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
borrow(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
&margin_account.open_orders,
|
||||
mango_group.oracles.iter().map(|m| m.pubkey).collect::<Vec<Pubkey>>().as_slice(),
|
||||
borrow_token_index,
|
||||
borrow_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction failed
|
||||
assert!(banks_client.process_transaction(transaction).await.is_err());
|
||||
|
||||
let mut margin_account = banks_client
|
||||
.get_account(margin_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut margin_account).into();
|
||||
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
// Test no borrow is in margin account
|
||||
assert_eq!(margin_account.borrows[borrow_token_index], 0);
|
||||
|
||||
//Test has_borrows flag is set correctly
|
||||
assert_eq!(margin_account.has_borrows, false);
|
||||
|
||||
let mut mango_group = banks_client
|
||||
.get_account(mango_group_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&mango_group_pk, &mut mango_group).into();
|
||||
|
||||
let mango_group = MangoGroup::load_mut_checked(
|
||||
&account_info,
|
||||
&program_id,
|
||||
)
|
||||
.unwrap();
|
||||
// Test nothing is added to total in mango group
|
||||
assert_eq!(mango_group.total_borrows[borrow_token_index], 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
// Tests related to depositing in a MangoGroup
|
||||
#![cfg(feature="test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use std::mem::size_of;
|
||||
use helpers::*;
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
signature::{Signer, Keypair},
|
||||
transaction::Transaction,
|
||||
account::Account,
|
||||
};
|
||||
|
||||
use mango::{
|
||||
entrypoint::process_instruction,
|
||||
instruction::{deposit, init_margin_account},
|
||||
state::MarginAccount,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_succeeds() {
|
||||
// Test that the deposit instruction succeeds and the expected side effects occurr
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"mango",
|
||||
program_id,
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let initial_amount = 2;
|
||||
let deposit_amount = 1;
|
||||
|
||||
// setup mango group
|
||||
let mango_group = add_mango_group_prodlike(&mut test, program_id);
|
||||
|
||||
// setup user account
|
||||
let user = Keypair::new();
|
||||
test.add_account(user.pubkey(), Account::new(u32::MAX as u64, 0, &user.pubkey()));
|
||||
|
||||
// setup user token accounts
|
||||
let user_account = add_token_account(
|
||||
&mut test,
|
||||
user.pubkey(),
|
||||
mango_group.mints[0].pubkey,
|
||||
initial_amount,
|
||||
);
|
||||
|
||||
// setup marginaccount account
|
||||
let margin_account_pk = Pubkey::new_unique();
|
||||
test.add_account(margin_account_pk, Account::new(u32::MAX as u64, size_of::<MarginAccount>(), &program_id));
|
||||
|
||||
// setup test harness
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey()),
|
||||
init_margin_account(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
).unwrap(),
|
||||
deposit(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
&user_account.pubkey,
|
||||
&mango_group.vaults[0].pubkey,
|
||||
deposit_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction succeeded
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
// Test expected amount is deducted from user wallet
|
||||
let final_user_balance = get_token_balance(&mut banks_client, user_account.pubkey).await;
|
||||
assert_eq!(final_user_balance, initial_amount - deposit_amount);
|
||||
|
||||
// Test expected amount is added to the vault
|
||||
let mango_vault_balance = get_token_balance(&mut banks_client, mango_group.vaults[0].pubkey).await;
|
||||
assert_eq!(mango_vault_balance, deposit_amount);
|
||||
|
||||
// Test expected amount is in margin account
|
||||
let mut margin_account = banks_client
|
||||
.get_account(margin_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut margin_account).into();
|
||||
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(margin_account.deposits[0], deposit_amount);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_fails_invalid_margin_account_owner() {
|
||||
// Test that the deposit instruction fails if the margin account owner is not the payer
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"mango",
|
||||
program_id,
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let initial_amount = 2;
|
||||
let deposit_amount = 3;
|
||||
|
||||
// setup mango group
|
||||
let mango_group = add_mango_group_prodlike(&mut test, program_id);
|
||||
|
||||
// setup user accounts
|
||||
let user = Keypair::new();
|
||||
test.add_account(user.pubkey(), Account::new(u32::MAX as u64, 0, &user.pubkey()));
|
||||
let other_user = Keypair::new();
|
||||
test.add_account(other_user.pubkey(), Account::new(u32::MAX as u64, 0, &other_user.pubkey()));
|
||||
|
||||
// setup user token accounts
|
||||
let user_account = add_token_account(
|
||||
&mut test,
|
||||
user.pubkey(),
|
||||
mango_group.mints[0].pubkey,
|
||||
initial_amount,
|
||||
);
|
||||
|
||||
// setup marginaccount account
|
||||
let margin_account_pk = Pubkey::new_unique();
|
||||
test.add_account(margin_account_pk, Account::new(u32::MAX as u64, size_of::<MarginAccount>(), &program_id));
|
||||
|
||||
// setup test harness
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey()),
|
||||
init_margin_account(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&other_user.pubkey(),
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &other_user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction succeeded
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
}
|
||||
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
deposit(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
&user_account.pubkey,
|
||||
&mango_group.vaults[0].pubkey,
|
||||
deposit_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction failed
|
||||
assert!(banks_client.process_transaction(transaction).await.is_err());
|
||||
|
||||
// Test no deductions from user wallet
|
||||
let final_user_balance = get_token_balance(&mut banks_client, user_account.pubkey).await;
|
||||
assert_eq!(final_user_balance, initial_amount);
|
||||
|
||||
// Test nothing is added to the vault
|
||||
let mango_vault_balance = get_token_balance(&mut banks_client, mango_group.vaults[0].pubkey).await;
|
||||
assert_eq!(mango_vault_balance, 0);
|
||||
|
||||
// Test nothing is in margin account
|
||||
let mut margin_account = banks_client
|
||||
.get_account(margin_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut margin_account).into();
|
||||
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(margin_account.deposits[0], 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// Tests related to initializing MangoGroups and MarginAccounts
|
||||
#![cfg(feature="test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use std::mem::size_of;
|
||||
use helpers::*;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
signature::{Signer, Keypair},
|
||||
transaction::Transaction,
|
||||
account::Account,
|
||||
};
|
||||
use solana_program::account_info::AccountInfo;
|
||||
|
||||
use mango::{
|
||||
entrypoint::process_instruction,
|
||||
instruction::init_margin_account,
|
||||
state::MarginAccount,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_init_mango_group() {
|
||||
// Mostly a test to ensure we can successfully create the testing harness
|
||||
// Also gives us an alert if the InitMangoGroup tx ends up using too much gas
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"mango",
|
||||
program_id,
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let mango_group = add_mango_group_prodlike(&mut test, program_id);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey()),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer],
|
||||
recent_blockhash,
|
||||
);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_init_margin_account() {
|
||||
// Test that we can create a MarginAccount
|
||||
// Also make sure that new accounts start with 0 balance
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"mango",
|
||||
program_id,
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let mango_group = add_mango_group_prodlike(&mut test, program_id);
|
||||
let margin_account_pk = Pubkey::new_unique();
|
||||
test.add_account(margin_account_pk, Account::new(u32::MAX as u64, size_of::<MarginAccount>(), &program_id));
|
||||
let user = Keypair::new();
|
||||
test.add_account(user.pubkey(), Account::new(u32::MAX as u64, 0, &user.pubkey()));
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey()),
|
||||
init_margin_account(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let mut account = banks_client.get_account(margin_account_pk).await.unwrap().unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut account).into();
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
).unwrap();
|
||||
for dep in &margin_account.deposits {
|
||||
assert_eq!(dep.to_bits(), 0);
|
||||
}
|
||||
for borrow in &margin_account.borrows {
|
||||
assert_eq!(borrow.to_bits(), 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
// Tests related to settling borrows in a MangoGroup
|
||||
#![cfg(feature="test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use std::mem::size_of;
|
||||
use helpers::*;
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
signature::{Signer, Keypair},
|
||||
transaction::Transaction,
|
||||
account::Account,
|
||||
};
|
||||
use mango::{
|
||||
entrypoint::process_instruction,
|
||||
instruction::{deposit, borrow, settle_borrow, init_margin_account},
|
||||
state::MarginAccount,
|
||||
state::MangoGroup,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_settle_borrow_succeeds() {
|
||||
// Test that the settle_borrow instruction succeeds and the expected side effects occurr
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"mango",
|
||||
program_id,
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let deposit_token_index = 0;
|
||||
let borrow_token_index = 1;
|
||||
let initial_amount = 2;
|
||||
let deposit_amount = 1;
|
||||
// 5x leverage
|
||||
let borrow_amount = (deposit_amount * PRICE_BTC * 5) / PRICE_ETH;
|
||||
|
||||
let mango_group = add_mango_group_prodlike(&mut test, program_id);
|
||||
let mango_group_pk = mango_group.mango_group_pk;
|
||||
|
||||
let user = Keypair::new();
|
||||
test.add_account(user.pubkey(), Account::new(u32::MAX as u64, 0, &user.pubkey()));
|
||||
|
||||
let user_account = add_token_account(
|
||||
&mut test,
|
||||
user.pubkey(),
|
||||
mango_group.mints[deposit_token_index].pubkey,
|
||||
initial_amount,
|
||||
);
|
||||
|
||||
let margin_account_pk = Pubkey::new_unique();
|
||||
test.add_account(margin_account_pk, Account::new(u32::MAX as u64, size_of::<MarginAccount>(), &program_id));
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
// setup mango group and make a deposit
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey()),
|
||||
init_margin_account(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
).unwrap(),
|
||||
deposit(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
&user_account.pubkey,
|
||||
&mango_group.vaults[deposit_token_index].pubkey,
|
||||
deposit_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction succeeded
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
}
|
||||
|
||||
// make a borrow
|
||||
{
|
||||
let mut margin_account = banks_client
|
||||
.get_account(margin_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut margin_account).into();
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
borrow(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
&margin_account.open_orders,
|
||||
mango_group.oracles.iter().map(|m| m.pubkey).collect::<Vec<Pubkey>>().as_slice(),
|
||||
borrow_token_index,
|
||||
borrow_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction succeeded
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
}
|
||||
|
||||
// settle the borrow
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
settle_borrow(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&margin_account_pk,
|
||||
&user.pubkey(),
|
||||
borrow_token_index,
|
||||
borrow_amount,
|
||||
).unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer, &user],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
// Test transaction succeeded
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let mut margin_account = banks_client
|
||||
.get_account(margin_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&margin_account_pk, &mut margin_account).into();
|
||||
|
||||
let margin_account = MarginAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
// Test borrow is removed from the margin account
|
||||
assert_eq!(margin_account.borrows[borrow_token_index], 0);
|
||||
|
||||
//Test has_borrows flag is set correctly
|
||||
assert_eq!(margin_account.has_borrows, false);
|
||||
|
||||
let mut mango_group = banks_client
|
||||
.get_account(mango_group_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&mango_group_pk, &mut mango_group).into();
|
||||
|
||||
let mango_group = MangoGroup::load_mut_checked(
|
||||
&account_info,
|
||||
&program_id,
|
||||
)
|
||||
.unwrap();
|
||||
// Test borrow is removed from the total in mango group
|
||||
assert_eq!(mango_group.total_borrows[borrow_token_index], 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,265 @@
|
|||
// Tests related to the SRM vault of a MangoGroup
|
||||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::*;
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program::instruction::InstructionError;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
account::Account,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
transaction::{Transaction, TransactionError},
|
||||
};
|
||||
use std::mem::size_of;
|
||||
|
||||
use mango::{
|
||||
entrypoint::process_instruction,
|
||||
error::MangoErrorCode,
|
||||
instruction::{deposit_srm, withdraw_srm},
|
||||
state::MangoSrmAccount,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_and_withdraw_srm() {
|
||||
// Test that Deposit and Withdraw works
|
||||
// Test that Withdraw fails if you withdraw more than deposit
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new("mango", program_id, processor!(process_instruction));
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let initial_amount = 500;
|
||||
let deposit_amount = 100;
|
||||
let withdraw_amount = 10;
|
||||
let excess_withdraw_amount = 1000;
|
||||
|
||||
let user = Keypair::new();
|
||||
let user_pk = user.pubkey();
|
||||
|
||||
let mango_group = add_mango_group_nosrm(&mut test, program_id);
|
||||
let mango_srm_account_pk = Pubkey::new_unique();
|
||||
test.add_account(
|
||||
mango_srm_account_pk,
|
||||
Account::new(u32::MAX as u64, size_of::<MangoSrmAccount>(), &program_id),
|
||||
);
|
||||
let user_srm_account = add_token_account(
|
||||
&mut test,
|
||||
user_pk,
|
||||
mango_group.srm_mint.pubkey,
|
||||
initial_amount,
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey()),
|
||||
deposit_srm(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&mango_srm_account_pk,
|
||||
&user_pk,
|
||||
&user_srm_account.pubkey,
|
||||
&mango_group.srm_vault.pubkey,
|
||||
deposit_amount,
|
||||
)
|
||||
.unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(&[&payer, &user], recent_blockhash);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let final_user_balance =
|
||||
get_token_balance(&mut banks_client, user_srm_account.pubkey).await;
|
||||
assert_eq!(final_user_balance, initial_amount - deposit_amount);
|
||||
let mango_vault_srm_balance =
|
||||
get_token_balance(&mut banks_client, mango_group.srm_vault.pubkey).await;
|
||||
assert_eq!(mango_vault_srm_balance, deposit_amount);
|
||||
|
||||
let mut mango_srm_account = banks_client
|
||||
.get_account(mango_srm_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&mango_srm_account_pk, &mut mango_srm_account).into();
|
||||
|
||||
let mango_srm_account = MangoSrmAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(mango_srm_account.amount, deposit_amount);
|
||||
}
|
||||
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[withdraw_srm(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&mango_srm_account_pk,
|
||||
&user_pk,
|
||||
&user_srm_account.pubkey,
|
||||
&mango_group.srm_vault.pubkey,
|
||||
&mango_group.signer_pk,
|
||||
withdraw_amount,
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&user, &payer], recent_blockhash);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let final_user_balance =
|
||||
get_token_balance(&mut banks_client, user_srm_account.pubkey).await;
|
||||
assert_eq!(
|
||||
final_user_balance,
|
||||
initial_amount - deposit_amount + withdraw_amount
|
||||
);
|
||||
let mango_vault_srm_balance =
|
||||
get_token_balance(&mut banks_client, mango_group.srm_vault.pubkey).await;
|
||||
assert_eq!(mango_vault_srm_balance, deposit_amount - withdraw_amount);
|
||||
|
||||
let mut mango_srm_account = banks_client
|
||||
.get_account(mango_srm_account_pk)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let account_info: AccountInfo = (&mango_srm_account_pk, &mut mango_srm_account).into();
|
||||
|
||||
let mango_srm_account = MangoSrmAccount::load_mut_checked(
|
||||
&program_id,
|
||||
&account_info,
|
||||
&mango_group.mango_group_pk,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(mango_srm_account.amount, deposit_amount - withdraw_amount);
|
||||
}
|
||||
|
||||
{
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[withdraw_srm(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&mango_srm_account_pk,
|
||||
&user_pk,
|
||||
&user_srm_account.pubkey,
|
||||
&mango_group.srm_vault.pubkey,
|
||||
&mango_group.signer_pk,
|
||||
excess_withdraw_amount,
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&user, &payer], recent_blockhash);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_and_withdraw_srm_fails_when_srm_in_group() {
|
||||
// Test that Deposit and Withdraw fails when SRM is one of the tokens in the group
|
||||
// Test that Withdraw fails if you withdraw more than deposit
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let mut test = ProgramTest::new("mango", program_id, processor!(process_instruction));
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(50_000);
|
||||
|
||||
let initial_amount = 500;
|
||||
let deposit_amount = 100;
|
||||
let withdraw_amount = 10;
|
||||
|
||||
let user = Keypair::new();
|
||||
let user_pk = user.pubkey();
|
||||
|
||||
let mango_group = add_mango_group_prodlike(&mut test, program_id);
|
||||
let mango_srm_account_pk = mango_group.srm_vault.pubkey;
|
||||
|
||||
let user_srm_account = add_token_account(
|
||||
&mut test,
|
||||
user_pk,
|
||||
mango_group.srm_mint.pubkey,
|
||||
initial_amount,
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
mango_group.init_mango_group(&payer.pubkey())
|
||||
],
|
||||
Some(&payer.pubkey())
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&[&payer],
|
||||
recent_blockhash,
|
||||
);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let mut transaction_deposit = Transaction::new_with_payer(
|
||||
&[
|
||||
deposit_srm(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&mango_srm_account_pk,
|
||||
&user_pk,
|
||||
&user_srm_account.pubkey,
|
||||
&mango_group.srm_vault.pubkey,
|
||||
deposit_amount,
|
||||
)
|
||||
.unwrap(),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction_deposit.sign(&[&payer, &user], recent_blockhash);
|
||||
assert_eq!(
|
||||
banks_client
|
||||
.process_transaction(transaction_deposit)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.unwrap(),
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::Custom(
|
||||
MangoErrorCode::FeeDiscountFunctionality.into()))
|
||||
);
|
||||
|
||||
let mut transaction_withdraw = Transaction::new_with_payer(
|
||||
&[withdraw_srm(
|
||||
&program_id,
|
||||
&mango_group.mango_group_pk,
|
||||
&mango_srm_account_pk,
|
||||
&user_pk,
|
||||
&user_srm_account.pubkey,
|
||||
&mango_group.srm_vault.pubkey,
|
||||
&mango_group.signer_pk,
|
||||
withdraw_amount,
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction_withdraw.sign(&[&user, &payer], recent_blockhash);
|
||||
assert_eq!(
|
||||
banks_client
|
||||
.process_transaction(transaction_withdraw)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.unwrap(),
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::Custom(
|
||||
MangoErrorCode::FeeDiscountFunctionality.into()))
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue