├── .cargo └── audit.toml ├── .github └── workflows │ ├── ci-cargo-audit.yml │ ├── ci-lint-test.yml │ ├── ci-soteria.yml │ └── ci-uxd.yml ├── .gitignore ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── audit.pdf ├── changelog.md ├── common ├── Cargo.toml └── src │ └── lib.rs ├── consumed_per_instruction_uniq.log ├── cu_per_ix.sh ├── mango-logs ├── Cargo.toml └── src │ └── lib.rs ├── mango-macro ├── Cargo.toml └── src │ └── lib.rs ├── program ├── Cargo.toml ├── README.md ├── devnet_deploy.sh ├── src │ ├── entrypoint.rs │ ├── error.rs │ ├── ids.rs │ ├── instruction.rs │ ├── lib.rs │ ├── matching.rs │ ├── oracle.rs │ ├── processor.rs │ ├── queue.rs │ ├── state.rs │ └── utils.rs └── tests │ ├── fixtures │ └── serum_dex.so │ ├── program_test │ ├── assertions.rs │ ├── cookies.rs │ ├── mod.rs │ └── scenarios.rs │ ├── test_borrow_withdraw.rs │ ├── test_compute.rs │ ├── test_consume_events.rs │ ├── test_create_account.rs │ ├── test_delegate.rs │ ├── test_deposit.rs │ ├── test_funding_rate.rs │ ├── test_init.rs │ ├── test_instructions.rs │ ├── test_interest_rate.rs │ ├── test_liquidation_delisting_token.rs │ ├── test_liquidation_perp_market.rs │ ├── test_liquidation_perp_market_max_cu.rs │ ├── test_liquidation_token_and_perp.rs │ ├── test_liquidation_token_and_perp_max_cu.rs │ ├── test_liquidation_token_and_token.rs │ ├── test_liquidation_token_and_token_max_cu.rs │ ├── test_misc.rs │ ├── test_perp_markets.rs │ ├── test_perp_trigger_orders.rs │ ├── test_place_perp_order.rs │ ├── test_sanity.rs │ ├── test_spot_market_mode_closeonly.rs │ ├── test_spot_markets.rs │ ├── test_update_banks.rs │ └── test_worst_case.rs ├── proposals.md └── rustfmt.toml /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | # All of the options which can be passed via CLI arguments can also be 2 | # permanently specified in this file. 3 | 4 | # RUSTSEC-2020-0071, RUSTSEC-2020-0159 and RUSTSEC-2021-0124 are low severity vulnerable upstream Solana crates. Ignored for now. 5 | # RUSTSEC-2022-0006 has been reported upstream 6 | 7 | [advisories] 8 | ignore = ["RUSTSEC-2020-0071","RUSTSEC-2020-0159", "RUSTSEC-2021-0124", "RUSTSEC-2022-0006"] # advisory IDs to ignore e.g. ["RUSTSEC-2019-0001", ...] 9 | informational_warnings = ["unmaintained"] # warn for categories of informational advisories 10 | severity_threshold = "medium" # CVSS severity ("none", "low", "medium", "high", "critical") 11 | 12 | # Advisory Database Configuration 13 | [database] 14 | path = "~/.cargo/advisory-db" # Path where advisory git repo will be cloned 15 | url = "https://github.com/RustSec/advisory-db.git" # URL to git repo 16 | fetch = true # Perform a `git fetch` before auditing (default: true) 17 | stale = false # Allow stale advisory DB (i.e. no commits for 90 days, default: false) 18 | 19 | # Output Configuration 20 | [output] 21 | deny = [] # exit on error if unmaintained dependencies are found 22 | format = "terminal" # "terminal" (human readable report) or "json" 23 | quiet = false # Only print information on error 24 | show_tree = true # Show inverse dependency trees along with advisories (default: true) 25 | 26 | # Target Configuration 27 | [target] 28 | arch = "x86_64" # Ignore advisories for CPU architectures other than this one 29 | os = "linux" # Ignore advisories for operating systems other than this one 30 | 31 | [yanked] 32 | enabled = false # Warn for yanked crates in Cargo.lock (default: true) 33 | update_index = true # Auto-update the crates.io index (default: true) 34 | -------------------------------------------------------------------------------- /.github/workflows/ci-cargo-audit.yml: -------------------------------------------------------------------------------- 1 | # CI job for scanning Cargo dependencies for vulnerabilities and report/fail job based on criticality. 2 | # Critically vulnerable dependencies with fix available will mark the run as failed (X) 3 | 4 | name: Cargo Audit 5 | 6 | on: 7 | push: 8 | branches: main 9 | pull_request: 10 | branches: main 11 | 12 | # Optimisation option by targeting direct paths to only scan when there are changes to dependencies in the push/PR 13 | # Can also be used to limit testing to specific programs 14 | 15 | # push: 16 | # paths: 17 | # - 'Cargo.toml' 18 | # - 'Cargo.lock' 19 | # pull_request: 20 | # paths: 21 | # - 'Cargo.toml' 22 | # - 'Cargo.lock' 23 | 24 | # Example of running scheduled scans at 6AM UTC every Monday to regularly check for vulnerable dependencies 25 | # schedule: 26 | # - cron: '0 6 * * 1' 27 | 28 | # Run the job 29 | jobs: 30 | cargo-audit: 31 | name: Cargo Vulnerability Scanner 32 | runs-on: ubuntu-latest 33 | steps: 34 | # Check out GitHub repo 35 | - uses: actions/checkout@v2 36 | 37 | # Install cargo audit 38 | - name: Install Cargo Audit 39 | uses: actions-rs/install@v0.1 40 | with: 41 | crate: cargo-audit 42 | version: latest 43 | 44 | # Run cargo audit using args from .cargo/audit.toml (ignores, etc.) 45 | - name: Run Cargo Audit 46 | run: cargo audit -c always 47 | -------------------------------------------------------------------------------- /.github/workflows/ci-lint-test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: 4 | push: 5 | branches: [main, v*.*] 6 | pull_request: 7 | branches: [main, v*.*] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | SOLANA_VERSION: "1.9.1" 12 | RUST_TOOLCHAIN: nightly-2021-12-15 13 | 14 | defaults: 15 | run: 16 | working-directory: ./ 17 | 18 | jobs: 19 | lint: 20 | name: Run ftm and clippy 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Install Rust nightly 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | override: true 29 | profile: minimal 30 | toolchain: ${{ env.RUST_TOOLCHAIN }} 31 | components: rustfmt, clippy 32 | - name: Cache dependencies 33 | uses: Swatinem/rust-cache@v1 34 | 35 | - name: Run fmt 36 | run: cargo fmt -- --check 37 | # The style and complexity lints have not been processed yet. 38 | - name: Run clippy 39 | run: cargo clippy -- --deny=warnings --allow=clippy::style --allow=clippy::complexity 40 | 41 | tests: 42 | name: Run tests 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - uses: actions/checkout@v2 47 | - name: Install Linux dependencies 48 | run: | 49 | sudo apt-get update 50 | sudo apt-get install -y pkg-config build-essential libudev-dev 51 | - name: Install Rust nightly 52 | uses: actions-rs/toolchain@v1 53 | with: 54 | override: true 55 | profile: minimal 56 | toolchain: ${{ env.RUST_TOOLCHAIN }} 57 | - name: Cache dependencies 58 | uses: Swatinem/rust-cache@v1 59 | 60 | # Install Solana 61 | - name: Cache Solana binaries 62 | uses: actions/cache@v2 63 | with: 64 | path: ~/.cache/solana 65 | key: ${{ runner.os }}-${{ env.SOLANA_VERSION }} 66 | - name: Install Solana 67 | run: | 68 | sh -c "$(curl -sSfL https://release.solana.com/v${{ env.SOLANA_VERSION }}/install)" 69 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 70 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 71 | solana --version 72 | echo "Generating keypair..." 73 | solana-keygen new -o "$HOME/.config/solana/id.json" --no-passphrase --silent 74 | 75 | - name: Run unit tests 76 | run: cargo test --lib 77 | - name: Build program 78 | run: cargo build-bpf 79 | - name: Run tests 80 | run: cargo test-bpf 81 | -------------------------------------------------------------------------------- /.github/workflows/ci-soteria.yml: -------------------------------------------------------------------------------- 1 | name: Soteria Scan 2 | 3 | on: 4 | push: 5 | branches: [main, v*.*] 6 | pull_request: 7 | branches: [main, v*.*] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | SOLANA_VERSION: "1.9.5" 12 | 13 | jobs: 14 | build: 15 | name: Soteria 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Check-out repo 19 | uses: actions/checkout@v2 20 | 21 | - name: Cache Solana binaries 22 | uses: actions/cache@v2 23 | id: solana-cache 24 | with: 25 | path: | 26 | ~/.cache/solana 27 | ~/.local/share/solana 28 | ~/.rustup 29 | key: solana-${{ env.SOLANA_VERSION }} 30 | 31 | - name: Cache build dependencies 32 | uses: Swatinem/rust-cache@v1 33 | with: 34 | target-dir: .coderrect/build 35 | 36 | - name: Install Solana 37 | if: steps.solana-cache.outputs.cache-hit != 'true' 38 | run: | 39 | echo Installing Solana v${{ env.SOLANA_VERSION }}... 40 | sh -c "$(curl -sSfL https://release.solana.com/v${{ env.SOLANA_VERSION }}/install)" 41 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 42 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 43 | echo Installing bpf toolchain... 44 | (cd /home/runner/.local/share/solana/install/active_release/bin/sdk/bpf/scripts; ./install.sh) 45 | shell: bash 46 | 47 | - name: Install Soteria 48 | run: | 49 | echo Installing Soteria... 50 | sh -c "$(curl -k https://supercompiler.xyz/install)" 51 | export PATH=$PWD/soteria-linux-develop/bin/:$PATH 52 | echo "$PWD/soteria-linux-develop/bin" >> $GITHUB_PATH 53 | shell: bash 54 | 55 | - name: Run Soteria # Not failing for the time being 56 | run: soteria -analyzeAll . || exit 0 57 | shell: bash 58 | -------------------------------------------------------------------------------- /.github/workflows/ci-uxd.yml: -------------------------------------------------------------------------------- 1 | # name: UXD Composability testing 2 | 3 | # on: 4 | # push: 5 | # branches: main 6 | # pull_request: 7 | # branches: [main, v*.*] 8 | # workflow_dispatch: 9 | 10 | # permissions: 11 | # contents: read 12 | # pull-requests: write 13 | 14 | # env: 15 | # CARGO_TERM_COLOR: always 16 | # SOLANA_VERSION: 1.9.7 17 | # ANCHOR_VERSION: 0.21.0 18 | # RUST_TOOLCHAIN: nightly-2021-12-15 19 | 20 | # jobs: 21 | 22 | # cargo-tests: 23 | # runs-on: ubuntu-latest 24 | # name: Run tests 25 | # steps: 26 | # # Checkout Mango-v3 Repo 27 | # - uses: actions/checkout@v2 28 | # # Checkout UXDProtocol/UXD-Program 29 | # - name: Checkout @UXDProtocol/UXD-Program, install dependencies 30 | # uses: actions/checkout@v2 31 | # with: 32 | # repository: UXDProtocol/uxd-program 33 | # ref: main 34 | # # GitHub's personal access token with access to repository 35 | # token: ${{ secrets.MANGO_CI_UXD }} 36 | # persist-credentials: false 37 | # path: ./uxd-program 38 | # # Installs Rust 39 | # - name: Cache Rust 40 | # uses: Swatinem/rust-cache@v1 41 | # - name: Rust toolchain installation 42 | # uses: actions-rs/toolchain@v1 43 | # with: 44 | # toolchain: ${{ env.RUST_TOOLCHAIN }} 45 | # override: true 46 | # profile: minimal 47 | # - name: Rust toolchain update 48 | # run: | 49 | # rustup update 50 | # - name: Rust override set nightly 51 | # run: | 52 | # rustup override set nightly 53 | # - name: Cache dependencies 54 | # uses: Swatinem/rust-cache@v1 55 | # # Install Solana 56 | # - name: Install Linux dependencies 57 | # run: | 58 | # sudo apt-get update 59 | # sudo apt-get install -y pkg-config build-essential libudev-dev 60 | # - name: Cache Solana binaries 61 | # uses: actions/cache@v2 62 | # with: 63 | # path: ~/.cache/solana 64 | # key: ${{ runner.os }}-${{ env.SOLANA_VERSION }} 65 | # - name: Install Solana 66 | # run: | 67 | # sh -c "$(curl -sSfL https://release.solana.com/v${{ env.SOLANA_VERSION }}/install)" 68 | # echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 69 | # export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 70 | # solana --version 71 | # echo "Generating keypair..." 72 | # solana-keygen new -o "$HOME/.config/solana/id.json" --no-passphrase --silent 73 | # # Run tests 74 | # - name: Move to UXD Program folder and replace the dependency to local mango 75 | # run: | 76 | # cd ./uxd-program 77 | # sed -i.bak 's/^mango.*/mango={path="..\/..\/..\/program\/",features=["no-entrypoint"]}/' ./programs/uxd/Cargo.toml 78 | # - name: Run unit tests 79 | # run: cargo test --manifest-path ./uxd-program/programs/uxd/Cargo.toml 80 | # - name: Build program 81 | # run: cargo build-bpf --manifest-path ./uxd-program/programs/uxd/Cargo.toml 82 | # - name: Run tests 83 | # run: cargo test-bpf --manifest-path ./uxd-program/programs/uxd/Cargo.toml 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | program/target 2 | common/target 3 | mango-macro/target 4 | target 5 | 6 | .idea 7 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | anchor_version = "0.24.2" 2 | 3 | [workspace] 4 | members = [ 5 | "common", 6 | "mango-macro", 7 | "program", 8 | "mango-logs" 9 | ] 10 | 11 | [provider] 12 | cluster = "mainnet" 13 | wallet = "~/.config/solana/id.json" 14 | 15 | [programs.mainnet] 16 | mango = "mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68" 17 | 18 | [programs.devnet] 19 | mango = "4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA" 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "common", 4 | "mango-macro", 5 | "program", 6 | "mango-logs" 7 | ] 8 | 9 | [profile.release] 10 | lto = true 11 | codegen-units = 1 12 | 13 | [profile.release.build-override] 14 | opt-level = 3 15 | incremental = false 16 | codegen-units = 1 17 | 18 | #[profile.test] 19 | #lto = true 20 | #codegen-units = 1 21 | # 22 | #[profile.test.build-override] 23 | #opt-level = 3 24 | #incremental = false 25 | #codegen-units = 1 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Blockworks Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mango Markets V3 - Decentralized Margin Trading 2 | 3 | ## ⚠️ Warning 4 | Any content produced by Blockworks, or developer resources that Blockworks provides, are for educational and inspiration purposes only. Blockworks does not encourage, induce or sanction the deployment of any such applications in violation of applicable laws or regulations. 5 | 6 | ## Contribute 7 | Significant contributions to the source code may be compensated with a grant from the Blockworks Foundation. 8 | 9 | ## Security 10 | Mango has been audited by Neodyme, you can find the report [here](./audit.pdf). 11 | 12 | You will be compensated by the Mango DAO for privately reporting vulnerabilities to the maintainers of this repo. 13 | Email hello@blockworks.foundation 14 | 15 | ## Branches and Tags 16 | - New development happens on `main`. Pull requests should always target `main` by default. 17 | - Release branches have names like `release/3.3.0` and are branched off of `main` or a parent release branch. 18 | 19 | - Bugfixes for releases should usually be merged into `main` and then be cherry-picked to 20 | the release branch. 21 | - Only push to release branches after talking to the release owner. 22 | - When a release is done, the released version is tagged and the release branch is 23 | deleted. 24 | - Release tags have names like `v3.3.1`. 25 | - To know what is deployed on mainnet, check the [Anchor Builds](https://anchor.projectserum.com/program/mv3ekLzLbnVPNxjSKvqBpU3ZeZXPQdEC3bp5MDEBG68). 26 | -------------------------------------------------------------------------------- /audit.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blockworks-foundation/mango-v3/c4d52dc7f08e9ba4ae13728d0a2f1c298c0c4a86/audit.pdf -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Mango Program Change Log 2 | 3 | ## v3.7.0 4 | Deployed: Oct 20, 2022 at 15:54:54 UTC | Slot: 156387768 5 | 1. Add RecoveryForceSettleSpotOrders instruction 6 | 2. Add RecoveryWithdrawTokenVault instruction 7 | 3. Add RecoveryWithdrawMngoVault instruction 8 | 4. Add RecoveryWithdrawInsuranceVault instruction 9 | 5. Re-enable WithdrawMsrm 10 | 11 | ## v3.6.3 12 | Deployed: Oct 12, 2022 at 02:37:32 UTC | Slot: 154889307 13 | 1. Disable all instructions 14 | 15 | ## v3.6.1 16 | Deployed: Sep 8, 2022 at 21:50:42 UTC | Slot: 149,790,592 17 | 1. Fix perp health simulation for advanced order execution 18 | 19 | ## v3.6.0 20 | Deployed: Sep 3, 2022 at 23:05:59 UTC | Slot: 148,862,380 21 | 1. Fix remove_oracle bug 22 | 2. Add second fee tier option with new instruction ChangeReferralFeeParams2 23 | 3. Allow delegate to close spot open orders accounts 24 | 25 | ## v3.5.1 26 | Deployed: Jul 9, 2022 at 17:07:37 UTC | Slot: 140,876,554 27 | 1. Remove luna related hard codings 28 | 2. Fix deposit leach bug 29 | 3. Fix bugs in ForceSettlePerpPosition 30 | 31 | ## v3.5.0 32 | Deployed: Jun 27, 2022 at 18:30:08 UTC | Slot: 139,265,834 33 | 1. Add an ExpiryType argument to PlacePerpOrder2 34 | 2. Add delisting instructions 35 | 3. Add Withdraw2 which passes in compact open orders 36 | 37 | ## v3.4.7 38 | Deployed: May 14, 2022 at 21:27:20 UTC | Slot: 133,813,868 39 | 1. Fix overflow in SettlePnl 40 | 2. Fix 0 base quantity trade for perps 41 | 42 | ## v3.4.6 43 | Deployed: May 13, 2022 at 17:09:10 UTC | Slot: 133,663,345 44 | 1. Force reduce only for perp market advanced order 45 | 2. Allow LUNA-PERP bids to be as high as 9c 46 | 3. Fix LUNA spot orders to be reduce only 47 | 4. Fix LUNA-PERP reduce only to take into account open orders 48 | 5. Allow LUNA spot bids to be as high as 9c 49 | 50 | ## v3.4.5 51 | Deployed: May 12, 2022 at 14:29:36 UTC | Slot: 133,529,809 52 | 1. LUNA perp market has been moved to reduce only. Only orders with reduce_only flag set will go through 53 | 2. LUNA deposits are only allowed if you're offsetting a borrow 54 | 3. LUNA borrows not allowed 55 | 4. LUNA spot orders only allowed if reducing position 56 | 5. LUNA price cache updates ignore confidence interval 57 | 58 | ## v3.4.4 59 | Deployed: Apr 21, 2022 at 19:16:14 UTC | Slot: 130,742,427 60 | 1. Update serum-dex package and allow for max_ts param 61 | 2. Allow solana libs beyond 1.10 62 | 3. Update anchor to 0.24.2 63 | 4. Update all packages 64 | 5. Mark RootBank and MangoCache as writable when passed into Deposit instruction 65 | 6. CancelAllSpotOrders 66 | 7. Add more checked math 67 | 8. Clean up tests and speed them up 68 | 69 | ## v3.4.3 70 | Deployed: Apr 3, 2022 at 19:00:13 UTC | Slot: 128,066,047 71 | 1. SettleFees will return Ok and fail silently if not pnl negative and fees accrued positive 72 | 2. Use pyth_client crate instead of copy pasta 73 | 3. Remove pyth status check and just rely on confidence intervals 74 | 75 | ## v3.4.2 76 | Deployed: Mar 19, 2022 at 16:22:04 UTC | Slot: 125,699,230 77 | 1. Increase PriceCache and PerpMarketCache validity by 2x 78 | 2. Upgrade anchor to 0.22.1 and upgrade all other packages 79 | 3. Move interest rate calculation outside of RootBank 80 | 81 | ## v3.4.1 82 | Deployed: Feb 28, 2022 at 15:43:29 UTC | Slot: 122,878,778 83 | 1. Fix div by zero bug in market order sells 84 | 85 | ## v3.4.0 86 | Deployed: Feb 28, 2022 at 13:57:00 UTC | Slot: 122,868,568 87 | 1. Breaking change: Orders on the perp orderbook can now expire. 88 | Either use the iterator that returns only valid orders or manually filter out invalid orders. 89 | 2. New instruction: PlacePerpOrder2 90 | - can set an expiry timestamp 91 | - can have a quote quantity limit 92 | - limits the depth of orderbook iteration 93 | 3. Reduce heap memory use of event logging: ConsumeEvents limit raised back to 8 94 | 95 | ## v3.3.5 96 | Deployed: Feb 11, 2022 at 17:36:15 UTC | Slot: 120,380,891 97 | 1. reduce consume_events limit to 4 to prevent memory issues 98 | 2. record maker fees on PerpMarket at time of trade to prevent it later going negative 99 | 3. fix typo in emit_perp_balances to print correct short_funding 100 | 101 | ## v3.3.4 102 | Deployed: Feb 11, 2022 at 01:55:57 UTC | Slot: 120,283,217 103 | 1. Added three instructions (ChangeReferralFeeParams, SetReferrerMemory, RegisterReferrerId) to help with referral program 104 | 2. Assess taker fees at the time of the taker trade 105 | 3. Add back Pyth status checks 106 | 107 | ## v3.3.3 108 | Deployed: Feb 4, 2022 at 01:47:33 UTC | Slot: 119,226,876 109 | 1. Pyth status check temporarily removed to let people use accounts with COPE 110 | 111 | ## v3.3.2 112 | Deployed: Jan 28, 2022 at 20:38:57 UTC | Slot: 118,276,295 113 | 1. Fix the bug in cancel_all where some orders weren't canceled 114 | 2. Add optional payer account to CreateMangoAccount and CreateSpotOpenOrders for better composability 115 | 3. Clean up iteration code and add better documentation 116 | 117 | ## v3.3.1 118 | Deployed: Jan 18, 2022 at 21:06:57 UTC | Slot: 116,847,318 119 | 1. Check quote token vault inside resolve_token_bankruptcy 120 | 2. Add checked to num for general safety 121 | 122 | ## v3.3.0 123 | Deployed: Jan 17, 2022 at 00:45:05 UTC | Slot: 116,585,405 124 | 1. CancelAllPerpOrdersSide - cancels all order on one side 125 | 2. CloseMangoAccount - close account and retrieve lamports 126 | 3. ResolveDust - settle anything below 1 native SPL against the dust account 127 | 4. CreateDustAccount - create a PDA tied to the MangoGroup useful for settling dust against 128 | 5. SetDelegate - delegate authority to operate MangoAccount to another account 129 | 6. upgrade packages 130 | 7. impose price limits for placing limit orders 131 | 8. ChangeSpotMarketParams 132 | 9. CreateSpotOpenOrders using PDA for better UX 133 | 134 | ## v3.2.16 135 | Deployed: Jan 11, 2022 at 01:59:05 UTC | Slot: 115,691,635 136 | 1. Checked math in all areas touched by place_perp_order 137 | 138 | ## v3.2.15 139 | Deployed: Jan 10, 2022 at 22:00:54 UTC | Slot: 115,666,186 140 | 1. Impose price limits on spot orders 141 | 142 | ## v3.2.14 143 | Deployed: Jan 2, 2022 at 20:48:01 UTC | Slot: 114,518,931 144 | 1. Check bids and asks when loading perp market book 145 | 146 | ## v3.2.13 147 | Deployed: Dec 16, 2021 at 21:16:50 UTC | Slot: 111,865,268 148 | 1. Fixed FillLog maker_fee and taker_fee 149 | 2. Now logging order id in new_bid and new_ask 150 | 151 | ## v3.2.12 152 | Deployed: Dec 16, 2021 at 16:15:19 UTC | Slot: 111,832,202 153 | 1. Add CancelAllPerpOrdersLog to mango_logs and start logging cancel_all_with_size_incentives 154 | 2. For reduce_only on perp orders, now checking base position that's sitting on EventQueue unprocessed 155 | 2. Fix bug in check_exit_bankruptcy; now checking all borrows 156 | 157 | ## v3.2.11 158 | Deployed: Dec 9, 2021 at 18:59:28 UTC | Slot: 110,796,491 159 | 1. Fixed bug where perp limit orders past price limit would fail due to simulation 160 | 2. Remove unnecessary Rent account in InitMangoAccount 161 | 162 | ## v3.2.10 163 | Deployed: Dec 9, 2021 at 01:49:38 UTC | Slot: 110,691,491 164 | 1. Limit placing bids to oracle + maint margin req and asks to oracle - maint margin req 165 | 2. Add more checked math in FillEvent struct method and execute_maker() 166 | 167 | ## v3.2.9 168 | Deployed: Dec 8, 2021 at 22:29:47 UTC | Slot: 110,669,751 169 | 1. Add ChangeMaxMangoAccounts 170 | 2. Add some checked math in MangoAccount and matching 171 | 172 | ## v3.2.8 173 | Deployed: Dec 4, 2021 at 21:04:59 | Slot: 110,056,063 174 | 1. Add check to Pyth CachePrice so conf intervals larger than 10% result in no change to cache price 175 | 176 | ## v3.2.7 177 | Deployed: Nov 30, 2021 at 03:23:08 UTC | Slot: 109,359,973 178 | 1. Update margin basket check in ForceCancelSpot 179 | 2. Update margin baskets in PlaceSpotOrder and PlaceSpotOrder2; intended to free up unused margin basket elements 180 | 3. Allow passing in base_decimals when CreatePerpMarket before AddSpotMarket 181 | 4. Make bids and asks pub in Book 182 | 183 | ## v3.2.6 184 | Deployed: Nov 20, 2021 at 20:53:42 UTC | Slot: 107,876,588 185 | 1. Checking the owner of OpenOrders accounts now 186 | 187 | ## v3.2.5 188 | Deployed: Nov 20, 2021 at 14:35:26 UTC | Slot: 107,833,583 189 | 1. Fixed init_spot_open_orders bug not checking signer_key 190 | 2. checking signer_key wherever it's passed it 191 | 192 | ## v3.2.4 193 | Deployed: Nov 15, 2021 at 19:38:22 UTC | Slot: 107,052,828 194 | 1. Updated the update_margin_basket function to include Serum dex OpenOrders accounts with any open orders. 195 | 2. Add instruction UpdateMarginBasket to bring MangoAccount into compliance with new standard 196 | 197 | ## v3.2.3 198 | Deployed: Deployed: Nov 15, 2021 at 15:25:19 UTC | Slot: 107,024,833 199 | 1. Comment out in_margin_basket check in ForceCancelSpot due to to it being wrong for an account 200 | 201 | ## v3.2.2 202 | Deployed: Deployed: Nov 7, 2021 at 14:20:04 UTC | Slot: 105,693,864 203 | 1. Get rid of destructuring assignment feature 204 | 2. Use impact bid/ask for calculating funding (100 contracts) 205 | 206 | ## v3.2.1 207 | Deployed: Nov 1, 2021 at 18:09:05 UTC | Slot: 104,689,370 208 | 1. If perp market is added before spot market, fix decimals to 6 209 | 2. remove ChangePerpMarketParams 210 | 211 | ## v3.2.0 212 | Deployed: Oct 28, 2021 at 23:53:49 UTC | Slot: 104,038,884 213 | 1. Added Size LM functionality 214 | 2. Added ChangePerpMarketParams2 215 | 3. Added CreatePerpMarket which uses PDAs for MNGO vault and PerpMarket 216 | 4. Updated to solana 1.8.1 and anchor 0.18.0 217 | 218 | ## v3.1.4 219 | Deployed: Oct 26, 2021 at 17:04:50 UTC | Slot: 103,646,150 220 | 1. fixed bug when book is full 221 | 2. Adjusted max rate adj back to 4 for LM 222 | 223 | ## v3.1.3 224 | Deployed: 225 | 1. Change rate adjustment for liquidity mining to 10 so changes are fast 226 | 227 | ## v3.1.2 228 | Deployed: Oct 18, 2021 at 22:12:08 UTC | Slot: 102,256,816 229 | 1. Allow for 0 max depth bps 230 | 231 | ## v3.1.1 232 | Deployed: Oct 15, 2021 at 17:45:59 UTC 233 | 234 | 1. Fixed bug in liquidate_token_and_perp div by zero bug 235 | 236 | ## v3.1.0 237 | Deployed: Oct 11, 2021 at 16:57:51 UTC 238 | 1. Add reduce only to PlacePerpOrder 239 | 2. Add Market OrderType to PlacePerpOrder 240 | 3. Reduce MAX_IN_MARGIN_BASKET to 9 from 10 to reflect tx size limits 241 | 4. Add PlaceSpotOrder2 which is optimized for smaller tx size 242 | 5. Add new way to pass in open orders pubkeys to reduce tx size 243 | 6. Add InitAdvancedOrders, AddPerpTriggerOrder, RemovePerpTriggerOrder, ExecutePerpTriggerOrder to allow stop loss, stop limit, take profit orders 244 | 7. Remove ForceSettleQuotePositions because it mixes in the risk from all perp markets into USDC lending pool 245 | 8. All cache valid checks are done independently of one another and have different valid_interval 246 | 9. Remove CacheRootBank instruction 247 | 10. Add new param for exponent in liquidity incentives 248 | 11. FillEvent logging is now done via FillLog borsh serialized and base64 encoded to save compute 249 | 12. Added mango-logs and replaced all logging with more efficient Anchor event 250 | 13. Added logging of OpenOrders balance to keep full track of acocunt value 251 | 14. Added PostOnlySlide for Perp orders (including trigger) 252 | 15. Added OrderType into LeafNode for ability to modify order on TradingView 253 | 16. Added MngoAccrualLog 254 | 17. added DepositLog, WithdrawLog, RedeemMngoLog 255 | 18. sending u64::MAX in withdraw function withdraws total amount in deposit 256 | 19. UpdateFunding now takes in MangoCache as writable and caches the result and UpdateFundingLog is emitted 257 | 258 | ## v3.0.6 259 | Deployed: October 5, 2:00 UTC 260 | 1. Changed the being_liquidated threshold inside liquidate functions to -1 to account for dust issues. 261 | 2. Upgrade anchor version for verifiable build 262 | 263 | ## v3.0.5 264 | Deployed: September 26, 16:40 UTC 265 | 1. Fixed bug in check_enter_bankruptcy 266 | 2. updated anchor version and packages 267 | -------------------------------------------------------------------------------- /common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mango-common" 3 | version = "3.0.0" 4 | edition = "2018" 5 | 6 | [dependencies] 7 | solana-program = ">=1.9.0" 8 | bytemuck = "^1.7.2" 9 | -------------------------------------------------------------------------------- /common/src/lib.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{from_bytes, from_bytes_mut, Pod}; 2 | use solana_program::account_info::AccountInfo; 3 | use solana_program::program_error::ProgramError; 4 | use std::cell::{Ref, RefMut}; 5 | 6 | pub trait Loadable: Pod { 7 | fn load_mut<'a>(account: &'a AccountInfo) -> Result, ProgramError> { 8 | Ok(RefMut::map(account.try_borrow_mut_data()?, |data| from_bytes_mut(data))) 9 | } 10 | fn load<'a>(account: &'a AccountInfo) -> Result, ProgramError> { 11 | Ok(Ref::map(account.try_borrow_data()?, |data| from_bytes(data))) 12 | } 13 | 14 | fn load_from_bytes(data: &[u8]) -> Result<&Self, ProgramError> { 15 | Ok(from_bytes(data)) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /consumed_per_instruction_uniq.log: -------------------------------------------------------------------------------- 1 | AddOracle,1865 2 | AddOracle,1865 3 | AddOracle,20512 4 | AddPerpMarket,20524 5 | AddPerpMarket,21032 6 | AddPerpMarket,56013 7 | AddPerpTriggerOrder,11760 8 | AddSpotMarket,20512 9 | AddSpotMarket,21059 10 | AddSpotMarket,91739 11 | CachePerpMarkets,23463 12 | CachePerpMarkets,23481 13 | CachePerpMarkets,4331 14 | CachePrices,18318 15 | CachePrices,21086 16 | CachePrices,3669 17 | CacheRootBanks,24418 18 | CacheRootBanks,24427 19 | CacheRootBanks,5857 20 | ConsumeEvents,19818 21 | ConsumeEvents,20337 22 | CreateMangoAccount,11837 23 | CreateSpotOpenOrders,16058 24 | CreateSpotOpenOrders,19242 25 | Deposit,20618 26 | ExecutePerpTriggerOrder,28875 27 | InitAdvancedOrders,6497 28 | InitMangoAccount,1828 29 | InitMangoGroup,13160 30 | LiquidatePerpMarket,28467 31 | LiquidateTokenAndPerp,37539 32 | LiquidateTokenAndPerp,37542 33 | LiquidateTokenAndToken,56363 34 | PlacePerpOrder,22058 35 | PlacePerpOrder,62186 36 | PlaceSpotOrder,24427 37 | RemoveAdvancedOrder,2232 38 | SetDelegate,1358 39 | SetOracle,1550 40 | SetOracle,21071 41 | SettlePnl,26769 42 | UpdateFunding,101496 43 | UpdateFunding,16621 44 | UpdateFunding,6381 45 | UpdateFunding,6772 46 | UpdateFunding,6850 47 | UpdateFunding,6928 48 | UpdateRootBank,12111 49 | UpdateRootBank,12149 50 | UpdateRootBank,12344 51 | UpdateRootBank,24427 52 | UpdateRootBank,8519 53 | UpdateRootBank,8523 54 | UpdateRootBank,9360 55 | Withdraw,5523 56 | -------------------------------------------------------------------------------- /cu_per_ix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # gather logs from tests 4 | cargo test-bpf > test.log 2<&1 5 | 6 | # filter mango instructions and logging of consumed compute units 7 | rg -oNI "(Mango:|Instruction: |Program 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM consumed).*$" test.log \ 8 | | rg -U 'Mango: .*\nProgram 4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM.*' \ 9 | | awk 'NR % 2 == 1 { o=$0 ; next } { print o " " $0 }' \ 10 | | sort | uniq -c | sort > consumed_per_instruction.log 11 | 12 | rg -N 'Mango: (\w+) .* consumed (\d+) .*' consumed_per_instruction.log -r '$1,$2' \ 13 | | uniq | xsv sort -s 2 -N -R \ 14 | | sort -t ',' -k 1,1 -u \ 15 | | sort > consumed_per_instruction_uniq.log 16 | 17 | cat consumed_per_instruction_uniq.log| awk '{print $2}' | sort > consumed_per_instruction_uniq.log 18 | 19 | rm test.log 20 | rm consumed_per_instruction.log -------------------------------------------------------------------------------- /mango-logs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mango-logs" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "mango_logs" 10 | doctest = false 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | cpi = ["no-entrypoint"] 16 | devnet = [] 17 | default = [] 18 | 19 | [dependencies] 20 | anchor-lang = ">=0.24.2" 21 | base64 = "0.13.0" 22 | -------------------------------------------------------------------------------- /mango-logs/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_lang::Discriminator; 3 | use std::io::Write; 4 | declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); 5 | 6 | /// Log to Program Log with a prologue so transaction scraper knows following line is valid mango log 7 | /// 8 | /// Warning: This will allocate large buffers on the heap which will never be released as Solana 9 | /// uses a simple bump allocator where free() is a noop. Since the max heap size is limited 10 | // (32k currently), calling this multiple times can lead to memory allocation failures. 11 | #[macro_export] 12 | macro_rules! mango_emit_heap { 13 | ($e:expr) => { 14 | msg!("mango-log"); 15 | emit!($e); 16 | }; 17 | } 18 | 19 | /// Log to Program Log with a prologue so transaction scraper knows following line is valid mango log 20 | /// 21 | /// Warning: This stores intermediate results on the stack, which must have 2*N+ free bytes. 22 | /// This function will panic if the generated event does not fit the buffer of size N. 23 | pub fn mango_emit_stack(event: T) { 24 | let mut data_buf = [0u8; N]; 25 | let mut out_buf = [0u8; N]; 26 | 27 | mango_emit_buffers(event, &mut data_buf[..], &mut out_buf[..]) 28 | } 29 | 30 | /// Log to Program Log with a prologue so transaction scraper knows following line is valid mango log 31 | /// 32 | /// This function will write intermediate data to data_buf and out_buf. The buffers must be 33 | /// large enough to hold this data, or the function will panic. 34 | pub fn mango_emit_buffers( 35 | event: T, 36 | data_buf: &mut [u8], 37 | out_buf: &mut [u8], 38 | ) { 39 | let mut data_writer = std::io::Cursor::new(data_buf); 40 | data_writer.write_all(&::discriminator()).unwrap(); 41 | borsh::to_writer(&mut data_writer, &event).unwrap(); 42 | let data_len = data_writer.position() as usize; 43 | 44 | let out_len = base64::encode_config_slice( 45 | &data_writer.into_inner()[0..data_len], 46 | base64::STANDARD, 47 | out_buf, 48 | ); 49 | 50 | let msg_bytes = &out_buf[0..out_len]; 51 | let msg_str = unsafe { std::str::from_utf8_unchecked(&msg_bytes) }; 52 | 53 | msg!("mango-log"); 54 | msg!(msg_str); 55 | } 56 | 57 | // This is a dummy program to take advantage of Anchor events 58 | #[program] 59 | pub mod mango_logs {} 60 | 61 | #[event] 62 | pub struct FillLog { 63 | pub mango_group: Pubkey, 64 | pub market_index: u64, 65 | pub taker_side: u8, // side from the taker's POV 66 | pub maker_slot: u8, 67 | pub maker_out: bool, // true if maker order quantity == 0 68 | pub timestamp: u64, 69 | pub seq_num: u64, // note: usize same as u64 70 | 71 | pub maker: Pubkey, 72 | pub maker_order_id: i128, 73 | pub maker_client_order_id: u64, 74 | pub maker_fee: i128, 75 | 76 | // The best bid/ask at the time the maker order was placed. Used for liquidity incentives 77 | pub best_initial: i64, 78 | 79 | // Timestamp of when the maker order was placed; copied over from the LeafNode 80 | pub maker_timestamp: u64, 81 | 82 | pub taker: Pubkey, 83 | pub taker_order_id: i128, 84 | pub taker_client_order_id: u64, 85 | pub taker_fee: i128, 86 | 87 | pub price: i64, 88 | pub quantity: i64, // number of base lots 89 | } 90 | 91 | #[event] 92 | pub struct TokenBalanceLog { 93 | pub mango_group: Pubkey, 94 | pub mango_account: Pubkey, 95 | pub token_index: u64, // IDL doesn't support usize 96 | pub deposit: i128, // on client convert i128 to I80F48 easily by passing in the BN to I80F48 ctor 97 | pub borrow: i128, 98 | pub deposit_index: i128, // I80F48 99 | pub borrow_index: i128, // I80F48 100 | } 101 | 102 | #[event] 103 | pub struct CachePricesLog { 104 | pub mango_group: Pubkey, 105 | pub oracle_indexes: Vec, 106 | pub oracle_prices: Vec, // I80F48 format 107 | } 108 | #[event] 109 | pub struct CacheRootBanksLog { 110 | pub mango_group: Pubkey, 111 | pub token_indexes: Vec, // usize 112 | pub deposit_indexes: Vec, // I80F48 113 | pub borrow_indexes: Vec, // I80F48 114 | } 115 | 116 | #[event] 117 | pub struct CachePerpMarketsLog { 118 | pub mango_group: Pubkey, 119 | pub market_indexes: Vec, 120 | pub long_fundings: Vec, // I80F48 121 | pub short_fundings: Vec, // I80F48 122 | } 123 | 124 | #[event] 125 | pub struct SettlePnlLog { 126 | pub mango_group: Pubkey, 127 | pub mango_account_a: Pubkey, 128 | pub mango_account_b: Pubkey, 129 | pub market_index: u64, 130 | pub settlement: i128, // I80F48 131 | } 132 | 133 | #[event] 134 | pub struct SettleFeesLog { 135 | pub mango_group: Pubkey, 136 | pub mango_account: Pubkey, 137 | pub market_index: u64, 138 | pub settlement: i128, // I80F48 139 | } 140 | 141 | #[event] 142 | pub struct LiquidateTokenAndTokenLog { 143 | pub mango_group: Pubkey, 144 | pub liqee: Pubkey, 145 | pub liqor: Pubkey, 146 | pub asset_index: u64, 147 | pub liab_index: u64, 148 | pub asset_transfer: i128, // I80F48 149 | pub liab_transfer: i128, // I80F48 150 | pub asset_price: i128, // I80F48 151 | pub liab_price: i128, // I80F48 152 | pub bankruptcy: bool, 153 | } 154 | 155 | #[event] 156 | pub struct LiquidateTokenAndPerpLog { 157 | pub mango_group: Pubkey, 158 | pub liqee: Pubkey, 159 | pub liqor: Pubkey, 160 | pub asset_index: u64, 161 | pub liab_index: u64, 162 | pub asset_type: u8, 163 | pub liab_type: u8, 164 | pub asset_price: i128, // I80F48 165 | pub liab_price: i128, // I80F48 166 | pub asset_transfer: i128, // I80F48 167 | pub liab_transfer: i128, // I80F48 168 | pub bankruptcy: bool, 169 | } 170 | 171 | #[event] 172 | pub struct LiquidatePerpMarketLog { 173 | pub mango_group: Pubkey, 174 | pub liqee: Pubkey, 175 | pub liqor: Pubkey, 176 | pub market_index: u64, 177 | pub price: i128, // I80F48 178 | pub base_transfer: i64, 179 | pub quote_transfer: i128, // I80F48 180 | pub bankruptcy: bool, 181 | } 182 | 183 | #[event] 184 | pub struct PerpBankruptcyLog { 185 | pub mango_group: Pubkey, 186 | pub liqee: Pubkey, 187 | pub liqor: Pubkey, 188 | pub liab_index: u64, 189 | pub insurance_transfer: u64, 190 | pub socialized_loss: i128, // I80F48 191 | pub cache_long_funding: i128, // I80F48 192 | pub cache_short_funding: i128, // I80F48 193 | } 194 | 195 | #[event] 196 | pub struct TokenBankruptcyLog { 197 | pub mango_group: Pubkey, 198 | pub liqee: Pubkey, 199 | pub liqor: Pubkey, 200 | pub liab_index: u64, 201 | pub insurance_transfer: u64, 202 | /// This is in native units for the liab token NOT static units 203 | pub socialized_loss: i128, // I80F48 204 | pub percentage_loss: i128, // I80F48 205 | pub cache_deposit_index: i128, // I80F48 206 | } 207 | 208 | #[event] 209 | pub struct UpdateRootBankLog { 210 | pub mango_group: Pubkey, 211 | pub token_index: u64, 212 | pub deposit_index: i128, // I80F48 213 | pub borrow_index: i128, // I80F48 214 | } 215 | 216 | #[event] 217 | pub struct UpdateFundingLog { 218 | pub mango_group: Pubkey, 219 | pub market_index: u64, 220 | pub long_funding: i128, // I80F48 221 | pub short_funding: i128, // I80F48 222 | } 223 | 224 | #[event] 225 | pub struct OpenOrdersBalanceLog { 226 | pub mango_group: Pubkey, 227 | pub mango_account: Pubkey, 228 | pub market_index: u64, 229 | pub base_total: u64, 230 | pub base_free: u64, 231 | /// this field does not include the referrer_rebates; need to add that in to get true total 232 | pub quote_total: u64, 233 | pub quote_free: u64, 234 | pub referrer_rebates_accrued: u64, 235 | } 236 | 237 | #[event] 238 | pub struct MngoAccrualLog { 239 | pub mango_group: Pubkey, 240 | pub mango_account: Pubkey, 241 | pub market_index: u64, 242 | /// incremental mngo accrual from canceling/filling this order or set of orders 243 | pub mngo_accrual: u64, 244 | } 245 | 246 | #[event] 247 | pub struct WithdrawLog { 248 | pub mango_group: Pubkey, 249 | pub mango_account: Pubkey, 250 | pub owner: Pubkey, 251 | pub token_index: u64, 252 | pub quantity: u64, 253 | } 254 | 255 | #[event] 256 | pub struct DepositLog { 257 | pub mango_group: Pubkey, 258 | pub mango_account: Pubkey, 259 | pub owner: Pubkey, 260 | pub token_index: u64, 261 | pub quantity: u64, 262 | } 263 | 264 | #[event] 265 | pub struct RedeemMngoLog { 266 | pub mango_group: Pubkey, 267 | pub mango_account: Pubkey, 268 | pub market_index: u64, 269 | pub redeemed_mngo: u64, 270 | } 271 | 272 | #[event] 273 | pub struct CancelAllPerpOrdersLog { 274 | pub mango_group: Pubkey, 275 | pub mango_account: Pubkey, 276 | pub market_index: u64, 277 | pub all_order_ids: Vec, 278 | pub canceled_order_ids: Vec, 279 | } 280 | 281 | #[event] 282 | pub struct PerpBalanceLog { 283 | pub mango_group: Pubkey, 284 | pub mango_account: Pubkey, 285 | pub market_index: u64, // IDL doesn't support usize 286 | pub base_position: i64, 287 | pub quote_position: i128, // I80F48 288 | pub long_settled_funding: i128, // I80F48 289 | pub short_settled_funding: i128, // I80F48 290 | 291 | pub long_funding: i128, // I80F48 292 | pub short_funding: i128, // I80F48 293 | } 294 | 295 | #[event] 296 | pub struct ReferralFeeAccrualLog { 297 | pub mango_group: Pubkey, 298 | pub referrer_mango_account: Pubkey, 299 | pub referree_mango_account: Pubkey, 300 | pub market_index: u64, 301 | pub referral_fee_accrual: i128, // I80F48 302 | } 303 | 304 | #[event] 305 | pub struct CreateMangoAccountLog { 306 | pub mango_group: Pubkey, 307 | pub mango_account: Pubkey, 308 | pub owner: Pubkey, 309 | } 310 | 311 | #[event] 312 | pub struct CloseMangoAccountLog { 313 | pub mango_group: Pubkey, 314 | pub mango_account: Pubkey, 315 | pub owner: Pubkey, 316 | } 317 | 318 | #[event] 319 | pub struct CreateSpotOpenOrdersLog { 320 | pub mango_group: Pubkey, 321 | pub mango_account: Pubkey, 322 | pub open_orders: Pubkey, 323 | pub spot_market: Pubkey, 324 | } 325 | 326 | #[event] 327 | pub struct CloseSpotOpenOrdersLog { 328 | pub mango_group: Pubkey, 329 | pub mango_account: Pubkey, 330 | pub open_orders: Pubkey, 331 | pub spot_market: Pubkey, 332 | } 333 | 334 | #[event] 335 | pub struct ForceSettlePerpPositionLog { 336 | pub mango_account_a: Pubkey, 337 | pub mango_account_b: Pubkey, 338 | pub market_index: u64, 339 | pub base_settle: i64, 340 | pub quote_settle: i128, // I80F48 341 | pub cache_price: i128, // I80F48 342 | } 343 | -------------------------------------------------------------------------------- /mango-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mango-macro" 3 | version = "3.0.0" 4 | edition = "2018" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | syn = "1.0.74" 11 | solana-program = ">=1.9.0" 12 | bytemuck = "^1.7.2" 13 | quote = "^1.0.9" 14 | safe-transmute = "^0.11.1" 15 | 16 | mango-common = { path = "../common" } 17 | -------------------------------------------------------------------------------- /mango-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::{parse_macro_input, DeriveInput}; 4 | 5 | /// Derive `Pod` first before deriving this 6 | #[proc_macro_derive(Loadable)] 7 | pub fn loadable(input: TokenStream) -> TokenStream { 8 | let DeriveInput { ident, data, .. } = parse_macro_input!(input); 9 | 10 | match data { 11 | syn::Data::Struct(_) => { 12 | quote! { 13 | impl mango_common::Loadable for #ident {} 14 | } 15 | } 16 | 17 | _ => panic!(), 18 | } 19 | .into() 20 | } 21 | 22 | /// This must be derived first before Loadable can be derived 23 | #[proc_macro_derive(Pod)] 24 | pub fn pod(input: TokenStream) -> TokenStream { 25 | let DeriveInput { ident, data, .. } = parse_macro_input!(input); 26 | 27 | match data { 28 | syn::Data::Struct(_) => { 29 | quote! { 30 | unsafe impl bytemuck::Zeroable for #ident {} 31 | unsafe impl bytemuck::Pod for #ident {} 32 | } 33 | } 34 | 35 | _ => panic!(), 36 | } 37 | .into() 38 | } 39 | 40 | /// Makes a struct trivially transmutable i.e. safe to read and write into an arbitrary slice of bytes 41 | #[proc_macro_derive(TriviallyTransmutable)] 42 | pub fn trivially_transmutable(input: TokenStream) -> TokenStream { 43 | let DeriveInput { ident, data, .. } = parse_macro_input!(input); 44 | 45 | match data { 46 | syn::Data::Struct(_) => { 47 | quote! { 48 | unsafe impl safe_transmute::trivial::TriviallyTransmutable for #ident {} 49 | } 50 | } 51 | 52 | _ => panic!(), 53 | } 54 | .into() 55 | } 56 | -------------------------------------------------------------------------------- /program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mango" 3 | version = "3.7.0" 4 | authors = ["blockworks"] 5 | edition = "2018" 6 | 7 | [features] 8 | no-entrypoint = [] 9 | test-bpf = [] 10 | devnet = [] 11 | client = ["no-entrypoint"] 12 | 13 | [dependencies] 14 | solana-program = ">=1.9.0" 15 | arrayref = "^0.3.6" 16 | serde = "^1.0.118" 17 | bs58 = "0.4.0" 18 | bytemuck = "^1.7.2" 19 | bincode = "^1.3.1" 20 | # higher versions of fixed don't work until rustc used by solana bpf is upgraded 21 | fixed = { version = ">=1.11.0, <1.12.0", features = ["serde"] } 22 | fixed-macro = "^1.1.1" 23 | enumflags2 = "^0.6.4" 24 | num_enum = "^0.5.1" 25 | thiserror = "^1.0.24" 26 | spl-token = { version = "^3.0.0", features = ["no-entrypoint"] } 27 | serum_dex = { rev = "7f55a5ef5f7937b74381a3124021a261cd7d7283", git = "https://github.com/blockworks-foundation/serum-dex.git", default-features=false, features = ["no-entrypoint", "program"] } 28 | static_assertions = "^1.1.0" 29 | safe-transmute = "^0.11.1" 30 | mango-macro = { path = "../mango-macro" } 31 | mango-common = { path = "../common" } 32 | mango-logs = { path = "../mango-logs", features=["no-entrypoint"] } 33 | pyth-client = {version = ">=0.5.0", features = ["no-entrypoint"]} 34 | 35 | switchboard-program = ">=0.2.0" 36 | switchboard-utils = ">=0.1.36" 37 | 38 | anchor-lang = ">=0.24.2" 39 | 40 | [dev-dependencies] 41 | solana-sdk = ">=1.9.0" 42 | solana-program-test = ">=1.9.0" 43 | solana-logger = ">=1.9.0" 44 | tarpc = { version = "^0.26.2", features = ["full"] } 45 | rand = "0.8.4" 46 | 47 | [lib] 48 | name = "mango" 49 | crate-type = ["cdylib", "lib"] 50 | -------------------------------------------------------------------------------- /program/README.md: -------------------------------------------------------------------------------- 1 | ## Tests 2 | 3 | ``` 4 | cargo test-bpf -- --show-output 5 | ``` 6 | 7 | ## Deploy to devnet 8 | 9 | ``` 10 | cargo build-bpf && solana program deploy -k ~/.config/solana/devnet.json --program-id viQTKtBmaGvx3nugHcvijedy9ApbDowqiGYq35qAJqq ./target/deploy/mango.so 11 | ``` 12 | 13 | ## Log Events 14 | If you make changes to the log events defined in mango-logs/src/lib.rs, make sure to generate the IDL and copy it over 15 | to mango-client-v3 for use in transaction logs scraper: 16 | ``` 17 | anchor build -p mango_logs 18 | cp ~/blockworks-foundation/mango-v3/target/idl/mango_logs.json ~/blockworks-foundation/mango-client-v3/src/mango_logs.json 19 | ``` 20 | -------------------------------------------------------------------------------- /program/devnet_deploy.sh: -------------------------------------------------------------------------------- 1 | # devnet 2 | if [ $# -eq 0 ] 3 | then 4 | KEYPAIR=~/.config/solana/devnet.json 5 | else 6 | KEYPAIR=$1 7 | fi 8 | 9 | CLUSTER_URL="https://mango.devnet.rpcpool.com" 10 | solana config set --url $CLUSTER_URL 11 | 12 | cd ~/blockworks-foundation/mango-v3/ 13 | 14 | 15 | mkdir target/devnet 16 | cargo build-bpf --features devnet --bpf-out-dir target/devnet 17 | 18 | # nightly 19 | #MANGO_PROGRAM_ID="EwG6vXKHmTPAS3K17CPu62AK3bdrrDJS3DibwUjv5ayT" 20 | 21 | # devnet.1 22 | #MANGO_PROGRAM_ID="5fP7Z7a87ZEVsKr2tQPApdtq83GcTW4kz919R6ou5h5E" 23 | # devnet.2 24 | MANGO_PROGRAM_ID="4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA" 25 | solana program deploy target/devnet/mango.so --keypair $KEYPAIR --program-id $MANGO_PROGRAM_ID --skip-fee-check 26 | anchor build -p mango_logs 27 | cp ~/blockworks-foundation/mango-v3/target/idl/mango_logs.json ~/blockworks-foundation/mango-client-v3/src/mango_logs.json 28 | 29 | #solana program deploy target/devnet/mango.so --keypair $KEYPAIR --output json-compact 30 | 31 | # serum dex 32 | DEX_PROGRAM_ID=DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY 33 | cd ~/blockworks-foundation/serum-dex/dex 34 | anchor build --verifiable 35 | solana program deploy target/verifiable/serum_dex.so --keypair $KEYPAIR --program-id $DEX_PROGRAM_ID --skip-fee-check 36 | 37 | VERSION=v1.7.11 38 | sh -c "$(curl -sSfL https://release.solana.com/$VERSION/install)" 39 | 40 | ### Example Mango Client CLI commands to launch a new group from source/cli.ts in mango-client-v3 41 | ### 42 | ### yarn cli init-group mango_test_v3.4 32WeJ46tuY6QEkgydqzHYU5j85UT9m1cPJwFxPjuSVCt DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY EMjjdsqERN4wJUR9jMBax2pzqQPeGLNn5NeucbHpDUZK 43 | ### yarn cli add-oracle mango_test_v3.4 BTC 44 | ### yarn cli set-oracle mango_test_v3.4 BTC 40000000 45 | ### yarn cli add-spot-market mango_test_v3.4 BTC E1mfsnnCcL24JcDQxr7F2BpWjkyy5x2WHys8EL2pnCj9 bypQzRBaSDWiKhoAw3hNkf35eF3z3AZCU8Sxks6mTPP 46 | ### yarn cli add-perp-market mango_test_v3.4 BTC 47 | -------------------------------------------------------------------------------- /program/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | use crate::processor::Processor; 2 | use solana_program::{ 3 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey, 4 | }; 5 | 6 | entrypoint!(process_instruction); 7 | pub fn process_instruction( 8 | program_id: &Pubkey, 9 | accounts: &[AccountInfo], 10 | instruction_data: &[u8], 11 | ) -> ProgramResult { 12 | Processor::process(program_id, accounts, instruction_data).map_err(|e| { 13 | msg!("{}", e); // log the error 14 | e.into() // convert MangoError to generic ProgramError 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /program/src/error.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::Contiguous; 2 | use solana_program::program_error::ProgramError; 3 | 4 | use num_enum::IntoPrimitive; 5 | use thiserror::Error; 6 | 7 | pub type MangoResult = Result; 8 | 9 | #[repr(u8)] 10 | #[derive(Debug, Clone, Eq, PartialEq, Copy)] 11 | pub enum SourceFileId { 12 | Processor = 0, 13 | State = 1, 14 | Critbit = 2, 15 | Queue = 3, 16 | Matching = 4, 17 | Oracle = 5, 18 | } 19 | 20 | impl std::fmt::Display for SourceFileId { 21 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 22 | match self { 23 | SourceFileId::Processor => write!(f, "src/processor.rs"), 24 | SourceFileId::State => write!(f, "src/state.rs"), 25 | SourceFileId::Critbit => write!(f, "src/critbit"), 26 | SourceFileId::Queue => write!(f, "src/queue.rs"), 27 | SourceFileId::Matching => write!(f, "src/matching.rs"), 28 | SourceFileId::Oracle => write!(f, "src/oracle.rs"), 29 | } 30 | } 31 | } 32 | 33 | #[derive(Error, Debug, PartialEq, Eq)] 34 | pub enum MangoError { 35 | #[error(transparent)] 36 | ProgramError(#[from] ProgramError), 37 | #[error("{mango_error_code}; {source_file_id}:{line}")] 38 | MangoErrorCode { mango_error_code: MangoErrorCode, line: u32, source_file_id: SourceFileId }, 39 | } 40 | 41 | #[derive(Debug, Error, Clone, Copy, PartialEq, Eq, IntoPrimitive)] 42 | #[repr(u32)] 43 | pub enum MangoErrorCode { 44 | #[error("MangoErrorCode::InvalidCache")] // 0 45 | InvalidCache, 46 | #[error("MangoErrorCode::InvalidOwner")] 47 | InvalidOwner, 48 | #[error("MangoErrorCode::InvalidGroupOwner")] 49 | InvalidGroupOwner, 50 | #[error("MangoErrorCode::InvalidSignerKey")] 51 | InvalidSignerKey, 52 | #[error("MangoErrorCode::InvalidAdminKey")] 53 | InvalidAdminKey, 54 | #[error("MangoErrorCode::InvalidVault")] 55 | InvalidVault, 56 | #[error("MangoErrorCode::MathError")] 57 | MathError, 58 | #[error("MangoErrorCode::InsufficientFunds")] 59 | InsufficientFunds, 60 | #[error("MangoErrorCode::InvalidToken")] 61 | InvalidToken, 62 | #[error("MangoErrorCode::InvalidMarket")] 63 | InvalidMarket, 64 | #[error("MangoErrorCode::InvalidProgramId")] // 10 65 | InvalidProgramId, 66 | #[error("MangoErrorCode::GroupNotRentExempt")] 67 | GroupNotRentExempt, 68 | #[error("MangoErrorCode::OutOfSpace")] 69 | OutOfSpace, 70 | #[error("MangoErrorCode::TooManyOpenOrders Reached the maximum number of open orders for this market")] 71 | TooManyOpenOrders, 72 | 73 | #[error("MangoErrorCode::AccountNotRentExempt")] 74 | AccountNotRentExempt, 75 | 76 | #[error("MangoErrorCode::ClientIdNotFound")] 77 | ClientIdNotFound, 78 | #[error("MangoErrorCode::InvalidNodeBank")] 79 | InvalidNodeBank, 80 | #[error("MangoErrorCode::InvalidRootBank")] 81 | InvalidRootBank, 82 | #[error("MangoErrorCode::MarginBasketFull")] 83 | MarginBasketFull, 84 | #[error("MangoErrorCode::NotLiquidatable")] 85 | NotLiquidatable, 86 | #[error("MangoErrorCode::Unimplemented")] // 20 87 | Unimplemented, 88 | #[error("MangoErrorCode::PostOnly")] 89 | PostOnly, 90 | #[error("MangoErrorCode::Bankrupt Invalid instruction for bankrupt account")] 91 | Bankrupt, 92 | #[error("MangoErrorCode::InsufficientHealth")] 93 | InsufficientHealth, 94 | #[error("MangoErrorCode::InvalidParam")] 95 | InvalidParam, 96 | #[error("MangoErrorCode::InvalidAccount")] 97 | InvalidAccount, 98 | #[error("MangoErrorCode::InvalidAccountState")] 99 | InvalidAccountState, 100 | #[error("MangoErrorCode::SignerNecessary")] 101 | SignerNecessary, 102 | #[error("MangoErrorCode::InsufficientLiquidity Not enough deposits in this node bank")] 103 | InsufficientLiquidity, 104 | #[error("MangoErrorCode::InvalidOrderId")] 105 | InvalidOrderId, 106 | #[error("MangoErrorCode::InvalidOpenOrdersAccount")] // 30 107 | InvalidOpenOrdersAccount, 108 | #[error("MangoErrorCode::BeingLiquidated Invalid instruction while being liquidated")] 109 | BeingLiquidated, 110 | #[error("MangoErrorCode::InvalidRootBankCache Cache the root bank to resolve")] 111 | InvalidRootBankCache, 112 | #[error("MangoErrorCode::InvalidPriceCache Cache the oracle price to resolve")] 113 | InvalidPriceCache, 114 | #[error("MangoErrorCode::InvalidPerpMarketCache Cache the perp market to resolve")] 115 | InvalidPerpMarketCache, 116 | #[error("MangoErrorCode::TriggerConditionFalse The trigger condition for this TriggerOrder is not met")] 117 | TriggerConditionFalse, 118 | #[error("MangoErrorCode::InvalidSeeds Invalid seeds. Unable to create PDA")] 119 | InvalidSeeds, 120 | #[error("MangoErrorCode::InvalidOracleType The oracle account was not recognized")] 121 | InvalidOracleType, 122 | #[error("MangoErrorCode::InvalidOraclePrice")] 123 | InvalidOraclePrice, 124 | #[error("MangoErrorCode::MaxAccountsReached The maximum number of accounts for this group has been reached")] 125 | MaxAccountsReached, 126 | #[error("MangoErrorCode::ReduceOnlyRequired This market requires reduce-only flag to be set")] 127 | // 40 128 | ReduceOnlyRequired, 129 | #[error( 130 | "MangoErrorCode::InvalidAllowBorrow This market requires allow-borrow flag to be false" 131 | )] 132 | InvalidAllowBorrow, 133 | #[error( 134 | "MangoErrorCode::InvalidOrderInClosingMarket You may only have one open order at a time and it must be reducing position" 135 | )] 136 | InvalidOrderInClosingMarket, 137 | #[error( 138 | "MangoErrorCode::NewOrdersNotAllowed This market is in mode ForceCloseOnly. New orders not allowed." 139 | )] 140 | NewOrdersNotAllowed, 141 | 142 | #[error("MangoErrorCode::Default Check the source code for more info")] // 44 143 | Default = u32::MAX_VALUE, 144 | } 145 | 146 | impl From for ProgramError { 147 | fn from(e: MangoError) -> ProgramError { 148 | match e { 149 | MangoError::ProgramError(pe) => pe, 150 | MangoError::MangoErrorCode { mango_error_code, line: _, source_file_id: _ } => { 151 | ProgramError::Custom(mango_error_code.into()) 152 | } 153 | } 154 | } 155 | } 156 | 157 | impl From for MangoError { 158 | fn from(de: serum_dex::error::DexError) -> Self { 159 | let pe: ProgramError = de.into(); 160 | pe.into() 161 | } 162 | } 163 | 164 | #[inline] 165 | pub fn check_assert( 166 | cond: bool, 167 | mango_error_code: MangoErrorCode, 168 | line: u32, 169 | source_file_id: SourceFileId, 170 | ) -> MangoResult<()> { 171 | if cond { 172 | Ok(()) 173 | } else { 174 | Err(MangoError::MangoErrorCode { mango_error_code, line, source_file_id }) 175 | } 176 | } 177 | 178 | #[macro_export] 179 | macro_rules! declare_check_assert_macros { 180 | ($source_file_id:expr) => { 181 | #[allow(unused_macros)] 182 | macro_rules! check { 183 | ($cond:expr, $err:expr) => { 184 | check_assert($cond, $err, line!(), $source_file_id) 185 | }; 186 | } 187 | 188 | #[allow(unused_macros)] 189 | macro_rules! check_eq { 190 | ($x:expr, $y:expr, $err:expr) => { 191 | check_assert($x == $y, $err, line!(), $source_file_id) 192 | }; 193 | } 194 | 195 | #[allow(unused_macros)] 196 | macro_rules! throw { 197 | () => { 198 | MangoError::MangoErrorCode { 199 | mango_error_code: MangoErrorCode::Default, 200 | line: line!(), 201 | source_file_id: $source_file_id, 202 | } 203 | }; 204 | } 205 | 206 | #[allow(unused_macros)] 207 | macro_rules! throw_err { 208 | ($err:expr) => { 209 | MangoError::MangoErrorCode { 210 | mango_error_code: $err, 211 | line: line!(), 212 | source_file_id: $source_file_id, 213 | } 214 | }; 215 | } 216 | 217 | #[allow(unused_macros)] 218 | macro_rules! math_err { 219 | () => { 220 | MangoError::MangoErrorCode { 221 | mango_error_code: MangoErrorCode::MathError, 222 | line: line!(), 223 | source_file_id: $source_file_id, 224 | } 225 | }; 226 | } 227 | }; 228 | } 229 | -------------------------------------------------------------------------------- /program/src/ids.rs: -------------------------------------------------------------------------------- 1 | pub mod srm_token { 2 | use solana_program::declare_id; 3 | #[cfg(feature = "devnet")] 4 | declare_id!("AvtB6w9xboLwA145E221vhof5TddhqsChYcx7Fy3xVMH"); 5 | #[cfg(not(feature = "devnet"))] 6 | declare_id!("SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt"); 7 | } 8 | 9 | pub mod msrm_token { 10 | use solana_program::declare_id; 11 | #[cfg(feature = "devnet")] 12 | declare_id!("8DJBo4bF4mHNxobjdax3BL9RMh5o71Jf8UiKsf5C5eVH"); 13 | #[cfg(not(feature = "devnet"))] 14 | declare_id!("MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L"); 15 | } 16 | 17 | pub mod mngo_token { 18 | use solana_program::declare_id; 19 | #[cfg(feature = "devnet")] 20 | declare_id!("Bb9bsTQa1bGEtQ5KagGkvSHyuLqDWumFUcRqFusFNJWC"); 21 | #[cfg(not(feature = "devnet"))] 22 | declare_id!("MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac"); 23 | } 24 | 25 | pub mod luna_pyth_oracle { 26 | use solana_program::declare_id; 27 | declare_id!("5bmWuR1dgP4avtGYMNKLuxumZTVKGgoN2BCMXWDNL9nY"); 28 | } 29 | 30 | pub mod mainnet_1_group { 31 | use solana_program::declare_id; 32 | #[cfg(feature = "devnet")] 33 | declare_id!("Ec2enZyoC4nGpEfu2sUNAa2nUGJHWxoUWYSEJ2hNTWTA"); 34 | #[cfg(not(feature = "devnet"))] 35 | declare_id!("98pjRuQjK3qA6gXts96PqZT4Ze5QmnCmt3QYjhbUSPue"); 36 | } 37 | 38 | // Owner of the reimbursement fund multisig accounts 39 | pub mod recovery_authority { 40 | use solana_program::declare_id; 41 | #[cfg(feature = "devnet")] 42 | declare_id!("8pANRWCcw8vn8DszUP7hh4xFbCiBiMWX3WbwUTipArSJ"); 43 | #[cfg(not(feature = "devnet"))] 44 | declare_id!("9mM6NfXauEFviFY1S1thbo7HXYNiSWSvwZEhguJw26wY"); 45 | } 46 | -------------------------------------------------------------------------------- /program/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod error; 3 | 4 | pub mod ids; 5 | pub mod instruction; 6 | pub mod matching; 7 | pub mod oracle; 8 | pub mod processor; 9 | pub mod queue; 10 | pub mod state; 11 | pub mod utils; 12 | 13 | #[cfg(not(feature = "no-entrypoint"))] 14 | pub mod entrypoint; 15 | -------------------------------------------------------------------------------- /program/src/oracle.rs: -------------------------------------------------------------------------------- 1 | use arrayref::array_ref; 2 | use fixed::types::I80F48; 3 | use mango_common::Loadable; 4 | use mango_macro::{Loadable, Pod}; 5 | use solana_program::{account_info::AccountInfo, pubkey::Pubkey, rent::Rent}; 6 | use std::{cell::RefMut, mem::size_of}; 7 | 8 | use crate::error::{check_assert, MangoErrorCode, MangoResult, SourceFileId}; 9 | 10 | declare_check_assert_macros!(SourceFileId::Oracle); 11 | 12 | // oracle can be of different types 13 | #[derive(PartialEq)] 14 | #[repr(C)] 15 | pub enum OracleType { 16 | Stub, 17 | Pyth, 18 | Switchboard, 19 | Unknown, 20 | } 21 | 22 | pub const STUB_MAGIC: u32 = 0x6F676E4D; 23 | 24 | #[derive(Copy, Clone, Pod, Loadable)] 25 | #[repr(C)] 26 | pub struct StubOracle { 27 | pub magic: u32, // Magic byte 28 | pub price: I80F48, // unit is interpreted as how many quote native tokens for 1 base native token 29 | pub last_update: u64, 30 | } 31 | 32 | // TODO move to separate program 33 | impl StubOracle { 34 | pub fn load_mut_checked<'a>( 35 | account: &'a AccountInfo, 36 | program_id: &Pubkey, 37 | ) -> MangoResult> { 38 | check_eq!(account.data_len(), size_of::(), MangoErrorCode::Default)?; 39 | check_eq!(account.owner, program_id, MangoErrorCode::InvalidOwner)?; 40 | 41 | let oracle = Self::load_mut(account)?; 42 | 43 | Ok(oracle) 44 | } 45 | 46 | pub fn load_and_init<'a>( 47 | account: &'a AccountInfo, 48 | program_id: &Pubkey, 49 | rent: &Rent, 50 | ) -> MangoResult> { 51 | check_eq!(account.owner, program_id, MangoErrorCode::InvalidOwner)?; 52 | check!( 53 | rent.is_exempt(account.lamports(), account.data_len()), 54 | MangoErrorCode::AccountNotRentExempt 55 | )?; 56 | 57 | let oracle = Self::load_mut(account)?; 58 | 59 | Ok(oracle) 60 | } 61 | } 62 | 63 | pub fn determine_oracle_type(account: &AccountInfo) -> OracleType { 64 | let borrowed = account.data.borrow(); 65 | let magic = u32::from_le_bytes(*array_ref![borrowed, 0, 4]); 66 | if magic == pyth_client::MAGIC { 67 | OracleType::Pyth 68 | } else if borrowed.len() == 1000 { 69 | OracleType::Switchboard 70 | } else if magic == STUB_MAGIC { 71 | OracleType::Stub 72 | } else { 73 | OracleType::Unknown 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /program/src/queue.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{check_assert, MangoErrorCode, MangoResult, SourceFileId}; 2 | use crate::matching::Side; 3 | use crate::state::{DataType, MetaData, PerpMarket}; 4 | use crate::utils::strip_header_mut; 5 | 6 | use fixed::types::I80F48; 7 | use mango_logs::FillLog; 8 | use mango_macro::Pod; 9 | use num_enum::{IntoPrimitive, TryFromPrimitive}; 10 | use safe_transmute::{self, trivial::TriviallyTransmutable}; 11 | use solana_program::account_info::AccountInfo; 12 | use solana_program::pubkey::Pubkey; 13 | use solana_program::sysvar::rent::Rent; 14 | use static_assertions::const_assert_eq; 15 | use std::cell::RefMut; 16 | use std::mem::size_of; 17 | 18 | declare_check_assert_macros!(SourceFileId::Queue); 19 | 20 | // Don't want event queue to become single threaded if it's logging liquidations 21 | // Most common scenario will be liqors depositing USDC and withdrawing some other token 22 | // So tying it to token deposited is not wise 23 | // also can't tie it to token withdrawn because during bull market, liqs will be depositing all base tokens and withdrawing quote 24 | // 25 | 26 | pub trait QueueHeader: bytemuck::Pod { 27 | type Item: bytemuck::Pod + Copy; 28 | 29 | fn head(&self) -> usize; 30 | fn set_head(&mut self, value: usize); 31 | fn count(&self) -> usize; 32 | fn set_count(&mut self, value: usize); 33 | 34 | fn incr_event_id(&mut self); 35 | fn decr_event_id(&mut self, n: usize); 36 | } 37 | 38 | pub struct Queue<'a, H: QueueHeader> { 39 | pub header: RefMut<'a, H>, 40 | pub buf: RefMut<'a, [H::Item]>, 41 | } 42 | 43 | impl<'a, H: QueueHeader> Queue<'a, H> { 44 | pub fn new(header: RefMut<'a, H>, buf: RefMut<'a, [H::Item]>) -> Self { 45 | Self { header, buf } 46 | } 47 | 48 | pub fn load_mut(account: &'a AccountInfo) -> MangoResult { 49 | let (header, buf) = strip_header_mut::(account)?; 50 | Ok(Self { header, buf }) 51 | } 52 | 53 | pub fn len(&self) -> usize { 54 | self.header.count() 55 | } 56 | 57 | pub fn full(&self) -> bool { 58 | self.header.count() == self.buf.len() 59 | } 60 | 61 | pub fn empty(&self) -> bool { 62 | self.header.count() == 0 63 | } 64 | 65 | pub fn push_back(&mut self, value: H::Item) -> Result<(), H::Item> { 66 | if self.full() { 67 | return Err(value); 68 | } 69 | let slot = (self.header.head() + self.header.count()) % self.buf.len(); 70 | self.buf[slot] = value; 71 | 72 | let count = self.header.count(); 73 | self.header.set_count(count + 1); 74 | 75 | self.header.incr_event_id(); 76 | Ok(()) 77 | } 78 | 79 | pub fn peek_front(&self) -> Option<&H::Item> { 80 | if self.empty() { 81 | return None; 82 | } 83 | Some(&self.buf[self.header.head()]) 84 | } 85 | 86 | pub fn peek_front_mut(&mut self) -> Option<&mut H::Item> { 87 | if self.empty() { 88 | return None; 89 | } 90 | Some(&mut self.buf[self.header.head()]) 91 | } 92 | 93 | pub fn pop_front(&mut self) -> Result { 94 | if self.empty() { 95 | return Err(()); 96 | } 97 | let value = self.buf[self.header.head()]; 98 | 99 | let count = self.header.count(); 100 | self.header.set_count(count - 1); 101 | 102 | let head = self.header.head(); 103 | self.header.set_head((head + 1) % self.buf.len()); 104 | 105 | Ok(value) 106 | } 107 | 108 | pub fn revert_pushes(&mut self, desired_len: usize) -> MangoResult<()> { 109 | check!(desired_len <= self.header.count(), MangoErrorCode::Default)?; 110 | let len_diff = self.header.count() - desired_len; 111 | self.header.set_count(desired_len); 112 | self.header.decr_event_id(len_diff); 113 | Ok(()) 114 | } 115 | 116 | pub fn iter(&self) -> impl Iterator { 117 | QueueIterator { queue: self, index: 0 } 118 | } 119 | } 120 | 121 | struct QueueIterator<'a, 'b, H: QueueHeader> { 122 | queue: &'b Queue<'a, H>, 123 | index: usize, 124 | } 125 | 126 | impl<'a, 'b, H: QueueHeader> Iterator for QueueIterator<'a, 'b, H> { 127 | type Item = &'b H::Item; 128 | fn next(&mut self) -> Option { 129 | if self.index == self.queue.len() { 130 | None 131 | } else { 132 | let item = 133 | &self.queue.buf[(self.queue.header.head() + self.index) % self.queue.buf.len()]; 134 | self.index += 1; 135 | Some(item) 136 | } 137 | } 138 | } 139 | 140 | #[derive(Copy, Clone, Pod)] 141 | #[repr(C)] 142 | pub struct EventQueueHeader { 143 | pub meta_data: MetaData, 144 | head: usize, 145 | count: usize, 146 | pub seq_num: usize, 147 | } 148 | unsafe impl TriviallyTransmutable for EventQueueHeader {} 149 | 150 | impl QueueHeader for EventQueueHeader { 151 | type Item = AnyEvent; 152 | 153 | fn head(&self) -> usize { 154 | self.head 155 | } 156 | fn set_head(&mut self, value: usize) { 157 | self.head = value; 158 | } 159 | fn count(&self) -> usize { 160 | self.count 161 | } 162 | fn set_count(&mut self, value: usize) { 163 | self.count = value; 164 | } 165 | fn incr_event_id(&mut self) { 166 | self.seq_num += 1; 167 | } 168 | fn decr_event_id(&mut self, n: usize) { 169 | self.seq_num -= n; 170 | } 171 | } 172 | 173 | pub type EventQueue<'a> = Queue<'a, EventQueueHeader>; 174 | 175 | impl<'a> EventQueue<'a> { 176 | pub fn load_mut_checked( 177 | account: &'a AccountInfo, 178 | program_id: &Pubkey, 179 | perp_market: &PerpMarket, 180 | ) -> MangoResult { 181 | check_eq!(account.owner, program_id, MangoErrorCode::InvalidOwner)?; 182 | check_eq!(&perp_market.event_queue, account.key, MangoErrorCode::InvalidAccount)?; 183 | Self::load_mut(account) 184 | } 185 | 186 | pub fn load_and_init( 187 | account: &'a AccountInfo, 188 | program_id: &Pubkey, 189 | rent: &Rent, 190 | ) -> MangoResult { 191 | // NOTE: check this first so we can borrow account later 192 | check!( 193 | rent.is_exempt(account.lamports(), account.data_len()), 194 | MangoErrorCode::AccountNotRentExempt 195 | )?; 196 | 197 | let mut state = Self::load_mut(account)?; 198 | check!(account.owner == program_id, MangoErrorCode::InvalidOwner)?; 199 | 200 | check!(!state.header.meta_data.is_initialized, MangoErrorCode::Default)?; 201 | state.header.meta_data = MetaData::new(DataType::EventQueue, 0, true); 202 | 203 | Ok(state) 204 | } 205 | } 206 | 207 | #[derive(Copy, Clone, IntoPrimitive, TryFromPrimitive, Eq, PartialEq)] 208 | #[repr(u8)] 209 | pub enum EventType { 210 | Fill, 211 | Out, 212 | Liquidate, 213 | } 214 | 215 | const EVENT_SIZE: usize = 200; 216 | #[derive(Copy, Clone, Debug, Pod)] 217 | #[repr(C)] 218 | pub struct AnyEvent { 219 | pub event_type: u8, 220 | pub padding: [u8; EVENT_SIZE - 1], 221 | } 222 | unsafe impl TriviallyTransmutable for AnyEvent {} 223 | 224 | #[derive(Copy, Clone, Debug, Pod)] 225 | #[repr(C)] 226 | pub struct FillEvent { 227 | pub event_type: u8, 228 | pub taker_side: Side, // side from the taker's POV 229 | pub maker_slot: u8, 230 | pub maker_out: bool, // true if maker order quantity == 0 231 | pub version: u8, 232 | pub market_fees_applied: bool, 233 | pub padding: [u8; 2], 234 | pub timestamp: u64, 235 | pub seq_num: usize, // note: usize same as u64 236 | 237 | pub maker: Pubkey, 238 | pub maker_order_id: i128, 239 | pub maker_client_order_id: u64, 240 | pub maker_fee: I80F48, 241 | 242 | // The best bid/ask at the time the maker order was placed. Used for liquidity incentives 243 | pub best_initial: i64, 244 | 245 | // Timestamp of when the maker order was placed; copied over from the LeafNode 246 | pub maker_timestamp: u64, 247 | 248 | pub taker: Pubkey, 249 | pub taker_order_id: i128, 250 | pub taker_client_order_id: u64, 251 | pub taker_fee: I80F48, 252 | 253 | pub price: i64, 254 | pub quantity: i64, // number of quote lots 255 | } 256 | unsafe impl TriviallyTransmutable for FillEvent {} 257 | 258 | impl FillEvent { 259 | pub fn new( 260 | taker_side: Side, 261 | maker_slot: u8, 262 | maker_out: bool, 263 | timestamp: u64, 264 | seq_num: usize, 265 | maker: Pubkey, 266 | maker_order_id: i128, 267 | maker_client_order_id: u64, 268 | maker_fee: I80F48, 269 | best_initial: i64, 270 | maker_timestamp: u64, 271 | 272 | taker: Pubkey, 273 | taker_order_id: i128, 274 | taker_client_order_id: u64, 275 | taker_fee: I80F48, 276 | price: i64, 277 | quantity: i64, 278 | version: u8, 279 | ) -> FillEvent { 280 | Self { 281 | event_type: EventType::Fill as u8, 282 | taker_side, 283 | maker_slot, 284 | maker_out, 285 | version, 286 | market_fees_applied: true, // Since mango v3.3.5, market fees are adjusted at matching time 287 | padding: [0u8; 2], 288 | timestamp, 289 | seq_num, 290 | maker, 291 | maker_order_id, 292 | maker_client_order_id, 293 | maker_fee, 294 | best_initial, 295 | maker_timestamp, 296 | taker, 297 | taker_order_id, 298 | taker_client_order_id, 299 | taker_fee, 300 | price, 301 | quantity, 302 | } 303 | } 304 | 305 | pub fn base_quote_change(&self, side: Side) -> (i64, i64) { 306 | match side { 307 | Side::Bid => (self.quantity, -self.price.checked_mul(self.quantity).unwrap()), 308 | Side::Ask => (-self.quantity, self.price.checked_mul(self.quantity).unwrap()), 309 | } 310 | } 311 | 312 | pub fn to_fill_log(&self, mango_group: Pubkey, market_index: usize) -> FillLog { 313 | FillLog { 314 | mango_group, 315 | market_index: market_index as u64, 316 | taker_side: self.taker_side as u8, 317 | maker_slot: self.maker_slot, 318 | maker_out: self.maker_out, 319 | timestamp: self.timestamp, 320 | seq_num: self.seq_num as u64, 321 | maker: self.maker, 322 | maker_order_id: self.maker_order_id, 323 | maker_client_order_id: self.maker_client_order_id, 324 | maker_fee: self.maker_fee.to_bits(), 325 | best_initial: self.best_initial, 326 | maker_timestamp: self.maker_timestamp, 327 | taker: self.taker, 328 | taker_order_id: self.taker_order_id, 329 | taker_client_order_id: self.taker_client_order_id, 330 | taker_fee: self.taker_fee.to_bits(), 331 | price: self.price, 332 | quantity: self.quantity, 333 | } 334 | } 335 | } 336 | 337 | #[derive(Copy, Clone, Debug, Pod)] 338 | #[repr(C)] 339 | pub struct OutEvent { 340 | pub event_type: u8, 341 | pub side: Side, 342 | pub slot: u8, 343 | padding0: [u8; 5], 344 | pub timestamp: u64, 345 | pub seq_num: usize, 346 | pub owner: Pubkey, 347 | pub quantity: i64, 348 | padding1: [u8; EVENT_SIZE - 64], 349 | } 350 | unsafe impl TriviallyTransmutable for OutEvent {} 351 | impl OutEvent { 352 | pub fn new( 353 | side: Side, 354 | slot: u8, 355 | timestamp: u64, 356 | seq_num: usize, 357 | owner: Pubkey, 358 | quantity: i64, 359 | ) -> Self { 360 | Self { 361 | event_type: EventType::Out.into(), 362 | side, 363 | slot, 364 | padding0: [0; 5], 365 | timestamp, 366 | seq_num, 367 | owner, 368 | quantity, 369 | padding1: [0; EVENT_SIZE - 64], 370 | } 371 | } 372 | } 373 | 374 | #[derive(Copy, Clone, Debug, Pod)] 375 | #[repr(C)] 376 | /// Liquidation for the PerpMarket this EventQueue is for 377 | pub struct LiquidateEvent { 378 | pub event_type: u8, 379 | padding0: [u8; 7], 380 | pub timestamp: u64, 381 | pub seq_num: usize, 382 | pub liqee: Pubkey, 383 | pub liqor: Pubkey, 384 | pub price: I80F48, // oracle price at the time of liquidation 385 | pub quantity: i64, // number of contracts that were moved from liqee to liqor 386 | pub liquidation_fee: I80F48, // liq fee for this earned for this market 387 | padding1: [u8; EVENT_SIZE - 128], 388 | } 389 | unsafe impl TriviallyTransmutable for LiquidateEvent {} 390 | impl LiquidateEvent { 391 | pub fn new( 392 | timestamp: u64, 393 | seq_num: usize, 394 | liqee: Pubkey, 395 | liqor: Pubkey, 396 | price: I80F48, 397 | quantity: i64, 398 | liquidation_fee: I80F48, 399 | ) -> Self { 400 | Self { 401 | event_type: EventType::Liquidate.into(), 402 | padding0: [0u8; 7], 403 | timestamp, 404 | seq_num, 405 | liqee, 406 | liqor, 407 | price, 408 | quantity, 409 | liquidation_fee, 410 | padding1: [0u8; EVENT_SIZE - 128], 411 | } 412 | } 413 | } 414 | const_assert_eq!(size_of::(), size_of::()); 415 | const_assert_eq!(size_of::(), size_of::()); 416 | const_assert_eq!(size_of::(), size_of::()); 417 | -------------------------------------------------------------------------------- /program/src/utils.rs: -------------------------------------------------------------------------------- 1 | use bytemuck::{bytes_of, cast_slice_mut, from_bytes_mut, Contiguous, Pod}; 2 | 3 | use crate::error::MangoResult; 4 | use crate::matching::Side; 5 | use crate::state::{RootBank, ONE_I80F48}; 6 | use fixed::types::I80F48; 7 | use solana_program::account_info::AccountInfo; 8 | use solana_program::program_error::ProgramError; 9 | use solana_program::pubkey::Pubkey; 10 | use std::cell::RefMut; 11 | use std::mem::size_of; 12 | 13 | use crate::state::{PerpAccount, PerpMarketCache}; 14 | 15 | use mango_logs::{mango_emit_stack, PerpBalanceLog}; 16 | 17 | pub fn gen_signer_seeds<'a>(nonce: &'a u64, acc_pk: &'a Pubkey) -> [&'a [u8]; 2] { 18 | [acc_pk.as_ref(), bytes_of(nonce)] 19 | } 20 | 21 | pub fn gen_signer_key( 22 | nonce: u64, 23 | acc_pk: &Pubkey, 24 | program_id: &Pubkey, 25 | ) -> Result { 26 | let seeds = gen_signer_seeds(&nonce, acc_pk); 27 | Ok(Pubkey::create_program_address(&seeds, program_id)?) 28 | } 29 | 30 | pub fn create_signer_key_and_nonce(program_id: &Pubkey, acc_pk: &Pubkey) -> (Pubkey, u64) { 31 | for i in 0..=u64::MAX_VALUE { 32 | if let Ok(pk) = gen_signer_key(i, acc_pk, program_id) { 33 | return (pk, i); 34 | } 35 | } 36 | panic!("Could not generate signer key"); 37 | } 38 | 39 | #[inline] 40 | pub fn remove_slop_mut(bytes: &mut [u8]) -> &mut [T] { 41 | let slop = bytes.len() % size_of::(); 42 | let new_len = bytes.len() - slop; 43 | cast_slice_mut(&mut bytes[..new_len]) 44 | } 45 | 46 | pub fn strip_header_mut<'a, H: Pod, D: Pod>( 47 | account: &'a AccountInfo, 48 | ) -> MangoResult<(RefMut<'a, H>, RefMut<'a, [D]>)> { 49 | Ok(RefMut::map_split(account.try_borrow_mut_data()?, |data| { 50 | let (header_bytes, inner_bytes) = data.split_at_mut(size_of::()); 51 | (from_bytes_mut(header_bytes), remove_slop_mut(inner_bytes)) 52 | })) 53 | } 54 | 55 | pub fn invert_side(side: Side) -> Side { 56 | if side == Side::Bid { 57 | Side::Ask 58 | } else { 59 | Side::Bid 60 | } 61 | } 62 | 63 | /// Return (quote_free, quote_locked, base_free, base_locked) in I80F48 64 | #[inline(always)] 65 | pub fn split_open_orders( 66 | open_orders: &serum_dex::state::OpenOrders, 67 | ) -> (I80F48, I80F48, I80F48, I80F48) { 68 | ( 69 | I80F48::from_num(open_orders.native_pc_free + open_orders.referrer_rebates_accrued), 70 | I80F48::from_num(open_orders.native_pc_total - open_orders.native_pc_free), 71 | I80F48::from_num(open_orders.native_coin_free), 72 | I80F48::from_num(open_orders.native_coin_total - open_orders.native_coin_free), 73 | ) 74 | } 75 | 76 | /// exponentiate by squaring; send in 1 / base if you want neg 77 | pub fn pow_i80f48(mut base: I80F48, mut exp: u8) -> I80F48 { 78 | let mut result = ONE_I80F48; 79 | loop { 80 | if exp & 1 == 1 { 81 | result = result.checked_mul(base).unwrap(); 82 | } 83 | exp >>= 1; 84 | if exp == 0 { 85 | break result; 86 | } 87 | base = base.checked_mul(base).unwrap(); 88 | } 89 | } 90 | 91 | /// Warning: This function needs 512+ bytes free on the stack 92 | pub fn emit_perp_balances( 93 | mango_group: Pubkey, 94 | mango_account: Pubkey, 95 | market_index: u64, 96 | pa: &PerpAccount, 97 | perp_market_cache: &PerpMarketCache, 98 | ) { 99 | mango_emit_stack::<_, 256>(PerpBalanceLog { 100 | mango_group: mango_group, 101 | mango_account: mango_account, 102 | market_index: market_index, 103 | base_position: pa.base_position, 104 | quote_position: pa.quote_position.to_bits(), 105 | long_settled_funding: pa.long_settled_funding.to_bits(), 106 | short_settled_funding: pa.short_settled_funding.to_bits(), 107 | long_funding: perp_market_cache.long_funding.to_bits(), 108 | short_funding: perp_market_cache.short_funding.to_bits(), 109 | }); 110 | } 111 | 112 | /// returns the current interest rate in APR for a given RootBank 113 | #[inline(always)] 114 | pub fn compute_interest_rate(root_bank: &RootBank, utilization: I80F48) -> I80F48 { 115 | interest_rate_curve_calculator( 116 | utilization, 117 | root_bank.optimal_util, 118 | root_bank.optimal_rate, 119 | root_bank.max_rate, 120 | ) 121 | } 122 | 123 | /// returns a tuple of (deposit_rate, interest_rate) for a given RootBank 124 | /// values are in APR 125 | #[inline(always)] 126 | pub fn compute_deposit_rate(root_bank: &RootBank, utilization: I80F48) -> Option<(I80F48, I80F48)> { 127 | let interest_rate = compute_interest_rate(root_bank, utilization); 128 | if let Some(deposit_rate) = interest_rate.checked_mul(utilization) { 129 | Some((deposit_rate, interest_rate)) 130 | } else { 131 | None 132 | } 133 | } 134 | 135 | /// calcualtor function that can be used to compute an interest 136 | /// rate based on the given parameters 137 | #[inline(always)] 138 | pub fn interest_rate_curve_calculator( 139 | utilization: I80F48, 140 | optimal_util: I80F48, 141 | optimal_rate: I80F48, 142 | max_rate: I80F48, 143 | ) -> I80F48 { 144 | if utilization > optimal_util { 145 | let extra_util = utilization - optimal_util; 146 | let slope = (max_rate - optimal_rate) / (ONE_I80F48 - optimal_util); 147 | optimal_rate + slope * extra_util 148 | } else { 149 | let slope = optimal_rate / optimal_util; 150 | slope * utilization 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /program/tests/fixtures/serum_dex.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blockworks-foundation/mango-v3/c4d52dc7f08e9ba4ae13728d0a2f1c298c0c4a86/program/tests/fixtures/serum_dex.so -------------------------------------------------------------------------------- /program/tests/program_test/assertions.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use fixed::types::I80F48; 3 | use fixed_macro::types::I80F48; 4 | use mango::state::*; 5 | use solana_program::pubkey::Pubkey; 6 | use std::collections::HashMap; 7 | 8 | // Test equality within an epsilon for I80F48 or float 9 | #[allow(dead_code)] 10 | pub const EPSILON: I80F48 = I80F48!(0.001); 11 | 12 | #[macro_export] 13 | macro_rules! assert_approx_eq { 14 | ($a:expr, $b:expr) => {{ 15 | let (a, b) = (&$a, &$b); 16 | assert!( 17 | (*a - *b).abs() < EPSILON, 18 | "assertion failed: `(left !== right)` \ 19 | (left: `{:?}`, right: `{:?}`, expect diff: `{:?}`, real diff: `{:?}`)", 20 | *a, 21 | *b, 22 | EPSILON, 23 | (*a - *b).abs() 24 | ); 25 | }}; 26 | 27 | ($a:expr, $b:expr, $eps:expr) => {{ 28 | let (a, b) = (&$a, &$b); 29 | let eps = $eps; 30 | assert!( 31 | (*a - *b).abs() < eps, 32 | "assertion failed: `(left !== right)` \ 33 | (left: `{:?}`, right: `{:?}`, expect diff: `{:?}`, real diff: `{:?}`)", 34 | *a, 35 | *b, 36 | eps, 37 | (*a - *b).abs() 38 | ); 39 | }}; 40 | } 41 | 42 | #[allow(dead_code)] 43 | pub fn assert_deposits( 44 | mango_group_cookie: &MangoGroupCookie, 45 | expected_values: (usize, HashMap), 46 | ) { 47 | let (user_index, expected_value) = expected_values; 48 | for (mint_index, expected_deposit) in expected_value.iter() { 49 | let actual_deposit = &mango_group_cookie.mango_accounts[user_index] 50 | .mango_account 51 | .get_native_deposit( 52 | &mango_group_cookie.mango_cache.root_bank_cache[*mint_index], 53 | *mint_index, 54 | ) 55 | .unwrap(); 56 | println!( 57 | "==\nUser: {}, Mint: {}\nExpected deposit: {}, Actual deposit: {}\n==", 58 | user_index, 59 | mint_index, 60 | expected_deposit.to_string(), 61 | actual_deposit.to_string(), 62 | ); 63 | assert!(expected_deposit == actual_deposit); 64 | } 65 | } 66 | 67 | #[allow(dead_code)] 68 | pub fn assert_deposits_approx( 69 | mango_group_cookie: &MangoGroupCookie, 70 | expected_values: (usize, HashMap), 71 | epsilon: I80F48, 72 | ) { 73 | let (user_index, expected_value) = expected_values; 74 | for (mint_index, expected_deposit) in expected_value.iter() { 75 | let actual_deposit = &mango_group_cookie.mango_accounts[user_index] 76 | .mango_account 77 | .get_native_deposit( 78 | &mango_group_cookie.mango_cache.root_bank_cache[*mint_index], 79 | *mint_index, 80 | ) 81 | .unwrap(); 82 | println!( 83 | "==\nUser: {}, Mint: {}\nExpected deposit: {}, Actual deposit: {}\n==", 84 | user_index, 85 | mint_index, 86 | expected_deposit.to_string(), 87 | actual_deposit.to_string(), 88 | ); 89 | assert_approx_eq!(expected_deposit, actual_deposit, epsilon); 90 | } 91 | } 92 | 93 | #[allow(dead_code)] 94 | pub fn assert_open_spot_orders( 95 | mango_group_cookie: &MangoGroupCookie, 96 | user_spot_orders: &Vec<(usize, usize, serum_dex::matching::Side, f64, f64)>, 97 | ) { 98 | for i in 0..user_spot_orders.len() { 99 | let (user_index, mint_index, _, _, _) = user_spot_orders[i]; 100 | let mango_account = mango_group_cookie.mango_accounts[user_index].mango_account; 101 | assert_ne!(mango_account.spot_open_orders[mint_index], Pubkey::default()); 102 | } 103 | } 104 | 105 | #[allow(dead_code)] 106 | pub async fn assert_user_spot_orders( 107 | test: &mut MangoProgramTest, 108 | mango_group_cookie: &MangoGroupCookie, 109 | expected_values: (usize, usize, HashMap<&str, I80F48>), 110 | ) { 111 | let (mint_index, user_index, expected_value) = expected_values; 112 | let (actual_quote_free, actual_quote_locked, actual_base_free, actual_base_locked) = 113 | test.get_oo_info(&mango_group_cookie, user_index, mint_index).await; 114 | 115 | println!( 116 | "User index: {} quote_free {} quote_locked {} base_free {} base_locked {}", 117 | user_index, 118 | actual_quote_free.to_string(), 119 | actual_quote_locked.to_string(), 120 | actual_base_free.to_string(), 121 | actual_base_locked.to_string() 122 | ); 123 | if let Some(quote_free) = expected_value.get("quote_free") { 124 | // println!( 125 | // "==\nUser: {}, Mint: {}\nExpected quote_free: {}, Actual quote_free: {}\n==", 126 | // user_index, 127 | // mint_index, 128 | // quote_free.to_string(), 129 | // actual_quote_free.to_string(), 130 | // ); 131 | assert!(*quote_free == actual_quote_free); 132 | } 133 | if let Some(quote_locked) = expected_value.get("quote_locked") { 134 | // println!( 135 | // "==\nUser: {}, Mint: {}\nExpected quote_locked: {}, Actual quote_locked: {}\n==", 136 | // user_index, 137 | // mint_index, 138 | // quote_locked.to_string(), 139 | // actual_quote_locked.to_string(), 140 | // ); 141 | assert!(*quote_locked == actual_quote_locked); 142 | } 143 | if let Some(base_free) = expected_value.get("base_free") { 144 | println!( 145 | "==\nUser: {}, Mint: {}\nExpected base_free: {}, Actual base_free: {}\n==", 146 | user_index, 147 | mint_index, 148 | base_free.to_string(), 149 | actual_base_free.to_string(), 150 | ); 151 | assert!(*base_free == actual_base_free); 152 | } 153 | if let Some(base_locked) = expected_value.get("base_locked") { 154 | println!( 155 | "==\nUser: {}, Mint: {}\nExpected base_locked: {}, Actual base_locked: {}\n==", 156 | user_index, 157 | mint_index, 158 | base_locked.to_string(), 159 | actual_base_locked.to_string(), 160 | ); 161 | assert!(*base_locked == actual_base_locked); 162 | } 163 | } 164 | 165 | // #[allow(dead_code)] 166 | // pub fn assert_matched_spot_orders( 167 | // mango_group_cookie: &MangoGroupCookie, 168 | // user_spot_orders: &Vec<(usize, usize, serum_dex::matching::Side, f64, f64)>, 169 | // ) { 170 | // let mut balances_map: HashMap = HashMap::new(); 171 | // for i in 0..user_spot_orders.len() { 172 | // let (user_index, _, arranged_order_side, arranged_order_size, arranged_order_price) = user_spot_orders[i]; 173 | // let balances_map_key = format!("{}", user_index); 174 | // let sign = match arranged_order_side { 175 | // serum_dex::matching::Side::Bid => 1.0, 176 | // serum_dex::matching::Side::Ask => -1.0, 177 | // } 178 | // if let Some((base_balance, quote_balance)) = balances_map.get_mut(&balances_map_key) { 179 | // *base_balance += arranged_order_size * arranged_order_price * sign; 180 | // *quote_balance += arranged_order_size * arranged_order_price * (sign * -1.0); 181 | // } else { 182 | // perp_orders_map.insert(perp_orders_map_key.clone(), 0); 183 | // } 184 | // } 185 | // } 186 | 187 | #[allow(dead_code)] 188 | pub fn assert_open_perp_orders( 189 | mango_group_cookie: &MangoGroupCookie, 190 | user_perp_orders: &Vec<(usize, usize, mango::matching::Side, f64, f64)>, 191 | starting_order_id: u64, 192 | ) { 193 | let mut perp_orders_map: HashMap = HashMap::new(); 194 | 195 | for i in 0..user_perp_orders.len() { 196 | let (user_index, _, arranged_order_side, _, _) = user_perp_orders[i]; 197 | let perp_orders_map_key = format!("{}", user_index); 198 | if let Some(x) = perp_orders_map.get_mut(&perp_orders_map_key) { 199 | *x += 1; 200 | } else { 201 | perp_orders_map.insert(perp_orders_map_key.clone(), 0); 202 | } 203 | let mango_account = mango_group_cookie.mango_accounts[user_index].mango_account; 204 | let client_order_id = mango_account.client_order_ids[perp_orders_map[&perp_orders_map_key]]; 205 | let order_side = mango_account.order_side[perp_orders_map[&perp_orders_map_key]]; 206 | assert_eq!(client_order_id, starting_order_id + i as u64,); 207 | assert_eq!(order_side, arranged_order_side); 208 | } 209 | } 210 | 211 | // #[allow(dead_code)] 212 | // pub fn assert_matched_perp_orders( 213 | // test: &mut MangoProgramTest, 214 | // mango_group_cookie: &MangoGroupCookie, 215 | // user_perp_orders: &Vec<(usize, usize, mango::matching::Side, f64, f64)>, 216 | // ) { 217 | // let mut matched_perp_orders_map: HashMap = HashMap::new(); 218 | // let (_, _, _, maker_side, _) = user_perp_orders[0]; 219 | // for i in 0..user_perp_orders.len() { 220 | // let (user_index, mint_index, arranged_order_side, arranged_order_size, arranged_order_price) = user_perp_orders[i]; 221 | // let mango_group = mango_group_cookie.mango_group; 222 | // let perp_market_info = mango_group.perp_markets[mint_index]; 223 | // 224 | // let mint = test.with_mint(mint_index); 225 | // 226 | // let order_size = test.base_size_number_to_lots(&self.mint, arranged_order_size); 227 | // let order_price = test.price_number_to_lots(&self.mint, arranged_order_price); 228 | // 229 | // let mut taker = None; 230 | // let mut base_position: I80F48; 231 | // let mut quote_position: I80F48; 232 | // 233 | // let fee = maker_side 234 | // 235 | // if arranged_order_side == mango::matching::Side::Bid { 236 | // base_position = order_size; 237 | // quote_position = -order_size * order_price - (order_size * order_price * perp_market_info.maker_fee); 238 | // } else { 239 | // base_position = -order_size; 240 | // quote_position = order_size * order_price - (order_size * order_price * perp_market_info.taker_fee); 241 | // } 242 | // 243 | // let perp_orders_map_key = format!("{}_{}", user_index, mint_index); 244 | // 245 | // if let Some(x) = perp_orders_map.get_mut(&perp_orders_map_key) { 246 | // 247 | // *x += 1; 248 | // } else { 249 | // perp_orders_map.insert(perp_orders_map_key.clone(), 0); 250 | // } 251 | // } 252 | // } 253 | 254 | fn get_net(mango_account: &MangoAccount, bank_cache: &RootBankCache, mint_index: usize) -> I80F48 { 255 | if mango_account.deposits[mint_index].is_positive() { 256 | mango_account.deposits[mint_index].checked_mul(bank_cache.deposit_index).unwrap() 257 | } else if mango_account.borrows[mint_index].is_positive() { 258 | -mango_account.borrows[mint_index].checked_mul(bank_cache.borrow_index).unwrap() 259 | } else { 260 | ZERO_I80F48 261 | } 262 | } 263 | 264 | #[allow(dead_code)] 265 | pub async fn assert_vault_net_deposit_diff( 266 | test: &mut MangoProgramTest, 267 | mango_group_cookie: &MangoGroupCookie, 268 | mint_index: usize, 269 | ) { 270 | let mango_cache = mango_group_cookie.mango_cache; 271 | let root_bank_cache = mango_cache.root_bank_cache[mint_index]; 272 | let (_root_bank_pk, root_bank) = 273 | test.with_root_bank(&mango_group_cookie.mango_group, mint_index).await; 274 | 275 | let mut total_net = ZERO_I80F48; 276 | for mango_account in &mango_group_cookie.mango_accounts { 277 | total_net += get_net(&mango_account.mango_account, &root_bank_cache, mint_index); 278 | } 279 | 280 | total_net = total_net.checked_round().unwrap(); 281 | 282 | let mut vault_amount = ZERO_I80F48; 283 | for node_bank_pk in root_bank.node_banks { 284 | if node_bank_pk != Pubkey::default() { 285 | let node_bank = test.load_account::(node_bank_pk).await; 286 | let balance = test.get_token_balance(node_bank.vault).await; 287 | vault_amount += I80F48::from_num(balance); 288 | } 289 | } 290 | 291 | println!("total_net: {}", total_net.to_string()); 292 | println!("vault_amount: {}", vault_amount.to_string()); 293 | 294 | assert!(total_net == vault_amount); 295 | } 296 | -------------------------------------------------------------------------------- /program/tests/program_test/scenarios.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use solana_sdk::transport::TransportError; 3 | 4 | #[allow(dead_code)] 5 | pub fn arrange_deposit_all_scenario( 6 | test: &mut MangoProgramTest, 7 | user_index: usize, 8 | mint_amount: f64, 9 | quote_amount: f64, 10 | ) -> Vec<(usize, usize, f64)> { 11 | let mut user_deposits = Vec::new(); 12 | for mint_index in 0..test.num_mints - 1 { 13 | user_deposits.push((user_index, mint_index, mint_amount)); 14 | } 15 | user_deposits.push((user_index, test.quote_index, quote_amount)); 16 | return user_deposits; 17 | } 18 | 19 | #[allow(dead_code)] 20 | pub async fn deposit_scenario( 21 | test: &mut MangoProgramTest, 22 | mango_group_cookie: &mut MangoGroupCookie, 23 | deposits: &Vec<(usize, usize, f64)>, 24 | ) { 25 | mango_group_cookie.run_keeper(test).await; 26 | 27 | for deposit in deposits { 28 | let (user_index, mint_index, amount) = deposit; 29 | let mint = test.with_mint(*mint_index); 30 | let deposit_amount = (*amount * mint.unit) as u64; 31 | test.perform_deposit(&mango_group_cookie, *user_index, *mint_index, deposit_amount) 32 | .await 33 | .unwrap(); 34 | } 35 | } 36 | 37 | #[allow(dead_code)] 38 | pub async fn withdraw_scenario( 39 | test: &mut MangoProgramTest, 40 | mango_group_cookie: &mut MangoGroupCookie, 41 | withdraws: &Vec<(usize, usize, f64, bool)>, 42 | ) { 43 | mango_group_cookie.run_keeper(test).await; 44 | 45 | for (user_index, mint_index, amount, allow_borrow) in withdraws { 46 | let mint = test.with_mint(*mint_index); 47 | let withdraw_amount = (*amount * mint.unit) as u64; 48 | test.perform_withdraw2( 49 | &mango_group_cookie, 50 | *user_index, 51 | *mint_index, 52 | withdraw_amount, 53 | *allow_borrow, 54 | ) 55 | .await 56 | .unwrap(); 57 | } 58 | } 59 | 60 | #[allow(dead_code)] 61 | pub async fn withdraw_scenario_with_delegate( 62 | test: &mut MangoProgramTest, 63 | mango_group_cookie: &mut MangoGroupCookie, 64 | withdraw: &(usize, usize, usize, f64, bool), 65 | ) -> Result<(), TransportError> { 66 | mango_group_cookie.run_keeper(test).await; 67 | 68 | let (user_index, delegate_user_index, mint_index, amount, allow_borrow) = withdraw; 69 | let mint = test.with_mint(*mint_index); 70 | let withdraw_amount = (*amount * mint.unit) as u64; 71 | test.perform_withdraw_with_delegate( 72 | &mango_group_cookie, 73 | *user_index, 74 | *delegate_user_index, 75 | *mint_index, 76 | withdraw_amount, 77 | *allow_borrow, 78 | ) 79 | .await 80 | } 81 | 82 | #[allow(dead_code)] 83 | pub async fn delegate_scenario( 84 | test: &mut MangoProgramTest, 85 | mango_group_cookie: &mut MangoGroupCookie, 86 | user_index: usize, 87 | delegate_user_index: usize, 88 | ) { 89 | test.perform_set_delegate(&mango_group_cookie, user_index, delegate_user_index).await; 90 | } 91 | 92 | #[allow(dead_code)] 93 | pub async fn reset_delegate_scenario( 94 | test: &mut MangoProgramTest, 95 | mango_group_cookie: &mut MangoGroupCookie, 96 | user_index: usize, 97 | ) { 98 | test.perform_reset_delegate(&mango_group_cookie, user_index).await; 99 | } 100 | 101 | #[allow(dead_code)] 102 | pub async fn place_spot_order_scenario( 103 | test: &mut MangoProgramTest, 104 | mango_group_cookie: &mut MangoGroupCookie, 105 | spot_orders: &Vec<(usize, usize, serum_dex::matching::Side, f64, f64)>, 106 | ) { 107 | mango_group_cookie.run_keeper(test).await; 108 | 109 | for spot_order in spot_orders { 110 | let (user_index, market_index, order_side, order_size, order_price) = *spot_order; 111 | let mut spot_market_cookie = mango_group_cookie.spot_markets[market_index]; 112 | spot_market_cookie 113 | .place_order(test, mango_group_cookie, user_index, order_side, order_size, order_price) 114 | .await; 115 | 116 | mango_group_cookie.users_with_spot_event[market_index].push(user_index); 117 | } 118 | } 119 | 120 | #[allow(dead_code)] 121 | pub async fn place_spot_order_scenario_with_delegate( 122 | test: &mut MangoProgramTest, 123 | mango_group_cookie: &mut MangoGroupCookie, 124 | spot_order: &(usize, usize, usize, serum_dex::matching::Side, f64, f64), 125 | ) -> Result<(), TransportError> { 126 | mango_group_cookie.run_keeper(test).await; 127 | 128 | let (user_index, delegate_user_index, market_index, order_side, order_size, order_price) = 129 | *spot_order; 130 | let mut spot_market_cookie = mango_group_cookie.spot_markets[market_index]; 131 | mango_group_cookie.users_with_spot_event[market_index].push(user_index); 132 | spot_market_cookie 133 | .place_order_with_delegate( 134 | test, 135 | mango_group_cookie, 136 | user_index, 137 | delegate_user_index, 138 | order_side, 139 | order_size, 140 | order_price, 141 | ) 142 | .await 143 | } 144 | 145 | #[allow(dead_code)] 146 | pub async fn place_perp_order_scenario( 147 | test: &mut MangoProgramTest, 148 | mango_group_cookie: &mut MangoGroupCookie, 149 | perp_orders: &Vec<(usize, usize, mango::matching::Side, f64, f64)>, 150 | ) { 151 | mango_group_cookie.run_keeper(test).await; 152 | 153 | for perp_order in perp_orders { 154 | let (user_index, market_index, order_side, order_size, order_price) = *perp_order; 155 | let mut perp_market_cookie = mango_group_cookie.perp_markets[market_index]; 156 | perp_market_cookie 157 | .place_order( 158 | test, 159 | mango_group_cookie, 160 | user_index, 161 | order_side, 162 | order_size, 163 | order_price, 164 | PlacePerpOptions::default(), 165 | ) 166 | .await; 167 | 168 | mango_group_cookie.users_with_perp_event[market_index].push(user_index); 169 | } 170 | } 171 | 172 | #[allow(dead_code)] 173 | pub async fn match_spot_order_scenario( 174 | test: &mut MangoProgramTest, 175 | mango_group_cookie: &mut MangoGroupCookie, 176 | matched_spot_orders: &Vec>, 177 | ) { 178 | for matched_spot_order in matched_spot_orders { 179 | // place_spot_order_scenario() starts by running the keeper 180 | place_spot_order_scenario(test, mango_group_cookie, matched_spot_order).await; 181 | mango_group_cookie.run_keeper(test).await; 182 | mango_group_cookie.consume_spot_events(test).await; 183 | } 184 | mango_group_cookie.run_keeper(test).await; 185 | } 186 | 187 | #[allow(dead_code)] 188 | pub async fn match_perp_order_scenario( 189 | test: &mut MangoProgramTest, 190 | mango_group_cookie: &mut MangoGroupCookie, 191 | matched_perp_orders: &Vec>, 192 | ) { 193 | for matched_perp_order in matched_perp_orders { 194 | // place_perp_order_scenario() starts by running the keeper 195 | place_perp_order_scenario(test, mango_group_cookie, matched_perp_order).await; 196 | mango_group_cookie.run_keeper(test).await; 197 | mango_group_cookie.consume_perp_events(test).await; 198 | } 199 | mango_group_cookie.run_keeper(test).await; 200 | } 201 | -------------------------------------------------------------------------------- /program/tests/test_borrow_withdraw.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | use program_test::assertions::*; 3 | use program_test::cookies::*; 4 | use program_test::scenarios::*; 5 | use program_test::*; 6 | use solana_program_test::*; 7 | 8 | #[tokio::test] 9 | async fn test_vault_net_deposit_diff() { 10 | // === Arrange === 11 | let config = 12 | MangoProgramTestConfig { num_users: 2, ..MangoProgramTestConfig::default_two_mints() }; 13 | 14 | let mut test = MangoProgramTest::start_new(&config).await; 15 | 16 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 17 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 18 | mango_group_cookie.set_oracle(&mut test, 0, 2.0).await; 19 | 20 | // General parameters 21 | let base_index = 0; 22 | let quote_index = 1; 23 | let base_deposit_size: f64 = 1000.0001; 24 | let base_withdraw_size: f64 = 1400.0001; 25 | 26 | // Deposit amounts 27 | let user_deposits = vec![ 28 | (0, base_index, base_deposit_size), 29 | (0, quote_index, base_deposit_size), 30 | (1, base_index, base_deposit_size), 31 | (1, quote_index, base_deposit_size), 32 | ]; 33 | 34 | // Withdraw amounts 35 | let user_withdraws = vec![(0, base_index, base_withdraw_size, true)]; 36 | // === Act === 37 | // Step 1: Make deposits 38 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 39 | 40 | // Step 2: Make withdraws 41 | withdraw_scenario(&mut test, &mut mango_group_cookie, &user_withdraws).await; 42 | 43 | // === Assert === 44 | mango_group_cookie.run_keeper(&mut test).await; 45 | assert_vault_net_deposit_diff(&mut test, &mut mango_group_cookie, 0).await; 46 | } 47 | -------------------------------------------------------------------------------- /program/tests/test_compute.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | use program_test::cookies::*; 3 | use program_test::scenarios::*; 4 | use program_test::*; 5 | use solana_program_test::*; 6 | 7 | #[tokio::test] 8 | async fn test_add_all_markets_to_mango_group() { 9 | // === Arrange === 10 | let config = MangoProgramTestConfig { num_users: 1, ..MangoProgramTestConfig::default() }; 11 | 12 | let mut test = MangoProgramTest::start_new(&config).await; 13 | 14 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 15 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 16 | 17 | let user_index = 0; 18 | println!("Performing deposit"); 19 | 20 | let user_deposits = arrange_deposit_all_scenario(&mut test, user_index, 1000000.0, 1000000.0); 21 | 22 | // === Act === 23 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 24 | } 25 | -------------------------------------------------------------------------------- /program/tests/test_consume_events.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | use program_test::cookies::*; 3 | use program_test::scenarios::*; 4 | use program_test::*; 5 | use solana_program_test::*; 6 | 7 | #[tokio::test] 8 | async fn test_consume_events() { 9 | // === Arrange === 10 | let config = MangoProgramTestConfig { 11 | consume_perp_events_count: 10, 12 | ..MangoProgramTestConfig::default_two_mints() 13 | }; 14 | 15 | let mut test = MangoProgramTest::start_new(&config).await; 16 | 17 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 18 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 19 | 20 | // General parameters 21 | let bidder_user_index: usize = 0; 22 | let asker_user_index: usize = 1; 23 | let mint_index: usize = 0; 24 | let base_price: f64 = 10_000.0; 25 | let base_size: f64 = 1.0; 26 | 27 | // Set oracles 28 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 29 | 30 | // Deposit amounts 31 | let user_deposits = vec![ 32 | (bidder_user_index, test.quote_index, 100.0 * base_price), 33 | (asker_user_index, mint_index, 100.0), 34 | ]; 35 | 36 | let perp_orders = vec![ 37 | (asker_user_index, mint_index, mango::matching::Side::Ask, base_size, base_price), 38 | (bidder_user_index, mint_index, mango::matching::Side::Bid, base_size, base_price), 39 | ]; 40 | 41 | // === Act === 42 | // Step 1: Make deposits 43 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 44 | 45 | // Step 2: Place matching spot orders several times 46 | for _ in 0..10 { 47 | place_perp_order_scenario(&mut test, &mut mango_group_cookie, &perp_orders).await; 48 | } 49 | 50 | // Step 3: Check that the call to consume events does not fail 51 | mango_group_cookie.consume_perp_events(&mut test).await; 52 | } 53 | -------------------------------------------------------------------------------- /program/tests/test_create_account.rs: -------------------------------------------------------------------------------- 1 | use solana_program_test::*; 2 | use solana_sdk::signature::Signer; 3 | use solana_sdk::signer::keypair::Keypair; 4 | 5 | use mango::state::{MangoAccount, ZERO_I80F48}; 6 | use program_test::cookies::*; 7 | use program_test::*; 8 | 9 | mod program_test; 10 | 11 | #[tokio::test] 12 | async fn test_create_account() { 13 | // === Arrange === 14 | let config = MangoProgramTestConfig::default_two_mints(); 15 | let mut test = MangoProgramTest::start_new(&config).await; 16 | 17 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 18 | let num_precreated_mango_users = 0; // create manually 19 | mango_group_cookie 20 | .full_setup(&mut test, num_precreated_mango_users, config.num_mints - 1) 21 | .await; 22 | 23 | let mango_group_pk = &mango_group_cookie.address; 24 | 25 | // 26 | // paid for by owner (test.users[0]) 27 | // 28 | let account0_pk = test.create_mango_account(mango_group_pk, 0, 0, None).await; 29 | test.create_spot_open_orders( 30 | mango_group_pk, 31 | &mango_group_cookie.mango_group, 32 | &account0_pk, 33 | 0, 34 | 0, 35 | None, 36 | ) 37 | .await; 38 | 39 | // 40 | // paid for by separate payer (test.users[1]) still owned by test.users[0] 41 | // 42 | let payer = Keypair::from_base58_string(&test.users[1].to_base58_string()); 43 | let payer_lamports = test.get_lamport_balance(payer.pubkey()).await; 44 | let owner_lamports = test.get_lamport_balance(test.users[0].pubkey()).await; 45 | let account1_pk = test.create_mango_account(mango_group_pk, 0, 1, Some(&payer)).await; 46 | let account1 = test.load_account::(account1_pk).await; 47 | assert_eq!(account1.owner, test.users[0].pubkey()); 48 | assert_eq!(test.get_lamport_balance(test.users[0].pubkey()).await, owner_lamports); 49 | assert!(test.get_lamport_balance(payer.pubkey()).await < payer_lamports); 50 | assert!(account0_pk != account1_pk); 51 | 52 | test.create_spot_open_orders( 53 | mango_group_pk, 54 | &mango_group_cookie.mango_group, 55 | &account1_pk, 56 | 0, 57 | 0, 58 | Some(&payer), 59 | ) 60 | .await; 61 | assert_eq!(test.get_lamport_balance(test.users[0].pubkey()).await, owner_lamports); 62 | } 63 | -------------------------------------------------------------------------------- /program/tests/test_delegate.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use fixed::types::I80F48; 4 | use solana_program_test::*; 5 | 6 | use mango::state::ZERO_I80F48; 7 | use program_test::assertions::*; 8 | use program_test::cookies::*; 9 | use program_test::scenarios::*; 10 | use program_test::*; 11 | 12 | mod program_test; 13 | 14 | #[tokio::test] 15 | async fn test_delegate() { 16 | // === Arrange === 17 | let config = MangoProgramTestConfig::default_two_mints(); 18 | let mut test = MangoProgramTest::start_new(&config).await; 19 | 20 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 21 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 22 | 23 | // General parameters 24 | let user_index: usize = 0; 25 | let delegate_user_index: usize = 1; 26 | let mint_index: usize = 0; 27 | let base_price: f64 = 10_000.0; 28 | let base_size: f64 = 1.0; 29 | let quote_mint = test.quote_mint; 30 | 31 | // Set oracles 32 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 33 | 34 | // Deposit amounts 35 | let user_deposits = vec![(user_index, test.quote_index, base_price * 3.)]; 36 | 37 | // Withdraw amounts 38 | let user_withdraw_with_delegate = 39 | (user_index, delegate_user_index, test.quote_index, base_price, false); 40 | 41 | // Spot Orders 42 | let user_spot_orders = ( 43 | user_index, 44 | delegate_user_index, 45 | mint_index, 46 | serum_dex::matching::Side::Bid, 47 | base_size, 48 | base_price, 49 | ); 50 | 51 | // === Act === 52 | // Step 1: Make deposits 53 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 54 | 55 | // Step2: Setup delegate authority which can place orders on behalf 56 | delegate_scenario(&mut test, &mut mango_group_cookie, user_index, delegate_user_index).await; 57 | 58 | // Step 3: Place spot orders 59 | place_spot_order_scenario_with_delegate(&mut test, &mut mango_group_cookie, &user_spot_orders) 60 | .await 61 | .unwrap(); 62 | 63 | // === Assert === 64 | mango_group_cookie.run_keeper(&mut test).await; 65 | 66 | let expected_values_vec: Vec<(usize, usize, HashMap<&str, I80F48>)> = vec![( 67 | mint_index, // Mint index 68 | user_index, // User index 69 | [ 70 | ("quote_free", ZERO_I80F48), 71 | ("quote_locked", test.to_native("e_mint, base_price * base_size)), 72 | ("base_free", ZERO_I80F48), 73 | ("base_locked", ZERO_I80F48), 74 | ] 75 | .iter() 76 | .cloned() 77 | .collect(), 78 | )]; 79 | 80 | for expected_values in expected_values_vec { 81 | assert_user_spot_orders(&mut test, &mango_group_cookie, expected_values).await; 82 | } 83 | 84 | // Step 4: Withdraw, should fail 85 | withdraw_scenario_with_delegate( 86 | &mut test, 87 | &mut mango_group_cookie, 88 | &user_withdraw_with_delegate, 89 | ) 90 | .await 91 | .unwrap_err(); 92 | 93 | // Step5: Reset delegate 94 | reset_delegate_scenario(&mut test, &mut mango_group_cookie, user_index).await; 95 | 96 | // Step6: Test placing orders again, should fail 97 | place_spot_order_scenario_with_delegate(&mut test, &mut mango_group_cookie, &user_spot_orders) 98 | .await 99 | .unwrap_err(); 100 | } 101 | -------------------------------------------------------------------------------- /program/tests/test_deposit.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | // Tests related to depositing into mango group 3 | mod program_test; 4 | 5 | use program_test::cookies::*; 6 | use program_test::*; 7 | use solana_program_test::*; 8 | 9 | #[tokio::test] 10 | async fn test_deposit_succeeds() { 11 | // === Arrange === 12 | let config = MangoProgramTestConfig::default_two_mints(); 13 | let mut test = MangoProgramTest::start_new(&config).await; 14 | 15 | let user_index = 0; 16 | let amount = 10_000; 17 | 18 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 19 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 20 | 21 | let user_token_account = test.with_user_token_account(user_index, test.quote_index); 22 | let initial_balance = test.get_token_balance(user_token_account).await; 23 | let deposit_amount = amount * (test.quote_mint.unit as u64); 24 | 25 | // === Act === 26 | mango_group_cookie.run_keeper(&mut test).await; 27 | 28 | test.perform_deposit(&mango_group_cookie, user_index, test.quote_index, deposit_amount).await; 29 | 30 | // === Assert === 31 | mango_group_cookie.run_keeper(&mut test).await; 32 | 33 | let post_balance = test.get_token_balance(user_token_account).await; 34 | assert_eq!(post_balance, initial_balance - deposit_amount); 35 | 36 | let (_root_bank_pk, root_bank) = 37 | test.with_root_bank(&mango_group_cookie.mango_group, test.quote_index).await; 38 | let (_node_bank_pk, node_bank) = test.with_node_bank(&root_bank, 0).await; 39 | let mango_vault_balance = test.get_token_balance(node_bank.vault).await; 40 | assert_eq!(mango_vault_balance, deposit_amount); 41 | 42 | let mango_account_deposit = test 43 | .with_mango_account_deposit( 44 | &mango_group_cookie.mango_accounts[user_index].address, 45 | test.quote_index, 46 | ) 47 | .await; 48 | assert_eq!(mango_account_deposit, deposit_amount); 49 | } 50 | -------------------------------------------------------------------------------- /program/tests/test_funding_rate.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | use mango::matching::*; 3 | use program_test::cookies::*; 4 | use program_test::scenarios::*; 5 | use program_test::*; 6 | use solana_program_test::*; 7 | 8 | #[tokio::test] 9 | async fn test_funding_rate() { 10 | // === Arrange === 11 | let config = MangoProgramTestConfig::default_two_mints(); 12 | let mut test = MangoProgramTest::start_new(&config).await; 13 | 14 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 15 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 16 | 17 | // General parameters 18 | let bidder_user_index: usize = 0; 19 | let asker_user_index: usize = 1; 20 | let mint_index: usize = 0; 21 | let base_price: f64 = 10_000.0; 22 | let base_size: f64 = 1.0; 23 | let new_bid_price: f64 = 10_000.0; 24 | let new_ask_price: f64 = 10_200.0; 25 | let clock = test.get_clock().await; 26 | let start_time = clock.unix_timestamp; 27 | let end_time = start_time + 3600 * 1; // 1 Hour 28 | // TODO: Figure out assertion 29 | 30 | // Set oracles 31 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 32 | 33 | // Deposit amounts 34 | let user_deposits = vec![ 35 | (bidder_user_index, test.quote_index, base_price), 36 | (asker_user_index, mint_index, 1.0), 37 | ]; 38 | 39 | // Matched Perp Orders 40 | let matched_perp_orders = vec![vec![ 41 | (asker_user_index, mint_index, mango::matching::Side::Ask, base_size, base_price), 42 | (bidder_user_index, mint_index, mango::matching::Side::Bid, base_size, base_price), 43 | ]]; 44 | 45 | // Perp Orders 46 | let user_perp_orders = vec![ 47 | (bidder_user_index, mint_index, Side::Bid, base_size, new_bid_price), 48 | (asker_user_index, mint_index, Side::Ask, base_size, new_ask_price), 49 | ]; 50 | 51 | // === Act === 52 | // Step 1: Make deposits 53 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 54 | 55 | // Step 2: Place and match spot order 56 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 57 | 58 | // Step 3: Place perp orders 59 | place_perp_order_scenario(&mut test, &mut mango_group_cookie, &user_perp_orders).await; 60 | 61 | // Step 4: Record / Log quote positions before funding 62 | let bidder_quote_position_before = mango_group_cookie.mango_accounts[bidder_user_index] 63 | .mango_account 64 | .perp_accounts[mint_index] 65 | .quote_position; 66 | let asker_quote_position_before = 67 | mango_group_cookie.mango_accounts[asker_user_index].mango_account.perp_accounts[mint_index] 68 | .quote_position; 69 | println!("bidder_quote_position before: {}", bidder_quote_position_before.to_string()); 70 | println!("asker_quote_position before: {}", asker_quote_position_before.to_string()); 71 | 72 | // Step 5: Skip x hours ahead 73 | test.advance_clock_past_timestamp(end_time).await; 74 | 75 | // Step 6: Settle pnl 76 | mango_group_cookie.run_keeper(&mut test).await; 77 | for matched_perp_order in matched_perp_orders { 78 | mango_group_cookie.settle_perp_funds(&mut test, &matched_perp_order).await; 79 | } 80 | 81 | // === Assert === 82 | mango_group_cookie.run_keeper(&mut test).await; 83 | 84 | let bidder_quote_position_after = mango_group_cookie.mango_accounts[bidder_user_index] 85 | .mango_account 86 | .perp_accounts[mint_index] 87 | .quote_position; 88 | let asker_quote_position_after = 89 | mango_group_cookie.mango_accounts[asker_user_index].mango_account.perp_accounts[mint_index] 90 | .quote_position; 91 | println!("bidder_quote_position after: {}", bidder_quote_position_after.to_string()); 92 | println!("asker_quote_position after: {}", asker_quote_position_after.to_string()); 93 | } 94 | 95 | // bidder_quote_position after 0 hours: -10100000000.000015631940187 96 | // bidder_quote_position after 24 hours: -10200000000.000031263880373 97 | // 98 | // 99 | // asker_quote_position after 0 hours: 9900000000 100 | // asker_quote_position after 1 hours: 9904762731.481464873013465 101 | // asker_quote_position after 2 hours: 9910231481.4814637849949 102 | // asker_quote_position after 3 hours: 9913765046.296277919957163 103 | // asker_quote_position after 6 hours: 9928306712.962941613014323 104 | // asker_quote_position after 12 hours: 9953276620.370343922061807 105 | // asker_quote_position after 18 hours: 9980568287.037005206379092 106 | // asker_quote_position after 20 hours: 9987693287.037003703581206 107 | // asker_quote_position after 22 hours: 9999422453.703668027245044 108 | // asker_quote_position after 24 hours: 10000000000 109 | // asker_quote_position after 48 hours: 10000000000 110 | -------------------------------------------------------------------------------- /program/tests/test_init.rs: -------------------------------------------------------------------------------- 1 | // #![cfg(feature = "test-bpf")] 2 | // 3 | // mod helpers; 4 | // 5 | // use helpers::*; 6 | // use solana_program::account_info::AccountInfo; 7 | // use solana_program_test::*; 8 | // use solana_sdk::{ 9 | // account::Account, 10 | // pubkey::Pubkey, 11 | // signature::{Keypair, Signer}, 12 | // transaction::Transaction, 13 | // }; 14 | // use std::mem::size_of; 15 | // 16 | // use mango::{ 17 | // entrypoint::process_instruction, instruction::init_mango_account, state::MangoAccount, 18 | // state::MangoGroup, 19 | // }; 20 | // 21 | // #[tokio::test] 22 | // async fn test_init_mango_group() { 23 | // // Mostly a test to ensure we can successfully create the testing harness 24 | // // Also gives us an alert if the InitMangoGroup tx ends up using too much gas 25 | // let program_id = Pubkey::new_unique(); 26 | // 27 | // let mut test = ProgramTest::new("mango", program_id, processor!(process_instruction)); 28 | // 29 | // // limit to track compute unit increase 30 | // test.set_bpf_compute_max_units(20_000); 31 | // 32 | // let mango_group = add_mango_group_prodlike(&mut test, program_id); 33 | // 34 | // assert_eq!(mango_group.num_oracles, 0); 35 | // 36 | // let (mut banks_client, payer, recent_blockhash) = test.start().await; 37 | // 38 | // let mut transaction = Transaction::new_with_payer( 39 | // &[mango_group.init_mango_group(&payer.pubkey())], 40 | // Some(&payer.pubkey()), 41 | // ); 42 | // 43 | // transaction.sign(&[&payer], recent_blockhash); 44 | // 45 | // assert!(banks_client.process_transaction(transaction).await.is_ok()); 46 | // 47 | // let mut account = banks_client.get_account(mango_group.mango_group_pk).await.unwrap().unwrap(); 48 | // let account_info: AccountInfo = (&mango_group.mango_group_pk, &mut account).into(); 49 | // let mango_group_loaded = MangoGroup::load_mut_checked(&account_info, &program_id).unwrap(); 50 | // 51 | // assert_eq!(mango_group_loaded.valid_interval, 5) 52 | // } 53 | // 54 | // #[tokio::test] 55 | // async fn test_init_mango_account() { 56 | // let program_id = Pubkey::new_unique(); 57 | // 58 | // let mut test = ProgramTest::new("mango", program_id, processor!(process_instruction)); 59 | // 60 | // // limit to track compute unit increase 61 | // test.set_bpf_compute_max_units(20_000); 62 | // 63 | // let mango_group = add_mango_group_prodlike(&mut test, program_id); 64 | // let mango_account_pk = Pubkey::new_unique(); 65 | // test.add_account( 66 | // mango_account_pk, 67 | // Account::new(u32::MAX as u64, size_of::(), &program_id), 68 | // ); 69 | // let user = Keypair::new(); 70 | // test.add_account(user.pubkey(), Account::new(u32::MAX as u64, 0, &user.pubkey())); 71 | // 72 | // let (mut banks_client, payer, recent_blockhash) = test.start().await; 73 | // 74 | // let mut transaction = Transaction::new_with_payer( 75 | // &[ 76 | // mango_group.init_mango_group(&payer.pubkey()), 77 | // init_mango_account( 78 | // &program_id, 79 | // &mango_group.mango_group_pk, 80 | // &mango_account_pk, 81 | // &user.pubkey(), 82 | // ) 83 | // .unwrap(), 84 | // ], 85 | // Some(&payer.pubkey()), 86 | // ); 87 | // 88 | // transaction.sign(&[&payer, &user], recent_blockhash); 89 | // assert!(banks_client.process_transaction(transaction).await.is_ok()); 90 | // 91 | // let mut account = banks_client.get_account(mango_account_pk).await.unwrap().unwrap(); 92 | // let account_info: AccountInfo = (&mango_account_pk, &mut account).into(); 93 | // let mango_account = 94 | // MangoAccount::load_mut_checked(&account_info, &program_id, &mango_group.mango_group_pk) 95 | // .unwrap(); 96 | // } 97 | -------------------------------------------------------------------------------- /program/tests/test_instructions.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | use fixed::types::I80F48; 3 | use mango::{instruction::*, matching::*, state::*}; 4 | use program_test::cookies::*; 5 | use program_test::*; 6 | 7 | #[test] 8 | fn test_instruction_serialization() { 9 | use std::num::NonZeroU64; 10 | let cases = vec![ 11 | MangoInstruction::InitMangoGroup { 12 | signer_nonce: 126, 13 | valid_interval: 79846, 14 | quote_optimal_util: I80F48::from_num(1.0), 15 | quote_optimal_rate: I80F48::from_num(7897891.12310), 16 | quote_max_rate: I80F48::from_num(1546.0), 17 | }, 18 | MangoInstruction::InitMangoAccount {}, 19 | MangoInstruction::Deposit { quantity: 1234567 }, 20 | MangoInstruction::Withdraw { quantity: 1234567, allow_borrow: true }, 21 | MangoInstruction::AddSpotMarket { 22 | maint_leverage: I80F48::from_num(1546.0), 23 | init_leverage: I80F48::from_num(1546789.0), 24 | liquidation_fee: I80F48::from_num(1546.789470), 25 | optimal_util: I80F48::from_num(6156.0), 26 | optimal_rate: I80F48::from_num(8791.150), 27 | max_rate: I80F48::from_num(46.9870), 28 | }, 29 | MangoInstruction::AddToBasket { market_index: 156489 }, 30 | MangoInstruction::Borrow { quantity: 1264 }, 31 | MangoInstruction::AddPerpMarket { 32 | maint_leverage: I80F48::from_num(1546.0), 33 | init_leverage: I80F48::from_num(1546789.0), 34 | liquidation_fee: I80F48::from_num(1546.789470), 35 | maker_fee: I80F48::from_num(6156.0), 36 | taker_fee: I80F48::from_num(8791.150), 37 | base_lot_size: -4597, 38 | quote_lot_size: 45644597, 39 | rate: I80F48::from_num(8791.150), 40 | max_depth_bps: I80F48::from_num(87.99), 41 | target_period_length: 1234, 42 | mngo_per_period: 987, 43 | exp: 5, 44 | }, 45 | MangoInstruction::PlacePerpOrder { 46 | price: 898726, 47 | quantity: 54689789456, 48 | client_order_id: 42, 49 | side: Side::Ask, 50 | order_type: OrderType::PostOnly, 51 | reduce_only: true, 52 | }, 53 | MangoInstruction::CancelPerpOrderByClientId { client_order_id: 78, invalid_id_ok: true }, 54 | MangoInstruction::CancelPerpOrder { order_id: 497894561564897, invalid_id_ok: true }, 55 | MangoInstruction::ConsumeEvents { limit: 77 }, 56 | MangoInstruction::SetOracle { price: I80F48::from_num(6156.0) }, 57 | MangoInstruction::PlaceSpotOrder { 58 | order: serum_dex::instruction::NewOrderInstructionV3 { 59 | side: serum_dex::matching::Side::Bid, 60 | limit_price: NonZeroU64::new(456789).unwrap(), 61 | max_coin_qty: NonZeroU64::new(789456).unwrap(), 62 | max_native_pc_qty_including_fees: NonZeroU64::new(42).unwrap(), 63 | order_type: serum_dex::matching::OrderType::PostOnly, 64 | self_trade_behavior: serum_dex::instruction::SelfTradeBehavior::CancelProvide, 65 | client_order_id: 8941, 66 | limit: 1597, 67 | max_ts: i64::MAX, 68 | }, 69 | }, 70 | MangoInstruction::PlaceSpotOrder2 { 71 | order: serum_dex::instruction::NewOrderInstructionV3 { 72 | side: serum_dex::matching::Side::Ask, 73 | limit_price: NonZeroU64::new(456789).unwrap(), 74 | max_coin_qty: NonZeroU64::new(789456).unwrap(), 75 | max_native_pc_qty_including_fees: NonZeroU64::new(42).unwrap(), 76 | order_type: serum_dex::matching::OrderType::ImmediateOrCancel, 77 | self_trade_behavior: serum_dex::instruction::SelfTradeBehavior::CancelProvide, 78 | client_order_id: 8941, 79 | limit: 1597, 80 | max_ts: i64::MAX, 81 | }, 82 | }, 83 | MangoInstruction::CancelSpotOrder { 84 | order: serum_dex::instruction::CancelOrderInstructionV2 { 85 | side: serum_dex::matching::Side::Ask, 86 | order_id: 587945166, 87 | }, 88 | }, 89 | MangoInstruction::SettlePnl { market_index: 7897 }, 90 | MangoInstruction::SettleBorrow { token_index: 25, quantity: 8979846 }, 91 | MangoInstruction::ForceCancelSpotOrders { limit: 254 }, 92 | MangoInstruction::ForceCancelPerpOrders { limit: 254 }, 93 | MangoInstruction::LiquidateTokenAndToken { max_liab_transfer: I80F48::from_num(6156.33) }, 94 | MangoInstruction::LiquidateTokenAndPerp { 95 | asset_type: AssetType::Perp, 96 | asset_index: 1234, 97 | liab_type: AssetType::Token, 98 | liab_index: 598789, 99 | max_liab_transfer: I80F48::from_num(6156.33), 100 | }, 101 | MangoInstruction::LiquidatePerpMarket { base_transfer_request: -8974 }, 102 | MangoInstruction::ResolvePerpBankruptcy { 103 | liab_index: 254, 104 | max_liab_transfer: I80F48::from_num(6156.33), 105 | }, 106 | MangoInstruction::ResolveTokenBankruptcy { max_liab_transfer: I80F48::from_num(6156.33) }, 107 | MangoInstruction::AddMangoAccountInfo { info: [7u8; INFO_LEN] }, 108 | MangoInstruction::DepositMsrm { quantity: 15 }, 109 | MangoInstruction::WithdrawMsrm { quantity: 98784615 }, 110 | MangoInstruction::ChangePerpMarketParams { 111 | maint_leverage: Some(I80F48::from_num(6156.33)), 112 | init_leverage: None, 113 | liquidation_fee: Some(I80F48::from_num(6156.33)), 114 | maker_fee: None, 115 | taker_fee: Some(I80F48::from_num(999.73)), 116 | rate: None, 117 | max_depth_bps: None, 118 | target_period_length: Some(87985461), 119 | mngo_per_period: None, 120 | exp: Some(7), 121 | }, 122 | MangoInstruction::CancelAllPerpOrders { limit: 7 }, 123 | MangoInstruction::AddPerpTriggerOrder { 124 | order_type: OrderType::Limit, 125 | side: Side::Ask, 126 | trigger_condition: TriggerCondition::Above, 127 | reduce_only: true, 128 | client_order_id: 42, 129 | price: 898726, 130 | quantity: 54689789456, 131 | trigger_price: I80F48::from_num(45643.45645646), 132 | }, 133 | MangoInstruction::AddPerpTriggerOrder { 134 | order_type: OrderType::PostOnly, 135 | side: Side::Bid, 136 | trigger_condition: TriggerCondition::Below, 137 | reduce_only: false, 138 | client_order_id: 4242, 139 | price: 898, 140 | quantity: 897894561, 141 | trigger_price: I80F48::from_num(1.0), 142 | }, 143 | MangoInstruction::RemoveAdvancedOrder { order_index: 42 }, 144 | MangoInstruction::ExecutePerpTriggerOrder { order_index: 249 }, 145 | ]; 146 | for case in cases { 147 | assert!(MangoInstruction::unpack(&case.pack()).unwrap() == case); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /program/tests/test_interest_rate.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | use fixed::types::I80F48; 3 | use mango::state::*; 4 | use program_test::cookies::*; 5 | use program_test::scenarios::*; 6 | use program_test::*; 7 | use solana_program_test::*; 8 | 9 | #[tokio::test] 10 | async fn test_interest_rate() { 11 | // === Arrange === 12 | let config = MangoProgramTestConfig::default_two_mints(); 13 | let mut test = MangoProgramTest::start_new(&config).await; 14 | 15 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 16 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 17 | 18 | // General parameters 19 | let borrower_user_index: usize = 0; 20 | let lender_user_index: usize = 1; 21 | let mint_index: usize = 0; 22 | let base_price: f64 = 10_000.0; 23 | let base_deposit_size: f64 = 0.01; 24 | let base_withdraw_size: f64 = 0.0001; 25 | let mut clock = test.get_clock().await; 26 | let start_time = clock.unix_timestamp; 27 | // TODO: Make into variables and figure out assert 28 | 29 | // Set oracles 30 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 31 | 32 | // Deposit amounts 33 | let user_deposits = vec![ 34 | (borrower_user_index, test.quote_index, base_price * base_deposit_size * 2.0), 35 | (lender_user_index, mint_index, base_deposit_size), 36 | ]; 37 | 38 | // Withdraw amounts 39 | let user_withdraws = vec![(borrower_user_index, mint_index, base_withdraw_size, true)]; 40 | 41 | // === Act === 42 | // Step 1: Make deposits 43 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 44 | 45 | // Step 2: Make withdraws 46 | withdraw_scenario(&mut test, &mut mango_group_cookie, &user_withdraws).await; 47 | 48 | //Assert 49 | clock = test.get_clock().await; 50 | let end_time = clock.unix_timestamp - 1; 51 | mango_group_cookie.run_keeper(&mut test).await; 52 | let borrower_base_borrow = &mango_group_cookie.mango_accounts[borrower_user_index] 53 | .mango_account 54 | .get_native_borrow(&mango_group_cookie.mango_cache.root_bank_cache[mint_index], mint_index) 55 | .unwrap(); 56 | let _lender_base_deposit = &mango_group_cookie.mango_accounts[lender_user_index] 57 | .mango_account 58 | .get_native_deposit(&mango_group_cookie.mango_cache.root_bank_cache[mint_index], mint_index) 59 | .unwrap(); 60 | 61 | let mint = test.with_mint(mint_index); 62 | let optimal_util = I80F48::from_num(0.7); 63 | let optimal_rate = I80F48::from_num(0.06) / YEAR; 64 | let max_rate = I80F48::from_num(1.5) / YEAR; 65 | let native_borrows = I80F48::from_num(test.base_size_number_to_lots(&mint, 1000000.0)); 66 | let native_deposits = I80F48::from_num(test.base_size_number_to_lots(&mint, 1000000.0)); 67 | 68 | let utilization = native_borrows.checked_div(native_deposits).unwrap_or(ZERO_I80F48); 69 | let interest_rate = if utilization > optimal_util { 70 | let extra_util = utilization - optimal_util; 71 | let slope = (max_rate - optimal_rate) / (ONE_I80F48 - optimal_util); 72 | optimal_rate + slope * extra_util 73 | } else { 74 | let slope = optimal_rate / optimal_util; 75 | slope * utilization 76 | }; 77 | let borrow_interest = 78 | interest_rate.checked_mul(I80F48::from_num(start_time - end_time)).unwrap(); 79 | let _deposit_interest = borrow_interest.checked_mul(utilization).unwrap(); 80 | let new_borrow = borrower_base_borrow + borrow_interest; 81 | println!("new_borrow: {}", new_borrow.to_string()); 82 | // TODO: Assert 83 | // Deposit: 1, Borrow: 1 = 0.000000047564683 84 | // Deposit: 1, Borrow: 0.5 = 0.000000001358988 85 | // Deposit: 1, Borrow: 0.05 = 0.0000000001359 86 | // Deposit: 2, Borrow: 0.05 = 0.00000000006795 87 | } 88 | -------------------------------------------------------------------------------- /program/tests/test_liquidation_delisting_token.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | 3 | use mango::state::*; 4 | use program_test::cookies::*; 5 | use program_test::*; 6 | use solana_program_test::*; 7 | 8 | #[tokio::test] 9 | async fn test_liquidation_delisting_token_only_deposits() { 10 | let config = MangoProgramTestConfig::default_two_mints(); 11 | let mut test = MangoProgramTest::start_new(&config).await; 12 | 13 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 14 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 15 | 16 | let market_index: usize = 0; 17 | let price: u64 = 5; 18 | 19 | let liqee_index: usize = 0; 20 | let liqor_index: usize = 1; 21 | 22 | let liqee_deposit = 100; 23 | 24 | // Set asset price to expected value 25 | mango_group_cookie.set_oracle(&mut test, market_index, price as f64).await; 26 | 27 | // Deposit some asset to be delisted 28 | mango_group_cookie.run_keeper(&mut test).await; 29 | test.perform_deposit(&mango_group_cookie, liqee_index, market_index, liqee_deposit) 30 | .await 31 | .unwrap(); 32 | 33 | // Set market to force close 34 | test.perform_set_market_mode( 35 | &mango_group_cookie, 36 | market_index, 37 | MarketMode::CloseOnly, 38 | AssetType::Token, 39 | ) 40 | .await 41 | .unwrap(); 42 | test.perform_set_market_mode( 43 | &mango_group_cookie, 44 | market_index, 45 | MarketMode::ForceCloseOnly, 46 | AssetType::Token, 47 | ) 48 | .await 49 | .unwrap(); 50 | 51 | // Expect deposit to be completely withdrawn to the liqee ATA 52 | test.perform_liquidate_delisting_token( 53 | &mango_group_cookie, 54 | liqee_index, 55 | liqor_index, 56 | market_index, 57 | test.quote_index, 58 | ) 59 | .await 60 | .unwrap(); 61 | let deposit_post = test 62 | .with_mango_account_deposit_I80F48( 63 | &mango_group_cookie.mango_accounts[liqee_index].address, 64 | market_index, 65 | ) 66 | .await; 67 | 68 | assert!(deposit_post == ZERO_I80F48); 69 | // TODO: check the correct ATA gets the balance 70 | } 71 | 72 | #[tokio::test] 73 | async fn test_liquidation_delisting_token_deposits_as_collateral() { 74 | let config = MangoProgramTestConfig::default_two_mints(); 75 | let mut test = MangoProgramTest::start_new(&config).await; 76 | 77 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 78 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 79 | 80 | let market_index: usize = 0; 81 | let price: u64 = 5; 82 | 83 | let liqee_index: usize = 0; 84 | let liqor_index: usize = 1; 85 | 86 | let liqee_deposit = 100; 87 | let liqee_borrow = 20; 88 | 89 | // Set asset price to expected value 90 | mango_group_cookie.set_oracle(&mut test, market_index, price as f64).await; 91 | 92 | // Deposit some asset to be delisted, withdraw some quote to make a borrow 93 | test.update_all_root_banks(&mango_group_cookie, &mango_group_cookie.address).await; 94 | test.cache_all_prices( 95 | &mango_group_cookie.mango_group, 96 | &mango_group_cookie.address, 97 | &mango_group_cookie.mango_group.oracles[0..mango_group_cookie.mango_group.num_oracles], 98 | ) 99 | .await; 100 | test.perform_deposit(&mango_group_cookie, liqee_index, market_index, liqee_deposit) 101 | .await 102 | .unwrap(); 103 | test.perform_deposit(&mango_group_cookie, liqor_index, test.quote_index, liqee_borrow * 5) 104 | .await 105 | .unwrap(); 106 | test.perform_withdraw(&mango_group_cookie, liqee_index, test.quote_index, liqee_borrow, true) 107 | .await 108 | .unwrap(); 109 | 110 | // Set market to force close 111 | test.perform_set_market_mode( 112 | &mango_group_cookie, 113 | market_index, 114 | MarketMode::CloseOnly, 115 | AssetType::Token, 116 | ) 117 | .await 118 | .unwrap(); 119 | test.perform_set_market_mode( 120 | &mango_group_cookie, 121 | market_index, 122 | MarketMode::ForceCloseOnly, 123 | AssetType::Token, 124 | ) 125 | .await 126 | .unwrap(); 127 | 128 | // Expect deposit to be completely withdrawn to the liqor ATA 129 | // Expect 130 | test.perform_liquidate_delisting_token( 131 | &mango_group_cookie, 132 | liqee_index, 133 | liqor_index, 134 | market_index, 135 | test.quote_index, 136 | ) 137 | .await 138 | .unwrap(); 139 | let deposit_post = test 140 | .with_mango_account_deposit( 141 | &mango_group_cookie.mango_accounts[liqee_index].address, 142 | market_index, 143 | ) 144 | .await; 145 | println!("{}", deposit_post); 146 | assert!(deposit_post == 0); 147 | // TODO: check the correct ATA gets the balance 148 | } 149 | 150 | #[tokio::test] 151 | async fn test_liquidation_delisting_token_borrows() { 152 | let config = MangoProgramTestConfig::default_two_mints(); 153 | let mut test = MangoProgramTest::start_new(&config).await; 154 | 155 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 156 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 157 | 158 | let market_index: usize = 0; 159 | let price: u64 = 5; 160 | 161 | let liqee_index: usize = 0; 162 | let liqor_index: usize = 1; 163 | 164 | let liqee_deposit = 100; 165 | let liqee_borrow = 10; 166 | 167 | // Set asset price to expected value 168 | mango_group_cookie.set_oracle(&mut test, market_index, price as f64).await; 169 | 170 | // Deposit some asset to be delisted, withdraw some quote to make a borrow 171 | test.update_all_root_banks(&mango_group_cookie, &mango_group_cookie.address).await; 172 | test.cache_all_prices( 173 | &mango_group_cookie.mango_group, 174 | &mango_group_cookie.address, 175 | &mango_group_cookie.mango_group.oracles[0..mango_group_cookie.mango_group.num_oracles], 176 | ) 177 | .await; 178 | test.perform_deposit(&mango_group_cookie, liqee_index, test.quote_index, liqee_deposit) 179 | .await 180 | .unwrap(); 181 | test.perform_deposit(&mango_group_cookie, liqor_index, market_index, liqee_borrow * 2) 182 | .await 183 | .unwrap(); 184 | test.perform_deposit(&mango_group_cookie, liqor_index, test.quote_index, liqee_borrow * 10) 185 | .await 186 | .unwrap(); 187 | test.perform_withdraw(&mango_group_cookie, liqee_index, market_index, liqee_borrow, true) 188 | .await 189 | .unwrap(); 190 | 191 | let liqor_deposit_pre = test 192 | .with_mango_account_deposit_I80F48( 193 | &mango_group_cookie.mango_accounts[liqor_index].address, 194 | market_index, 195 | ) 196 | .await; 197 | let borrow_pre = test 198 | .with_mango_account_borrow_I80F48( 199 | &mango_group_cookie.mango_accounts[liqee_index].address, 200 | market_index, 201 | ) 202 | .await; 203 | 204 | // Set market to force close 205 | test.perform_set_market_mode( 206 | &mango_group_cookie, 207 | market_index, 208 | MarketMode::CloseOnly, 209 | AssetType::Token, 210 | ) 211 | .await 212 | .unwrap(); 213 | test.perform_set_market_mode( 214 | &mango_group_cookie, 215 | market_index, 216 | MarketMode::ForceCloseOnly, 217 | AssetType::Token, 218 | ) 219 | .await 220 | .unwrap(); 221 | 222 | // Expect deposit to be completely withdrawn to the liqor ATA 223 | // Expect 224 | test.perform_liquidate_delisting_token( 225 | &mango_group_cookie, 226 | liqee_index, 227 | liqor_index, 228 | market_index, 229 | test.quote_index, 230 | ) 231 | .await 232 | .unwrap(); 233 | // TODO: check i80f48 for dust 234 | let deposit_post = test 235 | .with_mango_account_deposit_I80F48( 236 | &mango_group_cookie.mango_accounts[liqee_index].address, 237 | market_index, 238 | ) 239 | .await; 240 | assert!(deposit_post == ZERO_I80F48); 241 | let borrow_post = test 242 | .with_mango_account_borrow_I80F48( 243 | &mango_group_cookie.mango_accounts[liqee_index].address, 244 | market_index, 245 | ) 246 | .await; 247 | let liqor_deposit_post = test 248 | .with_mango_account_deposit_I80F48( 249 | &mango_group_cookie.mango_accounts[liqor_index].address, 250 | market_index, 251 | ) 252 | .await; 253 | 254 | assert!(borrow_post == ZERO_I80F48); 255 | println!("{} {} {}", liqor_deposit_post, liqor_deposit_pre, borrow_pre); 256 | // assert!(liqor_deposit_post == liqor_deposit_pre - borrow_pre); 257 | // TODO: check the correct ATA gets the balance 258 | } 259 | -------------------------------------------------------------------------------- /program/tests/test_liquidation_perp_market.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | 3 | use fixed_macro::types::I80F48; 4 | use program_test::assertions::*; 5 | use program_test::cookies::*; 6 | use program_test::scenarios::*; 7 | use program_test::*; 8 | use solana_program_test::*; 9 | 10 | #[tokio::test] 11 | /// Simple test for ix liquidate_perp_market 12 | /// Transfers liqees base and quote positions to liqor 13 | /// note: doesnt check the numbers to exact accuracy 14 | async fn test_liquidation_perp_market_basic() { 15 | // === Arrange === 16 | let config = 17 | MangoProgramTestConfig { num_users: 3, ..MangoProgramTestConfig::default_two_mints() }; 18 | let mut test = MangoProgramTest::start_new(&config).await; 19 | 20 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 21 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 22 | 23 | // General parameters 24 | let bidder_user_index: usize = 0; 25 | let asker_user_index: usize = 1; 26 | let liqor_user_index: usize = 2; 27 | let mint_index: usize = 0; 28 | let base_price: f64 = 10_000.0; 29 | let base_size: f64 = 1.0; 30 | 31 | // Set oracles 32 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 33 | 34 | // Deposit amounts 35 | let user_deposits = vec![ 36 | (bidder_user_index, test.quote_index, base_price), 37 | (asker_user_index, mint_index, 1.0), 38 | (liqor_user_index, test.quote_index, base_price), 39 | ]; 40 | 41 | // Matched Perp Orders 42 | let matched_perp_orders = vec![vec![ 43 | (asker_user_index, mint_index, mango::matching::Side::Ask, base_size, base_price), 44 | (bidder_user_index, mint_index, mango::matching::Side::Bid, base_size, base_price), 45 | ]]; 46 | 47 | // === Act === 48 | // Step 1: Make deposits 49 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 50 | 51 | // Step 2: Place and match perp order 52 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 53 | 54 | // assert that bidder has open LONG 55 | let bidder_quote_position = mango_group_cookie.mango_accounts[bidder_user_index] 56 | .mango_account 57 | .perp_accounts[mint_index] 58 | .quote_position; 59 | let bidder_base_position = mango_group_cookie.mango_accounts[bidder_user_index] 60 | .mango_account 61 | .perp_accounts[mint_index] 62 | .base_position; 63 | // dbg!(bidder_quote_position); 64 | // dbg!(bidder_base_position); 65 | // [program/tests/test_liquidation_perp_market.rs:93] bidder_quote_position = -10100000000.000015631940187 66 | // [program/tests/test_liquidation_perp_market.rs:94] bidder_base_position = 10000 67 | assert_approx_eq!(bidder_quote_position, I80F48!(-10100000000)); 68 | assert!(bidder_base_position == I80F48!(10000)); 69 | 70 | // assert that liqor has no base & quote positions 71 | let liqor_quote_position = 72 | mango_group_cookie.mango_accounts[liqor_user_index].mango_account.perp_accounts[mint_index] 73 | .quote_position; 74 | let liqor_base_position = 75 | mango_group_cookie.mango_accounts[liqor_user_index].mango_account.perp_accounts[mint_index] 76 | .base_position; 77 | // dbg!(liqor_quote_position); 78 | // dbg!(liqor_base_position); 79 | // [program/tests/test_liquidation_perp_market.rs:95] liqor_quote_position = 0 80 | // [program/tests/test_liquidation_perp_market.rs:96] liqor_base_position = 0 81 | assert!(liqor_quote_position.is_zero()); 82 | assert_eq!(liqor_base_position, 0); 83 | 84 | // Step 3: lower oracle price artificially to induce bad health 85 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price / 150.0).await; 86 | mango_group_cookie.run_keeper(&mut test).await; 87 | 88 | // Step 4: Perform a couple of liquidations 89 | for _ in 0..6 { 90 | mango_group_cookie.run_keeper(&mut test).await; 91 | test.perform_liquidate_perp_market( 92 | &mut mango_group_cookie, 93 | mint_index, 94 | bidder_user_index, 95 | liqor_user_index, 96 | 1000, 97 | ) 98 | .await; 99 | } 100 | 101 | // quote and base position should have been transferred to liqor 102 | 103 | // assert that bidder has lowered quote and base positions 104 | let bidder_quote_position = mango_group_cookie.mango_accounts[bidder_user_index] 105 | .mango_account 106 | .perp_accounts[mint_index] 107 | .quote_position; 108 | let bidder_base_position = mango_group_cookie.mango_accounts[bidder_user_index] 109 | .mango_account 110 | .perp_accounts[mint_index] 111 | .base_position; 112 | // dbg!(bidder_quote_position); 113 | // dbg!(bidder_base_position); 114 | // [program/tests/test_liquidation_perp_market.rs:127] bidder_quote_position = -10061000000.000015572325644 115 | // [program/tests/test_liquidation_perp_market.rs:128] bidder_base_position = 4000 116 | assert_approx_eq!(I80F48!(-10061000000), bidder_quote_position); 117 | assert_eq!(bidder_base_position, I80F48!(4000)); 118 | 119 | // assert that liqor has non zero quote and base positions 120 | let liqor_quote_position = 121 | mango_group_cookie.mango_accounts[liqor_user_index].mango_account.perp_accounts[mint_index] 122 | .quote_position; 123 | let liqor_base_position = 124 | mango_group_cookie.mango_accounts[liqor_user_index].mango_account.perp_accounts[mint_index] 125 | .base_position; 126 | // dbg!(liqor_quote_position); 127 | // dbg!(liqor_base_position); 128 | // [program/tests/test_liquidation_perp_market.rs:129] liqor_quote_position = -39000000.000000059614543 129 | // [program/tests/test_liquidation_perp_market.rs:130] liqor_base_position = 6000 130 | assert_approx_eq!(liqor_quote_position, I80F48!(-39000000)); 131 | assert_eq!(liqor_base_position, I80F48!(6000)); 132 | } 133 | -------------------------------------------------------------------------------- /program/tests/test_liquidation_perp_market_max_cu.rs: -------------------------------------------------------------------------------- 1 | use solana_program_test::*; 2 | 3 | use program_test::cookies::*; 4 | use program_test::scenarios::*; 5 | use program_test::*; 6 | 7 | mod program_test; 8 | 9 | /// for ix liquidate_perp_market, test max cu usage (that it doesnt exceed 200k), 10 | /// by having spot open orders accounts, orders, 11 | /// and perp positions across as many markets as possible 12 | #[tokio::test] 13 | async fn test_liquidation_perp_market_max_cu() { 14 | let config = MangoProgramTestConfig { 15 | num_users: 3, 16 | // other ixs (CreateSpotOpenOrders) take more cu than the liquidate ix in this case, 17 | // the liquidate ix 'consumed 83426 of 200000 compute units' 18 | compute_limit: 200_000, 19 | ..MangoProgramTestConfig::default() 20 | }; 21 | let mut test = MangoProgramTest::start_new(&config).await; 22 | 23 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 24 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 25 | 26 | let bidder_user_index: usize = 0; 27 | let asker_user_index: usize = 1; 28 | let liqor_user_index: usize = 2; 29 | let mint_index: usize = 0; 30 | let base_price: f64 = 10_000.0; 31 | let base_size: f64 = 1.0; 32 | 33 | { 34 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 35 | 36 | let user_deposits = vec![ 37 | (bidder_user_index, test.quote_index, base_price), 38 | (asker_user_index, mint_index, 1.0), 39 | (liqor_user_index, test.quote_index, base_price), 40 | ]; 41 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 42 | 43 | // create a perp position which would cause bad health, and make liquidation succeed 44 | let matched_perp_orders = vec![vec![ 45 | (asker_user_index, mint_index, mango::matching::Side::Ask, base_size, base_price), 46 | (bidder_user_index, mint_index, mango::matching::Side::Bid, base_size, base_price), 47 | ]]; 48 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 49 | 50 | // create a corresponding spot open orders account and some position to max out cu usage 51 | let matched_spot_orders = vec![vec![ 52 | (bidder_user_index, mint_index, serum_dex::matching::Side::Bid, 0.0001, base_price), 53 | (asker_user_index, mint_index, serum_dex::matching::Side::Ask, 0.0001, base_price), 54 | ]]; 55 | match_spot_order_scenario(&mut test, &mut mango_group_cookie, &matched_spot_orders).await; 56 | for matched_spot_order in matched_spot_orders { 57 | mango_group_cookie.settle_spot_funds(&mut test, &matched_spot_order).await; 58 | } 59 | } 60 | 61 | // create open orders account for 5 markets, place and settle trade across all these 62 | // 5 markets, 63 | // also create perp positions across all markets 64 | // ...to max out cu usage 65 | for market_index in 1..6 { 66 | mango_group_cookie.set_oracle(&mut test, market_index, 1.0).await; 67 | 68 | let user_deposits = vec![ 69 | (bidder_user_index, test.quote_index, 2.0), 70 | (asker_user_index, market_index, 1.0), 71 | (asker_user_index, test.quote_index, 1.0), 72 | ]; 73 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 74 | 75 | let matched_spot_orders = vec![vec![ 76 | (bidder_user_index, market_index, serum_dex::matching::Side::Bid, base_size, 1.), 77 | (asker_user_index, market_index, serum_dex::matching::Side::Ask, base_size, 1.), 78 | ]]; 79 | match_spot_order_scenario(&mut test, &mut mango_group_cookie, &matched_spot_orders).await; 80 | for matched_spot_order in matched_spot_orders { 81 | mango_group_cookie.settle_spot_funds(&mut test, &matched_spot_order).await; 82 | } 83 | 84 | let matched_perp_orders = vec![vec![ 85 | (asker_user_index, market_index, mango::matching::Side::Ask, base_size, 1.), 86 | (bidder_user_index, market_index, mango::matching::Side::Bid, base_size, 1.), 87 | ]]; 88 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 89 | } 90 | 91 | // create open orders account for across all remaining 9 markets, place (unmatched) orders across 92 | // all these 9 markets, 9 is maximum number of markets across which user can have orders, 93 | // also create perp positions across all markets 94 | // ...to max out cu usage 95 | for market_index in 6..15 { 96 | mango_group_cookie.set_oracle(&mut test, market_index, 1.0).await; 97 | 98 | let user_deposits = vec![ 99 | (bidder_user_index, test.quote_index, 2.0), 100 | (asker_user_index, market_index, 1.0), 101 | (asker_user_index, test.quote_index, 1.0), 102 | ]; 103 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 104 | 105 | let placed_spot_orders = vec![ 106 | (bidder_user_index, market_index, serum_dex::matching::Side::Bid, base_size, 0.9), 107 | (asker_user_index, market_index, serum_dex::matching::Side::Ask, base_size, 1.1), 108 | ]; 109 | place_spot_order_scenario(&mut test, &mut mango_group_cookie, &placed_spot_orders).await; 110 | 111 | let matched_perp_orders = vec![vec![ 112 | (asker_user_index, market_index, mango::matching::Side::Ask, base_size, 1.), 113 | (bidder_user_index, market_index, mango::matching::Side::Bid, base_size, 1.), 114 | ]]; 115 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 116 | } 117 | 118 | // lower oracle price artificially to induce bad health 119 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price / 150.0).await; 120 | mango_group_cookie.run_keeper(&mut test).await; 121 | 122 | // perform a liquidation to test cu usage 123 | mango_group_cookie.run_keeper(&mut test).await; 124 | test.perform_liquidate_perp_market( 125 | &mut mango_group_cookie, 126 | mint_index, 127 | bidder_user_index, 128 | liqor_user_index, 129 | 1000, 130 | ) 131 | .await; 132 | } 133 | -------------------------------------------------------------------------------- /program/tests/test_liquidation_token_and_perp.rs: -------------------------------------------------------------------------------- 1 | use fixed::types::I80F48; 2 | use fixed_macro::types::I80F48; 3 | use solana_program_test::*; 4 | 5 | use mango::state::{AssetType, QUOTE_INDEX}; 6 | use program_test::assertions::*; 7 | use program_test::cookies::*; 8 | use program_test::scenarios::*; 9 | use program_test::*; 10 | 11 | // Tests related to liquidations 12 | mod program_test; 13 | 14 | fn get_deposit_for_user( 15 | mango_group_cookie: &MangoGroupCookie, 16 | user_index: usize, 17 | mint_index: usize, 18 | ) -> I80F48 { 19 | mango_group_cookie.mango_accounts[user_index] 20 | .mango_account 21 | .get_native_deposit(&mango_group_cookie.mango_cache.root_bank_cache[mint_index], mint_index) 22 | .unwrap() 23 | } 24 | 25 | #[tokio::test] 26 | /// Simple test for ix liquidate_token_and_perp 27 | /// Transfers liqees quote deposits and quote positions to liqor 28 | /// note: doesnt check the numbers to exact accuracy 29 | async fn test_liquidation_token_and_perp_basic() { 30 | // === Arrange === 31 | let config = 32 | MangoProgramTestConfig { num_users: 3, ..MangoProgramTestConfig::default_two_mints() }; 33 | let mut test = MangoProgramTest::start_new(&config).await; 34 | 35 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 36 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 37 | 38 | // General parameters 39 | let bidder_user_index: usize = 0; 40 | let asker_user_index: usize = 1; 41 | let liqor_user_index: usize = 2; 42 | let mint_index: usize = 0; 43 | let base_price: f64 = 10_000.0; 44 | let base_size: f64 = 1.0; 45 | 46 | // Set oracles 47 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 48 | 49 | // Deposit amounts 50 | let user_deposits = vec![ 51 | (bidder_user_index, test.quote_index, base_price), 52 | (asker_user_index, mint_index, 1.0), 53 | (liqor_user_index, test.quote_index, base_price), 54 | ]; 55 | 56 | // Matched Perp Orders 57 | let matched_perp_orders = vec![vec![ 58 | (asker_user_index, mint_index, mango::matching::Side::Ask, base_size, base_price), 59 | (bidder_user_index, mint_index, mango::matching::Side::Bid, base_size, base_price), 60 | ]]; 61 | 62 | // === Act === 63 | // Step 1: Make deposits 64 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 65 | 66 | // assert deposit 67 | mango_group_cookie.run_keeper(&mut test).await; 68 | let bidder_quote_deposit = 69 | get_deposit_for_user(&mango_group_cookie, bidder_user_index, QUOTE_INDEX); 70 | // dbg!(bidder_quote_deposit); 71 | // [program/tests/test_liquidation_token_and_perp.rs:81] bidder_quote_deposit = 10000000000 72 | assert_eq!(bidder_quote_deposit, I80F48!(10000000000)); 73 | 74 | // Step 2: Place and match perp order 75 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 76 | 77 | // assert that bidder has a LONG 78 | let bidder_quote_position = mango_group_cookie.mango_accounts[bidder_user_index] 79 | .mango_account 80 | .perp_accounts[mint_index] 81 | .quote_position; 82 | let bidder_base_position = mango_group_cookie.mango_accounts[bidder_user_index] 83 | .mango_account 84 | .perp_accounts[mint_index] 85 | .base_position; 86 | // dbg!(bidder_quote_position); 87 | // dbg!(bidder_base_position); 88 | // [program/tests/test_liquidation_token_and_perp.rs:93] bidder_quote_position = -10100000000.000015631940187 89 | // [program/tests/test_liquidation_token_and_perp.rs:94] bidder_base_position = 10000 90 | assert_approx_eq!(bidder_quote_position, I80F48!(-10100000000)); 91 | assert_eq!(bidder_base_position, 10000); 92 | 93 | // Step 4: lower oracle price artificially to induce bad health 94 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price / 150.0).await; 95 | mango_group_cookie.run_keeper(&mut test).await; 96 | 97 | // Step 5: close base position by doing a reverse order of sorts 98 | let matched_perp_orders = vec![vec![ 99 | (asker_user_index, mint_index, mango::matching::Side::Bid, base_size, base_price / 150.0), 100 | (bidder_user_index, mint_index, mango::matching::Side::Ask, base_size, base_price / 150.0), 101 | ]]; 102 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 103 | 104 | // assert that bidder has no base position, but still a quote position due to price drop 105 | let bidder_quote_position = mango_group_cookie.mango_accounts[bidder_user_index] 106 | .mango_account 107 | .perp_accounts[mint_index] 108 | .quote_position; 109 | let bidder_base_position = mango_group_cookie.mango_accounts[bidder_user_index] 110 | .mango_account 111 | .perp_accounts[mint_index] 112 | .base_position; 113 | // dbg!(bidder_quote_position); 114 | // dbg!(bidder_base_position); 115 | // [program/tests/test_liquidation_token_and_perp.rs:123] bidder_quote_position = -10034066000.00001573604891 116 | // [program/tests/test_liquidation_token_and_perp.rs:124] bidder_base_position = 0 117 | assert_approx_eq!(bidder_quote_position, I80F48!(-10034066000)); 118 | assert_eq!(bidder_base_position, 0); 119 | 120 | // Step 6: Perform a couple of liquidations 121 | for _ in 0..6 { 122 | mango_group_cookie.run_keeper(&mut test).await; 123 | test.perform_liquidate_token_and_perp( 124 | &mut mango_group_cookie, 125 | bidder_user_index, // The liqee 126 | liqor_user_index, 127 | AssetType::Token, 128 | QUOTE_INDEX, 129 | AssetType::Perp, 130 | mint_index, 131 | I80F48!(100000), 132 | ) 133 | .await; 134 | } 135 | 136 | mango_group_cookie.run_keeper(&mut test).await; 137 | 138 | // assert that bidders quote deposit has reduced 139 | let bidder_quote_deposit = 140 | get_deposit_for_user(&mango_group_cookie, bidder_user_index, QUOTE_INDEX); 141 | // dbg!(bidder_quote_deposit); 142 | // [program/tests/test_liquidation_token_and_perp.rs:155] bidder_quote_deposit = 9999400000.00000001278977 143 | assert_approx_eq!(bidder_quote_deposit, I80F48!(9999400000)); 144 | // assert that liqors quote deposit has increased 145 | let liqor_quote_deposit = 146 | get_deposit_for_user(&mango_group_cookie, liqor_user_index, QUOTE_INDEX); 147 | // dbg!(liqor_quote_deposit); 148 | // [program/tests/test_liquidation_token_and_perp.rs:158] liqor_quote_deposit = 10000599999.99999998721023 149 | assert_approx_eq!(liqor_quote_deposit, I80F48!(10000600000)); 150 | 151 | // assert that bidders quote position has reduced 152 | let bidder_quote_position = mango_group_cookie.mango_accounts[bidder_user_index] 153 | .mango_account 154 | .perp_accounts[mint_index] 155 | .quote_position; 156 | // dbg!(bidder_quote_position); 157 | // [program/tests/test_liquidation_token_and_perp.rs:173] bidder_quote_position = -10033466000.00001573604891 158 | assert_approx_eq!(bidder_quote_position, I80F48!(-10033466000)); 159 | 160 | // assert that liqor has a quote position now 161 | let liqor_quote_position = 162 | mango_group_cookie.mango_accounts[liqor_user_index].mango_account.perp_accounts[mint_index] 163 | .quote_position; 164 | // dbg!(liqor_quote_position); 165 | // [program/tests/test_liquidation_token_and_perp.rs:164] liqor_quote_position = -600000 166 | assert_eq!(liqor_quote_position, I80F48!(-600000)); 167 | } 168 | -------------------------------------------------------------------------------- /program/tests/test_liquidation_token_and_perp_max_cu.rs: -------------------------------------------------------------------------------- 1 | // Tests related to liquidations 2 | mod program_test; 3 | 4 | use fixed_macro::types::I80F48; 5 | use mango::state::{AssetType, QUOTE_INDEX}; 6 | use program_test::cookies::*; 7 | use program_test::scenarios::*; 8 | use program_test::*; 9 | use solana_program_test::*; 10 | 11 | /// for ix liquidate_token_and_perp, test max cu usage (that it doesnt exceed 200k), 12 | /// by having spot open orders accounts, orders, 13 | /// and perp positions across as many markets as possible 14 | #[tokio::test] 15 | async fn test_liquidation_token_and_perp_max_cu() { 16 | let config = MangoProgramTestConfig { 17 | num_users: 3, 18 | compute_limit: 160000, // consumed 130094 of 140000 compute units 19 | ..MangoProgramTestConfig::default() 20 | }; 21 | let mut test = MangoProgramTest::start_new(&config).await; 22 | 23 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 24 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 25 | 26 | let bidder_user_index: usize = 0; 27 | let asker_user_index: usize = 1; 28 | let mint_index: usize = 0; 29 | let base_price: f64 = 10_000.0; 30 | let base_size: f64 = 1.0; 31 | 32 | // create a perp position 33 | { 34 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 35 | 36 | let user_deposits = vec![ 37 | (bidder_user_index, test.quote_index, base_price), 38 | (asker_user_index, mint_index, 1.0), 39 | (asker_user_index, test.quote_index, base_price), 40 | ]; 41 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 42 | mango_group_cookie.run_keeper(&mut test).await; 43 | 44 | // create a perp position which would cause bad health, and make liquidation succeed 45 | let matched_perp_orders = vec![vec![ 46 | (asker_user_index, mint_index, mango::matching::Side::Ask, base_size, base_price), 47 | (bidder_user_index, mint_index, mango::matching::Side::Bid, base_size, base_price), 48 | ]]; 49 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 50 | 51 | // create a corresponding spot open orders account and some position to max out cu usage 52 | let matched_spot_orders = vec![vec![ 53 | (bidder_user_index, mint_index, serum_dex::matching::Side::Bid, 0.0001, base_price), 54 | (asker_user_index, mint_index, serum_dex::matching::Side::Ask, 0.0001, base_price), 55 | ]]; 56 | match_spot_order_scenario(&mut test, &mut mango_group_cookie, &matched_spot_orders).await; 57 | for matched_spot_order in matched_spot_orders { 58 | mango_group_cookie.settle_spot_funds(&mut test, &matched_spot_order).await; 59 | } 60 | } 61 | 62 | // create open orders account for 5 these markets, place and settle trade across all these 63 | // 5 markets, 64 | // also create perp positions across all markets 65 | // ...to max out cu usage 66 | for market_index in 1..6 { 67 | mango_group_cookie.set_oracle(&mut test, market_index, 1.0).await; 68 | 69 | let user_deposits = vec![ 70 | (bidder_user_index, test.quote_index, 2.0), 71 | (asker_user_index, market_index, 1.0), 72 | (asker_user_index, test.quote_index, 1.0), 73 | ]; 74 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 75 | 76 | let matched_spot_orders = vec![vec![ 77 | (bidder_user_index, market_index, serum_dex::matching::Side::Bid, base_size, 1.), 78 | (asker_user_index, market_index, serum_dex::matching::Side::Ask, base_size, 1.), 79 | ]]; 80 | match_spot_order_scenario(&mut test, &mut mango_group_cookie, &matched_spot_orders).await; 81 | for matched_spot_order in matched_spot_orders { 82 | mango_group_cookie.settle_spot_funds(&mut test, &matched_spot_order).await; 83 | } 84 | 85 | let matched_perp_orders = vec![vec![ 86 | (asker_user_index, market_index, mango::matching::Side::Ask, base_size, 1.), 87 | (bidder_user_index, market_index, mango::matching::Side::Bid, base_size, 1.), 88 | ]]; 89 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 90 | } 91 | 92 | // create open orders account for across all remaining 9 markets, place (unmatched) orders across 93 | // all these 9 markets, 9 is maximum number of markets across which user can have orders, 94 | // also create perp positions across all markets 95 | // ...to max out cu usage 96 | for market_index in 6..15 { 97 | mango_group_cookie.set_oracle(&mut test, market_index, 1.0).await; 98 | 99 | let user_deposits = vec![ 100 | (bidder_user_index, test.quote_index, 2.0), 101 | (asker_user_index, market_index, 1.0), 102 | (asker_user_index, test.quote_index, 1.0), 103 | ]; 104 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 105 | 106 | let placed_spot_orders = vec![ 107 | (bidder_user_index, market_index, serum_dex::matching::Side::Bid, base_size, 0.9), 108 | (asker_user_index, market_index, serum_dex::matching::Side::Ask, base_size, 1.1), 109 | ]; 110 | place_spot_order_scenario(&mut test, &mut mango_group_cookie, &placed_spot_orders).await; 111 | 112 | let matched_perp_orders = vec![vec![ 113 | (asker_user_index, market_index, mango::matching::Side::Ask, base_size, 1.), 114 | (bidder_user_index, market_index, mango::matching::Side::Bid, base_size, 1.), 115 | ]]; 116 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 117 | } 118 | 119 | // close base position by doing a reverse order of sorts 120 | { 121 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price / 150.0).await; 122 | mango_group_cookie.run_keeper(&mut test).await; 123 | 124 | let matched_perp_orders = vec![vec![ 125 | ( 126 | asker_user_index, 127 | mint_index, 128 | mango::matching::Side::Bid, 129 | base_size, 130 | base_price / 150.0, 131 | ), 132 | ( 133 | bidder_user_index, 134 | mint_index, 135 | mango::matching::Side::Ask, 136 | base_size, 137 | base_price / 150.0, 138 | ), 139 | ]]; 140 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 141 | } 142 | 143 | // perform a liquidation to test cu usage 144 | mango_group_cookie.run_keeper(&mut test).await; 145 | test.perform_liquidate_token_and_perp( 146 | &mut mango_group_cookie, 147 | bidder_user_index, // The liqee 148 | asker_user_index, // The liqor 149 | AssetType::Token, 150 | QUOTE_INDEX, 151 | AssetType::Perp, 152 | mint_index, 153 | I80F48!(100000), 154 | ) 155 | .await; 156 | } 157 | -------------------------------------------------------------------------------- /program/tests/test_liquidation_token_and_token.rs: -------------------------------------------------------------------------------- 1 | use fixed::types::I80F48; 2 | use fixed_macro::types::I80F48; 3 | use mango::state::QUOTE_INDEX; 4 | use solana_program_test::*; 5 | 6 | use crate::assertions::EPSILON; 7 | use program_test::cookies::*; 8 | use program_test::scenarios::*; 9 | use program_test::*; 10 | 11 | // Tests related to liquidations 12 | mod program_test; 13 | 14 | pub fn get_deposit_for_user( 15 | mango_group_cookie: &MangoGroupCookie, 16 | user_index: usize, 17 | mint_index: usize, 18 | ) -> I80F48 { 19 | mango_group_cookie.mango_accounts[user_index] 20 | .mango_account 21 | .get_native_deposit(&mango_group_cookie.mango_cache.root_bank_cache[mint_index], mint_index) 22 | .unwrap() 23 | } 24 | 25 | pub fn get_borrow_for_user( 26 | mango_group_cookie: &MangoGroupCookie, 27 | user_index: usize, 28 | mint_index: usize, 29 | ) -> I80F48 { 30 | mango_group_cookie.mango_accounts[user_index] 31 | .mango_account 32 | .get_native_borrow(&mango_group_cookie.mango_cache.root_bank_cache[mint_index], mint_index) 33 | .unwrap() 34 | } 35 | 36 | #[tokio::test] 37 | async fn test_token_and_token_liquidation_v1() { 38 | // === Arrange === 39 | let config = 40 | MangoProgramTestConfig { num_users: 3, ..MangoProgramTestConfig::default_two_mints() }; 41 | 42 | let mut test = MangoProgramTest::start_new(&config).await; 43 | 44 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 45 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 46 | 47 | // General parameters 48 | let bidder_user_index: usize = 0; 49 | let asker_user_index: usize = 1; 50 | let liqor_user_index: usize = 2; 51 | let mint_index: usize = 0; 52 | let base_price: f64 = 15_000.0; 53 | let base_size: f64 = 1.0; 54 | 55 | // Set oracles 56 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 57 | 58 | // Deposit amounts 59 | let user_deposits = vec![ 60 | (bidder_user_index, test.quote_index, 10_000.0), 61 | (asker_user_index, mint_index, 1.0), 62 | (asker_user_index, test.quote_index, 10_000.0), 63 | (liqor_user_index, test.quote_index, 10_000.0), 64 | ]; 65 | 66 | // Matched Spot Orders 67 | let matched_spot_orders = vec![vec![ 68 | (bidder_user_index, mint_index, serum_dex::matching::Side::Bid, base_size, base_price), 69 | (asker_user_index, mint_index, serum_dex::matching::Side::Ask, base_size, base_price), 70 | ]]; 71 | 72 | // === Act === 73 | // Step 1: Make deposits 74 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 75 | 76 | // Step 2: Place and match an order for 1 BTC @ 15_000 77 | match_spot_order_scenario(&mut test, &mut mango_group_cookie, &matched_spot_orders).await; 78 | 79 | // Step 3: Settle all spot order 80 | for matched_spot_order in matched_spot_orders { 81 | mango_group_cookie.settle_spot_funds(&mut test, &matched_spot_order).await; 82 | } 83 | 84 | // Step 4: Assert that the order has been matched and the bidder has 1 BTC in deposits 85 | mango_group_cookie.run_keeper(&mut test).await; 86 | 87 | // assert that bidder has btc deposit and quote borrows 88 | let bidder_btc_deposit = 89 | get_deposit_for_user(&mango_group_cookie, bidder_user_index, mint_index); 90 | let bidder_quote_borrow = 91 | get_borrow_for_user(&mango_group_cookie, bidder_user_index, QUOTE_INDEX); 92 | // dbg!(bidder_btc_deposit); 93 | // dbg!(bidder_quote_borrow); 94 | // [program/tests/test_liquidation_token_and_token:92] bidder_btc_deposit = 1000000 95 | // [program/tests/test_liquidation_token_and_token:93] bidder_quote_borrow = 5000000000 96 | assert!(bidder_btc_deposit == I80F48!(1000000)); 97 | assert!(bidder_quote_borrow == I80F48!(5000000000)); 98 | 99 | // assert that liqor has no btc deposit and full quote deposits 100 | let liqor_btc_deposit = get_deposit_for_user(&mango_group_cookie, liqor_user_index, mint_index); 101 | let liqor_quote_deposit = 102 | get_deposit_for_user(&mango_group_cookie, liqor_user_index, QUOTE_INDEX); 103 | // dbg!(liqor_btc_deposit); 104 | // dbg!(liqor_quote_deposit); 105 | // [program/tests/test_liquidation_token_and_token.rs:101] liqor_btc_deposit = 0 106 | // [program/tests/test_liquidation_token_and_token.rs:102] liqor_quote_deposit = 10000000000 107 | assert!(liqor_btc_deposit.is_zero()); 108 | assert!(liqor_quote_deposit == I80F48!(10000000000)); 109 | 110 | // Step 5: Change the oracle price so that bidder becomes liqee 111 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price / 15.0).await; 112 | 113 | // Step 6: Perform a couple of liquidations 114 | for _ in 0..6 { 115 | mango_group_cookie.run_keeper(&mut test).await; 116 | test.perform_liquidate_token_and_token( 117 | &mut mango_group_cookie, 118 | bidder_user_index, // The liqee 119 | liqor_user_index, 120 | mint_index, // Asset index 121 | QUOTE_INDEX, // Liab index 122 | I80F48!(10_000), 123 | ) 124 | .await; 125 | } 126 | 127 | // === Assert === 128 | mango_group_cookie.run_keeper(&mut test).await; 129 | 130 | // assert that bidders btc deposits and quote borrows have reduced 131 | let bidder_btc_deposit = 132 | get_deposit_for_user(&mango_group_cookie, bidder_user_index, mint_index); 133 | let bidder_quote_borrow = 134 | get_borrow_for_user(&mango_group_cookie, bidder_user_index, QUOTE_INDEX); 135 | // dbg!(bidder_btc_deposit); 136 | // dbg!(bidder_quote_borrow); 137 | // [program/tests/test_liquidation_token_and_token:123] bidder_btc_deposit = 999938.5000000060586 138 | // [program/tests/test_liquidation_token_and_token:124] bidder_quote_borrow = 4999940000.000000011937118 139 | assert_approx_eq!(bidder_btc_deposit, I80F48!(999938.5), I80F48::ONE); 140 | assert_approx_eq!(bidder_quote_borrow, I80F48!(4999940000), I80F48::ONE); 141 | 142 | // assert that liqors btc deposits have increased and quote deposits have reduced 143 | let liqor_btc_deposit = get_deposit_for_user(&mango_group_cookie, liqor_user_index, mint_index); 144 | let liqor_quote_deposit = 145 | get_deposit_for_user(&mango_group_cookie, liqor_user_index, QUOTE_INDEX); 146 | // dbg!(liqor_btc_deposit); 147 | // dbg!(liqor_quote_deposit); 148 | // [program/tests/test_liquidation_token_and_token:125] liqor_btc_deposit = 61.4999999939414 149 | // [program/tests/test_liquidation_token_and_token:126] liqor_quote_deposit = 9999940000.000000011937118 150 | assert_approx_eq!(liqor_btc_deposit, I80F48!(61.5), I80F48::ONE); 151 | assert_approx_eq!(liqor_quote_deposit, I80F48!(9999940000), I80F48::ONE); 152 | } 153 | 154 | #[tokio::test] 155 | async fn test_token_and_token_liquidation_v2() { 156 | // === Arrange === 157 | let config = MangoProgramTestConfig { num_users: 3, ..MangoProgramTestConfig::default() }; 158 | let mut test = MangoProgramTest::start_new(&config).await; 159 | 160 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 161 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 162 | 163 | // General parameters 164 | let bidder_user_index: usize = 0; 165 | let asker_user_index: usize = 1; 166 | let liqor_user_index: usize = 2; 167 | let num_orders: usize = test.num_mints - 1; 168 | let base_price: f64 = 15_000.0; 169 | let base_size: f64 = 2.0; 170 | let liq_mint_index: usize = 0; 171 | // TODO: Make the order prices into variables 172 | 173 | // Set oracles 174 | for mint_index in 0..num_orders { 175 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 176 | } 177 | 178 | // Deposit amounts 179 | let mut user_deposits = vec![ 180 | (asker_user_index, liq_mint_index, 2.0), 181 | (asker_user_index, test.quote_index, 100_000.0), 182 | (liqor_user_index, test.quote_index, 10_000.0), 183 | ]; 184 | user_deposits.extend(arrange_deposit_all_scenario(&mut test, bidder_user_index, 1.0, 0.0)); 185 | 186 | // // Perp Orders 187 | let mut user_perp_orders = vec![]; 188 | for mint_index in 0..num_orders { 189 | user_perp_orders.push(( 190 | bidder_user_index, 191 | mint_index, 192 | mango::matching::Side::Ask, 193 | 1.0, 194 | base_price, 195 | )); 196 | } 197 | 198 | // Matched Spot Orders 199 | let matched_spot_orders = vec![vec![ 200 | (bidder_user_index, liq_mint_index, serum_dex::matching::Side::Bid, base_size, base_price), 201 | (asker_user_index, liq_mint_index, serum_dex::matching::Side::Ask, base_size, base_price), 202 | ]]; 203 | 204 | // === Act === 205 | // Step 1: Make deposits 206 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 207 | 208 | // Step 2: Place perp orders 209 | place_perp_order_scenario(&mut test, &mut mango_group_cookie, &user_perp_orders).await; 210 | 211 | // Step 3: Place and match an order for 1 BTC @ 15_000 212 | match_spot_order_scenario(&mut test, &mut mango_group_cookie, &matched_spot_orders).await; 213 | 214 | // Step 4: Settle all spot orders 215 | for matched_spot_order in matched_spot_orders { 216 | mango_group_cookie.settle_spot_funds(&mut test, &matched_spot_order).await; 217 | } 218 | 219 | // Step 5: Assert that the order has been matched and the bidder has 3 BTC in deposits 220 | mango_group_cookie.run_keeper(&mut test).await; 221 | 222 | let bidder_base_deposit = mango_group_cookie.mango_accounts[bidder_user_index] 223 | .mango_account 224 | .get_native_deposit( 225 | &mango_group_cookie.mango_cache.root_bank_cache[liq_mint_index], 226 | liq_mint_index, 227 | ) 228 | .unwrap(); 229 | let asker_base_deposit = mango_group_cookie.mango_accounts[asker_user_index] 230 | .mango_account 231 | .get_native_deposit( 232 | &mango_group_cookie.mango_cache.root_bank_cache[liq_mint_index], 233 | liq_mint_index, 234 | ) 235 | .unwrap(); 236 | assert_eq!(bidder_base_deposit, I80F48!(3_000_000)); 237 | assert_eq!(asker_base_deposit, I80F48::ZERO); 238 | 239 | // Step 6: Change the oracle price so that bidder becomes liqee 240 | for mint_index in 0..num_orders { 241 | mango_group_cookie.set_oracle(&mut test, mint_index, 1000.0).await; 242 | } 243 | 244 | // Step 7: Force cancel perp orders 245 | mango_group_cookie.run_keeper(&mut test).await; 246 | for mint_index in 0..num_orders { 247 | let perp_market_cookie = mango_group_cookie.perp_markets[mint_index]; 248 | test.force_cancel_perp_orders(&mango_group_cookie, &perp_market_cookie, bidder_user_index) 249 | .await; 250 | } 251 | 252 | // Step 8: Perform a couple liquidations 253 | for _ in 0..5 { 254 | mango_group_cookie.run_keeper(&mut test).await; 255 | test.perform_liquidate_token_and_token( 256 | &mut mango_group_cookie, 257 | bidder_user_index, // The liqee 258 | liqor_user_index, 259 | liq_mint_index, // Asset index 260 | QUOTE_INDEX, // Liab index 261 | I80F48!(100_000_000), 262 | ) 263 | .await; 264 | } 265 | 266 | // === Assert === 267 | mango_group_cookie.run_keeper(&mut test).await; 268 | 269 | let bidder_base_net = mango_group_cookie.mango_accounts[bidder_user_index] 270 | .mango_account 271 | .get_net(&mango_group_cookie.mango_cache.root_bank_cache[liq_mint_index], liq_mint_index); 272 | let liqor_base_net = mango_group_cookie.mango_accounts[liqor_user_index] 273 | .mango_account 274 | .get_net(&mango_group_cookie.mango_cache.root_bank_cache[liq_mint_index], liq_mint_index); 275 | 276 | let bidder_quote_net = 277 | mango_group_cookie.mango_accounts[bidder_user_index].mango_account.get_net( 278 | &mango_group_cookie.mango_cache.root_bank_cache[test.quote_index], 279 | test.quote_index, 280 | ); 281 | let liqor_quote_net = 282 | mango_group_cookie.mango_accounts[liqor_user_index].mango_account.get_net( 283 | &mango_group_cookie.mango_cache.root_bank_cache[test.quote_index], 284 | test.quote_index, 285 | ); 286 | 287 | assert_approx_eq!(bidder_base_net, I80F48!(2487500), EPSILON); 288 | assert_approx_eq!(liqor_base_net, I80F48!(512500), EPSILON); 289 | assert_eq!(bidder_quote_net, I80F48!(-29500000000)); 290 | assert_eq!(liqor_quote_net, I80F48!(9500000000)); 291 | 292 | // TODO: Actually assert here 293 | } 294 | -------------------------------------------------------------------------------- /program/tests/test_liquidation_token_and_token_max_cu.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | 3 | use fixed_macro::types::I80F48; 4 | use mango::state::*; 5 | use program_test::cookies::*; 6 | use program_test::scenarios::*; 7 | use program_test::*; 8 | use solana_program_test::*; 9 | 10 | /// for ix liquidate_token_and_token, test max cu usage (that it doesnt exceed 200k), 11 | /// by having spot open orders accounts, orders, 12 | /// and perp positions across as many markets as possible 13 | #[tokio::test] 14 | async fn test_liquidation_token_and_token_max_cu() { 15 | let config = MangoProgramTestConfig { 16 | num_users: 3, 17 | compute_limit: 160_000, // 151171 of 160000 compute units 18 | ..MangoProgramTestConfig::default() 19 | }; 20 | 21 | let mut test = MangoProgramTest::start_new(&config).await; 22 | 23 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 24 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 25 | 26 | let bidder_user_index: usize = 0; 27 | let asker_user_index: usize = 1; 28 | let mint_index: usize = 0; 29 | let base_price: f64 = 15_000.0; 30 | let base_size: f64 = 1.0; 31 | 32 | { 33 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 34 | 35 | let user_deposits = vec![ 36 | (bidder_user_index, test.quote_index, 11_000.0), 37 | (asker_user_index, mint_index, 1.0), 38 | (asker_user_index, test.quote_index, 11_001.0), 39 | ]; 40 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 41 | 42 | // borrow some assets by placing and settling a trade 43 | let matched_spot_orders = vec![vec![ 44 | (bidder_user_index, mint_index, serum_dex::matching::Side::Bid, base_size, base_price), 45 | (asker_user_index, mint_index, serum_dex::matching::Side::Ask, base_size, base_price), 46 | ]]; 47 | match_spot_order_scenario(&mut test, &mut mango_group_cookie, &matched_spot_orders).await; 48 | for matched_spot_order in matched_spot_orders { 49 | mango_group_cookie.settle_spot_funds(&mut test, &matched_spot_order).await; 50 | } 51 | 52 | // create a corresponding perp position position to max out cu usage 53 | let matched_perp_orders = vec![vec![ 54 | (asker_user_index, mint_index, mango::matching::Side::Ask, 0.0001, base_price), 55 | (bidder_user_index, mint_index, mango::matching::Side::Bid, 0.0001, base_price), 56 | ]]; 57 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 58 | } 59 | 60 | // create open orders account for 5 these markets, place and settle trade across all these 61 | // 5 markets, 62 | // also create perp positions across all markets 63 | // ...to max out cu usage 64 | for market_index in 1..6 { 65 | mango_group_cookie.set_oracle(&mut test, market_index, 1.0).await; 66 | 67 | let user_deposits = vec![ 68 | (bidder_user_index, test.quote_index, 2.0), 69 | (asker_user_index, market_index, 1.0), 70 | (asker_user_index, test.quote_index, 1.0), 71 | ]; 72 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 73 | 74 | let matched_spot_orders = vec![vec![ 75 | (bidder_user_index, market_index, serum_dex::matching::Side::Bid, base_size, 1.), 76 | (asker_user_index, market_index, serum_dex::matching::Side::Ask, base_size, 1.), 77 | ]]; 78 | match_spot_order_scenario(&mut test, &mut mango_group_cookie, &matched_spot_orders).await; 79 | for matched_spot_order in matched_spot_orders { 80 | mango_group_cookie.settle_spot_funds(&mut test, &matched_spot_order).await; 81 | } 82 | 83 | let matched_perp_orders = vec![vec![ 84 | (asker_user_index, market_index, mango::matching::Side::Ask, base_size, 1.), 85 | (bidder_user_index, market_index, mango::matching::Side::Bid, base_size, 1.), 86 | ]]; 87 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 88 | } 89 | 90 | // create open orders account for across all remaining 9 markets, place (unmatched) orders across 91 | // all these 9 markets, 9 is maximum number of markets across which user can have orders, 92 | // also create perp positions across all markets 93 | // ...to max out cu usage 94 | for market_index in 6..15 { 95 | mango_group_cookie.set_oracle(&mut test, market_index, 1.0).await; 96 | 97 | let user_deposits = vec![ 98 | (bidder_user_index, test.quote_index, 2.0), 99 | (asker_user_index, market_index, 1.0), 100 | (asker_user_index, test.quote_index, 1.0), 101 | ]; 102 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 103 | 104 | let placed_spot_orders = vec![ 105 | (bidder_user_index, market_index, serum_dex::matching::Side::Bid, base_size, 0.9), 106 | (asker_user_index, market_index, serum_dex::matching::Side::Ask, base_size, 1.1), 107 | ]; 108 | place_spot_order_scenario(&mut test, &mut mango_group_cookie, &placed_spot_orders).await; 109 | 110 | let matched_perp_orders = vec![vec![ 111 | (asker_user_index, market_index, mango::matching::Side::Ask, base_size, 1.), 112 | (bidder_user_index, market_index, mango::matching::Side::Bid, base_size, 1.), 113 | ]]; 114 | match_perp_order_scenario(&mut test, &mut mango_group_cookie, &matched_perp_orders).await; 115 | } 116 | 117 | // change the oracle price so that bidder becomes liqee 118 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price / 15.0).await; 119 | 120 | mango_group_cookie.run_keeper(&mut test).await; 121 | 122 | // perform a liquidation to test cu usage 123 | test.perform_liquidate_token_and_token( 124 | &mut mango_group_cookie, 125 | bidder_user_index, // The liqee 126 | asker_user_index, 127 | mint_index, // Asset index 128 | QUOTE_INDEX, // Liab index 129 | I80F48!(10_000), 130 | ) 131 | .await; 132 | } 133 | -------------------------------------------------------------------------------- /program/tests/test_misc.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | use fixed::types::I80F48; 4 | use mango::matching::{AnyNode, InnerNode, LeafNode}; 5 | use mango::state::{MangoAccount, MangoCache}; // ONE_I80F48 6 | use solana_program_test::tokio; 7 | use std::mem::{align_of, size_of}; 8 | 9 | #[tokio::test] 10 | async fn test_size() { 11 | println!("LeafNode: {} {}", size_of::(), align_of::()); 12 | println!("InnerNode: {}", size_of::()); 13 | println!("AnyNode: {}", size_of::()); 14 | println!("MangoAccount: {}", size_of::()); 15 | println!("MangoCache: {}", size_of::()); 16 | } 17 | 18 | #[tokio::test] 19 | async fn test_i80f48() { 20 | let x = I80F48::from_num(500000.000123); 21 | let y = x >> 13; 22 | println!("y: {:?}", y); 23 | } 24 | 25 | #[tokio::test] 26 | async fn serum_dex_error() { 27 | let error_code = 0x2a; 28 | println!("file: {} line: {}", error_code >> 24, error_code & 0xffffff); 29 | } 30 | 31 | // #[tokio::test] 32 | // async fn test_fixmul() { 33 | // let y = I80F48::from_bits(fixmul(ONE_I80F48.to_bits(), ONE_I80F48.to_bits())); 34 | // println!("{}", y); 35 | // } 36 | -------------------------------------------------------------------------------- /program/tests/test_perp_trigger_orders.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "test-bpf")] 2 | 3 | mod program_test; 4 | use fixed::types::I80F48; 5 | use mango::matching::{OrderType, Side}; 6 | use mango::state::{TriggerCondition, ADVANCED_ORDER_FEE}; 7 | use program_test::assertions::*; 8 | use program_test::cookies::*; 9 | use program_test::scenarios::*; 10 | use program_test::*; 11 | use solana_program_test::*; 12 | 13 | #[tokio::test] 14 | async fn test_perp_trigger_orders_basic() { 15 | // === Arrange === 16 | let config = MangoProgramTestConfig::default_two_mints(); 17 | let mut test = MangoProgramTest::start_new(&config).await; 18 | // Disable all logs except error 19 | // solana_logger::setup_with("error"); 20 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 21 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 22 | 23 | // General parameters 24 | let user_index: usize = 0; 25 | let user2_index: usize = 1; 26 | let mint_index: usize = 0; 27 | let base_price: f64 = 10_000.0; 28 | let base_size: f64 = 1.0; 29 | 30 | // Set oracles 31 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 32 | 33 | // Deposit 34 | let user_deposits = vec![ 35 | (user_index, test.quote_index, base_price * base_size), 36 | (user_index, mint_index, base_size), 37 | (user2_index, test.quote_index, base_price * base_size), 38 | (user2_index, mint_index, base_size), 39 | ]; 40 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 41 | 42 | // Make an advanced orders account 43 | let mango_account_cookie = &mango_group_cookie.mango_accounts[user_index]; 44 | let mut advanced_orders_cookie = 45 | AdvancedOrdersCookie::init(&mut test, mango_account_cookie).await; 46 | assert!(!advanced_orders_cookie.advanced_orders.orders[0].is_active); 47 | assert!(!advanced_orders_cookie.advanced_orders.orders[1].is_active); 48 | let advanced_orders_initial_lamports = 49 | test.get_account(advanced_orders_cookie.address).await.lamports; 50 | 51 | // Add two advanced orders 52 | let mut perp_market = mango_group_cookie.perp_markets[0]; 53 | perp_market 54 | .add_trigger_order( 55 | &mut test, 56 | &mut mango_group_cookie, 57 | &mut advanced_orders_cookie, 58 | user_index, 59 | OrderType::Limit, 60 | Side::Bid, 61 | TriggerCondition::Above, 62 | base_price, 63 | base_size, 64 | I80F48::from_num(base_price * 1.1), 65 | ) 66 | .await; 67 | assert!(advanced_orders_cookie.advanced_orders.orders[0].is_active); 68 | assert!(!advanced_orders_cookie.advanced_orders.orders[1].is_active); 69 | assert!( 70 | test.get_account(advanced_orders_cookie.address).await.lamports 71 | - advanced_orders_initial_lamports 72 | == ADVANCED_ORDER_FEE 73 | ); 74 | perp_market 75 | .add_trigger_order( 76 | &mut test, 77 | &mut mango_group_cookie, 78 | &mut advanced_orders_cookie, 79 | user_index, 80 | OrderType::Limit, 81 | Side::Bid, 82 | TriggerCondition::Below, 83 | base_price * 0.91, 84 | base_size, 85 | I80F48::from_num(base_price * 0.9), 86 | ) 87 | .await; 88 | assert!(advanced_orders_cookie.advanced_orders.orders[0].is_active); 89 | assert!(advanced_orders_cookie.advanced_orders.orders[1].is_active); 90 | assert!( 91 | test.get_account(advanced_orders_cookie.address).await.lamports 92 | - advanced_orders_initial_lamports 93 | == 2 * ADVANCED_ORDER_FEE 94 | ); 95 | 96 | // Remove the first advanced order 97 | advanced_orders_cookie 98 | .remove_advanced_order(&mut test, &mut mango_group_cookie, user_index, 0) 99 | .await 100 | .expect("deletion succeeds"); 101 | assert!(!advanced_orders_cookie.advanced_orders.orders[0].is_active); 102 | assert!(advanced_orders_cookie.advanced_orders.orders[1].is_active); 103 | assert!( 104 | test.get_account(advanced_orders_cookie.address).await.lamports 105 | - advanced_orders_initial_lamports 106 | == ADVANCED_ORDER_FEE 107 | ); 108 | // advance slots, since we want to send the same tx a second time 109 | test.advance_clock_by_slots(2).await; 110 | advanced_orders_cookie 111 | .remove_advanced_order(&mut test, &mut mango_group_cookie, user_index, 0) 112 | .await 113 | .expect("deletion of inactive is ok"); 114 | advanced_orders_cookie 115 | .remove_advanced_order(&mut test, &mut mango_group_cookie, user_index, 2) 116 | .await 117 | .expect("deletion of unused is ok"); 118 | 119 | // Trigger the second advanced order 120 | let agent_user_index = user2_index; 121 | perp_market 122 | .execute_trigger_order( 123 | &mut test, 124 | &mut mango_group_cookie, 125 | &mut advanced_orders_cookie, 126 | user_index, 127 | agent_user_index, 128 | 1, 129 | ) 130 | .await 131 | .expect_err("order trigger condition should not be met"); 132 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price * 0.89).await; 133 | mango_group_cookie.run_keeper(&mut test).await; 134 | perp_market 135 | .execute_trigger_order( 136 | &mut test, 137 | &mut mango_group_cookie, 138 | &mut advanced_orders_cookie, 139 | user_index, 140 | agent_user_index, 141 | 1, 142 | ) 143 | .await 144 | .expect("order executed"); 145 | assert!(!advanced_orders_cookie.advanced_orders.orders[1].is_active); 146 | assert!( 147 | test.get_account(advanced_orders_cookie.address).await.lamports 148 | - advanced_orders_initial_lamports 149 | == 0 150 | ); 151 | 152 | // Check that order is in book now 153 | mango_group_cookie.run_keeper(&mut test).await; 154 | let user_perp_orders = vec![(user_index, mint_index, Side::Bid, base_size, base_price)]; 155 | assert_open_perp_orders(&mango_group_cookie, &user_perp_orders, STARTING_ADVANCED_ORDER_ID + 1); 156 | } 157 | 158 | #[tokio::test] 159 | async fn test_perp_trigger_orders_health() { 160 | // === Arrange === 161 | let config = MangoProgramTestConfig::default_two_mints(); 162 | let mut test = MangoProgramTest::start_new(&config).await; 163 | // Disable all logs except error 164 | // solana_logger::setup_with("error"); 165 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 166 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 167 | 168 | // General parameters 169 | let user_index: usize = 0; 170 | let user2_index: usize = 1; 171 | let agent_user_index = user2_index; 172 | let mint_index: usize = 0; 173 | let base_price: f64 = 10_000.0; 174 | let base_size: f64 = 1.0; 175 | let mint = test.with_mint(mint_index); 176 | 177 | // Set oracles 178 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 179 | 180 | // Deposit 181 | let user_deposits = vec![ 182 | (user_index, test.quote_index, base_price * base_size), 183 | //(user_index, mint_index, base_size), 184 | (user2_index, test.quote_index, base_price * base_size), 185 | (user2_index, mint_index, base_size), 186 | ]; 187 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 188 | 189 | // Make an advanced orders account 190 | let mango_account_cookie = &mango_group_cookie.mango_accounts[user_index]; 191 | let mut advanced_orders_cookie = 192 | AdvancedOrdersCookie::init(&mut test, mango_account_cookie).await; 193 | 194 | // Add trigger orders 195 | let mut perp_market = mango_group_cookie.perp_markets[0]; 196 | perp_market 197 | .add_trigger_order( 198 | &mut test, 199 | &mut mango_group_cookie, 200 | &mut advanced_orders_cookie, 201 | user_index, 202 | OrderType::Limit, 203 | Side::Ask, 204 | TriggerCondition::Above, 205 | base_price, 206 | 11.0 * base_size, 207 | I80F48::from_num(base_price * 0.01), 208 | ) 209 | .await; 210 | perp_market 211 | .add_trigger_order( 212 | &mut test, 213 | &mut mango_group_cookie, 214 | &mut advanced_orders_cookie, 215 | user_index, 216 | OrderType::Limit, 217 | Side::Ask, 218 | TriggerCondition::Above, 219 | base_price, 220 | 9.0 * base_size, 221 | I80F48::from_num(base_price * 0.01), 222 | ) 223 | .await; 224 | perp_market 225 | .add_trigger_order( 226 | &mut test, 227 | &mut mango_group_cookie, 228 | &mut advanced_orders_cookie, 229 | user_index, 230 | OrderType::Limit, 231 | Side::Ask, 232 | TriggerCondition::Above, 233 | base_price, 234 | 0.001 * base_size, 235 | I80F48::from_num(base_price * 0.01), 236 | ) 237 | .await; 238 | perp_market 239 | .add_trigger_order( 240 | &mut test, 241 | &mut mango_group_cookie, 242 | &mut advanced_orders_cookie, 243 | user_index, 244 | OrderType::Market, 245 | Side::Bid, 246 | TriggerCondition::Above, 247 | 0.99 * base_price, 248 | 0.001 * base_size, 249 | I80F48::from_num(base_price * 0.01), 250 | ) 251 | .await; 252 | 253 | // Triggering order 0 would drop health too much returns ok, but doesn't add 254 | // the order to the book due to health 255 | perp_market 256 | .execute_trigger_order( 257 | &mut test, 258 | &mut mango_group_cookie, 259 | &mut advanced_orders_cookie, 260 | user_index, 261 | agent_user_index, 262 | 0, 263 | ) 264 | .await 265 | .expect("order triggered, but not added to book"); 266 | assert!( 267 | mango_group_cookie.mango_accounts[user_index].mango_account.perp_accounts[0].asks_quantity 268 | == 0 269 | ); 270 | 271 | // Triggering order 1 is acceptable but brings health to the brink 272 | perp_market 273 | .execute_trigger_order( 274 | &mut test, 275 | &mut mango_group_cookie, 276 | &mut advanced_orders_cookie, 277 | user_index, 278 | agent_user_index, 279 | 1, 280 | ) 281 | .await 282 | .expect("order triggered, added to book"); 283 | assert!( 284 | mango_group_cookie.mango_accounts[user_index].mango_account.perp_accounts[0].asks_quantity 285 | == 90_000 286 | ); 287 | 288 | // Change the price oracle to make the account unhealthy 289 | mango_group_cookie.set_oracle(&mut test, mint_index, 2.0 * base_price).await; 290 | mango_group_cookie.run_keeper(&mut test).await; 291 | 292 | // Triggering order 2 would decrease health a tiny bit - not allowed 293 | perp_market 294 | .execute_trigger_order( 295 | &mut test, 296 | &mut mango_group_cookie, 297 | &mut advanced_orders_cookie, 298 | user_index, 299 | agent_user_index, 300 | 2, 301 | ) 302 | .await 303 | .expect("order triggered, but not added to book"); 304 | assert!( 305 | mango_group_cookie.mango_accounts[user_index].mango_account.perp_accounts[0].bids_quantity 306 | == 0 307 | ); 308 | assert!( 309 | mango_group_cookie.mango_accounts[user_index].mango_account.perp_accounts[0].asks_quantity 310 | == 90_000 311 | ); 312 | 313 | // Add an order for user1 to trade against 314 | perp_market 315 | .place_order( 316 | &mut test, 317 | &mut mango_group_cookie, 318 | user2_index, 319 | Side::Ask, 320 | base_size, 321 | 0.99 * base_price, 322 | PlacePerpOptions::default(), 323 | ) 324 | .await; 325 | 326 | // Triggering order 3 improves health and is allowed 327 | perp_market 328 | .execute_trigger_order( 329 | &mut test, 330 | &mut mango_group_cookie, 331 | &mut advanced_orders_cookie, 332 | user_index, 333 | agent_user_index, 334 | 3, 335 | ) 336 | .await 337 | .expect("order triggered"); 338 | assert!( 339 | mango_group_cookie.mango_accounts[user_index].mango_account.perp_accounts[0].taker_base 340 | == test.base_size_number_to_lots(&mint, 0.001 * base_size) as i64 341 | ); 342 | } 343 | -------------------------------------------------------------------------------- /program/tests/test_sanity.rs: -------------------------------------------------------------------------------- 1 | mod program_test; 2 | use program_test::assertions::*; 3 | use program_test::cookies::*; 4 | use program_test::scenarios::*; 5 | use program_test::*; 6 | use solana_program_test::*; 7 | 8 | #[tokio::test] 9 | async fn test_vault_net_deposit_diff() { 10 | // === Arrange === 11 | let config = 12 | MangoProgramTestConfig { num_users: 4, ..MangoProgramTestConfig::default_two_mints() }; 13 | 14 | let mut test = MangoProgramTest::start_new(&config).await; 15 | 16 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 17 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 18 | 19 | // General parameters 20 | let user_index_0: usize = 0; 21 | let user_index_1: usize = 1; 22 | let user_index_2: usize = 2; 23 | let user_index_3: usize = 3; 24 | let mint_index: usize = 0; 25 | let base_deposit_size: f64 = 1000.0001; 26 | let base_withdraw_size: f64 = 600.0001; 27 | 28 | // Deposit amounts 29 | let user_deposits = vec![ 30 | (user_index_0, mint_index, base_deposit_size), 31 | (user_index_1, mint_index, base_deposit_size * 2.3), 32 | (user_index_2, mint_index, base_deposit_size * 20.7), 33 | (user_index_3, mint_index, base_deposit_size * 2000.9), 34 | ]; 35 | 36 | // Withdraw amounts 37 | let user_withdraws = vec![ 38 | (user_index_0, mint_index, base_withdraw_size, true), 39 | (user_index_1, mint_index, base_withdraw_size * 2.3, true), 40 | (user_index_2, mint_index, base_withdraw_size * 2.7, true), 41 | (user_index_3, mint_index, base_withdraw_size * 2000.39, true), 42 | ]; 43 | 44 | // === Act === 45 | // Step 1: Make deposits 46 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 47 | 48 | // Step 2: Make withdraws 49 | withdraw_scenario(&mut test, &mut mango_group_cookie, &user_withdraws).await; 50 | 51 | // === Assert === 52 | mango_group_cookie.run_keeper(&mut test).await; 53 | assert_vault_net_deposit_diff(&mut test, &mut mango_group_cookie, mint_index).await; 54 | } 55 | -------------------------------------------------------------------------------- /program/tests/test_spot_market_mode_closeonly.rs: -------------------------------------------------------------------------------- 1 | // spot 2 | // set market to closeonly / 3 | // assert fails if not admin / 4 | // assert market mode changed on tokeninfo / 5 | // assert not possible to deposit in a fresh account / 6 | // assert possible to deposit in an account with borrows / 7 | // assert withdraw borrow not possible / 8 | // assert margin trade borrow not possible 9 | // assert open orders limited to one 10 | // assert order must be reduce only 11 | 12 | // perp 13 | // set market to closeonly 14 | // assert order must be reduce only 15 | mod program_test; 16 | 17 | use mango::state::*; 18 | use program_test::cookies::*; 19 | use program_test::*; 20 | use solana_program_test::*; 21 | 22 | #[tokio::test] 23 | async fn test_spot_market_mode_closeonly() { 24 | // === Arrange === 25 | let config = MangoProgramTestConfig::default(); 26 | let mut test = MangoProgramTest::start_new(&config).await; 27 | 28 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 29 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 30 | 31 | let market_index: usize = 0; 32 | let price: u64 = 5; 33 | let user_index: usize = 0; 34 | let user_deposit: u64 = 100; 35 | let user_with_borrow_index: usize = 1; 36 | let user_borrow: u64 = 10; 37 | 38 | // Set asset price to expected value 39 | mango_group_cookie.set_oracle(&mut test, market_index, price as f64).await; 40 | 41 | // Deposit some asset to allow borrowing 42 | // TODO: this is messy, replace keeper calls once updatefunding issues is fixed 43 | test.update_all_root_banks(&mango_group_cookie, &mango_group_cookie.address).await; 44 | test.cache_all_prices( 45 | &mango_group_cookie.mango_group, 46 | &mango_group_cookie.address, 47 | &mango_group_cookie.mango_group.oracles[0..mango_group_cookie.mango_group.num_oracles], 48 | ) 49 | .await; 50 | test.perform_deposit(&mango_group_cookie, user_index, market_index, user_deposit) 51 | .await 52 | .unwrap(); 53 | 54 | // Deposit some collateral and borrow asset 55 | test.perform_deposit( 56 | &mango_group_cookie, 57 | user_with_borrow_index, 58 | test.quote_index, 59 | user_borrow * price * 10, 60 | ) 61 | .await 62 | .unwrap(); 63 | test.perform_withdraw( 64 | &mango_group_cookie, 65 | user_with_borrow_index, 66 | market_index, 67 | user_borrow, 68 | true, 69 | ) 70 | .await 71 | .unwrap(); 72 | 73 | // Expect error if executing as non-admin 74 | test.perform_set_market_mode_as_user( 75 | &mango_group_cookie, 76 | market_index, 77 | MarketMode::CloseOnly, 78 | AssetType::Token, 79 | user_index, 80 | ) 81 | .await 82 | .unwrap_err(); 83 | 84 | // Expect success when executing as admin 85 | test.perform_set_market_mode( 86 | &mango_group_cookie, 87 | market_index, 88 | MarketMode::CloseOnly, 89 | AssetType::Token, 90 | ) 91 | .await 92 | .unwrap(); 93 | 94 | // Load group after changes 95 | let mango_group = test.load_account::(mango_group_cookie.address).await; 96 | // Expect mode to be changed for spot 97 | assert!(mango_group.tokens[market_index].spot_market_mode == MarketMode::CloseOnly); 98 | 99 | // Expect deposit to succeed but not update the balance as native_borrows is 0 and market is reduce_only 100 | let mango_account_deposit_pre = test 101 | .with_mango_account_deposit( 102 | &mango_group_cookie.mango_accounts[user_index].address, 103 | market_index, 104 | ) 105 | .await; 106 | test.perform_deposit(&mango_group_cookie, user_index, market_index, 10).await.unwrap(); 107 | let mango_account_deposit_post = test 108 | .with_mango_account_deposit( 109 | &mango_group_cookie.mango_accounts[user_index].address, 110 | market_index, 111 | ) 112 | .await; 113 | assert!(mango_account_deposit_post == mango_account_deposit_pre); 114 | 115 | // Expect deposit to succeed and only update balance to close borrows, leaving the extra 116 | test.perform_deposit( 117 | &mango_group_cookie, 118 | user_with_borrow_index, 119 | market_index, 120 | user_borrow * 100, 121 | ) 122 | .await 123 | .unwrap(); 124 | let mango_account_with_borrow_deposit = test 125 | .with_mango_account_deposit( 126 | &mango_group_cookie.mango_accounts[user_with_borrow_index].address, 127 | market_index, 128 | ) 129 | .await; 130 | let mango_account_with_borrow_borrow = test 131 | .with_mango_account_borrow( 132 | &mango_group_cookie.mango_accounts[user_with_borrow_index].address, 133 | market_index, 134 | ) 135 | .await; 136 | assert!(mango_account_with_borrow_deposit == 0); 137 | // This fails as 1 native spl left in borrows due to checked_floor, so check it's just dust left 138 | // assert!(mango_account_with_borrow_borrow == 0); 139 | assert!(mango_account_with_borrow_borrow <= ONE_I80F48); 140 | 141 | // Expect withdraw increasing borrow to fail 142 | test.perform_withdraw( 143 | &mango_group_cookie, 144 | user_with_borrow_index, 145 | market_index, 146 | user_borrow * 10, 147 | true, 148 | ) 149 | .await 150 | .unwrap_err(); 151 | } 152 | -------------------------------------------------------------------------------- /program/tests/test_update_banks.rs: -------------------------------------------------------------------------------- 1 | // #![cfg(feature = "test-bpf")] 2 | // 3 | // mod helpers; 4 | // 5 | // use helpers::*; 6 | // use solana_program::account_info::AccountInfo; 7 | // use solana_program_test::*; 8 | // use solana_sdk::{ 9 | // account::Account, 10 | // pubkey::Pubkey, 11 | // signature::{Keypair, Signer}, 12 | // transaction::Transaction, 13 | // }; 14 | // use std::mem::size_of; 15 | // 16 | // use mango::instruction::cache_root_banks; 17 | // use mango::{ 18 | // entrypoint::process_instruction, 19 | // instruction::{deposit, init_mango_account, update_root_bank}, 20 | // state::{MangoAccount, NodeBank, RootBank, QUOTE_INDEX}, 21 | // }; 22 | // 23 | // #[tokio::test] 24 | // async fn test_root_bank_update_succeeds() { 25 | // let program_id = Pubkey::new_unique(); 26 | // 27 | // let mut test = ProgramTest::new("mango", program_id, processor!(process_instruction)); 28 | // 29 | // // limit to track compute unit increase 30 | // test.set_bpf_compute_max_units(50_000); 31 | // 32 | // let initial_amount = 2; 33 | // let deposit_amount = 1; 34 | // 35 | // // setup mango group 36 | // let mango_group = add_mango_group_prodlike(&mut test, program_id); 37 | // 38 | // // setup user account 39 | // let user = Keypair::new(); 40 | // test.add_account(user.pubkey(), Account::new(u32::MAX as u64, 0, &user.pubkey())); 41 | // 42 | // // setup user token accounts 43 | // let user_account = 44 | // add_token_account(&mut test, user.pubkey(), mango_group.tokens[0].pubkey, initial_amount); 45 | // 46 | // let mango_account_pk = Pubkey::new_unique(); 47 | // test.add_account( 48 | // mango_account_pk, 49 | // Account::new(u32::MAX as u64, size_of::(), &program_id), 50 | // ); 51 | // 52 | // let (mut banks_client, payer, recent_blockhash) = test.start().await; 53 | // 54 | // { 55 | // let mut transaction = Transaction::new_with_payer( 56 | // &[ 57 | // mango_group.init_mango_group(&payer.pubkey()), 58 | // init_mango_account( 59 | // &program_id, 60 | // &mango_group.mango_group_pk, 61 | // &mango_account_pk, 62 | // &user.pubkey(), 63 | // ) 64 | // .unwrap(), 65 | // cache_root_banks( 66 | // &program_id, 67 | // &mango_group.mango_group_pk, 68 | // &mango_group.mango_cache_pk, 69 | // &[mango_group.root_banks[0].pubkey], 70 | // ) 71 | // .unwrap(), 72 | // deposit( 73 | // &program_id, 74 | // &mango_group.mango_group_pk, 75 | // &mango_account_pk, 76 | // &user.pubkey(), 77 | // &mango_group.mango_cache_pk, 78 | // &mango_group.root_banks[0].pubkey, 79 | // &mango_group.root_banks[0].node_banks[0].pubkey, 80 | // &mango_group.root_banks[0].node_banks[0].vault, 81 | // &user_account.pubkey, 82 | // deposit_amount, 83 | // ) 84 | // .unwrap(), 85 | // ], 86 | // Some(&payer.pubkey()), 87 | // ); 88 | // 89 | // transaction.sign(&[&payer, &user], recent_blockhash); 90 | // 91 | // let result = banks_client.process_transaction(transaction).await; 92 | // 93 | // let mut node_bank = banks_client 94 | // .get_account(mango_group.root_banks[0].node_banks[0].pubkey) 95 | // .await 96 | // .unwrap() 97 | // .unwrap(); 98 | // let account_info: AccountInfo = 99 | // (&mango_group.root_banks[0].node_banks[0].pubkey, &mut node_bank).into(); 100 | // let node_bank = NodeBank::load_mut_checked(&account_info, &program_id).unwrap(); 101 | // 102 | // assert_eq!(node_bank.deposits, 1); 103 | // assert_eq!(node_bank.borrows, 0); 104 | // 105 | // // Test transaction succeeded 106 | // assert!(result.is_ok()); 107 | // } 108 | // 109 | // { 110 | // let node_bank_pks: Vec = 111 | // mango_group.root_banks[0].node_banks.iter().map(|node_bank| node_bank.pubkey).collect(); 112 | // let mut transaction = Transaction::new_with_payer( 113 | // &[update_root_bank( 114 | // &program_id, 115 | // &mango_group.mango_group_pk, 116 | // &mango_group.root_banks[0].pubkey, 117 | // &node_bank_pks.as_slice(), 118 | // ) 119 | // .unwrap()], 120 | // Some(&payer.pubkey()), 121 | // ); 122 | // 123 | // transaction.sign(&[&payer], recent_blockhash); 124 | // 125 | // let result = banks_client.process_transaction(transaction).await; 126 | // 127 | // // Test transaction succeeded 128 | // assert!(result.is_ok()); 129 | // 130 | // let mut root_bank = 131 | // banks_client.get_account(mango_group.root_banks[0].pubkey).await.unwrap().unwrap(); 132 | // let account_info: AccountInfo = (&mango_group.root_banks[0].pubkey, &mut root_bank).into(); 133 | // let root_bank = RootBank::load_mut_checked(&account_info, &program_id).unwrap(); 134 | // 135 | // assert_eq!(root_bank.deposit_index, 1); 136 | // assert_eq!(root_bank.borrow_index, 1); 137 | // } 138 | // 139 | // { 140 | // let node_bank_pks: Vec = vec![]; 141 | // let mut transaction = Transaction::new_with_payer( 142 | // &[update_root_bank( 143 | // &program_id, 144 | // &mango_group.mango_group_pk, 145 | // &mango_group.root_banks[0].pubkey, 146 | // &node_bank_pks.as_slice(), 147 | // ) 148 | // .unwrap()], 149 | // Some(&payer.pubkey()), 150 | // ); 151 | // 152 | // transaction.sign(&[&payer], recent_blockhash); 153 | // 154 | // let result = banks_client.process_transaction(transaction).await; 155 | // 156 | // // Test transaction fails when no node bank accounts are passed in 157 | // assert!(result.is_err()); 158 | // } 159 | // 160 | // { 161 | // let node_bank_pks: Vec = vec![Pubkey::new_unique()]; 162 | // let mut transaction = Transaction::new_with_payer( 163 | // &[update_root_bank( 164 | // &program_id, 165 | // &mango_group.mango_group_pk, 166 | // &mango_group.root_banks[0].pubkey, 167 | // &node_bank_pks.as_slice(), 168 | // ) 169 | // .unwrap()], 170 | // Some(&payer.pubkey()), 171 | // ); 172 | // 173 | // transaction.sign(&[&payer], recent_blockhash); 174 | // 175 | // let result = banks_client.process_transaction(transaction).await; 176 | // 177 | // // Test transaction fails when invalid node bank accounts are passed in 178 | // assert!(result.is_err()); 179 | // } 180 | // } 181 | -------------------------------------------------------------------------------- /program/tests/test_worst_case.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use fixed::types::I80F48; 4 | use solana_program_test::*; 5 | 6 | use mango::state::{MAX_NUM_IN_MARGIN_BASKET, ZERO_I80F48}; 7 | use program_test::assertions::*; 8 | use program_test::cookies::*; 9 | use program_test::scenarios::*; 10 | use program_test::*; 11 | 12 | mod program_test; 13 | 14 | #[tokio::test] 15 | async fn test_worst_case_v1() { 16 | // === Arrange === 17 | let config = MangoProgramTestConfig::default(); 18 | let mut test = MangoProgramTest::start_new(&config).await; 19 | 20 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 21 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 22 | 23 | // General parameters 24 | let num_orders: usize = test.num_mints - 1; 25 | let user_index: usize = 0; 26 | let base_price: f64 = 10_000.0; 27 | let base_size: f64 = 1.0; 28 | let quote_mint = test.quote_mint; 29 | 30 | // Set oracles 31 | for mint_index in 0..num_orders { 32 | mango_group_cookie.set_oracle(&mut test, mint_index, base_price).await; 33 | } 34 | 35 | // Deposit amounts 36 | let user_deposits = vec![(user_index, test.quote_index, base_price * num_orders as f64)]; 37 | 38 | // Spot Orders 39 | let mut user_spot_orders = vec![]; 40 | for mint_index in 0..num_orders.min(MAX_NUM_IN_MARGIN_BASKET as usize) { 41 | user_spot_orders.push(( 42 | user_index, 43 | mint_index, 44 | serum_dex::matching::Side::Bid, 45 | base_size, 46 | base_price, 47 | )); 48 | } 49 | 50 | // Perp Orders 51 | let mut user_perp_orders = vec![]; 52 | for mint_index in 0..num_orders { 53 | user_perp_orders.push(( 54 | user_index, 55 | mint_index, 56 | mango::matching::Side::Bid, 57 | base_size, 58 | base_price, 59 | )); 60 | } 61 | 62 | // === Act === 63 | // Step 1: Deposit all tokens into mango account 64 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 65 | 66 | // Step 2: Place spot orders 67 | place_spot_order_scenario(&mut test, &mut mango_group_cookie, &user_spot_orders).await; 68 | 69 | // Step 3: Place perp orders 70 | place_perp_order_scenario(&mut test, &mut mango_group_cookie, &user_perp_orders).await; 71 | 72 | // === Assert === 73 | mango_group_cookie.run_keeper(&mut test).await; 74 | 75 | let mut expected_values_vec: Vec<(usize, usize, HashMap<&str, I80F48>)> = Vec::new(); 76 | for user_spot_order in user_spot_orders { 77 | let (user_index, mint_index, _, base_size, base_price) = user_spot_order; 78 | expected_values_vec.push(( 79 | mint_index, // Mint index 80 | user_index, // User index 81 | [ 82 | ("quote_free", ZERO_I80F48), 83 | ("quote_locked", test.to_native("e_mint, base_price * base_size)), 84 | ("base_free", ZERO_I80F48), 85 | ("base_locked", ZERO_I80F48), 86 | ] 87 | .iter() 88 | .cloned() 89 | .collect(), 90 | )); 91 | } 92 | 93 | for expected_values in expected_values_vec { 94 | assert_user_spot_orders(&mut test, &mango_group_cookie, expected_values).await; 95 | } 96 | 97 | assert_open_perp_orders(&mango_group_cookie, &user_perp_orders, STARTING_PERP_ORDER_ID); 98 | } 99 | 100 | #[tokio::test] 101 | async fn test_worst_case_v2() { 102 | // === Arrange === 103 | let config = MangoProgramTestConfig::default(); 104 | let mut test = MangoProgramTest::start_new(&config).await; 105 | 106 | let mut mango_group_cookie = MangoGroupCookie::default(&mut test).await; 107 | mango_group_cookie.full_setup(&mut test, config.num_users, config.num_mints - 1).await; 108 | 109 | // General parameters 110 | let borrower_user_index: usize = 0; 111 | let lender_user_index: usize = 1; 112 | let num_orders: usize = test.num_mints - 1; 113 | let base_price: f64 = 10_000.0; 114 | let base_deposit_size: f64 = 10.0; 115 | let base_withdraw_size: f64 = 1.0; 116 | let base_order_size: f64 = 1.0; 117 | 118 | // Set oracles 119 | for mint_index in 0..num_orders { 120 | mango_group_cookie.set_oracle(&mut test, mint_index, 10000.0000000001).await; 121 | } 122 | 123 | // Deposit amounts 124 | let mut user_deposits = vec![ 125 | ( 126 | borrower_user_index, 127 | test.quote_index, 128 | 2.0 * base_price * base_order_size * num_orders as f64, 129 | ), // NOTE: If depositing exact amount throws insufficient 130 | ]; 131 | user_deposits.extend(arrange_deposit_all_scenario( 132 | &mut test, 133 | lender_user_index, 134 | base_deposit_size, 135 | 0.0, 136 | )); 137 | 138 | // Withdraw amounts 139 | let mut user_withdraws = vec![]; 140 | for mint_index in 0..num_orders { 141 | user_withdraws.push((borrower_user_index, mint_index, base_withdraw_size, true)); 142 | } 143 | 144 | // Spot Orders 145 | let mut user_spot_orders = vec![]; 146 | for mint_index in 0..num_orders.min(MAX_NUM_IN_MARGIN_BASKET as usize) { 147 | user_spot_orders.push(( 148 | lender_user_index, 149 | mint_index, 150 | serum_dex::matching::Side::Ask, 151 | base_order_size, 152 | base_price, 153 | )); 154 | } 155 | 156 | // Perp Orders 157 | let mut user_perp_orders = vec![]; 158 | for mint_index in 0..num_orders { 159 | user_perp_orders.push(( 160 | lender_user_index, 161 | mint_index, 162 | mango::matching::Side::Ask, 163 | base_order_size, 164 | base_price, 165 | )); 166 | } 167 | 168 | // === Act === 169 | // Step 1: Make deposits 170 | deposit_scenario(&mut test, &mut mango_group_cookie, &user_deposits).await; 171 | 172 | // Step 2: Make withdraws 173 | withdraw_scenario(&mut test, &mut mango_group_cookie, &user_withdraws).await; 174 | 175 | // Step 3: Check that lenders all deposits are not a nice number anymore (> 10 mint) 176 | mango_group_cookie.run_keeper(&mut test).await; 177 | 178 | // for mint_index in 0..num_orders { 179 | // let base_mint = test.with_mint(mint_index); 180 | // let base_deposit_amount = (base_deposit_size * base_mint.unit) as u64; 181 | // let lender_base_deposit = &mango_group_cookie.mango_accounts[lender_user_index] 182 | // .mango_account 183 | // .get_native_deposit( 184 | // &mango_group_cookie.mango_cache.root_bank_cache[mint_index], 185 | // mint_index, 186 | // ) 187 | // .unwrap(); 188 | // assert_ne!( 189 | // lender_base_deposit.to_string(), 190 | // I80F48::from_num(base_deposit_amount).to_string() 191 | // ); 192 | // } 193 | 194 | // Step 4: Place spot orders 195 | place_spot_order_scenario(&mut test, &mut mango_group_cookie, &user_spot_orders).await; 196 | 197 | // Step 5: Place perp orders 198 | place_perp_order_scenario(&mut test, &mut mango_group_cookie, &user_perp_orders).await; 199 | 200 | // === Assert === 201 | mango_group_cookie.run_keeper(&mut test).await; 202 | 203 | let mut expected_values_vec: Vec<(usize, usize, HashMap<&str, I80F48>)> = Vec::new(); 204 | for user_spot_order in user_spot_orders { 205 | let (user_index, mint_index, _, base_size, _) = user_spot_order; 206 | let mint = test.with_mint(mint_index); 207 | expected_values_vec.push(( 208 | mint_index, // Mint index 209 | user_index, // User index 210 | [ 211 | ("quote_free", ZERO_I80F48), 212 | ("quote_locked", ZERO_I80F48), 213 | ("base_free", ZERO_I80F48), 214 | ("base_locked", test.to_native(&mint, base_size)), 215 | ] 216 | .iter() 217 | .cloned() 218 | .collect(), 219 | )); 220 | } 221 | 222 | for _expected_values in expected_values_vec { 223 | // assert_user_spot_orders(&mut test, &mango_group_cookie, expected_values).await; 224 | } 225 | 226 | assert_open_perp_orders(&mango_group_cookie, &user_perp_orders, STARTING_PERP_ORDER_ID); 227 | } 228 | -------------------------------------------------------------------------------- /proposals.md: -------------------------------------------------------------------------------- 1 | # Mango Program Improvement Proposals 2 | 3 | ## v3.2 4 | * implement change rate params so optimal utilization can be moved to 80% 5 | * use impact bid/ask for calculation of funding 6 | * replace all account initiation with PDAs 7 | * InitMangoAccount 8 | * AddPerpMarket 9 | * InitSpotOpenOrders 10 | * AddSpotMarket 11 | * auto migrate to v3 from v2 12 | * close MangoAccount and OpenOrders accounts to recoup funds 13 | * flash loans to reduce fees on serum dex 14 | 15 | 16 | ## v4.0 17 | * troll box -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Auto" 5 | use_small_heuristics = "Max" 6 | indent_style = "Block" 7 | wrap_comments = false 8 | format_code_in_doc_comments = false 9 | comment_width = 80 10 | normalize_comments = false 11 | normalize_doc_attributes = false 12 | license_template_path = "" 13 | format_strings = false 14 | format_macro_matchers = false 15 | format_macro_bodies = true 16 | empty_item_single_line = true 17 | struct_lit_single_line = true 18 | fn_single_line = false 19 | where_single_line = false 20 | imports_indent = "Block" 21 | imports_layout = "Mixed" 22 | imports_granularity = "Preserve" 23 | group_imports = "Preserve" 24 | reorder_imports = true 25 | reorder_modules = true 26 | reorder_impl_items = false 27 | type_punctuation_density = "Wide" 28 | space_before_colon = false 29 | space_after_colon = true 30 | spaces_around_ranges = false 31 | binop_separator = "Front" 32 | remove_nested_parens = true 33 | combine_control_expr = true 34 | overflow_delimited_expr = false 35 | struct_field_align_threshold = 0 36 | enum_discrim_align_threshold = 0 37 | match_arm_blocks = true 38 | match_arm_leading_pipes = "Never" 39 | force_multiline_blocks = false 40 | fn_args_layout = "Tall" 41 | brace_style = "SameLineWhere" 42 | control_brace_style = "AlwaysSameLine" 43 | trailing_semicolon = true 44 | trailing_comma = "Vertical" 45 | match_block_trailing_comma = false 46 | blank_lines_upper_bound = 1 47 | blank_lines_lower_bound = 0 48 | edition = "2015" 49 | version = "One" 50 | inline_attribute_width = 0 51 | merge_derives = true 52 | use_try_shorthand = false 53 | use_field_init_shorthand = false 54 | force_explicit_abi = true 55 | condense_wildcard_suffixes = false 56 | color = "Auto" 57 | # required_version = "1.4.36" 58 | unstable_features = false 59 | disable_all_formatting = false 60 | skip_children = false 61 | hide_parse_errors = false 62 | error_on_line_overflow = false 63 | error_on_unformatted = false 64 | report_todo = "Never" 65 | report_fixme = "Never" 66 | ignore = [] 67 | emit_mode = "Files" 68 | make_backup = false 69 | --------------------------------------------------------------------------------