Compare commits

...

77 Commits
v0.2.4 ... main

Author SHA1 Message Date
Maximilian Schneider 0d67dc1361 upgrade to switchboard v2 2023-11-15 12:16:16 +00:00
Maximilian Schneider 30e2bb4fec update switchboard oracle acc length 2023-11-14 14:46:31 +00:00
Maximilian Schneider eeddeb011c update switchboard oracle acc length 2023-11-14 14:17:32 +00:00
Maximilian Schneider 9a5a50efb7 add switchboard oracle 2023-11-14 13:31:09 +00:00
Maximilian Schneider 1cd9a9dd3f release v2.2.1 2022-08-17 16:28:13 +02:00
Maximilian Schneider 663d909d38 fix overflow issue due to larger oracle decimals 2022-08-17 16:25:21 +02:00
Maximilian Schneider a96083d1f9 update README and release 2.2.0 2022-08-16 22:34:00 +02:00
Maximilian Schneider 234d8e2682
support pyth oracle to deprecate flux aggregator (#33)
* add pyth client

* add pyth oracle and instruction to switch

* implement cli for oracle switch

* fix common build

* fix build for cli

* fix devnet url

* rename arg
2022-08-02 16:59:07 +02:00
dd 141b10577b Removed common library dependency from mango program; Added ability to deposit into other users' accounts for reimbursements; Added settle borrow at end of deposit 2021-08-27 12:15:38 -04:00
dafyddd fcfc26dba9
Merge pull request #31 from armaniferrante/publish
Add Anchor.toml for publishing verifiable source
2021-08-12 16:43:15 -04:00
armaniferrante 63d9eab005
Add Anchor.toml for publishing verifiable source 2021-08-11 01:41:51 -07:00
dafyddd 5556f483f9
Merge pull request #30 from blockworks-foundation/interest_change
Interest change
2021-07-04 11:27:38 -04:00
dd 8d49dc63ff updated version 2021-07-04 11:24:01 -04:00
dd 7a9af58186 interest function changed 2021-07-04 10:44:18 -04:00
dafyddd 02ca658620
Merge pull request #28 from blockworks-foundation/dd/gui-hoster
Dd/gui hoster
2021-06-12 16:38:36 -04:00
dd f4b60d7946 updated version num 2021-06-12 16:38:05 -04:00
dd df14fb62a2 got rid of the last_update check in partial_liquidate because devnet sucks 2021-06-12 16:32:59 -04:00
dd 5b3078cda1 updated assets calculations 2021-06-12 11:30:47 -04:00
dd 328756de3e fixed invoke_settle 2021-06-11 10:10:36 -04:00
dd 4d79e88ff6 gui hoster fee now goes to user's margin account; partial_liquidate will update indexes if invalid 2021-06-11 10:06:56 -04:00
dd 1c09e227d9 changed version number to 2.0.0 2021-06-09 11:37:12 -04:00
dd 9f62096058 updated version numbers to 1.0.0; added code in partial_liquidate to update indexes but not yet uncommented; 2021-06-09 11:37:12 -04:00
dd a5c18a6ccc reduce size of MarginAccount to keep it consistent with previous version; change location of info array 2021-06-09 11:37:12 -04:00
dd e4fe8e7084 Added AddMarginAccountInfo 2021-06-09 11:37:12 -04:00
dd c685014017 cleaned up for mainnet and cargo updated 2021-06-09 11:37:12 -04:00
dd 4acc2dc206 added todo 2021-06-09 11:37:12 -04:00
dd ec5c1862c3 got rid of update_indexes in partial_liquidate; not ready for prod because DUST_THRESHOLD is artificially high 2021-06-09 11:37:12 -04:00
dd d4fe5c879e bad code; WILL THROW STACK FRAME ERROR IN PROD 2021-06-09 11:37:12 -04:00
dd 43e5e83f13 fixed logging for partial_liquidation and deprecated liquidate() function. partial_liquidate() is not tested but compiles 2021-06-09 11:37:12 -04:00
dd 690ad95c99 changed devnet_deploy.sh for 5_tokens, did cargo update, made cli work for deploying new group 2021-06-09 11:37:12 -04:00
dd 2dc272b1cf moved the logging to one function in state.rs 2021-06-09 11:37:12 -04:00
Nicholas Clarke ae1b688cf8 Fix to incorrect merge (duplicated msg calls). 2021-06-09 11:37:12 -04:00
Nicholas Clarke 874076dcf5 Refactor partial_liquidate and liquidate to reduce compute. 2021-06-09 11:37:12 -04:00
Nicholas Clarke 5bfffa1f8d Log liquidation details (assets, liabs, prices, collateral ratio) at the start of the liquidation and partial liquidation transactions. 2021-06-09 11:37:12 -04:00
Riordan Panayides e86e43268b update tests to work with 5 tokens, add tests for disabled serum deposit/widthdraw 2021-06-09 11:37:12 -04:00
Riordan Panayides 7d895dcf66 increase marginaccount padding 2021-06-09 11:37:12 -04:00
Riordan Panayides ef0c45dc36 Fix return type, add assertions to borrow tests 2021-06-09 11:33:04 -04:00
Riordan Panayides 61790d6850 Fix Ok call 2021-06-09 11:33:04 -04:00
Riordan Panayides 81afd8bca7 Update comment 2021-06-09 11:33:04 -04:00
Riordan Panayides 85f64e9af9 Ensure errors in mango_group checked borrow funcs are propogated, return Ok on success 2021-06-09 11:33:04 -04:00
Riordan Panayides 8a76b9809d Use a better function for removing borrows, add quantity check to add_borrow 2021-06-09 11:33:04 -04:00
Riordan Panayides 2468a83232 Move borrow flag setting to lower order function 2021-06-09 11:33:04 -04:00
Riordan Panayides 7de3e4d65c Add has_borrows flag to margin account 2021-06-09 11:33:04 -04:00
Ralfs 0b7c6481fc Reuse SRM vault if it's in the group 2021-06-09 11:33:04 -04:00
Ralfs 0e0741ee4e Deposit/Withdraw SRM checks 2021-06-09 11:33:04 -04:00
Ralfs 64c16ba0c6 Change the NUM_TOKENS constant 2021-06-09 11:33:04 -04:00
Nicholas Clarke 8af143c35a
Log liquidation details (assets, liabs, prices, collateral ratio) at the start of the liquidation and partial liquidation transactions. (#25) 2021-05-17 12:21:00 +03:00
dafyddd 708c215b50
Merge pull request #24 from blockworks-foundation/interest_logs
Interest logs
2021-05-12 12:39:05 -04:00
dd e2483baba2 Merge remote-tracking branch 'origin/interest_logs' into interest_logs
# Conflicts:
#	cli/Cargo.lock
#	cli/new_group.md
#	program/Cargo.lock
2021-05-12 12:37:36 -04:00
dd 0aebcc3756 rebased onto master 2021-05-12 12:37:16 -04:00
dd d3d7c254b3 Added more checking 2021-05-12 12:36:15 -04:00
dd a96eff6ed0 added more checked math 2021-05-12 12:36:11 -04:00
dd 532aa92c9b Updated interest rates to account for added risk at the 100% bound. 2021-05-12 12:36:07 -04:00
Riordan Panayides 0dbe98fefe
Add basic borrow tests (#22)
* Add a test for borrow instruction happy path
* Add a test for borrow instruction failure when the borrow would exceed the user's leverage limit
* Add a test for settle_borrow instruction happy path
2021-05-07 17:35:46 +03:00
Riordan Panayides cbc527f587
Add basic Travis CI config 2021-05-05 17:23:07 +01:00
kevlubkcm 5d28cef78c
Test to make sure SRM withdrawal fails if quantity is larger than balance (#21)
* add test for withdrawal failure

Co-authored-by: Kevin Lu <kevin@bkcm.co>
2021-05-05 17:26:42 +03:00
Riordan Panayides 1ff2ec9e5c
Add deposit tests (#20) 2021-05-05 17:26:17 +03:00
Maximilian Schneider fc67807f3c
test srm withdrawal (#19)
* correctly setup signing to prevent silent TX rejection
* check balances & run rustfmt
2021-05-05 04:35:33 +03:00
kevlubkcm 971a8df0b8
Add test for IniMarginAccount (#18)
* split out srm tests. add init margin account test

Co-authored-by: Kevin Lu <kevin@bkcm.co>
2021-05-03 09:54:33 +03:00
kevlubkcm b4e360f037
Test harness refactor and DepositSrm test (#17)
* refactoring test harness into TestMangoGroup
* move mango group init code into helper
* add deposit srm test
* test flags and readme update

Co-authored-by: Kevin Lu <kevin@bkcm.co>
2021-05-01 01:38:24 +03:00
dd 734e9500d7 clarified new_group.md some more 2021-04-30 11:28:33 -04:00
Maximilian Schneider b7b85ab75c update serum_dex commit hash in cargo.lock 2021-04-30 10:28:30 +03:00
kevlubkcm ef9eb55f18
InitMangoGroup basic test (#15)
* also need to install libssl-dev to get a clean build
* additional deps for spl-token-cli
* also update solana version
* dex mock
* update readme

Co-authored-by: Kevin Lu <kevin@bkcm.co>
2021-04-30 01:11:09 +03:00
dd 2fc5289c21 SOL and SRM added to devnet.env 2021-04-28 15:12:22 -04:00
dd 9a10391005 new_group.md fixed crank isntruction 2021-04-28 12:05:14 -04:00
dd d5f8e96efd Added more checking 2021-04-26 13:52:36 -04:00
dd 2b7e619f48 added more checked math 2021-04-26 13:52:36 -04:00
dd 55883ef873 Added help on how to deploy a new group 2021-04-26 13:52:36 -04:00
dd 565b43c3be Added help on how to deploy a new group 2021-04-26 13:52:36 -04:00
dd 48095b30bd Added more help to devnet_deploy.sh 2021-04-26 13:52:36 -04:00
dd 884fe87660 Fixed settle borrow rounding error 2021-04-26 13:52:36 -04:00
dd 476b84a5eb Added help on how to deploy a new group 2021-04-26 10:05:52 -04:00
dd 16f9cad69f Added help on how to deploy a new group 2021-04-26 10:02:00 -04:00
dd 369e96c3d5 Added more help to devnet_deploy.sh 2021-04-26 00:59:55 -04:00
dafyddd 896a3c66ad
Merge pull request #14 from blockworks-foundation/borrow_rounding
Fixed settle borrow rounding error
2021-04-22 12:54:07 -04:00
dd a28d3923d9 Fixed settle borrow rounding error 2021-04-22 12:51:21 -04:00
dd 1fb1cc5eba Updated interest rates to account for added risk at the 100% bound. 2021-04-22 10:57:52 -04:00
26 changed files with 7166 additions and 2572 deletions

21
.travis.yml Normal file
View File

@ -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

11
Anchor.toml Normal file
View File

@ -0,0 +1,11 @@
anchor_version = "0.13.2"
[workspace]
members = ["program"]
[provider]
cluster = "mainnet"
wallet = "~/.config/solana/id.json"
[programs.mainnet]
mango = "5fNfvyp5czQVX77yoACa3JJVEhdRaWjPuazuWgjhTqEH"

View File

@ -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)
```

3288
cli/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"]}

View File

@ -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)

View File

@ -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 \

42
cli/new_group.md Normal file
View File

@ -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

View File

@ -17,7 +17,7 @@ use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::program_pack::Pack;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Signer};
use mango::instruction::{init_mango_group, init_margin_account, withdraw, borrow, deposit, settle_borrow, change_borrow_limit};
use mango::instruction::{init_mango_group, init_margin_account, withdraw, borrow, deposit, settle_borrow, change_borrow_limit, switch_oracles};
#[derive(Clap, Debug)]
pub struct Opts {
@ -136,7 +136,19 @@ pub enum Command {
token_symbol: String,
#[clap(long)]
borrow_limit: f64
}
},
SwitchOracle {
#[clap(long, short)]
payer: String, // assumes for now payer is same as admin
#[clap(long, short)]
ids_path: String,
#[clap(long)]
mango_group_name: String,
#[clap(long)]
pair_name: String,
#[clap(long)]
oracle: String,
},
}
impl Opts {
@ -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> {

988
common/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

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

3039
program/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -1,2 +0,0 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -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],

View File

@ -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,

View File

@ -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
})
}

View File

@ -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()))
}

View File

@ -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

View File

@ -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
}

View File

@ -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);
}
}

View File

@ -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);
}
}

111
program/tests/test_init.rs Normal file
View File

@ -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);
}
}

View File

@ -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);
}
}

265
program/tests/test_srm.rs Normal file
View File

@ -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()))
);
}