diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2d4688614c..d9192d2ef6 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,54 +1,56 @@ # -# Docker image to generate deterministic, verifiable builds of Anchor programs. -# This must be run *after* a given ANCHOR_CLI version is published and a git tag -# is released on GitHub. +# Drift Protocol Dev Container # -FROM rust:1.75 +FROM --platform=linux/amd64 rust:1.70.0 ARG DEBIAN_FRONTEND=noninteractive - -ARG SOLANA_CLI="1.14.7" -ARG ANCHOR_CLI="0.26.0" -ARG NODE_VERSION="v18.16.0" +ARG SOLANA_CLI="1.16.27" +ARG ANCHOR_CLI="0.29.0" +ARG NODE_VERSION="20.18.1" ENV HOME="/root" -ENV PATH="${HOME}/.cargo/bin:${PATH}" -ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}" -ENV PATH="${HOME}/.nvm/versions/node/${NODE_VERSION}/bin:${PATH}" - -# Install base utilities. -RUN mkdir -p /workdir && mkdir -p /tmp && \ - apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \ - build-essential git curl wget jq pkg-config python3-pip \ - libssl-dev libudev-dev - -RUN wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb -RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb - -# Install rust. -RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \ - sh rustup.sh -y && \ - rustup component add rustfmt clippy - -# Install node / npm / yarn. -RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash -ENV NVM_DIR="${HOME}/.nvm" -RUN . $NVM_DIR/nvm.sh && \ - nvm install ${NODE_VERSION} && \ - nvm use ${NODE_VERSION} && \ - nvm alias default node && \ - npm install -g yarn && \ - yarn add ts-mocha - -# Install Solana tools. -RUN sh -c "$(curl -sSfL https://release.solana.com/v${SOLANA_CLI}/install)" - -# Install anchor. -RUN cargo install --git https://github.com/coral-xyz/anchor avm --locked --force -RUN avm install ${ANCHOR_CLI} && avm use ${ANCHOR_CLI} - -RUN solana-keygen new --no-bip39-passphrase +ENV PATH="/usr/local/cargo/bin:${PATH}" +ENV PATH="/root/.local/share/solana/install/active_release/bin:${PATH}" + +RUN mkdir -p /workdir /tmp \ + && apt-get update -qq \ + && apt-get upgrade -qq \ + && apt-get install -y --no-install-recommends \ + build-essential git curl wget jq pkg-config python3-pip xz-utils ca-certificates \ + libssl-dev libudev-dev bash software-properties-common \ + && add-apt-repository 'deb http://deb.debian.org/debian bookworm main' \ + && apt-get update -qq \ + && apt-get install -y libc6 libc6-dev \ + && rm -rf /var/lib/apt/lists/* + +RUN rustup component add rustfmt + +RUN rustup install 1.78.0 \ + && rustup component add rustfmt clippy --toolchain 1.78.0 + +RUN curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o /tmp/node.tar.xz \ + && tar -xJf /tmp/node.tar.xz -C /usr/local --strip-components=1 \ + && rm /tmp/node.tar.xz \ + && corepack enable \ + && npm install -g ts-mocha typescript mocha \ + && node -v && npm -v && yarn -v + +# Solana CLI (x86_64 build) +RUN curl -sSfL "https://github.com/solana-labs/solana/releases/download/v${SOLANA_CLI}/solana-release-x86_64-unknown-linux-gnu.tar.bz2" \ + | tar -xjC /tmp \ + && mv /tmp/solana-release/bin/* /usr/local/bin/ \ + && rm -rf /tmp/solana-release + +# Anchor CLI +RUN cargo install --git https://github.com/coral-xyz/anchor --tag "v${ANCHOR_CLI}" anchor-cli --locked + +# Set up Solana key + config for root +RUN solana-keygen new --no-bip39-passphrase --force \ + && solana config set --url localhost + +RUN apt-get update && apt-get install -y zsh curl git \ + && sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended \ + && chsh -s /usr/bin/zsh root WORKDIR /workdir -#be sure to add `/root/.avm/bin` to your PATH to be able to run the installed binaries diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d5aa4e718b..3228a43367 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,3 +1,54 @@ { - "build": { "dockerfile": "Dockerfile" }, - } \ No newline at end of file + "name": "Drift Protocol Development", + "build": { + "dockerfile": "Dockerfile", + "platform": "linux/amd64" + }, + "workspaceFolder": "/workdir", + "remoteUser": "root", + "mounts": [ + "source=${localWorkspaceFolder},target=/workdir,type=bind,consistency=cached", + "source=drift-target,target=/workdir/target,type=volume,consistency=delegated" + ], + "postCreateCommand": "yarn config set ignore-package-manager true && echo 'Dev container ready! You can now run: anchor build, anchor test, cargo build, etc.'", + "customizations": { + "vscode": { + "extensions": [ + "rust-lang.rust-analyzer", + "ms-vscode.vscode-json", + "tamasfe.even-better-toml" + ], + "settings": { + "rust-analyzer.cachePriming.numThreads": 1, + "rust-analyzer.cargo.buildScripts.enable": true, + "rust-analyzer.procMacro.enable": true, + "rust-analyzer.checkOnSave": true, + "rust-analyzer.check.command": "clippy", + "rust-analyzer.server.extraEnv": { + "NODE_OPTIONS": "--max-old-space-size=4096", + "RUSTUP_TOOLCHAIN": "1.78.0-x86_64-unknown-linux-gnu" + }, + "editor.formatOnSave": true, + "git.ignoreLimitWarning": true + } + } + }, + "forwardPorts": [ + 8899, + 8900 + ], + "portsAttributes": { + "8899": { + "label": "Solana Test Validator", + "onAutoForward": "notify" + }, + "8900": { + "label": "Solana Test Validator RPC", + "onAutoForward": "notify" + } + }, + "containerEnv": { + "ANCHOR_WALLET": "/root/.config/solana/id.json", + "RUST_LOG": "solana_runtime::message_processor::stable_log=debug" + } +} \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cc4d922902..a327ad3bee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ on: push: branches: [master] pull_request: - branches: [master] + branches: [master, devnet] defaults: run: diff --git a/= b/= new file mode 100644 index 0000000000..e69de29bb2 diff --git a/CHANGELOG.md b/CHANGELOG.md index d0dbcfc239..a0a802019e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,108 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Features +- program: add isolated_position_deposit to signed msg params + +### Fixes + +### Breaking + +## [2.145.1] - 2025-10-20 + +### Features + +### Fixes + +- dlp ([#1998](https://github.com/drift-labs/protocol-v2/pull/1998)) +- dlp ([#1999](https://github.com/drift-labs/protocol-v2/pull/1999)) +- dlp ([#2000](https://github.com/drift-labs/protocol-v2/pull/2000)) + +### Breaking + +## [2.145.0] - 2025-10-28 + +### Features + +- dlp ([#1885](https://github.com/drift-labs/protocol-v2/pull/1885)) + +### Fixes + +### Breaking + +## [2.144.0] - 2025-10-27 + +### Features + +- program: use-5min-for-target-expiry-price ([#1967](https://github.com/drift-labs/protocol-v2/pull/1967)) + +### Fixes + +### Breaking + +## [2.143.0] - 2025-10-22 + +- program: relax filling conditions for low risk orders vs amm ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) +- sdk: make oracle validity match program and propogate to dlob and math functions ([#1968](https://github.com/drift-labs/protocol-v2/pull/1968)) + +### Features + +- program: make imf smoother between hlm and non hlm users ([#1969](https://github.com/drift-labs/protocol-v2/pull/1969)) + +### Fixes + +### Breaking + +## [2.142.0] - 2025-10-14 + +### Features + +- program: add titan to whitelisted swap programs ([#1952](https://github.com/drift-labs/protocol-v2/pull/1952)) +- program: allow hot wallet to increase max spread and pause funding ([#1957](https://github.com/drift-labs/protocol-v2/pull/1957)) + +### Fixes + +### Breaking + +## [2.141.0] - 2025-10-03 + +### Features + +- program: disallow builder to be escrow authority ([#1930](https://github.com/drift-labs/protocol-v2/pull/1930)) +- dont panic on settle-pnl when no position ([#1928](https://github.com/drift-labs/protocol-v2/pull/1928)) + +### Fixes + +### Breaking + +## [2.140.0] - 2025-09-29 + +### Features + +- program: builder codes ([#1805](https://github.com/drift-labs/protocol-v2/pull/1805)) + +### Fixes + +### Breaking + +## [2.139.0] - 2025-09-25 + +### Features + +- program: all token 22 use immutable owner ([#1904](https://github.com/drift-labs/protocol-v2/pull/1904)) +- program: allow resolve perp pnl deficit if pnl pool isnt 0 but at deficit ([#1909](https://github.com/drift-labs/protocol-v2/pull/1909)) +- program: auction order params account for twap divergence ([#1882](https://github.com/drift-labs/protocol-v2/pull/1882)) +- program: add delegate stake if ([#1859](https://github.com/drift-labs/protocol-v2/pull/1859)) + +### Fixes + +### Breaking + +## [2.138.0] - 2025-09-22 + +### Features + - program: support scaled ui extension ([#1894](https://github.com/drift-labs/protocol-v2/pull/1894)) +- Revert "Crispeaney/revert swift max margin ratio ([#1877](https://github.com/drift-labs/protocol-v2/pull/1877)) ### Fixes @@ -42,16 +143,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking -## [2.135.0] - 2025-08-22 - -### Features - -### Fixes - -- program: trigger price use 5min mark price ([#1830](https://github.com/drift-labs/protocol-v2/pull/1830)) - -### Breaking - ## [2.134.0] - 2025-08-13 ### Features diff --git a/Cargo.lock b/Cargo.lock index 58aef0bce6..6e04c98d96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -956,7 +956,7 @@ dependencies = [ [[package]] name = "drift" -version = "2.137.0" +version = "2.145.1" dependencies = [ "ahash 0.8.6", "anchor-lang", diff --git a/README.md b/README.md index 28e340d853..0bb99c9b0a 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,48 @@ cargo test bash test-scripts/run-anchor-tests.sh ``` +# Development (with devcontainer) + +We've provided a devcontainer `Dockerfile` to help you spin up a dev environment with the correct versions of Rust, Solana, and Anchor for program development. + +Build the container and tag it `drift-dev`: +``` +cd .devcontainer && docker build -t drift-dev . +``` + +Open a shell to the container: +``` +# Find the container ID first +docker ps + +# Then exec into it +docker exec -it /bin/bash +``` + +Alternatively use an extension provided by your IDE to make use of the dev container. For example on vscode/cursor: + +``` +1. Press Ctrl+Shift+P (or Cmd+Shift+P on Mac) +2. Type "Dev Containers: Reopen in Container" +3. Select it and wait for the container to build +4. The IDE terminal should be targeting the dev container now +``` + +Use the dev container as you would a local build environment: +``` +# build program +anchor build + +# update idl +anchor build -- --features anchor-test && cp target/idl/drift.json sdk/src/idl/drift.json + +# run cargo tests +cargo test + +# run typescript tests +bash test-scripts/run-anchor-tests.sh +``` + # Bug Bounty Information about the Bug Bounty can be found [here](./bug-bounty/README.md) diff --git a/bun.lock b/bun.lock index 7fa155ea36..865ade9e6e 100644 --- a/bun.lock +++ b/bun.lock @@ -15,7 +15,7 @@ "nanoid": "3.3.4", "rpc-websockets": "7.5.1", "solana-bankrun": "0.3.0", - "zod": "^4.0.17", + "zod": "4.0.17", "zstddec": "0.1.0", }, "devDependencies": { @@ -43,6 +43,26 @@ }, }, }, + "overrides": { + "ansi-regex": "5.0.1", + "ansi-styles": "4.3.0", + "backslash": "<0.2.1", + "chalk": "4.1.2", + "chalk-template": "<1.1.1", + "color-convert": "<3.1.1", + "color-name": "<2.0.1", + "color-string": "<2.1.1", + "debug": "<4.4.2", + "error-ex": "<1.3.3", + "has-ansi": "<6.0.1", + "is-arrayish": "<0.3.3", + "simple-swizzle": "<0.2.3", + "slice-ansi": "3.0.0", + "strip-ansi": "6.0.1", + "supports-color": "7.2.0", + "supports-hyperlinks": "<4.1.1", + "wrap-ansi": "7.0.0", + }, "packages": { "@babel/runtime": ["@babel/runtime@7.28.3", "", {}, "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA=="], @@ -806,8 +826,6 @@ "@solana/codecs-strings/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], - "@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/errors/commander": ["commander@14.0.0", "", {}, "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA=="], "@solana/options/@solana/codecs-numbers": ["@solana/codecs-numbers@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ=="], @@ -900,22 +918,14 @@ "@solana/buffer-layout-utils/@solana/web3.js/superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], - "@solana/codecs-core/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/codecs-core/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@solana/codecs-data-structures/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/codecs-data-structures/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "@solana/codecs-strings/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/codecs-strings/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@solana/codecs/@solana/codecs-numbers/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], - "@solana/options/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/options/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@switchboard-xyz/common/@solana/web3.js/bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], @@ -1018,8 +1028,6 @@ "@solana/buffer-layout-utils/@solana/web3.js/rpc-websockets/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "@solana/codecs/@solana/codecs-numbers/@solana/errors/chalk": ["chalk@5.6.0", "", {}, "sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ=="], - "@solana/codecs/@solana/codecs-numbers/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "@switchboard-xyz/common/@solana/web3.js/jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], diff --git a/package.json b/package.json index de9fdd10f5..4b30b26fde 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@project-serum/common": "0.0.1-beta.3", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.21.0", + "@pythnetwork/price-service-client": "1.9.0", "@solana/spl-token": "0.4.13", "@solana/web3.js": "1.73.2", "@types/bn.js": "5.1.6", @@ -24,8 +25,7 @@ "husky": "7.0.4", "prettier": "3.0.1", "typedoc": "0.23.23", - "typescript": "5.4.5", - "@pythnetwork/price-service-client": "1.9.0" + "typescript": "5.4.5" }, "dependencies": { "@ellipsis-labs/phoenix-sdk": "1.4.2", @@ -50,7 +50,7 @@ "prettify:fix": "prettier --write './sdk/src/**/*.ts' './tests/**.ts' './cli/**.ts'", "lint": "eslint . --ext ts --quiet --format unix", "lint:fix": "eslint . --ext ts --fix", - "update-idl": "cp target/idl/drift.json sdk/src/idl/drift.json" + "update-idl": "anchor build -- --features anchor-test && cp target/idl/drift.json sdk/src/idl/drift.json" }, "engines": { "node": ">=12" @@ -95,4 +95,4 @@ "supports-hyperlinks": "<4.1.1", "has-ansi": "<6.0.1" } -} \ No newline at end of file +} diff --git a/programs/drift/Cargo.toml b/programs/drift/Cargo.toml index 945e055dee..b2730356e9 100644 --- a/programs/drift/Cargo.toml +++ b/programs/drift/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drift" -version = "2.137.0" +version = "2.145.1" description = "Created with Anchor" edition = "2018" @@ -20,7 +20,7 @@ drift-rs=[] [dependencies] anchor-lang = "0.29.0" solana-program = "1.16" -anchor-spl = "0.29.0" +anchor-spl = { version = "0.29.0", features = [] } pyth-client = "0.2.2" pyth-lazer-solana-contract = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "d790d1cb4da873a949cf33ff70349b7614b232eb", features = ["no-entrypoint"]} pythnet-sdk = { git = "https://github.com/drift-labs/pyth-crosschain", rev = "3e8a24ecd0bcf22b787313e2020f4186bb22c729"} diff --git a/programs/drift/src/controller/amm.rs b/programs/drift/src/controller/amm.rs index f08b3b546d..cbfc47ff32 100644 --- a/programs/drift/src/controller/amm.rs +++ b/programs/drift/src/controller/amm.rs @@ -11,7 +11,7 @@ use crate::controller::spot_balance::{ }; use crate::error::{DriftResult, ErrorCode}; use crate::get_then_update_id; -use crate::math::amm::calculate_quote_asset_amount_swapped; +use crate::math::amm::{calculate_net_user_pnl, calculate_quote_asset_amount_swapped}; use crate::math::amm_spread::{calculate_spread_reserves, get_spread_reserves}; use crate::math::casting::Cast; use crate::math::constants::{ @@ -185,20 +185,33 @@ pub fn update_spreads( let max_ref_offset = market.amm.get_max_reference_price_offset()?; let reference_price_offset = if max_ref_offset > 0 { - let liquidity_ratio = amm_spread::calculate_inventory_liquidity_ratio( - market.amm.base_asset_amount_with_amm, - market.amm.base_asset_reserve, - market.amm.min_base_asset_reserve, - market.amm.max_base_asset_reserve, - )?; + let liquidity_ratio = + amm_spread::calculate_inventory_liquidity_ratio_for_reference_price_offset( + market.amm.base_asset_amount_with_amm, + market.amm.base_asset_reserve, + market.amm.min_base_asset_reserve, + market.amm.max_base_asset_reserve, + )?; let signed_liquidity_ratio = liquidity_ratio.safe_mul(market.amm.get_protocol_owned_position()?.signum().cast()?)?; + let deadband_pct = market.amm.get_reference_price_offset_deadband_pct()?; + let liquidity_fraction_after_deadband = + if signed_liquidity_ratio.unsigned_abs() <= deadband_pct { + 0 + } else { + signed_liquidity_ratio.safe_sub( + deadband_pct + .cast::()? + .safe_mul(signed_liquidity_ratio.signum())?, + )? + }; + amm_spread::calculate_reference_price_offset( reserve_price, market.amm.last_24h_avg_funding_rate, - signed_liquidity_ratio, + liquidity_fraction_after_deadband, market.amm.min_order_size, market .amm @@ -966,7 +979,7 @@ pub fn calculate_perp_market_amm_summary_stats( .safe_add(fee_pool_token_amount)? .cast()?; - let net_user_pnl = amm::calculate_net_user_pnl(&perp_market.amm, perp_market_oracle_price)?; + let net_user_pnl = calculate_net_user_pnl(&perp_market.amm, perp_market_oracle_price)?; // amm's mm_fee can be incorrect with drifting integer math error let mut new_total_fee_minus_distributions = pnl_tokens_available.safe_sub(net_user_pnl)?; diff --git a/programs/drift/src/controller/insurance.rs b/programs/drift/src/controller/insurance.rs index 91ad374ed5..fa53164733 100644 --- a/programs/drift/src/controller/insurance.rs +++ b/programs/drift/src/controller/insurance.rs @@ -14,9 +14,9 @@ use crate::error::ErrorCode; use crate::math::amm::calculate_net_user_pnl; use crate::math::casting::Cast; use crate::math::constants::{ - MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT, + FUEL_START_TS, GOV_SPOT_MARKET_INDEX, MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT, MAX_APR_PER_REVENUE_SETTLE_TO_INSURANCE_FUND_VAULT_GOV, ONE_YEAR, PERCENTAGE_PRECISION, - SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_DENOMINATOR, + QUOTE_SPOT_MARKET_INDEX, SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_DENOMINATOR, SHARE_OF_REVENUE_ALLOCATED_TO_INSURANCE_FUND_VAULT_NUMERATOR, }; use crate::math::fuel::calculate_insurance_fuel_bonus; @@ -40,7 +40,7 @@ use crate::state::perp_market::PerpMarket; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::state::State; use crate::state::user::UserStats; -use crate::{emit, validate, FUEL_START_TS, GOV_SPOT_MARKET_INDEX, QUOTE_SPOT_MARKET_INDEX}; +use crate::{emit, validate}; #[cfg(test)] mod tests; @@ -112,6 +112,7 @@ pub fn add_insurance_fund_stake( user_stats: &mut UserStats, spot_market: &mut SpotMarket, now: i64, + admin_deposit: bool, ) -> DriftResult { validate!( !(insurance_vault_amount == 0 && spot_market.insurance_fund.total_shares != 0), @@ -161,7 +162,11 @@ pub fn add_insurance_fund_stake( emit!(InsuranceFundStakeRecord { ts: now, user_authority: user_stats.authority, - action: StakeAction::Stake, + action: if admin_deposit { + StakeAction::AdminDeposit + } else { + StakeAction::Stake + }, amount, market_index: spot_market.market_index, insurance_vault_amount_before: insurance_vault_amount, @@ -843,11 +848,20 @@ pub fn resolve_perp_pnl_deficit( &SpotBalanceType::Deposit, )?; + let net_user_pnl = calculate_net_user_pnl( + &market.amm, + market + .amm + .historical_oracle_data + .last_oracle_price_twap_5min, + )?; + validate!( - pnl_pool_token_amount == 0, + pnl_pool_token_amount.cast::()? < net_user_pnl, ErrorCode::SufficientPerpPnlPool, - "pnl_pool_token_amount > 0 (={})", - pnl_pool_token_amount + "pnl_pool_token_amount >= net_user_pnl ({} >= {})", + pnl_pool_token_amount, + net_user_pnl )?; update_spot_market_cumulative_interest(spot_market, None, now)?; @@ -1098,6 +1112,14 @@ pub fn handle_if_end_swap( if_rebalance_config.epoch_max_in_amount )?; + validate!( + if_rebalance_config.current_in_amount <= if_rebalance_config.total_in_amount, + ErrorCode::InvalidIfRebalanceSwap, + "current_in_amount={} > total_in_amount={}", + if_rebalance_config.current_in_amount, + if_rebalance_config.total_in_amount + )?; + let oracle_twap = out_spot_market .historical_oracle_data .last_oracle_price_twap; diff --git a/programs/drift/src/controller/insurance/tests.rs b/programs/drift/src/controller/insurance/tests.rs index 4301a3d26f..c71948c3d3 100644 --- a/programs/drift/src/controller/insurance/tests.rs +++ b/programs/drift/src/controller/insurance/tests.rs @@ -41,6 +41,7 @@ pub fn basic_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -104,6 +105,7 @@ pub fn basic_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 1234); @@ -141,6 +143,7 @@ pub fn basic_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -202,6 +205,7 @@ pub fn basic_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 1234); @@ -245,6 +249,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -334,6 +339,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 20, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 199033744205760); @@ -346,6 +352,7 @@ pub fn large_num_seeded_stake_if_test() { &mut user_stats, &mut spot_market, 30, + false, ) .unwrap(); assert_eq!(if_stake.cost_basis, 398067488411520); @@ -378,6 +385,7 @@ pub fn gains_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -502,6 +510,7 @@ pub fn losses_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -631,6 +640,7 @@ pub fn escrow_losses_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); assert_eq!(if_stake.unchecked_if_shares(), amount as u128); @@ -729,7 +739,8 @@ pub fn escrow_gains_stake_if_test() { &mut if_stake, &mut user_stats, &mut spot_market, - 0 + 0, + false, ) .is_err()); @@ -741,6 +752,7 @@ pub fn escrow_gains_stake_if_test() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -858,6 +870,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut user_stats, &mut spot_market, 0, + false, ) .is_err()); @@ -877,6 +890,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); if_balance += amount; @@ -912,6 +926,7 @@ pub fn drained_stake_if_test_rebase_on_new_add() { &mut orig_user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -1010,6 +1025,7 @@ pub fn drained_stake_if_test_rebase_on_old_remove_all() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); @@ -1210,6 +1226,7 @@ pub fn drained_stake_if_test_rebase_on_old_remove_all_2() { &mut user_stats, &mut spot_market, 0, + false, ) .unwrap(); if_balance += 10_000_000_000_000; @@ -1254,6 +1271,7 @@ pub fn multiple_if_stakes_and_rebase() { &mut user_stats_1, &mut spot_market, 0, + false, ) .unwrap(); @@ -1266,6 +1284,7 @@ pub fn multiple_if_stakes_and_rebase() { &mut user_stats_2, &mut spot_market, 0, + false, ) .unwrap(); @@ -1392,6 +1411,7 @@ pub fn multiple_if_stakes_and_rebase_and_admin_remove() { &mut user_stats_1, &mut spot_market, 0, + false, ) .unwrap(); @@ -1404,6 +1424,7 @@ pub fn multiple_if_stakes_and_rebase_and_admin_remove() { &mut user_stats_2, &mut spot_market, 0, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/liquidation.rs b/programs/drift/src/controller/liquidation.rs index c70b9cd7a2..10da79fb5f 100644 --- a/programs/drift/src/controller/liquidation.rs +++ b/programs/drift/src/controller/liquidation.rs @@ -24,8 +24,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::bankruptcy::is_cross_margin_bankrupt; use crate::math::casting::Cast; use crate::math::constants::{ - LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, QUOTE_PRECISION, - QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, SPOT_WEIGHT_PRECISION, + LIQUIDATION_FEE_PRECISION, LIQUIDATION_FEE_PRECISION_U128, LIQUIDATION_PCT_PRECISION, + QUOTE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, + SPOT_WEIGHT_PRECISION, }; use crate::math::liquidation::{ calculate_asset_transfer_for_liability_transfer, @@ -51,6 +52,7 @@ use crate::math::orders::{ use crate::math::position::calculate_base_asset_value_with_oracle_price; use crate::math::safe_math::SafeMath; +use crate::math::constants::LST_POOL_ID; use crate::math::spot_balance::get_token_value; use crate::state::events::{ LiquidateBorrowForPerpPnlRecord, LiquidatePerpPnlForDepositRecord, LiquidatePerpRecord, @@ -69,8 +71,8 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User, UserStats}; use crate::state::user_map::{UserMap, UserStatsMap}; -use crate::{get_then_update_id, load_mut, LST_POOL_ID}; -use crate::{validate, LIQUIDATION_FEE_PRECISION}; +use crate::validate; +use crate::{get_then_update_id, load_mut}; #[cfg(test)] mod tests; @@ -690,6 +692,8 @@ pub fn liquidate_perp( maker_existing_quote_entry_amount: maker_existing_quote_entry_amount, maker_existing_base_asset_amount: maker_existing_base_asset_amount, trigger_price: None, + builder_idx: None, + builder_fee: None, }; emit!(fill_record); @@ -1067,6 +1071,7 @@ pub fn liquidate_perp_with_fill( clock, order_params, PlaceOrderOptions::default().explanation(OrderActionExplanation::Liquidation), + &mut None, )?; drop(user); @@ -1087,6 +1092,8 @@ pub fn liquidate_perp_with_fill( None, clock, FillMode::Liquidation, + &mut None, + false, )?; let mut user = load_mut!(user_loader)?; diff --git a/programs/drift/src/controller/mod.rs b/programs/drift/src/controller/mod.rs index 0a099f7cde..db15dfddf5 100644 --- a/programs/drift/src/controller/mod.rs +++ b/programs/drift/src/controller/mod.rs @@ -8,6 +8,7 @@ pub mod pda; pub mod pnl; pub mod position; pub mod repeg; +pub mod revenue_share; pub mod spot_balance; pub mod spot_position; pub mod token; diff --git a/programs/drift/src/controller/orders.rs b/programs/drift/src/controller/orders.rs index 94ed876b8a..e8529488dd 100644 --- a/programs/drift/src/controller/orders.rs +++ b/programs/drift/src/controller/orders.rs @@ -5,8 +5,12 @@ use std::u64; use crate::msg; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; +use crate::state::revenue_share::{ + RevenueShareEscrowZeroCopyMut, RevenueShareOrder, RevenueShareOrderBitFlag, +}; use anchor_lang::prelude::*; +use crate::controller; use crate::controller::funding::settle_funding_payment; use crate::controller::position; use crate::controller::position::{ @@ -29,7 +33,9 @@ use crate::math::amm::calculate_amm_available_liquidity; use crate::math::amm_jit::calculate_amm_jit_liquidity; use crate::math::auction::{calculate_auction_params_for_trigger_order, calculate_auction_prices}; use crate::math::casting::Cast; -use crate::math::constants::{BASE_PRECISION_U64, PERP_DECIMALS, QUOTE_SPOT_MARKET_INDEX}; +use crate::math::constants::{ + BASE_PRECISION_U64, MARGIN_PRECISION, PERP_DECIMALS, QUOTE_SPOT_MARKET_INDEX, +}; use crate::math::fees::{determine_user_fee_tier, ExternalFillFees, FillFees}; use crate::math::fulfillment::{ determine_perp_fulfillment_methods, determine_spot_fulfillment_methods, @@ -59,7 +65,7 @@ use crate::state::order_params::{ ModifyOrderParams, OrderParams, PlaceOrderOptions, PostOnlyParam, }; use crate::state::paused_operations::{PerpOperation, SpotOperation}; -use crate::state::perp_market::{AMMAvailability, MarketStatus, PerpMarket}; +use crate::state::perp_market::{MarketStatus, PerpMarket}; use crate::state::perp_market_map::PerpMarketMap; use crate::state::protected_maker_mode_config::ProtectedMakerParams; use crate::state::spot_fulfillment_params::{ExternalSpotFill, SpotFulfillmentParams}; @@ -78,17 +84,12 @@ use crate::validation; use crate::validation::order::{ validate_order, validate_order_for_force_reduce_only, validate_spot_order, }; -use crate::{controller, MARGIN_PRECISION}; #[cfg(test)] mod tests; #[cfg(test)] mod amm_jit_tests; - -#[cfg(test)] -mod amm_lp_jit_tests; - #[cfg(test)] mod fuel_tests; @@ -103,6 +104,7 @@ pub fn place_perp_order( clock: &Clock, mut params: OrderParams, mut options: PlaceOrderOptions, + rev_share_order: &mut Option<&mut RevenueShareOrder>, ) -> DriftResult { let now = clock.unix_timestamp; let slot: u64 = clock.slot; @@ -298,6 +300,10 @@ pub fn place_perp_order( OrderBitFlag::NewTriggerReduceOnly, ); + if rev_share_order.is_some() { + bit_flags = set_order_bit_flag(bit_flags, true, OrderBitFlag::HasBuilder); + } + let new_order = Order { status: OrderStatus::Open, order_type: params.order_type, @@ -438,6 +444,8 @@ pub fn place_perp_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -729,6 +737,8 @@ pub fn cancel_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; } @@ -855,6 +865,7 @@ pub fn modify_order( clock, order_params, PlaceOrderOptions::default(), + &mut None, )?; } else { place_spot_order( @@ -979,6 +990,8 @@ pub fn fill_perp_order( jit_maker_order_id: Option, clock: &Clock, fill_mode: FillMode, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let now = clock.unix_timestamp; let slot = clock.slot; @@ -994,13 +1007,8 @@ pub fn fill_perp_order( .position(|order| order.order_id == order_id && order.status == OrderStatus::Open) .ok_or_else(print_error!(ErrorCode::OrderDoesNotExist))?; - let (order_status, market_index, order_market_type, order_direction) = get_struct_values!( - user.orders[order_index], - status, - market_index, - market_type, - direction - ); + let (order_status, market_index, order_market_type) = + get_struct_values!(user.orders[order_index], status, market_index, market_type); validate!( order_market_type == MarketType::Perp, @@ -1066,14 +1074,9 @@ pub fn fill_perp_order( let safe_oracle_validity: OracleValidity; let oracle_price: i64; let oracle_twap_5min: i64; - let perp_market_index: u16; let user_can_skip_duration: bool; - let amm_can_skip_duration: bool; - let amm_has_low_enough_inventory: bool; - let oracle_valid_for_amm_fill: bool; let oracle_stale_for_margin: bool; - let min_auction_duration: u8; - let mut amm_is_available = !state.amm_paused()?; + let mut amm_is_available: bool = !state.amm_paused()?; { let market = &mut perp_market_map.get_ref_mut(&market_index)?; validation::perp_market::validate_perp_market(market)?; @@ -1100,10 +1103,19 @@ pub fn fill_perp_order( &market.amm.oracle_source, oracle::LogMode::SafeMMOracle, market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )?; - oracle_valid_for_amm_fill = - is_oracle_valid_for_action(safe_oracle_validity, Some(DriftAction::FillOrderAmm))?; + user_can_skip_duration = user.can_skip_auction_duration(user_stats)?; + amm_is_available &= market.amm_can_fill_order( + &user.orders[order_index], + slot, + fill_mode, + state, + safe_oracle_validity, + user_can_skip_duration, + &mm_oracle_price_data, + )?; oracle_stale_for_margin = mm_oracle_price_data.get_delay() > state @@ -1111,40 +1123,12 @@ pub fn fill_perp_order( .validity .slots_before_stale_for_margin; - amm_is_available &= oracle_valid_for_amm_fill; - amm_is_available &= !market.is_operation_paused(PerpOperation::AmmFill); - amm_is_available &= !market.has_too_much_drawdown()?; - - // We are already using safe oracle data from MM oracle. - // But AMM isnt available if we could have used MM oracle but fell back due to price diff - let amm_available_mm_oracle_recent_but_volatile = - if mm_oracle_price_data.is_enabled() && mm_oracle_price_data.is_mm_oracle_as_recent() { - let amm_available = !mm_oracle_price_data.is_mm_exchange_diff_bps_high(); - amm_available - } else { - true - }; - amm_is_available &= amm_available_mm_oracle_recent_but_volatile; - - let amm_wants_to_jit_make = market.amm.amm_wants_to_jit_make(order_direction)?; - amm_has_low_enough_inventory = market - .amm - .amm_has_low_enough_inventory(amm_wants_to_jit_make)?; - amm_can_skip_duration = - market.can_skip_auction_duration(&state, amm_has_low_enough_inventory)?; - - user_can_skip_duration = user.can_skip_auction_duration(user_stats)?; - reserve_price_before = market.amm.reserve_price()?; oracle_price = mm_oracle_price_data.get_price(); oracle_twap_5min = market .amm .historical_oracle_data .last_oracle_price_twap_5min; - perp_market_index = market.market_index; - - min_auction_duration = - market.get_min_perp_auction_duration(state.min_perp_auction_duration); } // allow oracle price to be used to calculate limit price if it's valid or stale for amm @@ -1152,7 +1136,7 @@ pub fn fill_perp_order( if is_oracle_valid_for_action(safe_oracle_validity, Some(DriftAction::OracleOrderPrice))? { Some(oracle_price) } else { - msg!("Perp market = {} oracle deemed invalid", perp_market_index); + msg!("Perp market = {} oracle deemed invalid", market_index); None }; @@ -1281,16 +1265,6 @@ pub fn fill_perp_order( return Ok((0, 0)); } - let amm_availability = if amm_is_available { - if amm_can_skip_duration && user_can_skip_duration { - AMMAvailability::Immediate - } else { - AMMAvailability::AfterMinDuration - } - } else { - AMMAvailability::Unavailable - }; - let (base_asset_amount, quote_asset_amount) = fulfill_perp_order( user, order_index, @@ -1311,10 +1285,11 @@ pub fn fill_perp_order( valid_oracle_price, now, slot, - min_auction_duration, - amm_availability, + amm_is_available, fill_mode, oracle_stale_for_margin, + rev_share_escrow, + builder_referral_feature_enabled, )?; if base_asset_amount != 0 { @@ -1725,6 +1700,37 @@ fn get_referrer_info( Ok(Some((referrer_authority_key, referrer_user_key))) } +#[inline(always)] +fn get_builder_escrow_info( + escrow_opt: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + sub_account_id: u16, + order_id: u32, + market_index: u16, + builder_referral_feature_enabled: bool, +) -> (Option, Option, Option, Option) { + if let Some(escrow) = escrow_opt { + let builder_order_idx = escrow.find_order_index(sub_account_id, order_id); + let referrer_builder_order_idx = if builder_referral_feature_enabled { + escrow.find_or_create_referral_index(market_index) + } else { + None + }; + + let builder_order = builder_order_idx.and_then(|idx| escrow.get_order(idx).ok()); + let builder_order_fee_bps = builder_order.map(|order| order.fee_tenth_bps); + let builder_idx = builder_order.map(|order| order.builder_idx); + + ( + builder_order_idx, + referrer_builder_order_idx, + builder_order_fee_bps, + builder_idx, + ) + } else { + (None, None, None, None) + } +} + fn fulfill_perp_order( user: &mut User, user_order_index: usize, @@ -1745,10 +1751,11 @@ fn fulfill_perp_order( valid_oracle_price: Option, now: i64, slot: u64, - min_auction_duration: u8, - amm_availability: AMMAvailability, + amm_is_available: bool, fill_mode: FillMode, oracle_stale_for_margin: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let market_index = user.orders[user_order_index].market_index; @@ -1768,19 +1775,13 @@ fn fulfill_perp_order( let fulfillment_methods = { let market = perp_market_map.get_ref(&market_index)?; - let oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; - determine_perp_fulfillment_methods( &user.orders[user_order_index], maker_orders_info, &market.amm, reserve_price_before, - Some(oracle_price), limit_price, - amm_availability, - slot, - min_auction_duration, - fill_mode, + amm_is_available, )? }; @@ -1847,6 +1848,8 @@ fn fulfill_perp_order( None, *maker_price, fill_mode.is_liquidation(), + rev_share_escrow, + builder_referral_feature_enabled, )?; (fill_base_asset_amount, fill_quote_asset_amount) @@ -1891,6 +1894,8 @@ fn fulfill_perp_order( fee_structure, oracle_map, fill_mode.is_liquidation(), + rev_share_escrow, + builder_referral_feature_enabled, )?; if maker_fill_base_asset_amount != 0 { @@ -2164,6 +2169,8 @@ pub fn fulfill_perp_order_with_amm( override_base_asset_amount: Option, override_fill_price: Option, is_liquidation: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64)> { let position_index = get_position_index(&user.perp_positions, market.market_index)?; let existing_base_asset_amount = user.perp_positions[position_index].base_asset_amount; @@ -2221,8 +2228,17 @@ pub fn fulfill_perp_order_with_amm( return Ok((0, 0)); } - let (order_post_only, order_slot, order_direction) = - get_struct_values!(user.orders[order_index], post_only, slot, direction); + let (order_post_only, order_slot, order_direction, order_id) = get_struct_values!( + user.orders[order_index], + post_only, + slot, + direction, + order_id + ); + let user_order_has_builder = user.orders[order_index].is_has_builder(); + if user_order_has_builder && rev_share_escrow.is_none() { + msg!("Order has builder but no escrow account included, in the future this will fail."); + } validation::perp_market::validate_amm_account_for_fill(&market.amm, order_direction)?; @@ -2264,10 +2280,24 @@ pub fn fulfill_perp_order_with_amm( )?; } - let reward_referrer = can_reward_user_with_perp_pnl(referrer, market.market_index); + let reward_referrer = can_reward_user_with_referral_reward( + referrer, + market.market_index, + rev_share_escrow, + builder_referral_feature_enabled, + ); let reward_filler = can_reward_user_with_perp_pnl(filler, market.market_index) || can_reward_user_with_perp_pnl(maker, market.market_index); + let (builder_order_idx, referrer_builder_order_idx, builder_order_fee_bps, builder_idx) = + get_builder_escrow_info( + rev_share_escrow, + user.sub_account_id, + order_id, + market.market_index, + builder_referral_feature_enabled, + ); + let FillFees { user_fee, fee_to_market, @@ -2276,6 +2306,7 @@ pub fn fulfill_perp_order_with_amm( referrer_reward, fee_to_market_for_lp: _fee_to_market_for_lp, maker_rebate, + builder_fee: builder_fee_option, } = fees::calculate_fee_for_fulfillment_with_amm( user_stats, quote_asset_amount, @@ -2289,8 +2320,20 @@ pub fn fulfill_perp_order_with_amm( order_post_only, market.fee_adjustment, user.is_high_leverage_mode(MarginRequirementType::Initial), + builder_order_fee_bps, )?; + let builder_fee = builder_fee_option.unwrap_or(0); + + if builder_fee != 0 { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_mut()) { + let order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(builder_fee)?; + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } + // Increment the protocol's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; market.amm.total_exchange_fee = market.amm.total_exchange_fee.safe_add(user_fee.cast()?)?; @@ -2312,7 +2355,12 @@ pub fn fulfill_perp_order_with_amm( user_stats.increment_total_rebate(maker_rebate)?; user_stats.increment_total_referee_discount(referee_discount)?; - if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) { + if let (Some(idx), Some(escrow)) = (referrer_builder_order_idx, rev_share_escrow.as_mut()) { + let order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(referrer_reward)?; + } else if let (Some(referrer), Some(referrer_stats)) = + (referrer.as_mut(), referrer_stats.as_mut()) + { if let Ok(referrer_position) = referrer.force_get_perp_position_mut(market.market_index) { if referrer_reward > 0 { update_quote_asset_amount(referrer_position, market, referrer_reward.cast()?)?; @@ -2323,11 +2371,11 @@ pub fn fulfill_perp_order_with_amm( let position_index = get_position_index(&user.perp_positions, market.market_index)?; - if user_fee != 0 { + if user_fee != 0 || builder_fee != 0 { controller::position::update_quote_asset_and_break_even_amount( &mut user.perp_positions[position_index], market, - -user_fee.cast()?, + -(user_fee.safe_add(builder_fee)?).cast()?, )?; } @@ -2367,11 +2415,18 @@ pub fn fulfill_perp_order_with_amm( )?; } - update_order_after_fill( + let is_filled = update_order_after_fill( &mut user.orders[order_index], base_asset_amount, quote_asset_amount, )?; + if is_filled { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_mut()) { + let _ = escrow + .get_order_mut(idx) + .map(|order| order.add_bit_flag(RevenueShareOrderBitFlag::Completed)); + } + } decrease_open_bids_and_asks( &mut user.perp_positions[position_index], @@ -2432,7 +2487,7 @@ pub fn fulfill_perp_order_with_amm( Some(filler_reward), Some(base_asset_amount), Some(quote_asset_amount), - Some(user_fee), + Some(user_fee.safe_add(builder_fee)?), if maker_rebate != 0 { Some(maker_rebate) } else { @@ -2452,6 +2507,8 @@ pub fn fulfill_perp_order_with_amm( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, None, + builder_idx, + builder_fee_option, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2520,6 +2577,8 @@ pub fn fulfill_perp_order_with_match( fee_structure: &FeeStructure, oracle_map: &mut OracleMap, is_liquidation: bool, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, ) -> DriftResult<(u64, u64, u64)> { if !are_orders_same_market_but_different_sides( &maker.orders[maker_order_index], @@ -2530,6 +2589,10 @@ pub fn fulfill_perp_order_with_match( let oracle_price = oracle_map.get_price_data(&market.oracle_id())?.price; let taker_direction: PositionDirection = taker.orders[taker_order_index].direction; + let taker_order_has_builder = taker.orders[taker_order_index].is_has_builder(); + if taker_order_has_builder && rev_share_escrow.is_none() { + msg!("Order has builder but no escrow account included, in the future this will fail."); + } let taker_price = if let Some(taker_limit_price) = taker_limit_price { taker_limit_price @@ -2632,6 +2695,8 @@ pub fn fulfill_perp_order_with_match( Some(jit_base_asset_amount), Some(maker_price), // match the makers price is_liquidation, + rev_share_escrow, + builder_referral_feature_enabled, )?; total_base_asset_amount = base_asset_amount_filled_by_amm; @@ -2724,9 +2789,23 @@ pub fn fulfill_perp_order_with_match( taker_stats.update_taker_volume_30d(market.fuel_boost_taker, quote_asset_amount, now)?; - let reward_referrer = can_reward_user_with_perp_pnl(referrer, market.market_index); + let reward_referrer = can_reward_user_with_referral_reward( + referrer, + market.market_index, + rev_share_escrow, + builder_referral_feature_enabled, + ); let reward_filler = can_reward_user_with_perp_pnl(filler, market.market_index); + let (builder_order_idx, referrer_builder_order_idx, builder_order_fee_bps, builder_idx) = + get_builder_escrow_info( + rev_share_escrow, + taker.sub_account_id, + taker.orders[taker_order_index].order_id, + market.market_index, + builder_referral_feature_enabled, + ); + let filler_multiplier = if reward_filler { calculate_filler_multiplier_for_matched_orders(maker_price, maker_direction, oracle_price)? } else { @@ -2740,6 +2819,7 @@ pub fn fulfill_perp_order_with_match( filler_reward, referrer_reward, referee_discount, + builder_fee: builder_fee_option, .. } = fees::calculate_fee_for_fulfillment_with_match( taker_stats, @@ -2754,7 +2834,18 @@ pub fn fulfill_perp_order_with_match( &MarketType::Perp, market.fee_adjustment, taker.is_high_leverage_mode(MarginRequirementType::Initial), + builder_order_fee_bps, )?; + let builder_fee = builder_fee_option.unwrap_or(0); + + if builder_fee != 0 { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_deref_mut()) { + let order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(builder_fee)?; + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } // Increment the markets house's total fee variables market.amm.total_fee = market.amm.total_fee.safe_add(fee_to_market.cast()?)?; @@ -2774,7 +2865,7 @@ pub fn fulfill_perp_order_with_match( controller::position::update_quote_asset_and_break_even_amount( &mut taker.perp_positions[taker_position_index], market, - -taker_fee.cast()?, + -(taker_fee.safe_add(builder_fee)?).cast()?, )?; taker_stats.increment_total_fees(taker_fee)?; @@ -2813,7 +2904,13 @@ pub fn fulfill_perp_order_with_match( filler.update_last_active_slot(slot); } - if let (Some(referrer), Some(referrer_stats)) = (referrer.as_mut(), referrer_stats.as_mut()) { + if let (Some(idx), Some(escrow)) = (referrer_builder_order_idx, rev_share_escrow.as_deref_mut()) + { + let order = escrow.get_order_mut(idx)?; + order.fees_accrued = order.fees_accrued.safe_add(referrer_reward)?; + } else if let (Some(referrer), Some(referrer_stats)) = + (referrer.as_mut(), referrer_stats.as_mut()) + { if let Ok(referrer_position) = referrer.force_get_perp_position_mut(market.market_index) { if referrer_reward > 0 { update_quote_asset_amount(referrer_position, market, referrer_reward.cast()?)?; @@ -2822,12 +2919,20 @@ pub fn fulfill_perp_order_with_match( } } - update_order_after_fill( + let is_filled = update_order_after_fill( &mut taker.orders[taker_order_index], base_asset_amount_fulfilled_by_maker, quote_asset_amount, )?; + if is_filled { + if let (Some(idx), Some(escrow)) = (builder_order_idx, rev_share_escrow.as_deref_mut()) { + escrow + .get_order_mut(idx)? + .add_bit_flag(RevenueShareOrderBitFlag::Completed); + } + } + decrease_open_bids_and_asks( &mut taker.perp_positions[taker_position_index], &taker.orders[taker_order_index].direction, @@ -2882,7 +2987,7 @@ pub fn fulfill_perp_order_with_match( Some(filler_reward), Some(base_asset_amount_fulfilled_by_maker), Some(quote_asset_amount), - Some(taker_fee), + Some(taker_fee.safe_add(builder_fee)?), Some(maker_rebate), Some(referrer_reward), None, @@ -2898,6 +3003,8 @@ pub fn fulfill_perp_order_with_match( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, None, + builder_idx, + builder_fee_option, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -2926,18 +3033,19 @@ pub fn update_order_after_fill( order: &mut Order, base_asset_amount: u64, quote_asset_amount: u64, -) -> DriftResult { +) -> DriftResult { order.base_asset_amount_filled = order.base_asset_amount_filled.safe_add(base_asset_amount)?; order.quote_asset_amount_filled = order .quote_asset_amount_filled .safe_add(quote_asset_amount)?; - if order.get_base_asset_amount_unfilled(None)? == 0 { + let is_filled = order.get_base_asset_amount_unfilled(None)? == 0; + if is_filled { order.status = OrderStatus::Filled; } - Ok(()) + Ok(is_filled) } #[allow(clippy::type_complexity)] @@ -3021,7 +3129,9 @@ pub fn trigger_order( .historical_oracle_data .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, - 0, + perp_market.amm.oracle_slot_delay_override, + perp_market.amm.oracle_low_risk_slot_delay_override, + None, )?; let is_oracle_valid = @@ -3133,6 +3243,8 @@ pub fn trigger_order( None, None, Some(trigger_price), + None, + None, )?; emit!(order_action_record); @@ -3359,6 +3471,22 @@ pub fn can_reward_user_with_perp_pnl(user: &mut Option<&mut User>, market_index: } } +pub fn can_reward_user_with_referral_reward( + user: &mut Option<&mut User>, + market_index: u16, + rev_share_escrow: &mut Option<&mut RevenueShareEscrowZeroCopyMut>, + builder_referral_feature_enabled: bool, +) -> bool { + if builder_referral_feature_enabled { + if let Some(escrow) = rev_share_escrow { + return escrow.find_or_create_referral_index(market_index).is_some(); + } + false + } else { + can_reward_user_with_perp_pnl(user, market_index) + } +} + pub fn pay_keeper_flat_reward_for_perps( user: &mut User, filler: Option<&mut User>, @@ -3715,6 +3843,8 @@ pub fn place_spot_order( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -4790,6 +4920,7 @@ pub fn fulfill_spot_order_with_match( &MarketType::Spot, base_market.fee_adjustment, false, + None, )?; // Update taker state @@ -4957,6 +5088,8 @@ pub fn fulfill_spot_order_with_match( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5231,6 +5364,8 @@ pub fn fulfill_spot_order_with_external_market( None, None, None, + None, + None, )?; emit_stack::<_, { OrderActionRecord::SIZE }>(order_action_record)?; @@ -5313,6 +5448,8 @@ pub fn trigger_spot_order( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, + None, )?; let strict_oracle_price = StrictOraclePrice { current: oracle_price_data.price, @@ -5435,6 +5572,8 @@ pub fn trigger_spot_order( None, None, Some(oracle_price.unsigned_abs()), + None, + None, )?; emit!(order_action_record); diff --git a/programs/drift/src/controller/orders/amm_jit_tests.rs b/programs/drift/src/controller/orders/amm_jit_tests.rs index 6d8ff77eef..87c160b78d 100644 --- a/programs/drift/src/controller/orders/amm_jit_tests.rs +++ b/programs/drift/src/controller/orders/amm_jit_tests.rs @@ -2,8 +2,14 @@ use anchor_lang::prelude::Pubkey; use anchor_lang::Owner; use crate::math::constants::ONE_BPS_DENOMINATOR; +use crate::math::oracle; +use crate::math::oracle::oracle_validity; +use crate::state::fill_mode::FillMode; use crate::state::oracle_map::OracleMap; +use crate::state::perp_market::PerpMarket; +use crate::state::state::State; use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::user::MarketType; use crate::state::user::{Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -25,6 +31,53 @@ fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { (Pubkey::default(), Pubkey::default(), Pubkey::default()) } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = oracle_validity( + MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + #[cfg(test)] pub mod amm_jit { use std::str::FromStr; @@ -41,6 +94,7 @@ pub mod amm_jit { SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::math::constants::{CONCENTRATION_PRECISION, PRICE_PRECISION_U64}; + use crate::state::fill_mode::FillMode; use crate::state::oracle::{HistoricalOracleData, OracleSource}; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; @@ -276,9 +330,21 @@ pub mod amm_jit { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -296,10 +362,11 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -466,9 +533,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -486,10 +565,11 @@ pub mod amm_jit { Some(PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -503,7 +583,10 @@ pub mod amm_jit { // nets to zero let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, 0); + assert_eq!( + market_after.amm.base_asset_amount_with_amm, + BASE_PRECISION_I128 / 2 + ); // make sure lps didnt get anything assert_eq!(market_after.amm.base_asset_amount_per_lp, 0); @@ -664,9 +747,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -684,10 +779,11 @@ pub mod amm_jit { Some(PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -861,9 +957,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -881,10 +989,11 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1058,11 +1167,24 @@ pub mod amm_jit { }; create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1080,10 +1202,11 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1268,9 +1391,21 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1288,10 +1423,11 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1494,10 +1630,11 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::Unavailable, + false, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1674,9 +1811,21 @@ pub mod amm_jit { let reserve_price_before = market.amm.reserve_price().unwrap(); assert_eq!(reserve_price_before, 100 * PRICE_PRECISION_U64); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1694,10 +1843,11 @@ pub mod amm_jit { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -1861,10 +2011,22 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + // fulfill with match let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -1882,10 +2044,11 @@ pub mod amm_jit { Some(1), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2061,10 +2224,22 @@ pub mod amm_jit { assert_eq!(market.amm.total_mm_fee, 0); assert_eq!(market.amm.total_fee_withdrawn, 0); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + // fulfill with match let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2082,10 +2257,11 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2312,10 +2488,11 @@ pub mod amm_jit { create_anchor_account_info!(maker, &maker_key, User, maker_account_info); let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); + let order_index = 0; // fulfill with match fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2333,10 +2510,11 @@ pub mod amm_jit { Some(1), now, slot, - auction_duration, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + false, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2597,9 +2775,23 @@ pub mod amm_jit { let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); // fulfill with match + + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = + taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2617,10 +2809,11 @@ pub mod amm_jit { Some(200 * PRICE_PRECISION_I64), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -2826,9 +3019,22 @@ pub mod amm_jit { assert_eq!(taker.perp_positions[0].open_orders, 1); // fulfill with match + + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -2846,10 +3052,11 @@ pub mod amm_jit { Some(1), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs b/programs/drift/src/controller/orders/amm_lp_jit_tests.rs deleted file mode 100644 index 550574a1c7..0000000000 --- a/programs/drift/src/controller/orders/amm_lp_jit_tests.rs +++ /dev/null @@ -1,2496 +0,0 @@ -use anchor_lang::prelude::Pubkey; -use anchor_lang::Owner; - -use crate::math::constants::ONE_BPS_DENOMINATOR; -use crate::state::oracle_map::OracleMap; -use crate::state::state::{FeeStructure, FeeTier}; -use crate::state::user::{Order, PerpPosition}; - -fn get_fee_structure() -> FeeStructure { - let mut fee_tiers = [FeeTier::default(); 10]; - fee_tiers[0] = FeeTier { - fee_numerator: 5, - fee_denominator: ONE_BPS_DENOMINATOR, - maker_rebate_numerator: 3, - maker_rebate_denominator: ONE_BPS_DENOMINATOR, - ..FeeTier::default() - }; - FeeStructure { - fee_tiers, - ..FeeStructure::test_default() - } -} - -fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { - (Pubkey::default(), Pubkey::default(), Pubkey::default()) -} - -#[cfg(test)] -pub mod amm_lp_jit { - use std::str::FromStr; - - use crate::controller::orders::fulfill_perp_order; - use crate::controller::position::PositionDirection; - use crate::create_account_info; - use crate::create_anchor_account_info; - use crate::math::constants::{ - PERCENTAGE_PRECISION_I128, PRICE_PRECISION_I64, QUOTE_PRECISION_I64, - }; - - use crate::math::amm_jit::calculate_amm_jit_liquidity; - use crate::math::amm_spread::calculate_inventory_liquidity_ratio; - use crate::math::constants::{ - AMM_RESERVE_PRECISION, BASE_PRECISION_I128, BASE_PRECISION_I64, BASE_PRECISION_U64, - PEG_PRECISION, PRICE_PRECISION, SPOT_BALANCE_PRECISION_U64, - SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, - }; - use crate::math::constants::{CONCENTRATION_PRECISION, PRICE_PRECISION_U64}; - use crate::state::fill_mode::FillMode; - use crate::state::oracle::{HistoricalOracleData, OracleSource}; - use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; - use crate::state::perp_market_map::PerpMarketMap; - use crate::state::spot_market::{SpotBalanceType, SpotMarket}; - use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{OrderStatus, OrderType, SpotPosition, User, UserStats}; - use crate::state::user_map::{UserMap, UserStatsMap}; - use crate::test_utils::*; - use crate::test_utils::{get_orders, get_positions, get_pyth_price, get_spot_positions}; - use crate::validation::perp_market::validate_perp_market; - - use super::*; - - #[test] - fn max_jit_amounts() { - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -1000000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 2000, - short_spread: 2000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99_920_000, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 300000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.long_spread = 11000; - market.amm.short_spread = 11000; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 99 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 45454000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 101 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 45454000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 102 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 104 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.short_spread = 20000; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 104 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 105 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - - market.amm.long_spread = 51000; - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 105 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 9803000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 95 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 0); - } - - #[test] - fn zero_asks_with_amm_lp_jit_taker_long() { - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -1000000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.base_asset_reserve = 0; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Short, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = crate::math::amm_jit::calculate_jit_base_asset_amount( - &market, - BASE_PRECISION_U64, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - PositionDirection::Long, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - - let jit_base_asset_amount = calculate_amm_jit_liquidity( - &mut market, - PositionDirection::Short, - 100 * PRICE_PRECISION_U64, - Some(100 * PRICE_PRECISION_I64), - BASE_PRECISION_U64, - BASE_PRECISION_U64, - BASE_PRECISION_U64, - false, - ) - .unwrap(); - assert_eq!(jit_base_asset_amount, 500000000); - } - - #[test] - fn no_fulfill_with_amm_lp_jit_taker_long() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(21, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (21 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (21 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (21 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - user_lp_shares: 10 * AMM_RESERVE_PRECISION, // some lps exist - concentration_coef: CONCENTRATION_PRECISION + 1, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Active, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - assert_eq!(new_bid_quote_asset_reserve, 99000000000); - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - price: 100 * PRICE_PRECISION_U64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - - let mut filler_stats = UserStats::default(); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - // amm jit doesnt take anything - assert_eq!( - market_after.amm.base_asset_amount_with_amm, - market.amm.base_asset_amount_with_amm - ); - assert_eq!( - market_after.amm.base_asset_amount_per_lp, - market.amm.base_asset_amount_per_lp - ); - assert_eq!( - market_after.amm.quote_asset_amount_per_lp, - market.amm.quote_asset_amount_per_lp - ); - assert_eq!(market_after.amm.total_fee_minus_distributions, 7500); - assert_eq!(market_after.amm.total_exchange_fee, 7500); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short_max_amount() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let taker_mul: i64 = 20; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 * taker_mul as u64, // if amm takes half it would flip - slot: 0, - price: 100 * PRICE_PRECISION_U64, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 * taker_mul, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64 * taker_mul as u64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 * taker_mul as u64, // maker wants full = amm wants BASE_PERCISION - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 * taker_mul, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64 * taker_mul as u64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - // nets to zero - assert_eq!(market_after.amm.base_asset_amount_with_amm, 0); - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - // maker got (full - net_baa) - assert_eq!( - maker_position.base_asset_amount as i128, - BASE_PRECISION_I128 * taker_mul as i128 - market.amm.base_asset_amount_with_amm - ); - } - - #[test] - fn no_fulfill_with_amm_lp_jit_taker_short() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // amm is short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - // bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - // bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - // ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - // ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - base_spread: 20000, - long_spread: 20000, - short_spread: 20000, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - assert_eq!(new_bid_quote_asset_reserve, 99000000000); - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, // doesnt improve balance - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64); - - let taker_position = &taker.perp_positions[0]; - assert_eq!(taker_position.base_asset_amount, -BASE_PRECISION_I64); - assert!(taker.orders[0].is_available()); - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - assert_eq!(maker_position.base_asset_amount, BASE_PRECISION_I64 / 2); - assert_eq!(maker_position.quote_asset_amount, -49985000); - assert_eq!(maker_position.quote_entry_amount, -50 * QUOTE_PRECISION_I64); - assert_eq!(maker_position.quote_break_even_amount, -49985000); - assert_eq!(maker_position.open_orders, 0); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, -1000000000); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - base_spread: 250, - long_spread: 125, - short_spread: 125, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - // base is filled - assert!(base_asset_amount > 0); - - let market_after = market_map.get_ref(&0).unwrap(); - assert!( - market_after.amm.base_asset_amount_with_amm.abs() - < market.amm.base_asset_amount_with_amm.abs() - ); - - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus > 0); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_long() { - let now = 0_i64; - let slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 99 * PRICE_PRECISION_I64, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 100 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - let reserve_price_before = market.amm.reserve_price().unwrap(); - assert_eq!(reserve_price_before, 100 * PRICE_PRECISION_U64); - - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 100 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(market.amm.historical_oracle_data.last_oracle_price), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - // net baa improves - let market_after = market_map.get_ref(&0).unwrap(); - assert!( - market_after.amm.base_asset_amount_with_amm.abs() - < market.amm.base_asset_amount_with_amm.abs() - ); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_long_neg_qas() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: -((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_short: -((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 50, // !! amm will bid before the ask spread price - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 10 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 10 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64 * 3 / 4); // auctions not over so no amm fill - - let maker = makers_and_referrers.get_ref_mut(&maker_key).unwrap(); - let maker_position = &maker.perp_positions[0]; - assert_eq!(maker_position.base_asset_amount, -BASE_PRECISION_I64 / 2); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.base_asset_amount_with_amm, -250000000); - - let taker_position = &taker.perp_positions[0]; - assert_eq!( - taker_position.base_asset_amount, - BASE_PRECISION_I64 + market_after.amm.base_asset_amount_with_amm as i64 - ); - - // market pays extra for trade - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus < 0); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_short_neg_qas() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: (AMM_RESERVE_PRECISION / 2) as i128, - base_asset_amount_long: (AMM_RESERVE_PRECISION / 2) as i128, - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 100, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_end_price: 0, - auction_start_price: 200 * PRICE_PRECISION as i64, - auction_duration: 50, // !! amm will bid before the ask spread price - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 200 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 200 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - assert_eq!(base_asset_amount, BASE_PRECISION_U64 * 3 / 4); // auctions not over so no amm fill - - let market_after = market_map.get_ref(&0).unwrap(); - - let taker_position = &taker.perp_positions[0]; - assert_eq!( - taker_position.base_asset_amount, - -3 * BASE_PRECISION_I64 / 4 - ); - - // mm gains from trade - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - market.amm.total_mm_fee; - assert!(quote_asset_amount_surplus < 0); - } - - #[allow(clippy::comparison_chain)] - #[test] - fn fulfill_with_amm_lp_jit_full_long() { - let now = 0_i64; - let mut slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let reserves = 5 * AMM_RESERVE_PRECISION; - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: reserves, - quote_asset_reserve: reserves, - base_asset_amount_with_amm: -(100 * AMM_RESERVE_PRECISION as i128), - base_asset_amount_short: -(100 * AMM_RESERVE_PRECISION as i128), - sqrt_k: reserves, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1000, - order_tick_size: 1, - oracle: oracle_price_key, - base_spread: 5000, - max_spread: 1000000, - long_spread: 50000, - short_spread: 50000, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let auction_duration = 50; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: 100 * BASE_PRECISION_U64, - slot: 0, - auction_duration, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: -100 * BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let auction_start_price = 95062500_i64; - let auction_end_price = 132154089_i64; - taker.orders[0].auction_start_price = auction_start_price; - taker.orders[0].auction_end_price = auction_end_price; - println!("start stop {} {}", auction_start_price, auction_end_price); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (mut neg, mut pos, mut none) = (false, false, false); - let mut prev_mm_fee = 0; - let mut prev_net_baa = market.amm.base_asset_amount_with_amm; - // track scaling - let mut prev_qas = 0; - let mut has_set_prev_qas = false; - loop { - println!("------"); - - // compute auction price - let is_complete = crate::math::auction::is_auction_complete( - taker.orders[0].slot, - auction_duration, - slot, - ) - .unwrap(); - if is_complete { - break; - } - - let auction_price = crate::math::auction::calculate_auction_price( - &taker.orders[0], - slot, - 1, - None, - false, - ) - .unwrap(); - let baa = market.amm.order_step_size * 4; - - let (mark, ask, bid) = { - let market = market_map.get_ref(&0).unwrap(); - let mark = market.amm.reserve_price().unwrap(); - let ask = market.amm.ask_price(mark).unwrap(); - let bid = market.amm.bid_price(mark).unwrap(); - (mark, ask, bid) - }; - println!("mark: {} bid ask: {} {}", mark, bid, ask); - - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: baa, - price: auction_price, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -(baa as i64), - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - // fulfill with match - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, auction_price)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - auction_duration, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - prev_mm_fee; - prev_mm_fee = market_after.amm.total_mm_fee; - - // imbalance decreases - assert!(market_after.amm.base_asset_amount_with_amm.abs() < prev_net_baa.abs()); - prev_net_baa = market_after.amm.base_asset_amount_with_amm; - - println!( - "slot {} auction: {} surplus: {}", - slot, auction_price, quote_asset_amount_surplus - ); - - if !has_set_prev_qas { - prev_qas = quote_asset_amount_surplus; - has_set_prev_qas = true; - } else { - // decreasing (amm paying less / earning more) - assert!(prev_qas < quote_asset_amount_surplus); - prev_qas = quote_asset_amount_surplus; - } - - if quote_asset_amount_surplus < 0 { - neg = true; - assert!(!pos); // neg first - } else if quote_asset_amount_surplus > 0 { - pos = true; - assert!(neg); // neg first - // sometimes skips over == 0 surplus - } else { - none = true; - assert!(neg); - assert!(!pos); - } - slot += 1; - } - // auction should go through both position and negative - assert!(neg); - assert!(pos); - // assert!(none); //todo: skips over this (-1 -> 1) - - println!("{} {} {}", neg, pos, none); - } - - #[allow(clippy::comparison_chain)] - #[test] - fn fulfill_with_amm_lp_jit_full_short() { - let now = 0_i64; - let mut slot = 0_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let reserves = 5 * AMM_RESERVE_PRECISION; - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: reserves, - quote_asset_reserve: reserves, - base_asset_amount_with_amm: 100 * AMM_RESERVE_PRECISION as i128, - base_asset_amount_long: 100 * AMM_RESERVE_PRECISION as i128, - sqrt_k: reserves, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 1, - order_tick_size: 1, - oracle: oracle_price_key, - base_spread: 5000, - max_spread: 1000000, - long_spread: 50000, - short_spread: 50000, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - let (new_ask_base_asset_reserve, new_ask_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Long) - .unwrap(); - let (new_bid_base_asset_reserve, new_bid_quote_asset_reserve) = - crate::math::amm_spread::calculate_spread_reserves(&market, PositionDirection::Short) - .unwrap(); - market.amm.ask_base_asset_reserve = new_ask_base_asset_reserve; - market.amm.bid_base_asset_reserve = new_bid_base_asset_reserve; - market.amm.ask_quote_asset_reserve = new_ask_quote_asset_reserve; - market.amm.bid_quote_asset_reserve = new_bid_quote_asset_reserve; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let auction_duration = 50; - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Short, - base_asset_amount: 100 * BASE_PRECISION_U64, - slot: 0, - auction_duration, // !! amm will bid before the ask spread price - auction_end_price: 0, - auction_start_price: 200 * PRICE_PRECISION as i64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -100 * BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - - let auction_start_price = 105062500; - let auction_end_price = 79550209; - taker.orders[0].auction_start_price = auction_start_price; - taker.orders[0].auction_end_price = auction_end_price; - println!("start stop {} {}", auction_start_price, auction_end_price); - - let mut filler = User::default(); - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - - let (mut neg, mut pos, mut none) = (false, false, false); - let mut prev_mm_fee = 0; - let mut prev_net_baa = market.amm.base_asset_amount_with_amm; - // track scaling - let mut prev_qas = 0; - let mut has_set_prev_qas = false; - - loop { - println!("------"); - - // compute auction price - let is_complete = crate::math::auction::is_auction_complete( - taker.orders[0].slot, - auction_duration, - slot, - ) - .unwrap(); - - if is_complete { - break; - } - - let auction_price = crate::math::auction::calculate_auction_price( - &taker.orders[0], - slot, - 1, - None, - false, - ) - .unwrap(); - let baa = 1000 * 4; - - let (mark, ask, bid) = { - let market = market_map.get_ref(&0).unwrap(); - let mark = market.amm.reserve_price().unwrap(); - let ask = market.amm.ask_price(mark).unwrap(); - let bid = market.amm.bid_price(mark).unwrap(); - (mark, ask, bid) - }; - println!("mark: {} bid ask: {} {}", mark, bid, ask); - - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Long, - base_asset_amount: baa as u64, - price: auction_price, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: baa as i64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - // fulfill with match - fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, auction_price)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(200 * PRICE_PRECISION_I64), - now, - slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - let market_after = market_map.get_ref(&0).unwrap(); - let quote_asset_amount_surplus = market_after.amm.total_mm_fee - prev_mm_fee; - prev_mm_fee = market_after.amm.total_mm_fee; - - // imbalance decreases or remains the same (damm wont always take on positions) - assert!(market_after.amm.base_asset_amount_with_amm.abs() <= prev_net_baa.abs()); - prev_net_baa = market_after.amm.base_asset_amount_with_amm; - - println!( - "slot {} auction: {} surplus: {}", - slot, auction_price, quote_asset_amount_surplus - ); - - if !has_set_prev_qas { - prev_qas = quote_asset_amount_surplus; - has_set_prev_qas = true; - } else { - // decreasing (amm paying less / earning more) - assert!(prev_qas <= quote_asset_amount_surplus); - prev_qas = quote_asset_amount_surplus; - } - - if quote_asset_amount_surplus < 0 { - neg = true; - assert!(!pos); // neg first - } else if quote_asset_amount_surplus > 0 { - pos = true; - assert!(neg); // neg first - // sometimes skips over == 0 surplus - } else { - none = true; - assert!(neg); - assert!(!pos); - } - slot += 1; - } - // auction should go through both position and negative - assert!(neg); - assert!(pos); - assert!(none); - - println!("{} {} {}", neg, pos, none); - } - - #[test] - fn fulfill_with_amm_lp_jit_taker_zero_price_long_imbalance() { - let now = 0_i64; - let slot = 10_u64; - - let mut oracle_price = get_pyth_price(100, 6); - let oracle_price_key = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let pyth_program = crate::ids::pyth_program::id(); - create_account_info!( - oracle_price, - &oracle_price_key, - &pyth_program, - oracle_account_info - ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); - - // net users are short - let mut market = PerpMarket { - amm: AMM { - base_asset_reserve: 100 * AMM_RESERVE_PRECISION, - quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, - base_asset_amount_per_lp: -505801343, - quote_asset_amount_per_lp: 10715933, - target_base_asset_amount_per_lp: -500000000, - bid_base_asset_reserve: 101 * AMM_RESERVE_PRECISION, - bid_quote_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_base_asset_reserve: 99 * AMM_RESERVE_PRECISION, - ask_quote_asset_reserve: 101 * AMM_RESERVE_PRECISION, - base_asset_amount_with_amm: ((AMM_RESERVE_PRECISION / 2) as i128), - base_asset_amount_long: ((AMM_RESERVE_PRECISION / 2) as i128), - sqrt_k: 100 * AMM_RESERVE_PRECISION, - peg_multiplier: 100 * PEG_PRECISION, - max_slippage_ratio: 50, - max_fill_reserve_fraction: 100, - order_step_size: 10000000, - order_tick_size: 1, - oracle: oracle_price_key, - amm_jit_intensity: 200, - historical_oracle_data: HistoricalOracleData { - last_oracle_price: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, - last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, - - ..HistoricalOracleData::default() - }, - - ..AMM::default() - }, - margin_ratio_initial: 1000, - margin_ratio_maintenance: 500, - status: MarketStatus::Initialized, - ..PerpMarket::default_test() - }; - market.amm.max_base_asset_reserve = u64::MAX as u128; - market.amm.min_base_asset_reserve = 0; - - create_anchor_account_info!(market, PerpMarket, market_account_info); - let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); - - let mut spot_market = SpotMarket { - market_index: 0, - oracle_source: OracleSource::QuoteAsset, - cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, - decimals: 6, - initial_asset_weight: SPOT_WEIGHT_PRECISION, - maintenance_asset_weight: SPOT_WEIGHT_PRECISION, - historical_oracle_data: HistoricalOracleData::default_price(QUOTE_PRECISION_I64), - ..SpotMarket::default() - }; - create_anchor_account_info!(spot_market, SpotMarket, spot_market_account_info); - let spot_market_map = SpotMarketMap::load_one(&spot_market_account_info, true).unwrap(); - - // taker wants to go long (would improve balance) - let mut taker = User { - orders: get_orders(Order { - market_index: 0, - status: OrderStatus::Open, - order_type: OrderType::Market, - direction: PositionDirection::Long, - base_asset_amount: BASE_PRECISION_U64, - slot: 0, - auction_start_price: 0, - auction_end_price: 100 * PRICE_PRECISION_I64, - auction_duration: 0, // expired auction - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_bids: BASE_PRECISION_I64, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - next_order_id: 1, - ..User::default() - }; - - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - let mut maker = User { - authority: maker_authority, - orders: get_orders(Order { - market_index: 0, - post_only: true, - order_type: OrderType::Limit, - direction: PositionDirection::Short, - base_asset_amount: BASE_PRECISION_U64 / 2, - price: 10 * PRICE_PRECISION_U64, - ..Order::default() - }), - perp_positions: get_positions(PerpPosition { - market_index: 0, - open_orders: 1, - open_asks: -BASE_PRECISION_I64 / 2, - ..PerpPosition::default() - }), - spot_positions: get_spot_positions(SpotPosition { - market_index: 0, - balance_type: SpotBalanceType::Deposit, - scaled_balance: 100 * 100 * SPOT_BALANCE_PRECISION_U64, - ..SpotPosition::default() - }), - ..User::default() - }; - create_anchor_account_info!(maker, &maker_key, User, maker_account_info); - let makers_and_referrers = UserMap::load_one(&maker_account_info).unwrap(); - - let mut filler = User::default(); - - let fee_structure = get_fee_structure(); - - let (taker_key, _, filler_key) = get_user_keys(); - let maker_key = Pubkey::from_str("My11111111111111111111111111111111111111113").unwrap(); - let maker_authority = - Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); - - let mut taker_stats = UserStats::default(); - - let mut maker_stats = UserStats { - authority: maker_authority, - ..UserStats::default() - }; - create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); - let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); - let mut filler_stats = UserStats::default(); - - assert_eq!(market.amm.total_fee, 0); - assert_eq!(market.amm.total_fee_minus_distributions, 0); - assert_eq!(market.amm.net_revenue_since_last_funding, 0); - assert_eq!(market.amm.total_mm_fee, 0); - assert_eq!(market.amm.total_fee_withdrawn, 0); - assert_eq!(taker.perp_positions[0].open_orders, 1); - - // fulfill with match - let (base_asset_amount, _) = fulfill_perp_order( - &mut taker, - 0, - &taker_key, - &mut taker_stats, - &makers_and_referrers, - &maker_and_referrer_stats, - &[(maker_key, 0, 10 * PRICE_PRECISION_U64)], - &mut Some(&mut filler), - &filler_key, - &mut Some(&mut filler_stats), - None, - &spot_market_map, - &market_map, - &mut oracle_map, - &fee_structure, - 0, - Some(1), - now, - slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - FillMode::Fill, - false, - ) - .unwrap(); - - assert_eq!(taker.perp_positions[0].open_orders, 0); - assert_eq!(base_asset_amount, 1000000000); - - let market_after = market_map.get_ref(&0).unwrap(); - assert_eq!(market_after.amm.total_mm_fee, 2033008); // jit occured even tho maker offered full amount - assert_eq!(market_after.amm.total_fee, 2057287); - } -} diff --git a/programs/drift/src/controller/orders/fuel_tests.rs b/programs/drift/src/controller/orders/fuel_tests.rs index f29b54addd..24959f9e3b 100644 --- a/programs/drift/src/controller/orders/fuel_tests.rs +++ b/programs/drift/src/controller/orders/fuel_tests.rs @@ -5,7 +5,7 @@ use crate::math::constants::ONE_BPS_DENOMINATOR; use crate::math::margin::MarginRequirementType; use crate::state::margin_calculation::{MarginCalculation, MarginContext}; use crate::state::oracle_map::OracleMap; -use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::state::{FeeStructure, FeeTier, State}; use crate::state::user::{Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -27,6 +27,53 @@ fn get_user_keys() -> (Pubkey, Pubkey, Pubkey) { (Pubkey::default(), Pubkey::default(), Pubkey::default()) } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &crate::state::perp_market::PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = crate::math::oracle::oracle_validity( + crate::state::user::MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + crate::math::oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + crate::state::fill_mode::FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + #[cfg(test)] pub mod fuel_scoring { use std::str::FromStr; @@ -221,9 +268,21 @@ pub mod fuel_scoring { assert_eq!(maker_stats.fuel_deposits, 0); assert_eq!(taker_stats.fuel_deposits, 0); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (ba, qa) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -241,10 +300,11 @@ pub mod fuel_scoring { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); diff --git a/programs/drift/src/controller/orders/tests.rs b/programs/drift/src/controller/orders/tests.rs index b5e681f090..5a51991cba 100644 --- a/programs/drift/src/controller/orders/tests.rs +++ b/programs/drift/src/controller/orders/tests.rs @@ -1,9 +1,11 @@ use anchor_lang::prelude::Pubkey; use anchor_lang::Owner; +use crate::math::oracle::oracle_validity; +use crate::state::fill_mode::FillMode; use crate::state::oracle_map::OracleMap; -use crate::state::perp_market::MarketStatus; -use crate::state::state::{FeeStructure, FeeTier}; +use crate::state::perp_market::{MarketStatus, PerpMarket}; +use crate::state::state::{FeeStructure, FeeTier, State}; use crate::state::user::{MarketType, Order, PerpPosition}; fn get_fee_structure() -> FeeStructure { @@ -29,6 +31,53 @@ fn get_oracle_map<'a>() -> OracleMap<'a> { OracleMap::empty() } +fn get_state(min_auction_duration: u8) -> State { + State { + min_perp_auction_duration: min_auction_duration, + ..State::default() + } +} + +pub fn get_amm_is_available( + order: &Order, + min_auction_duration: u8, + market: &PerpMarket, + oracle_map: &mut OracleMap, + slot: u64, + user_can_skip_auction_duration: bool, +) -> bool { + let state = get_state(min_auction_duration); + let oracle_price_data = oracle_map.get_price_data(&market.oracle_id()).unwrap(); + let mm_oracle_price_data = market + .get_mm_oracle_price_data(*oracle_price_data, slot, &state.oracle_guard_rails.validity) + .unwrap(); + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let safe_oracle_validity = oracle_validity( + MarketType::Perp, + market.market_index, + market.amm.historical_oracle_data.last_oracle_price_twap, + &safe_oracle_price_data, + &state.oracle_guard_rails.validity, + market.get_max_confidence_interval_multiplier().unwrap(), + &market.amm.oracle_source, + crate::math::oracle::LogMode::SafeMMOracle, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + ) + .unwrap(); + market + .amm_can_fill_order( + order, + slot, + FillMode::Fill, + &state, + safe_oracle_validity, + user_can_skip_auction_duration, + &mm_oracle_price_data, + ) + .unwrap() +} + pub mod fill_order_protected_maker { use std::str::FromStr; @@ -264,6 +313,8 @@ pub mod fill_order_protected_maker { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -373,6 +424,8 @@ pub mod fill_order_protected_maker { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -486,6 +539,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -609,6 +664,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -732,6 +789,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -855,6 +914,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -977,6 +1038,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1066,6 +1129,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1156,6 +1221,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1246,6 +1313,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1336,6 +1405,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1446,6 +1517,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1561,6 +1634,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1681,6 +1756,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1802,6 +1879,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -1947,6 +2026,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2067,6 +2148,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2145,6 +2228,8 @@ pub mod fulfill_order_with_maker_order { market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier().unwrap(), 0, + 0, + None, ) .unwrap(); @@ -2197,6 +2282,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2348,6 +2435,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2497,6 +2586,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2647,6 +2738,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut oracle_map, false, + &mut None, + false, ) .unwrap(); @@ -2778,6 +2871,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -2908,6 +3003,8 @@ pub mod fulfill_order_with_maker_order { &fee_structure, &mut get_oracle_map(), false, + &mut None, + false, ) .unwrap(); @@ -3268,9 +3365,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3292,10 +3401,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3513,9 +3623,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3536,10 +3658,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3706,9 +3829,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3726,10 +3861,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -3912,9 +4048,21 @@ pub mod fulfill_order { create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -3932,10 +4080,11 @@ pub mod fulfill_order { None, now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4078,9 +4227,21 @@ pub mod fulfill_order { let mut taker_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &UserMap::empty(), @@ -4098,10 +4259,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4276,9 +4438,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let result = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4296,10 +4470,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ); assert!(result.is_ok()); @@ -4463,9 +4638,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let result = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4483,10 +4670,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ); assert_eq!(result, Err(ErrorCode::InsufficientCollateral)); @@ -4603,9 +4791,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4623,10 +4823,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::Immediate, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4770,9 +4971,21 @@ pub mod fulfill_order { let mut filler_stats = UserStats::default(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market, + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -4790,10 +5003,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -4970,6 +5184,8 @@ pub mod fulfill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -4995,6 +5211,8 @@ pub mod fulfill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -5144,6 +5362,8 @@ pub mod fulfill_order { // slot, // false, // true, + // &mut None, + // false, // ) // .unwrap(); // @@ -5353,9 +5573,22 @@ pub mod fulfill_order { let taker_before = taker; let maker_before = maker; + + let order_index = 0; + let min_auction_duration = 10; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market_map.get_ref(&0).unwrap(), + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -5373,10 +5606,11 @@ pub mod fulfill_order { None, now, slot, - 10, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -5597,9 +5831,21 @@ pub mod fulfill_order { create_anchor_account_info!(maker_stats, UserStats, maker_stats_account_info); let maker_and_referrer_stats = UserStatsMap::load_one(&maker_stats_account_info).unwrap(); + let order_index = 0; + let min_auction_duration = 0; + let user_can_skip_auction_duration = taker.can_skip_auction_duration(&taker_stats).unwrap(); + let is_amm_available = get_amm_is_available( + &taker.orders[order_index], + min_auction_duration, + &market_map.get_ref(&0).unwrap(), + &mut oracle_map, + slot, + user_can_skip_auction_duration, + ); + let (base_asset_amount, _) = fulfill_perp_order( &mut taker, - 0, + order_index, &taker_key, &mut taker_stats, &makers_and_referrers, @@ -5617,10 +5863,11 @@ pub mod fulfill_order { Some(market.amm.historical_oracle_data.last_oracle_price), now, slot, - 0, - crate::state::perp_market::AMMAvailability::AfterMinDuration, + is_amm_available, FillMode::Fill, false, + &mut None, + false, ) .unwrap(); @@ -5878,6 +6125,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6080,6 +6329,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6209,6 +6460,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ) .unwrap(); @@ -6371,6 +6624,8 @@ pub mod fill_order { None, &clock, FillMode::Fill, + &mut None, + false, ); assert_eq!(err, Err(ErrorCode::MaxOpenInterest)); @@ -12799,3 +13054,58 @@ mod update_maker_fills_map { assert_eq!(*map.get(&maker_key).unwrap(), -2 * fill as i64); } } + +mod order_is_low_risk_for_amm { + use super::*; + use crate::state::user::{OrderBitFlag, OrderStatus}; + + fn base_perp_order() -> Order { + Order { + status: OrderStatus::Open, + market_type: MarketType::Perp, + slot: 100, + ..Order::default() + } + } + + #[test] + fn older_than_oracle_delay_returns_true() { + let order = base_perp_order(); + let clock_slot = 110u64; + let mm_oracle_delay = 10i64; + + let is_low = order + .is_low_risk_for_amm(mm_oracle_delay, clock_slot, false) + .unwrap(); + assert!(is_low); + } + + #[test] + fn not_older_than_delay_returns_false() { + let order = base_perp_order(); + let clock_slot = 110u64; + + let mm_oracle_delay = 11i64; + + let is_low = order + .is_low_risk_for_amm(mm_oracle_delay, clock_slot, false) + .unwrap(); + assert!(!is_low); + } + + #[test] + fn liquidation_always_low_risk() { + let order = base_perp_order(); + let is_low = order.is_low_risk_for_amm(0, order.slot, true).unwrap(); + assert!(is_low); + } + + #[test] + fn safe_trigger_order_flag_sets_low_risk() { + let mut order = base_perp_order(); + order.add_bit_flag(OrderBitFlag::SafeTriggerOrder); + + let is_low = order.is_low_risk_for_amm(0, order.slot, false).unwrap(); + assert!(is_low); + } +} diff --git a/programs/drift/src/controller/pnl.rs b/programs/drift/src/controller/pnl.rs index dd199eba9a..34f3af51a3 100644 --- a/programs/drift/src/controller/pnl.rs +++ b/programs/drift/src/controller/pnl.rs @@ -20,7 +20,10 @@ use crate::math::position::calculate_base_asset_value_with_expiry_price; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; +use crate::get_then_update_id; +use crate::math::orders::calculate_existing_position_fields_for_order_action; use crate::msg; +use crate::state::events::{OrderAction, OrderActionRecord, OrderRecord}; use crate::state::events::{OrderActionExplanation, SettlePnlExplanation, SettlePnlRecord}; use crate::state::oracle_map::OracleMap; use crate::state::paused_operations::PerpOperation; @@ -30,7 +33,7 @@ use crate::state::settle_pnl_mode::SettlePnlMode; use crate::state::spot_market::{SpotBalance, SpotBalanceType}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::state::State; -use crate::state::user::{MarketType, User}; +use crate::state::user::{MarketType, Order, OrderStatus, OrderType, User}; use crate::validate; use anchor_lang::prelude::Pubkey; use anchor_lang::prelude::*; @@ -79,7 +82,16 @@ pub fn settle_pnl( drop(market); - let position_index = get_position_index(&user.perp_positions, market_index)?; + let position_index = match get_position_index(&user.perp_positions, market_index) { + Ok(index) => index, + Err(e) => { + return mode.result( + e, + market_index, + &format!("User has no position in market {}", market_index), + ) + } + }; let unrealized_pnl = user.perp_positions[position_index].get_unrealized_pnl(oracle_price)?; // cannot settle negative pnl this way on a user who is in liquidation territory @@ -126,6 +138,8 @@ pub fn settle_pnl( .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, 0, + 0, + None, )?; if !is_oracle_valid_for_action(oracle_validity, Some(DriftAction::SettlePnl))? @@ -381,8 +395,20 @@ pub fn settle_expired_position( ) -> DriftResult { validate!(!user.is_bankrupt(), ErrorCode::UserBankrupt)?; + let position_index = match get_position_index(&user.perp_positions, perp_market_index) { + Ok(index) => index, + Err(_) => { + msg!("User has no position for market {}", perp_market_index); + return Ok(()); + } + }; + + let can_skip_margin_calc = user.perp_positions[position_index].base_asset_amount == 0 + && user.perp_positions[position_index].quote_asset_amount > 0; + // cannot settle pnl this way on a user who is in liquidation territory - if !(meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)?) + if !meets_maintenance_margin_requirement(user, perp_market_map, spot_market_map, oracle_map)? + && !can_skip_margin_calc { return Err(ErrorCode::InsufficientCollateralForSettlingPNL); } @@ -419,14 +445,6 @@ pub fn settle_expired_position( true, )?; - let position_index = match get_position_index(&user.perp_positions, perp_market_index) { - Ok(index) => index, - Err(_) => { - msg!("User has no position for market {}", perp_market_index); - return Ok(()); - } - }; - let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; let perp_market = &mut perp_market_map.get_ref_mut(&perp_market_index)?; validate!( @@ -461,6 +479,11 @@ pub fn settle_expired_position( let base_asset_amount = user.perp_positions[position_index].base_asset_amount; let quote_entry_amount = user.perp_positions[position_index].quote_entry_amount; + let user_position_direction_to_close = + user.perp_positions[position_index].get_direction_to_close(); + let user_existing_position_params_for_order_action = user.perp_positions[position_index] + .get_existing_position_params_for_order_action(user_position_direction_to_close); + let position_delta = PositionDelta { quote_asset_amount: base_asset_value, base_asset_amount: -user.perp_positions[position_index].base_asset_amount, @@ -493,6 +516,80 @@ pub fn settle_expired_position( -pnl_to_settle_with_user.cast()?, )?; + if position_delta.base_asset_amount != 0 { + // get ids for order fills + let user_order_id = get_then_update_id!(user, next_order_id); + let fill_record_id = get_then_update_id!(perp_market, next_fill_record_id); + + let base_asset_amount = position_delta.base_asset_amount; + let user_existing_position_direction = user.perp_positions[position_index].get_direction(); + + let user_order = Order { + slot, + base_asset_amount: base_asset_amount.unsigned_abs(), + order_id: user_order_id, + market_index: perp_market.market_index, + status: OrderStatus::Open, + order_type: OrderType::Market, + market_type: MarketType::Perp, + direction: user_position_direction_to_close, + existing_position_direction: user_existing_position_direction, + ..Order::default() + }; + + emit!(OrderRecord { + ts: now, + user: *user_key, + order: user_order + }); + + let (taker_existing_quote_entry_amount, taker_existing_base_asset_amount) = + calculate_existing_position_fields_for_order_action( + base_asset_amount.unsigned_abs(), + user_existing_position_params_for_order_action, + )?; + + let fill_record = OrderActionRecord { + ts: now, + action: OrderAction::Fill, + action_explanation: OrderActionExplanation::MarketExpired, + market_index: perp_market.market_index, + market_type: MarketType::Perp, + filler: None, + filler_reward: None, + fill_record_id: Some(fill_record_id), + base_asset_amount_filled: Some(base_asset_amount.unsigned_abs()), + quote_asset_amount_filled: Some(base_asset_value.unsigned_abs()), + taker_fee: Some(fee.unsigned_abs()), + maker_fee: None, + referrer_reward: None, + quote_asset_amount_surplus: None, + spot_fulfillment_method_fee: None, + taker: Some(*user_key), + taker_order_id: Some(user_order_id), + taker_order_direction: Some(user_position_direction_to_close), + taker_order_base_asset_amount: Some(base_asset_amount.unsigned_abs()), + taker_order_cumulative_base_asset_amount_filled: Some(base_asset_amount.unsigned_abs()), + taker_order_cumulative_quote_asset_amount_filled: Some(base_asset_value.unsigned_abs()), + maker: None, + maker_order_id: None, + maker_order_direction: None, + maker_order_base_asset_amount: None, + maker_order_cumulative_base_asset_amount_filled: None, + maker_order_cumulative_quote_asset_amount_filled: None, + oracle_price: perp_market.expiry_price, + bit_flags: 0, + taker_existing_quote_entry_amount, + taker_existing_base_asset_amount, + maker_existing_quote_entry_amount: None, + maker_existing_base_asset_amount: None, + trigger_price: None, + builder_idx: None, + builder_fee: None, + }; + emit!(fill_record); + } + update_settled_pnl(user, position_index, pnl_to_settle_with_user.cast()?)?; perp_market.amm.base_asset_amount_with_amm = perp_market diff --git a/programs/drift/src/controller/pnl/delisting.rs b/programs/drift/src/controller/pnl/delisting.rs index 70f81a716b..a2b5c99486 100644 --- a/programs/drift/src/controller/pnl/delisting.rs +++ b/programs/drift/src/controller/pnl/delisting.rs @@ -209,6 +209,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, quote_asset_amount: -(QUOTE_PRECISION_I128 * 50), //longs have $100 cost basis @@ -318,6 +319,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, quote_asset_amount: -(QUOTE_PRECISION_I128 * 10), //longs have $20 cost basis @@ -430,6 +432,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, total_fee_minus_distributions: -(100000 * QUOTE_PRECISION_I128), // down $100k @@ -543,6 +546,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, total_fee_minus_distributions: -(100000 * QUOTE_PRECISION_I128), // down $100k @@ -652,6 +656,7 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, ..HistoricalOracleData::default() }, quote_asset_amount: -(QUOTE_PRECISION_I128 * 10), //longs have $20 cost basis @@ -870,6 +875,8 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() }, quote_asset_amount: (QUOTE_PRECISION_I128 * 10), //longs have -$20 cost basis @@ -1091,6 +1098,8 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() }, quote_asset_amount: (QUOTE_PRECISION_I128 * 20 * 2000), //longs have -$20 cost basis @@ -1294,6 +1303,8 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() }, quote_asset_amount: -(QUOTE_PRECISION_I128 * 20 * 1000 - QUOTE_PRECISION_I128), @@ -1717,6 +1728,8 @@ pub mod delisting_test { amm_jit_intensity: 100, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: (99 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (99 * PRICE_PRECISION) as i64, + ..HistoricalOracleData::default() }, quote_asset_amount: (QUOTE_PRECISION_I128 * 200) diff --git a/programs/drift/src/controller/position/tests.rs b/programs/drift/src/controller/position/tests.rs index 39b72c5890..bfba862702 100644 --- a/programs/drift/src/controller/position/tests.rs +++ b/programs/drift/src/controller/position/tests.rs @@ -6,8 +6,7 @@ use crate::controller::repeg::_update_amm; use crate::math::amm::calculate_market_open_bids_asks; use crate::math::constants::{ - AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_I128, BASE_PRECISION, BASE_PRECISION_I64, - PRICE_PRECISION_I64, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, + BASE_PRECISION, BASE_PRECISION_I64, PRICE_PRECISION_I64, PRICE_PRECISION_U64, SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::math::oracle::OracleValidity; @@ -34,7 +33,6 @@ use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::SpotPosition; use crate::test_utils::get_anchor_account_bytes; use crate::test_utils::get_hardcoded_pyth_price; -use crate::QUOTE_PRECISION_I64; use anchor_lang::prelude::{AccountLoader, Clock}; use anchor_lang::Owner; use solana_program::pubkey::Pubkey; @@ -55,17 +53,7 @@ fn amm_pool_balance_liq_fees_example() { let perp_market_loader: AccountLoader = AccountLoader::try_from(&perp_market_account_info).unwrap(); - let perp_market_map = PerpMarketMap::load_one(&perp_market_account_info, true).unwrap(); - let now = 1725948560; - let clock_slot = 326319440; - let clock = Clock { - unix_timestamp: now, - slot: clock_slot, - ..Clock::default() - }; - - let mut state = State::default(); let mut prelaunch_oracle_price = PrelaunchOracle { price: PRICE_PRECISION_I64, @@ -79,9 +67,8 @@ fn amm_pool_balance_liq_fees_example() { prelaunch_oracle_price, &prelaunch_oracle_price_key, PrelaunchOracle, - oracle_account_info + _oracle_account_info ); - let mut oracle_map = OracleMap::load_one(&oracle_account_info, clock_slot, None).unwrap(); let mut spot_market = SpotMarket { cumulative_deposit_interest: 11425141382, @@ -612,11 +599,11 @@ fn amm_ref_price_decay_tail_test() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -657,7 +644,7 @@ fn amm_ref_price_decay_tail_test() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -690,7 +677,7 @@ fn amm_ref_price_decay_tail_test() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -789,11 +776,11 @@ fn amm_ref_price_offset_decay_logic() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -834,7 +821,7 @@ fn amm_ref_price_offset_decay_logic() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -844,7 +831,7 @@ fn amm_ref_price_offset_decay_logic() { .unwrap(); assert_eq!(perp_market.amm.last_update_slot, clock_slot); assert_eq!(perp_market.amm.last_oracle_valid, true); - assert_eq!(perp_market.amm.reference_price_offset, 7350); + assert_eq!(perp_market.amm.reference_price_offset, 4458); perp_market.amm.last_mark_price_twap_5min = (perp_market .amm @@ -874,7 +861,7 @@ fn amm_ref_price_offset_decay_logic() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -894,28 +881,28 @@ fn amm_ref_price_offset_decay_logic() { assert_eq!( offsets, [ - 7140, 6930, 6720, 6510, 6300, 6090, 6070, 6050, 6030, 6010, 5800, 5590, 5380, 5170, - 4960, 4750, 4540, 4330, 4120, 3910, 3700, 3490, 3280, 3070, 2860, 2650, 2440, 2230, - 2020, 1810, 1620, 1449, 1296, 1158, 1034, 922, 821, 730, 648, 575, 509, 450, 396, 348, - 305, 266, 231, 199, 171, 145, 122, 101, 81, 61, 41, 21, 1, 0, 0, 0 + 4248, 4038, 3828, 3618, 3408, 3198, 3178, 3158, 3138, 3118, 2908, 2698, 2488, 2278, + 2068, 1858, 1664, 1489, 1332, 1190, 1062, 947, 844, 751, 667, 592, 524, 463, 408, 359, + 315, 275, 239, 207, 178, 152, 128, 107, 87, 67, 47, 27, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 ] ); assert_eq!( lspreads, [ - 726, 726, 726, 726, 726, 726, 536, 536, 536, 536, 726, 726, 726, 726, 726, 726, 726, - 726, 726, 726, 726, 726, 726, 726, 726, 726, 726, 726, 726, 726, 706, 687, 669, 654, - 640, 628, 617, 607, 598, 589, 582, 575, 570, 564, 559, 555, 551, 548, 544, 542, 539, - 537, 536, 536, 536, 536, 536, 526, 526, 526 + 726, 726, 726, 726, 726, 726, 536, 536, 536, 536, 726, 726, 726, 726, 726, 726, 710, + 691, 673, 658, 644, 631, 619, 609, 600, 591, 584, 577, 571, 565, 560, 556, 552, 548, + 545, 542, 540, 537, 536, 536, 536, 536, 536, 526, 526, 526, 526, 526, 526, 526, 526, + 526, 526, 526, 526, 526, 526, 526, 526, 526 ] ); assert_eq!( sspreads, [ - 7150, 6940, 6730, 6520, 6310, 6100, 6080, 6060, 6040, 6020, 5810, 5600, 5390, 5180, - 4970, 4760, 4550, 4340, 4130, 3920, 3710, 3500, 3290, 3080, 2870, 2660, 2450, 2240, - 2030, 1820, 1630, 1459, 1306, 1168, 1044, 932, 831, 740, 658, 585, 519, 460, 406, 358, - 315, 276, 241, 209, 181, 155, 132, 111, 91, 71, 51, 31, 11, 10, 10, 10 + 4258, 4048, 3838, 3628, 3418, 3208, 3188, 3168, 3148, 3128, 2918, 2708, 2498, 2288, + 2078, 1868, 1674, 1499, 1342, 1200, 1072, 957, 854, 761, 677, 602, 534, 473, 418, 369, + 325, 285, 249, 217, 188, 162, 138, 117, 97, 77, 57, 37, 17, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 ] ); } @@ -950,6 +937,7 @@ fn amm_negative_ref_price_offset_decay_logic() { assert_eq!(perp_market.amm.last_update_slot, 353317544); perp_market.amm.curve_update_intensity = 200; + perp_market.amm.oracle_slot_delay_override = -1; let max_ref_offset = perp_market.amm.get_max_reference_price_offset().unwrap(); @@ -963,11 +951,11 @@ fn amm_negative_ref_price_offset_decay_logic() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -1008,7 +996,7 @@ fn amm_negative_ref_price_offset_decay_logic() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -1018,7 +1006,7 @@ fn amm_negative_ref_price_offset_decay_logic() { .unwrap(); assert_eq!(perp_market.amm.last_update_slot, clock_slot); assert_eq!(perp_market.amm.last_oracle_valid, true); - assert_eq!(perp_market.amm.reference_price_offset, 7350); + assert_eq!(perp_market.amm.reference_price_offset, 4458); perp_market.amm.last_mark_price_twap_5min = (perp_market .amm @@ -1049,7 +1037,7 @@ fn amm_negative_ref_price_offset_decay_logic() { ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -1069,34 +1057,31 @@ fn amm_negative_ref_price_offset_decay_logic() { assert_eq!( offsets, [ - -7140, -6930, -6720, -6510, -6300, -6090, -6070, -6050, -6030, -6010, -5800, -5590, - -5380, -5170, -4960, -4750, -4540, -4330, -4120, -3910, -3700, -3490, -3280, -3070, - -2860, -2650, -2440, -2230, -2020, -1810, -1600, -1390, -1180, -970, -760, -550, -340, - -130, 0, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, - 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, - 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, - 10000, 10000, 10000, 10000, 10000, 10000 + -4248, -4038, -3828, -3618, -3408, -3198, -3178, -3158, -3138, -3118, -2908, -2698, + -2488, -2278, -2068, -1858, -1648, -1438, -1228, -1018, -808, -598, -388, -178, 0, + 7654, 7652, 7651, 7649, 7648, 7646, 7645, 7643, 7641, 7640, 7638, 7637, 7635, 7634, + 7632, 7631, 7629, 7628, 7626, 7625, 7623, 7622, 7620, 7619, 7618, 7616, 7615, 7613, + 7612, 7610, 7609, 7607, 7606, 7605, 7603, 7602, 7600, 7599, 7597, 7596, 7595, 7593, + 7592, 7591, 7589, 7588, 7586, 7585, 7584, 7582, 7581, 7580, 7578, 7577, 7576 ] ); assert_eq!( sspreads, [ 210, 210, 210, 210, 210, 210, 20, 20, 20, 20, 210, 210, 210, 210, 210, 210, 210, 210, - 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, 210, - 210, 210, 210, 130, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 210, 210, 210, 210, 210, 210, 178, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, - 10, 10 + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 ] ); assert_eq!( lspreads, [ - 7666, 7456, 7246, 7036, 6826, 6616, 6596, 6576, 6556, 6536, 6326, 6116, 5906, 5696, - 5486, 5276, 5066, 4856, 4646, 4436, 4226, 4016, 3806, 3596, 3386, 3176, 2966, 2756, - 2546, 2336, 2126, 1916, 1706, 1496, 1286, 1076, 866, 656, 526, 526, 526, 526, 526, 526, + 4774, 4564, 4354, 4144, 3934, 3724, 3704, 3684, 3664, 3644, 3434, 3224, 3014, 2804, + 2594, 2384, 2174, 1964, 1754, 1544, 1334, 1124, 914, 704, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, - 526, 526 + 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526, 526 ] ); } @@ -1152,11 +1137,11 @@ fn amm_perp_ref_offset() { let signed_liquidity_ratio = liquidity_ratio .checked_mul( - (perp_market + perp_market .amm .get_protocol_owned_position() .unwrap() - .signum() as i128), + .signum() as i128, ) .unwrap(); @@ -1178,7 +1163,7 @@ fn amm_perp_ref_offset() { max_ref_offset, ) .unwrap(); - assert_eq!(res, (perp_market.amm.max_spread / 2) as i32); + assert_eq!(res, 45000); assert_eq!(perp_market.amm.reference_price_offset, 18000); // not updated vs market account let now = 1741207620 + 1; @@ -1198,7 +1183,7 @@ fn amm_perp_ref_offset() { &state.oracle_guard_rails.validity, ) .unwrap(); - let cost = _update_amm( + _update_amm( &mut perp_market, &mm_oracle_price_data, &state, @@ -1257,7 +1242,7 @@ fn amm_perp_ref_offset() { // Uses the original oracle if the slot is old, ignoring MM oracle perp_market.amm.mm_oracle_price = mm_oracle_price_data.get_price() * 995 / 1000; perp_market.amm.mm_oracle_slot = clock_slot - 100; - let mut mm_oracle_price = perp_market + let mm_oracle_price = perp_market .get_mm_oracle_price_data( oracle_price_data, clock_slot, @@ -1265,13 +1250,7 @@ fn amm_perp_ref_offset() { ) .unwrap(); - let _ = _update_amm( - &mut perp_market, - &mut mm_oracle_price, - &state, - now, - clock_slot, - ); + let _ = _update_amm(&mut perp_market, &mm_oracle_price, &state, now, clock_slot); let reserve_price_mm_offset_3 = perp_market.amm.reserve_price().unwrap(); let (b3, a3) = perp_market .amm diff --git a/programs/drift/src/controller/repeg.rs b/programs/drift/src/controller/repeg.rs index 707ccce35a..f41af93fdc 100644 --- a/programs/drift/src/controller/repeg.rs +++ b/programs/drift/src/controller/repeg.rs @@ -175,7 +175,8 @@ pub fn _update_amm( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, oracle::LogMode::SafeMMOracle, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )?; let mut amm_update_cost = 0; @@ -230,7 +231,7 @@ pub fn _update_amm( update_spreads(market, reserve_price_after, Some(clock_slot))?; - if is_oracle_valid_for_action(oracle_validity, Some(DriftAction::FillOrderAmm))? { + if is_oracle_valid_for_action(oracle_validity, Some(DriftAction::FillOrderAmmLowRisk))? { if !amm_not_successfully_updated { market.amm.last_update_slot = clock_slot; } @@ -264,7 +265,8 @@ pub fn update_amm_and_check_validity( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, LogMode::SafeMMOracle, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )?; validate!( @@ -429,7 +431,10 @@ pub fn settle_expired_market( let target_expiry_price = if market.amm.oracle_source == OracleSource::Prelaunch { market.amm.historical_oracle_data.last_oracle_price } else { - market.amm.historical_oracle_data.last_oracle_price_twap + market + .amm + .historical_oracle_data + .last_oracle_price_twap_5min }; crate::dlog!(target_expiry_price); diff --git a/programs/drift/src/controller/repeg/tests.rs b/programs/drift/src/controller/repeg/tests.rs index 92d012dd8e..cc369c25e1 100644 --- a/programs/drift/src/controller/repeg/tests.rs +++ b/programs/drift/src/controller/repeg/tests.rs @@ -114,7 +114,8 @@ pub fn update_amm_test() { market.get_max_confidence_interval_multiplier().unwrap(), &market.amm.oracle_source, LogMode::ExchangeOracle, - 0, + state.oracle_guard_rails.validity.slots_before_stale_for_amm as i8, + state.oracle_guard_rails.validity.slots_before_stale_for_amm as i8, ) .unwrap() == OracleValidity::Valid; @@ -249,6 +250,7 @@ pub fn update_amm_test_bad_oracle() { &market.amm.oracle_source, LogMode::None, 0, + 0, ) .unwrap() == OracleValidity::Valid; @@ -258,7 +260,7 @@ pub fn update_amm_test_bad_oracle() { #[test] pub fn update_amm_larg_conf_test() { let now = 1662800000 + 60; - let mut slot = 81680085; + let slot = 81680085; let mut market = PerpMarket::default_btc_test(); assert_eq!(market.amm.base_asset_amount_with_amm, -1000000000); @@ -407,7 +409,7 @@ pub fn update_amm_larg_conf_test() { #[test] pub fn update_amm_larg_conf_w_neg_tfmd_test() { let now = 1662800000 + 60; - let mut slot = 81680085; + let slot = 81680085; let mut market = PerpMarket::default_btc_test(); market.amm.concentration_coef = 1414213; diff --git a/programs/drift/src/controller/revenue_share.rs b/programs/drift/src/controller/revenue_share.rs new file mode 100644 index 0000000000..61681cd7a4 --- /dev/null +++ b/programs/drift/src/controller/revenue_share.rs @@ -0,0 +1,197 @@ +use anchor_lang::prelude::*; + +use crate::controller::spot_balance; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; +use crate::state::events::{emit_stack, RevenueShareSettleRecord}; +use crate::state::perp_market_map::PerpMarketMap; +use crate::state::revenue_share::{RevenueShareEscrowZeroCopyMut, RevenueShareOrder}; +use crate::state::revenue_share_map::RevenueShareMap; +use crate::state::spot_market::SpotBalance; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::traits::Size; +use crate::state::user::MarketType; + +/// Runs through the user's RevenueShareEscrow account and sweeps any accrued fees to the corresponding +/// builders and referrer. +pub fn sweep_completed_revenue_share_for_market<'a>( + market_index: u16, + revenue_share_escrow: &mut RevenueShareEscrowZeroCopyMut, + perp_market_map: &PerpMarketMap<'a>, + spot_market_map: &SpotMarketMap<'a>, + revenue_share_map: &RevenueShareMap<'a>, + now_ts: i64, + builder_codes_feature_enabled: bool, + builder_referral_feature_enabled: bool, +) -> crate::error::DriftResult<()> { + let perp_market = &mut perp_market_map.get_ref_mut(&market_index)?; + let quote_spot_market = &mut spot_market_map.get_quote_spot_market_mut()?; + + spot_balance::update_spot_market_cumulative_interest(quote_spot_market, None, now_ts)?; + + let orders_len = revenue_share_escrow.orders_len(); + for i in 0..orders_len { + let ( + is_completed, + is_referral_order, + order_market_type, + order_market_index, + fees_accrued, + builder_idx, + ) = { + let ord_ro = match revenue_share_escrow.get_order(i) { + Ok(o) => o, + Err(_) => { + continue; + } + }; + ( + ord_ro.is_completed(), + ord_ro.is_referral_order(), + ord_ro.market_type, + ord_ro.market_index, + ord_ro.fees_accrued, + ord_ro.builder_idx, + ) + }; + + if is_referral_order { + if fees_accrued == 0 + || !(order_market_type == MarketType::Perp && order_market_index == market_index) + { + continue; + } + } else if !(is_completed + && order_market_type == MarketType::Perp + && order_market_index == market_index + && fees_accrued > 0) + { + continue; + } + + let pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + quote_spot_market, + perp_market.pnl_pool.balance_type(), + )?; + + // TODO: should we add buffer on pnl pool? + if pnl_pool_token_amount < fees_accrued as u128 { + msg!( + "market {} PNL pool has insufficient balance to sweep fees for builder. pnl_pool_token_amount: {}, fees_accrued: {}", + market_index, + pnl_pool_token_amount, + fees_accrued + ); + break; + } + + if is_referral_order { + if builder_referral_feature_enabled { + let referrer_authority = + if let Some(referrer_authority) = revenue_share_escrow.get_referrer() { + referrer_authority + } else { + continue; + }; + + let referrer_user = revenue_share_map.get_user_ref_mut(&referrer_authority); + let referrer_rev_share = + revenue_share_map.get_revenue_share_account_mut(&referrer_authority); + + if referrer_user.is_ok() && referrer_rev_share.is_ok() { + let mut referrer_user = referrer_user.unwrap(); + let mut referrer_rev_share = referrer_rev_share.unwrap(); + + spot_balance::transfer_spot_balances( + fees_accrued as i128, + quote_spot_market, + &mut perp_market.pnl_pool, + referrer_user.get_quote_spot_position_mut(), + )?; + + referrer_rev_share.total_referrer_rewards = referrer_rev_share + .total_referrer_rewards + .safe_add(fees_accrued as u64)?; + + emit_stack::<_, { RevenueShareSettleRecord::SIZE }>( + RevenueShareSettleRecord { + ts: now_ts, + builder: None, + referrer: Some(referrer_authority), + fee_settled: fees_accrued as u64, + market_index: order_market_index, + market_type: order_market_type, + builder_total_referrer_rewards: referrer_rev_share + .total_referrer_rewards, + builder_total_builder_rewards: referrer_rev_share.total_builder_rewards, + builder_sub_account_id: referrer_user.sub_account_id, + }, + )?; + + // zero out the order + if let Ok(builder_order) = revenue_share_escrow.get_order_mut(i) { + builder_order.fees_accrued = 0; + } + } + } + } else if builder_codes_feature_enabled { + let builder_authority = match revenue_share_escrow + .get_approved_builder_mut(builder_idx) + .map(|builder| builder.authority) + { + Ok(auth) => auth, + Err(_) => { + msg!("failed to get approved_builder from escrow account"); + continue; + } + }; + + let builder_user = revenue_share_map.get_user_ref_mut(&builder_authority); + let builder_rev_share = + revenue_share_map.get_revenue_share_account_mut(&builder_authority); + + if builder_user.is_ok() && builder_rev_share.is_ok() { + let mut builder_user = builder_user.unwrap(); + let mut builder_revenue_share = builder_rev_share.unwrap(); + + spot_balance::transfer_spot_balances( + fees_accrued as i128, + quote_spot_market, + &mut perp_market.pnl_pool, + builder_user.get_quote_spot_position_mut(), + )?; + + builder_revenue_share.total_builder_rewards = builder_revenue_share + .total_builder_rewards + .safe_add(fees_accrued as u64)?; + + emit_stack::<_, { RevenueShareSettleRecord::SIZE }>(RevenueShareSettleRecord { + ts: now_ts, + builder: Some(builder_authority), + referrer: None, + fee_settled: fees_accrued as u64, + market_index: order_market_index, + market_type: order_market_type, + builder_total_referrer_rewards: builder_revenue_share.total_referrer_rewards, + builder_total_builder_rewards: builder_revenue_share.total_builder_rewards, + builder_sub_account_id: builder_user.sub_account_id, + })?; + + // remove order + if let Ok(builder_order) = revenue_share_escrow.get_order_mut(i) { + *builder_order = RevenueShareOrder::default(); + } + } else { + msg!( + "Builder user or builder not found for builder authority: {}", + builder_authority + ); + } + } else { + msg!("Builder codes nor builder referral feature is not enabled"); + } + } + + Ok(()) +} diff --git a/programs/drift/src/controller/spot_balance.rs b/programs/drift/src/controller/spot_balance.rs index 6e34edb369..f8e1b625e9 100644 --- a/programs/drift/src/controller/spot_balance.rs +++ b/programs/drift/src/controller/spot_balance.rs @@ -412,6 +412,7 @@ pub fn update_spot_market_and_check_validity( &spot_market.oracle_source, LogMode::ExchangeOracle, 0, + 0, )?; validate!( diff --git a/programs/drift/src/controller/spot_balance/tests.rs b/programs/drift/src/controller/spot_balance/tests.rs index a8158e5a73..629ef1e5eb 100644 --- a/programs/drift/src/controller/spot_balance/tests.rs +++ b/programs/drift/src/controller/spot_balance/tests.rs @@ -1387,7 +1387,7 @@ fn check_fee_collection_larger_nums() { #[test] fn test_multi_stage_borrow_rate_curve() { - let mut spot_market = SpotMarket { + let spot_market = SpotMarket { market_index: 0, oracle_source: OracleSource::QuoteAsset, cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, @@ -1457,7 +1457,7 @@ fn test_multi_stage_borrow_rate_curve_sol() { let spot_market_loader: AccountLoader = AccountLoader::try_from(&sol_market_account_info).unwrap(); - let mut spot_market = spot_market_loader.load_mut().unwrap(); + let spot_market = spot_market_loader.load_mut().unwrap(); // Store all rates to verify monotonicity and smoothness later let mut last_rate = 0_u128; diff --git a/programs/drift/src/controller/token.rs b/programs/drift/src/controller/token.rs index 6e5d03193e..a36a2e8404 100644 --- a/programs/drift/src/controller/token.rs +++ b/programs/drift/src/controller/token.rs @@ -9,7 +9,7 @@ use anchor_spl::token_2022::spl_token_2022::extension::{ }; use anchor_spl::token_2022::spl_token_2022::state::Mint as MintInner; use anchor_spl::token_interface::{ - self, CloseAccount, Mint, TokenAccount, TokenInterface, Transfer, TransferChecked, + self, Burn, CloseAccount, Mint, MintTo, TokenAccount, TokenInterface, Transfer, TransferChecked, }; use std::iter::Peekable; use std::slice::Iter; @@ -25,7 +25,31 @@ pub fn send_from_program_vault<'info>( remaining_accounts: Option<&mut Peekable>>>, ) -> Result<()> { let signature_seeds = get_signer_seeds(&nonce); - let signers = &[&signature_seeds[..]]; + + send_from_program_vault_with_signature_seeds( + token_program, + from, + to, + authority, + &signature_seeds, + amount, + mint, + remaining_accounts, + ) +} + +#[inline] +pub fn send_from_program_vault_with_signature_seeds<'info>( + token_program: &Interface<'info, TokenInterface>, + from: &InterfaceAccount<'info, TokenAccount>, + to: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &Option>, + remaining_accounts: Option<&mut Peekable>>>, +) -> Result<()> { + let signers = &[signature_seeds]; if let Some(mint) = mint { if let Some(remaining_accounts) = remaining_accounts { @@ -137,6 +161,56 @@ pub fn close_vault<'info>( token_interface::close_account(cpi_context) } +pub fn mint_tokens<'info>( + token_program: &Interface<'info, TokenInterface>, + destination: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &InterfaceAccount<'info, Mint>, +) -> Result<()> { + let signers = &[signature_seeds]; + + let mint_account_info = mint.to_account_info(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = MintTo { + mint: mint_account_info, + to: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::mint_to(cpi_context, amount) +} + +pub fn burn_tokens<'info>( + token_program: &Interface<'info, TokenInterface>, + destination: &InterfaceAccount<'info, TokenAccount>, + authority: &AccountInfo<'info>, + signature_seeds: &[&[u8]], + amount: u64, + mint: &InterfaceAccount<'info, Mint>, +) -> Result<()> { + let signers = &[signature_seeds]; + + let mint_account_info = mint.to_account_info(); + + validate_mint_fee(&mint_account_info)?; + + let cpi_accounts = Burn { + mint: mint_account_info, + from: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let cpi_program = token_program.to_account_info(); + let cpi_context = CpiContext::new_with_signer(cpi_program, cpi_accounts, signers); + token_interface::burn(cpi_context, amount) +} + pub fn validate_mint_fee(account_info: &AccountInfo) -> Result<()> { let mint_data = account_info.try_borrow_data()?; let mint_with_extension = StateWithExtensions::::unpack(&mint_data)?; @@ -217,3 +291,16 @@ pub fn initialize_token_account<'info>( Ok(()) } + +pub fn initialize_immutable_owner<'info>( + token_program: &Interface<'info, TokenInterface>, + account: &AccountInfo<'info>, +) -> Result<()> { + let accounts = ::anchor_spl::token_interface::InitializeImmutableOwner { + account: account.to_account_info(), + }; + let cpi_ctx = anchor_lang::context::CpiContext::new(token_program.to_account_info(), accounts); + ::anchor_spl::token_interface::initialize_immutable_owner(cpi_ctx)?; + + Ok(()) +} diff --git a/programs/drift/src/error.rs b/programs/drift/src/error.rs index acaa63947a..abc1aebd3d 100644 --- a/programs/drift/src/error.rs +++ b/programs/drift/src/error.rs @@ -1,5 +1,4 @@ use anchor_lang::prelude::*; - pub type DriftResult = std::result::Result; #[error_code] @@ -641,6 +640,60 @@ pub enum ErrorCode { InvalidIfRebalanceSwap, #[msg("Invalid Isolated Perp Market")] InvalidIsolatedPerpMarket, + #[msg("Invalid RevenueShare resize")] + InvalidRevenueShareResize, + #[msg("Builder has been revoked")] + BuilderRevoked, + #[msg("Builder fee is greater than max fee bps")] + InvalidBuilderFee, + #[msg("RevenueShareEscrow authority mismatch")] + RevenueShareEscrowAuthorityMismatch, + #[msg("RevenueShareEscrow has too many active orders")] + RevenueShareEscrowOrdersAccountFull, + #[msg("Invalid RevenueShareAccount")] + InvalidRevenueShareAccount, + #[msg("Cannot revoke builder with open orders")] + CannotRevokeBuilderWithOpenOrders, + #[msg("Unable to load builder account")] + UnableToLoadRevenueShareAccount, + #[msg("Invalid Constituent")] + InvalidConstituent, + #[msg("Invalid Amm Constituent Mapping argument")] + InvalidAmmConstituentMappingArgument, + #[msg("Constituent not found")] + ConstituentNotFound, + #[msg("Constituent could not load")] + ConstituentCouldNotLoad, + #[msg("Constituent wrong mutability")] + ConstituentWrongMutability, + #[msg("Wrong number of constituents passed to instruction")] + WrongNumberOfConstituents, + #[msg("Insufficient constituent token balance")] + InsufficientConstituentTokenBalance, + #[msg("Amm Cache data too stale")] + AMMCacheStale, + #[msg("LP Pool AUM not updated recently")] + LpPoolAumDelayed, + #[msg("Constituent oracle is stale")] + ConstituentOracleStale, + #[msg("LP Invariant failed")] + LpInvariantFailed, + #[msg("Invalid constituent derivative weights")] + InvalidConstituentDerivativeWeights, + #[msg("Max DLP AUM Breached")] + MaxDlpAumBreached, + #[msg("Settle Lp Pool Disabled")] + SettleLpPoolDisabled, + #[msg("Mint/Redeem Lp Pool Disabled")] + MintRedeemLpPoolDisabled, + #[msg("Settlement amount exceeded")] + LpPoolSettleInvariantBreached, + #[msg("Invalid constituent operation")] + InvalidConstituentOperation, + #[msg("Unauthorized for operation")] + Unauthorized, + #[msg("Invalid Lp Pool Id for Operation")] + InvalidLpPoolId, } #[macro_export] diff --git a/programs/drift/src/ids.rs b/programs/drift/src/ids.rs index 0c2a80addb..a37dd452fb 100644 --- a/programs/drift/src/ids.rs +++ b/programs/drift/src/ids.rs @@ -107,3 +107,32 @@ pub mod amm_spread_adjust_wallet { #[cfg(feature = "anchor-test")] declare_id!("1ucYHAGrBbi1PaecC4Ptq5ocZLWGLBmbGWysoDGNB1N"); } + +pub mod lp_pool_swap_wallet { + use solana_program::declare_id; + declare_id!("25qbsE2oWri76c9a86ubn17NKKdo6Am4HXD2Jm8vT8K4"); +} + +pub mod dflow_mainnet_aggregator_4 { + use solana_program::declare_id; + declare_id!("DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH"); +} + +pub mod titan_mainnet_argos_v1 { + use solana_program::declare_id; + declare_id!("T1TANpTeScyeqVzzgNViGDNrkQ6qHz9KrSBS4aNXvGT"); +} + +pub mod lp_pool_hot_wallet { + use solana_program::declare_id; + declare_id!("GP9qHLX8rx4BgRULGPV1poWQPdGuzbxGbvTB12DfmwFk"); +} + +pub const WHITELISTED_SWAP_PROGRAMS: &[solana_program::pubkey::Pubkey] = &[ + serum_program::id(), + jupiter_mainnet_3::id(), + jupiter_mainnet_4::id(), + jupiter_mainnet_6::id(), + dflow_mainnet_aggregator_4::id(), + titan_mainnet_argos_v1::id(), +]; diff --git a/programs/drift/src/instructions/admin.rs b/programs/drift/src/instructions/admin.rs index e552576f63..456886ea29 100644 --- a/programs/drift/src/instructions/admin.rs +++ b/programs/drift/src/instructions/admin.rs @@ -1,6 +1,3 @@ -use std::convert::{identity, TryInto}; -use std::mem::size_of; - use crate::{msg, FeatureBitFlags}; use anchor_lang::prelude::*; use anchor_spl::token_2022::Token2022; @@ -9,20 +6,27 @@ use phoenix::quantities::WrapperU64; use pyth_solana_receiver_sdk::cpi::accounts::InitPriceUpdate; use pyth_solana_receiver_sdk::program::PythSolanaReceiver; use serum_dex::state::ToAlignedBytes; +use std::convert::{identity, TryInto}; +use std::mem::size_of; -use crate::controller::token::{close_vault, initialize_token_account}; +use crate::controller; +use crate::controller::token::{close_vault, initialize_immutable_owner, initialize_token_account}; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::{admin_hot_wallet, amm_spread_adjust_wallet, mm_oracle_crank_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load; use crate::math::casting::Cast; use crate::math::constants::{ AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO, DEFAULT_LIQUIDATION_MARGIN_BUFFER_RATIO, - FEE_POOL_TO_REVENUE_POOL_THRESHOLD, IF_FACTOR_PRECISION, INSURANCE_A_MAX, INSURANCE_B_MAX, - INSURANCE_C_MAX, INSURANCE_SPECULATIVE_MAX, LIQUIDATION_FEE_PRECISION, - MAX_CONCENTRATION_COEFFICIENT, MAX_SQRT_K, MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, - PERCENTAGE_PRECISION_I64, QUOTE_SPOT_MARKET_INDEX, SPOT_CUMULATIVE_INTEREST_PRECISION, - SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, TWENTY_FOUR_HOUR, + EPOCH_DURATION, FEE_ADJUSTMENT_MAX, FEE_POOL_TO_REVENUE_POOL_THRESHOLD, GOV_SPOT_MARKET_INDEX, + IF_FACTOR_PRECISION, INSURANCE_A_MAX, INSURANCE_B_MAX, INSURANCE_C_MAX, + INSURANCE_SPECULATIVE_MAX, LIQUIDATION_FEE_PRECISION, MAX_CONCENTRATION_COEFFICIENT, + MAX_SQRT_K, MAX_UPDATE_K_PRICE_CHANGE, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, + QUOTE_PRECISION_I64, QUOTE_SPOT_MARKET_INDEX, SPOT_BALANCE_PRECISION, + SPOT_CUMULATIVE_INTEREST_PRECISION, SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, THIRTEEN_DAY, + TWENTY_FOUR_HOUR, }; use crate::math::cp_curve::get_update_k_result; use crate::math::helpers::get_proportion_u128; @@ -32,7 +36,9 @@ use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::math::{amm, bn}; +use crate::math_error; use crate::optional_accounts::get_token_mint; +use crate::state::amm_cache::{AmmCache, CacheInfo, AMM_POSITIONS_CACHE}; use crate::state::events::{ CurveRecord, DepositDirection, DepositExplanation, DepositRecord, SpotMarketVaultDepositRecord, }; @@ -45,6 +51,7 @@ use crate::state::fulfillment_params::serum::SerumContext; use crate::state::fulfillment_params::serum::SerumV3FulfillmentConfig; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use crate::state::if_rebalance_config::{IfRebalanceConfig, IfRebalanceConfigParams}; +use crate::state::insurance_fund_stake::InsuranceFundStake; use crate::state::insurance_fund_stake::ProtocolIfSharesTransferConfig; use crate::state::oracle::get_sb_on_demand_price; use crate::state::oracle::{ @@ -65,7 +72,9 @@ use crate::state::spot_market::{ TokenProgramFlag, }; use crate::state::spot_market_map::get_writable_spot_market_set; -use crate::state::state::{ExchangeStatus, FeeStructure, OracleGuardRails, State}; +use crate::state::state::{ + ExchangeStatus, FeeStructure, LpPoolFeatureBitFlags, OracleGuardRails, State, +}; use crate::state::traits::Size; use crate::state::user::{User, UserStats}; use crate::validate; @@ -73,12 +82,9 @@ use crate::validation::fee_structure::validate_fee_structure; use crate::validation::margin::{validate_margin, validate_margin_weights}; use crate::validation::perp_market::validate_perp_market; use crate::validation::spot_market::validate_borrow_rate; -use crate::{controller, QUOTE_PRECISION_I64}; -use crate::{get_then_update_id, EPOCH_DURATION}; -use crate::{load, FEE_ADJUSTMENT_MAX}; use crate::{load_mut, PTYH_PRICE_FEED_SEED_PREFIX}; use crate::{math, safe_decrement, safe_increment}; -use crate::{math_error, SPOT_BALANCE_PRECISION}; + use anchor_spl::token_2022::spl_token_2022::extension::transfer_hook::TransferHook; use anchor_spl::token_2022::spl_token_2022::extension::{ BaseStateWithExtensions, StateWithExtensions, @@ -115,7 +121,8 @@ pub fn handle_initialize(ctx: Context) -> Result<()> { max_number_of_sub_accounts: 0, max_initialize_user_fee: 0, feature_bit_flags: 0, - padding: [0; 9], + lp_pool_feature_bit_flags: 0, + padding: [0; 8], }; Ok(()) @@ -146,6 +153,26 @@ pub fn handle_initialize_spot_market( let state = &mut ctx.accounts.state; let spot_market_pubkey = ctx.accounts.spot_market.key(); + let is_token_2022 = *ctx.accounts.spot_market_mint.to_account_info().owner == Token2022::id(); + if is_token_2022 { + initialize_immutable_owner(&ctx.accounts.token_program, &ctx.accounts.spot_market_vault)?; + + initialize_immutable_owner( + &ctx.accounts.token_program, + &ctx.accounts.insurance_fund_vault, + )?; + } + + let is_token_2022 = *ctx.accounts.spot_market_mint.to_account_info().owner == Token2022::id(); + if is_token_2022 { + initialize_immutable_owner(&ctx.accounts.token_program, &ctx.accounts.spot_market_vault)?; + + initialize_immutable_owner( + &ctx.accounts.token_program, + &ctx.accounts.insurance_fund_vault, + )?; + } + initialize_token_account( &ctx.accounts.token_program, &ctx.accounts.spot_market_vault, @@ -687,6 +714,7 @@ pub fn handle_initialize_perp_market( curve_update_intensity: u8, amm_jit_intensity: u8, name: [u8; 32], + lp_pool_id: u8, ) -> Result<()> { msg!("perp market {}", market_index); let perp_market_pubkey = ctx.accounts.perp_market.to_account_info().key; @@ -969,9 +997,13 @@ pub fn handle_initialize_perp_market( high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding1: 0, + lp_fee_transfer_scalar: 1, + lp_status: 0, + lp_exchange_fee_excluscion_scalar: 0, + lp_paused_operations: 0, last_fill_price: 0, - padding: [0; 24], + lp_pool_id, + padding: [0; 23], amm: AMM { oracle: *ctx.accounts.oracle.key, oracle_source, @@ -1059,21 +1091,33 @@ pub fn handle_initialize_perp_market( last_oracle_valid: false, target_base_asset_amount_per_lp: 0, per_lp_base: 0, - oracle_slot_delay_override: 0, - taker_speed_bump_override: 0, + oracle_slot_delay_override: -1, + oracle_low_risk_slot_delay_override: 0, amm_spread_adjustment: 0, mm_oracle_sequence_id: 0, net_unsettled_funding_pnl: 0, quote_asset_amount_with_unsettled_lp: 0, reference_price_offset: 0, amm_inventory_spread_adjustment: 0, - padding: [0; 3], + reference_price_offset_deadband_pct: 0, + padding: [0; 2], last_funding_oracle_twap: 0, }, }; safe_increment!(state.number_of_markets, 1); + let amm_cache = &mut ctx.accounts.amm_cache; + let current_len = amm_cache.cache.len(); + amm_cache + .cache + .resize_with(current_len + 1, CacheInfo::default); + let current_market_info = amm_cache.cache.get_mut(current_len).unwrap(); + current_market_info.slot = clock_slot; + current_market_info.oracle = perp_market.amm.oracle; + current_market_info.oracle_source = u8::from(perp_market.amm.oracle_source); + amm_cache.validate(state)?; + controller::amm::update_concentration_coef(perp_market, concentration_coef_scale)?; crate::dlog!(oracle_price); @@ -1089,6 +1133,44 @@ pub fn handle_initialize_perp_market( Ok(()) } +pub fn handle_initialize_amm_cache(ctx: Context) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + amm_cache.bump = ctx.bumps.amm_cache; + + Ok(()) +} + +pub fn handle_resize_amm_cache(ctx: Context) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let state = &ctx.accounts.state; + let current_size = amm_cache.cache.len(); + let new_size = (state.number_of_markets as usize).min(current_size + 20_usize); + + msg!( + "resizing amm cache from {} entries to {}", + current_size, + new_size + ); + + let growth = new_size.saturating_sub(current_size); + validate!( + growth <= 20, + ErrorCode::DefaultError, + "cannot grow amm_cache by more than 20 entries in a single resize (requested +{})", + growth + )?; + + amm_cache.cache.resize_with(new_size, CacheInfo::default); + amm_cache.validate(state)?; + + Ok(()) +} + +pub fn handle_delete_amm_cache(_ctx: Context) -> Result<()> { + msg!("deleted amm cache"); + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] @@ -1945,6 +2027,12 @@ pub fn handle_deposit_into_perp_market_fee_pool<'c: 'info, 'info>( let quote_spot_market = &mut load_mut!(ctx.accounts.quote_spot_market)?; + controller::spot_balance::update_spot_market_cumulative_interest( + &mut *quote_spot_market, + None, + Clock::get()?.unix_timestamp, + )?; + controller::spot_balance::update_spot_balances( amount.cast::()?, &SpotBalanceType::Deposit, @@ -2770,6 +2858,25 @@ pub fn handle_update_perp_liquidation_fee( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_lp_pool_id( + ctx: Context, + lp_pool_id: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + + msg!( + "updating perp market {} lp pool id: {} -> {}", + perp_market.market_index, + perp_market.lp_pool_id, + lp_pool_id + ); + perp_market.lp_pool_id = lp_pool_id; + Ok(()) +} + #[access_control( spot_market_valid(&ctx.accounts.spot_market) )] @@ -3249,12 +3356,20 @@ pub fn handle_update_perp_market_status( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_paused_operations( - ctx: Context, + ctx: Context, paused_operations: u8, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; msg!("perp market {}", perp_market.market_index); + if *ctx.accounts.admin.key != ctx.accounts.state.admin { + validate!( + paused_operations == PerpOperation::UpdateFunding as u8, + ErrorCode::DefaultError, + "signer must be admin", + )?; + } + perp_market.paused_operations = paused_operations; if perp_market.is_prediction_market() { @@ -3287,6 +3402,7 @@ pub fn handle_update_perp_market_contract_tier( ); perp_market.contract_tier = contract_tier; + Ok(()) } @@ -3427,6 +3543,49 @@ pub fn handle_update_perp_market_curve_update_intensity( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_market_reference_price_offset_deadband_pct( + ctx: Context, + reference_price_offset_deadband_pct: u8, +) -> Result<()> { + validate!( + reference_price_offset_deadband_pct <= 100, + ErrorCode::DefaultError, + "invalid reference_price_offset_deadband_pct", + )?; + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + + msg!( + "perp_market.amm.reference_price_offset_deadband_pct: {} -> {}", + perp_market.amm.reference_price_offset_deadband_pct, + reference_price_offset_deadband_pct + ); + + let liquidity_ratio = + crate::math::amm_spread::calculate_inventory_liquidity_ratio_for_reference_price_offset( + perp_market.amm.base_asset_amount_with_amm, + perp_market.amm.base_asset_reserve, + perp_market.amm.min_base_asset_reserve, + perp_market.amm.max_base_asset_reserve, + )?; + + let signed_liquidity_ratio = liquidity_ratio.safe_mul( + perp_market + .amm + .get_protocol_owned_position()? + .signum() + .cast()?, + )?; + + msg!("current signed liquidity ratio: {}", signed_liquidity_ratio); + + perp_market.amm.reference_price_offset_deadband_pct = reference_price_offset_deadband_pct; + Ok(()) +} + pub fn handle_update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, @@ -3581,6 +3740,7 @@ pub fn handle_update_perp_market_oracle( skip_invariant_check: bool, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; msg!("perp market {}", perp_market.market_index); let clock = Clock::get()?; @@ -3659,6 +3819,8 @@ pub fn handle_update_perp_market_oracle( perp_market.amm.oracle = oracle; perp_market.amm.oracle_source = oracle_source; + amm_cache.update_perp_market_fields(perp_market)?; + Ok(()) } @@ -3727,7 +3889,7 @@ pub fn handle_update_amm_jit_intensity( perp_market_valid(&ctx.accounts.perp_market) )] pub fn handle_update_perp_market_max_spread( - ctx: Context, + ctx: Context, max_spread: u32, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; @@ -3809,6 +3971,40 @@ pub fn handle_update_perp_market_min_order_size( Ok(()) } +#[access_control( + perp_market_valid(&ctx.accounts.perp_market) +)] +pub fn handle_update_perp_market_lp_pool_fee_transfer_scalar( + ctx: Context, + optional_lp_fee_transfer_scalar: Option, + optional_lp_net_pnl_transfer_scalar: Option, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + + if let Some(lp_fee_transfer_scalar) = optional_lp_fee_transfer_scalar { + msg!( + "perp_market.: {:?} -> {:?}", + perp_market.lp_fee_transfer_scalar, + lp_fee_transfer_scalar + ); + + perp_market.lp_fee_transfer_scalar = lp_fee_transfer_scalar; + } + + if let Some(lp_net_pnl_transfer_scalar) = optional_lp_net_pnl_transfer_scalar { + msg!( + "perp_market.: {:?} -> {:?}", + perp_market.lp_exchange_fee_excluscion_scalar, + lp_net_pnl_transfer_scalar + ); + + perp_market.lp_exchange_fee_excluscion_scalar = lp_net_pnl_transfer_scalar; + } + + Ok(()) +} + #[access_control( spot_market_valid(&ctx.accounts.spot_market) )] @@ -4083,23 +4279,33 @@ pub fn handle_update_perp_market_protected_maker_params( Ok(()) } +pub fn handle_update_perp_market_lp_pool_paused_operations( + ctx: Context, + lp_paused_operations: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + msg!("perp market {}", perp_market.market_index); + perp_market.lp_paused_operations = lp_paused_operations; + Ok(()) +} + #[access_control( perp_market_valid(&ctx.accounts.perp_market) )] -pub fn handle_update_perp_market_taker_speed_bump_override( +pub fn handle_update_perp_market_oracle_low_risk_slot_delay_override( ctx: Context, - taker_speed_bump_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> Result<()> { let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; msg!("perp market {}", perp_market.market_index); msg!( - "perp_market.amm.taker_speed_bump_override: {:?} -> {:?}", - perp_market.amm.taker_speed_bump_override, - taker_speed_bump_override + "perp_market.amm.oracle_low_risk_slot_delay_override: {:?} -> {:?}", + perp_market.amm.oracle_low_risk_slot_delay_override, + oracle_low_risk_slot_delay_override ); - perp_market.amm.taker_speed_bump_override = taker_speed_bump_override; + perp_market.amm.oracle_low_risk_slot_delay_override = oracle_low_risk_slot_delay_override; Ok(()) } @@ -4609,8 +4815,8 @@ pub fn handle_update_protected_maker_mode_config( ) -> Result<()> { let mut config = load_mut!(ctx.accounts.protected_maker_mode_config)?; - if current_users.is_some() { - config.current_users = current_users.unwrap(); + if let Some(users) = current_users { + config.current_users = users; } config.max_users = max_users; config.reduce_only = reduce_only as u8; @@ -4915,6 +5121,152 @@ pub fn handle_update_feature_bit_flags_median_trigger_price( Ok(()) } +pub fn handle_update_delegate_user_gov_token_insurance_stake( + ctx: Context, +) -> Result<()> { + let insurance_fund_stake = &mut load_mut!(ctx.accounts.insurance_fund_stake)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + + validate!( + insurance_fund_stake.market_index == GOV_SPOT_MARKET_INDEX, + ErrorCode::IncorrectSpotMarketAccountPassed, + "insurance_fund_stake is not for governance market index = {}", + GOV_SPOT_MARKET_INDEX + )?; + + if insurance_fund_stake.market_index == GOV_SPOT_MARKET_INDEX + && spot_market.market_index == GOV_SPOT_MARKET_INDEX + { + let clock = Clock::get()?; + let now = clock.unix_timestamp; + + crate::controller::insurance::update_user_stats_if_stake_amount( + 0, + ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + now, + )?; + } + + Ok(()) +} + +pub fn handle_update_feature_bit_flags_builder_codes( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can enable feature bit flags" + )?; + + msg!("Setting 3rd bit to 1, enabling builder codes"); + state.feature_bit_flags = state.feature_bit_flags | (FeatureBitFlags::BuilderCodes as u8); + } else { + msg!("Setting 3rd bit to 0, disabling builder codes"); + state.feature_bit_flags = state.feature_bit_flags & !(FeatureBitFlags::BuilderCodes as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_builder_referral( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can enable feature bit flags" + )?; + + msg!("Setting 4th bit to 1, enabling builder referral"); + state.feature_bit_flags = + state.feature_bit_flags | (FeatureBitFlags::BuilderReferral as u8); + } else { + msg!("Setting 4th bit to 0, disabling builder referral"); + state.feature_bit_flags = + state.feature_bit_flags & !(FeatureBitFlags::BuilderReferral as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_settle_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting first bit to 1, enabling settle LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::SettleLpPool as u8); + } else { + msg!("Setting first bit to 0, disabling settle LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::SettleLpPool as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_swap_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting second bit to 1, enabling swapping with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::SwapLpPool as u8); + } else { + msg!("Setting second bit to 0, disabling swapping with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::SwapLpPool as u8); + } + Ok(()) +} + +pub fn handle_update_feature_bit_flags_mint_redeem_lp_pool( + ctx: Context, + enable: bool, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if enable { + validate!( + ctx.accounts.admin.key().eq(&state.admin), + ErrorCode::DefaultError, + "Only state admin can re-enable after kill switch" + )?; + + msg!("Setting third bit to 1, enabling minting and redeeming with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags | (LpPoolFeatureBitFlags::MintRedeemLpPool as u8); + } else { + msg!("Setting third bit to 0, disabling minting and redeeming with LP pool"); + state.lp_pool_feature_bit_flags = + state.lp_pool_feature_bit_flags & !(LpPoolFeatureBitFlags::MintRedeemLpPool as u8); + } + Ok(()) +} + #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] @@ -5159,12 +5511,79 @@ pub struct InitializePerpMarket<'info> { payer = admin )] pub perp_market: AccountLoader<'info, PerpMarket>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + realloc = AmmCache::space(amm_cache.cache.len() + 1_usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, /// CHECK: checked in `initialize_perp_market` pub oracle: AccountInfo<'info>, pub rent: Sysvar<'info, Rent>, pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct InitializeAmmCache<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account( + init, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + space = AmmCache::init_space(), + bump, + payer = admin + )] + pub amm_cache: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct ResizeAmmCache<'info> { + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump, + realloc = AmmCache::space(amm_cache.cache.len() + (state.number_of_markets as usize - amm_cache.cache.len()).min(20_usize)), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct DeleteAmmCache<'info> { + #[account(mut)] + pub admin: Signer<'info>, + #[account( + has_one = admin + )] + pub state: Box>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump, + close = admin, + )] + pub amm_cache: Box>, +} + #[derive(Accounts)] pub struct DeleteInitializedPerpMarket<'info> { #[account(mut)] @@ -5411,6 +5830,12 @@ pub struct AdminUpdatePerpMarketOracle<'info> { pub oracle: AccountInfo<'info>, /// CHECK: checked in `admin_update_perp_market_oracle` ix constraint pub old_oracle: AccountInfo<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, } #[derive(Accounts)] @@ -5759,3 +6184,27 @@ pub struct UpdateIfRebalanceConfig<'info> { )] pub state: Box>, } + +#[derive(Accounts)] +pub struct UpdateDelegateUserGovTokenInsuranceStake<'info> { + #[account( + mut, + seeds = [b"spot_market", 15_u16.to_le_bytes().as_ref()], + bump + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + pub insurance_fund_stake: AccountLoader<'info, InsuranceFundStake>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, + pub admin: Signer<'info>, + #[account( + mut, + seeds = [b"insurance_fund_vault".as_ref(), 15_u16.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_vault: Box>, + #[account( + has_one = admin + )] + pub state: Box>, +} diff --git a/programs/drift/src/instructions/constraints.rs b/programs/drift/src/instructions/constraints.rs index c983ea471f..23deccd715 100644 --- a/programs/drift/src/instructions/constraints.rs +++ b/programs/drift/src/instructions/constraints.rs @@ -159,15 +159,13 @@ pub fn get_vault_len(mint: &InterfaceAccount) -> anchor_lang::Result::unpack(&mint_data)?; let mint_extensions = match mint_state.get_extension_types() { Ok(extensions) => extensions, - // If we cant deserialize the mint, we use the default token account length + // If we cant deserialize the mint, try assuming no extensions // Init token will fail if this size doesnt work, so worst case init account just fails - Err(_) => { - msg!("Failed to deserialize mint. Falling back to default token account length"); - return Ok(::anchor_spl::token::TokenAccount::LEN); - } + Err(_) => vec![], }; - let required_extensions = + let mut required_extensions = ExtensionType::get_required_init_account_extensions(&mint_extensions); + required_extensions.push(ExtensionType::ImmutableOwner); ExtensionType::try_calculate_account_len::(&required_extensions)? } else { ::anchor_spl::token::TokenAccount::LEN diff --git a/programs/drift/src/instructions/if_staker.rs b/programs/drift/src/instructions/if_staker.rs index 51e4d7fd79..3e2381eb5f 100644 --- a/programs/drift/src/instructions/if_staker.rs +++ b/programs/drift/src/instructions/if_staker.rs @@ -3,9 +3,11 @@ use anchor_lang::Discriminator; use anchor_spl::token_interface::{TokenAccount, TokenInterface}; use crate::error::ErrorCode; -use crate::ids::if_rebalance_wallet; +use crate::ids::{admin_hot_wallet, if_rebalance_wallet}; use crate::instructions::constraints::*; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load_mut; +use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; use crate::optional_accounts::get_token_mint; use crate::state::insurance_fund_stake::{InsuranceFundStake, ProtocolIfSharesTransferConfig}; use crate::state::paused_operations::InsuranceFundOperation; @@ -23,7 +25,6 @@ use crate::{ spot_market_map::get_writable_spot_market_set_from_many, }, }; -use crate::{load_mut, QUOTE_SPOT_MARKET_INDEX}; use anchor_lang::solana_program::sysvar::instructions; use super::optional_accounts::get_token_interface; @@ -143,6 +144,7 @@ pub fn handle_add_insurance_fund_stake<'c: 'info, 'info>( user_stats, spot_market, clock.unix_timestamp, + false, )?; controller::token::receive( @@ -821,6 +823,114 @@ pub fn handle_transfer_protocol_if_shares_to_revenue_pool<'c: 'info, 'info>( Ok(()) } +pub fn handle_deposit_into_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoInsuranceFundStake<'info>>, + market_index: u16, + amount: u64, +) -> Result<()> { + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let clock = Clock::get()?; + let now = clock.unix_timestamp; + let insurance_fund_stake = &mut load_mut!(ctx.accounts.insurance_fund_stake)?; + let user_stats = &mut load_mut!(ctx.accounts.user_stats)?; + let spot_market = &mut load_mut!(ctx.accounts.spot_market)?; + let state = &ctx.accounts.state; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + + validate!( + !spot_market.is_insurance_fund_operation_paused(InsuranceFundOperation::Add), + ErrorCode::InsuranceFundOperationPaused, + "if staking add disabled", + )?; + + validate!( + insurance_fund_stake.market_index == market_index, + ErrorCode::IncorrectSpotMarketAccountPassed, + "insurance_fund_stake does not match market_index" + )?; + + validate!( + spot_market.status != MarketStatus::Initialized, + ErrorCode::InvalidSpotMarketState, + "spot market = {} not active for insurance_fund_stake", + spot_market.market_index + )?; + + validate!( + insurance_fund_stake.last_withdraw_request_shares == 0 + && insurance_fund_stake.last_withdraw_request_value == 0, + ErrorCode::IFWithdrawRequestInProgress, + "withdraw request in progress" + )?; + + { + if spot_market.has_transfer_hook() { + controller::insurance::attempt_settle_revenue_to_insurance_fund( + &ctx.accounts.spot_market_vault, + &ctx.accounts.insurance_fund_vault, + spot_market, + now, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + state, + &mint, + Some(&mut remaining_accounts_iter.clone()), + )?; + } else { + controller::insurance::attempt_settle_revenue_to_insurance_fund( + &ctx.accounts.spot_market_vault, + &ctx.accounts.insurance_fund_vault, + spot_market, + now, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + state, + &mint, + None, + )?; + }; + + // reload the vault balances so they're up-to-date + ctx.accounts.spot_market_vault.reload()?; + ctx.accounts.insurance_fund_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + spot_market, + ctx.accounts.spot_market_vault.amount, + )?; + } + + controller::insurance::add_insurance_fund_stake( + amount, + ctx.accounts.insurance_fund_vault.amount, + insurance_fund_stake, + user_stats, + spot_market, + clock.unix_timestamp, + true, + )?; + + controller::token::receive( + &ctx.accounts.token_program, + &ctx.accounts.user_token_account, + &ctx.accounts.insurance_fund_vault, + &ctx.accounts.signer.to_account_info(), + amount, + &mint, + if spot_market.has_transfer_hook() { + Some(remaining_accounts_iter) + } else { + None + }, + )?; + + Ok(()) +} + #[derive(Accounts)] #[instruction( market_index: u16, @@ -1082,3 +1192,49 @@ pub struct TransferProtocolIfSharesToRevenuePool<'info> { /// CHECK: forced drift_signer pub drift_signer: AccountInfo<'info>, } + +#[derive(Accounts)] +#[instruction(market_index: u16,)] +pub struct DepositIntoInsuranceFundStake<'info> { + pub signer: Signer<'info>, + #[account( + mut, + constraint = signer.key() == admin_hot_wallet::id() || signer.key() == state.admin + )] + pub state: Box>, + #[account( + mut, + seeds = [b"spot_market", market_index.to_le_bytes().as_ref()], + bump + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + seeds = [b"insurance_fund_stake", user_stats.load()?.authority.as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_stake: AccountLoader<'info, InsuranceFundStake>, + #[account(mut)] + pub user_stats: AccountLoader<'info, UserStats>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_vault: Box>, + #[account( + mut, + seeds = [b"insurance_fund_vault".as_ref(), market_index.to_le_bytes().as_ref()], + bump, + )] + pub insurance_fund_vault: Box>, + #[account( + mut, + token::mint = insurance_fund_vault.mint, + token::authority = signer + )] + pub user_token_account: Box>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, +} diff --git a/programs/drift/src/instructions/keeper.rs b/programs/drift/src/instructions/keeper.rs index 25d2bb7426..4232098614 100644 --- a/programs/drift/src/instructions/keeper.rs +++ b/programs/drift/src/instructions/keeper.rs @@ -16,23 +16,37 @@ use crate::controller::liquidation::{ liquidate_spot_with_swap_begin, liquidate_spot_with_swap_end, }; use crate::controller::orders::cancel_orders; +use crate::controller::orders::validate_market_within_price_band; use crate::controller::position::PositionDirection; use crate::controller::spot_balance::update_spot_balances; use crate::controller::token::{receive, send_from_program_vault}; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::admin_hot_wallet; -use crate::ids::{jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, serum_program}; +use crate::ids::{ + dflow_mainnet_aggregator_4, jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, + serum_program, titan_mainnet_argos_v1, +}; use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{load_maps, AccountMaps}; +use crate::load_mut; use crate::math::casting::Cast; -use crate::math::constants::QUOTE_SPOT_MARKET_INDEX; +use crate::math::constants::{ + GOV_SPOT_MARKET_INDEX, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, +}; +use crate::math::lp_pool::perp_lp_pool_settlement; use crate::math::margin::get_margin_calculation_for_disable_high_leverage_mode; use crate::math::margin::{calculate_user_equity, meets_settle_pnl_maintenance_margin_requirement}; use crate::math::orders::{estimate_price_from_side, find_bids_and_asks_from_users}; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; use crate::math::spot_withdraw::validate_spot_market_vault_amount; use crate::optional_accounts::{get_token_mint, update_prelaunch_oracle}; +use crate::signer::get_signer_seeds; +use crate::state::amm_cache::CacheInfo; +use crate::state::events::LPSettleRecord; use crate::state::events::{DeleteUserRecord, OrderActionExplanation, SignedMsgOrderRecord}; use crate::state::fill_mode::FillMode; use crate::state::fulfillment_params::drift::MatchFulfillmentParams; @@ -41,14 +55,23 @@ use crate::state::fulfillment_params::phoenix::PhoenixFulfillmentParams; use crate::state::fulfillment_params::serum::SerumFulfillmentParams; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; use crate::state::insurance_fund_stake::InsuranceFundStake; +use crate::state::lp_pool::Constituent; +use crate::state::lp_pool::LPPool; +use crate::state::lp_pool::CONSTITUENT_PDA_SEED; +use crate::state::lp_pool::SETTLE_AMM_ORACLE_MAX_DELAY; use crate::state::oracle_map::OracleMap; use crate::state::order_params::{OrderParams, PlaceOrderOptions}; +use crate::state::paused_operations::PerpLpOperation; use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::{ContractType, MarketStatus, PerpMarket}; use crate::state::perp_market_map::{ get_market_set_for_spot_positions, get_market_set_for_user_positions, get_market_set_from_list, get_writable_perp_market_set, get_writable_perp_market_set_from_vec, MarketSet, PerpMarketMap, }; +use crate::state::revenue_share::RevenueShareEscrowZeroCopyMut; +use crate::state::revenue_share::RevenueShareOrder; +use crate::state::revenue_share::RevenueShareOrderBitFlag; +use crate::state::revenue_share_map::load_revenue_share_map; use crate::state::settle_pnl_mode::SettlePnlMode; use crate::state::signed_msg_user::{ SignedMsgOrderId, SignedMsgUserOrdersLoader, SignedMsgUserOrdersZeroCopyMut, @@ -64,14 +87,13 @@ use crate::state::user::{ MarginMode, MarketType, OrderStatus, OrderTriggerCondition, OrderType, User, UserStats, }; use crate::state::user_map::{load_user_map, load_user_maps, UserMap, UserStatsMap}; +use crate::state::zero_copy::AccountZeroCopyMut; +use crate::state::zero_copy::ZeroCopyLoader; +use crate::validate; use crate::validation::sig_verification::verify_and_decode_ed25519_msg; use crate::validation::user::{validate_user_deletion, validate_user_is_idle}; -use crate::{ - controller, load, math, print_error, safe_decrement, OracleSource, GOV_SPOT_MARKET_INDEX, -}; -use crate::{load_mut, QUOTE_PRECISION_U64}; +use crate::{controller, load, math, print_error, safe_decrement, OracleSource}; use crate::{math_error, ID}; -use crate::{validate, QUOTE_PRECISION_I128}; use anchor_spl::associated_token::AssociatedToken; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; @@ -124,7 +146,7 @@ fn fill_order<'c: 'info, 'info>( let clock = &Clock::get()?; let state = &ctx.accounts.state; - let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mut remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); let AccountMaps { perp_market_map, spot_market_map, @@ -140,6 +162,17 @@ fn fill_order<'c: 'info, 'info>( let (makers_and_referrer, makers_and_referrer_stats) = load_user_maps(remaining_accounts_iter, true)?; + let builder_codes_enabled = state.builder_codes_enabled(); + let builder_referral_enabled = state.builder_referral_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + &mut remaining_accounts_iter, + &load!(ctx.accounts.user)?.authority, + )? + } else { + None + }; + controller::repeg::update_amm( market_index, &perp_market_map, @@ -163,6 +196,8 @@ fn fill_order<'c: 'info, 'info>( None, clock, FillMode::Fill, + &mut escrow.as_mut(), + builder_referral_enabled, )?; Ok(()) @@ -632,6 +667,12 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( let mut taker = load_mut!(ctx.accounts.user)?; let mut signed_msg_taker = ctx.accounts.signed_msg_user_orders.load_mut()?; + let escrow = if state.builder_codes_enabled() { + get_revenue_share_escrow_account(&mut remaining_accounts, &taker.authority)? + } else { + None + }; + place_signed_msg_taker_order( taker_key, &mut taker, @@ -642,6 +683,7 @@ pub fn handle_place_signed_msg_taker_order<'c: 'info, 'info>( &spot_market_map, &mut oracle_map, high_leverage_mode_config, + escrow, state, is_delegate_signer, )?; @@ -658,6 +700,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( spot_market_map: &SpotMarketMap, oracle_map: &mut OracleMap, high_leverage_mode_config: Option>, + escrow: Option>, state: &State, is_delegate_signer: bool, ) -> Result<()> { @@ -686,6 +729,43 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( is_delegate_signer, )?; + let mut escrow_zc: Option> = None; + let mut builder_fee_bps: Option = None; + if state.builder_codes_enabled() + && verified_message_and_signature.builder_idx.is_some() + && verified_message_and_signature + .builder_fee_tenth_bps + .is_some() + { + if let Some(mut escrow) = escrow { + let builder_idx = verified_message_and_signature.builder_idx.unwrap(); + let builder_fee = verified_message_and_signature + .builder_fee_tenth_bps + .unwrap(); + + validate!( + escrow.fixed.authority == taker.authority, + ErrorCode::InvalidUserAccount, + "RevenueShareEscrow account must be owned by taker", + )?; + + let builder = escrow.get_approved_builder_mut(builder_idx)?; + + if builder.is_revoked() { + return Err(ErrorCode::BuilderRevoked.into()); + } + + if builder_fee > builder.max_fee_tenth_bps { + return Err(ErrorCode::InvalidBuilderFee.into()); + } + + builder_fee_bps = Some(builder_fee); + escrow_zc = Some(escrow); + } else { + msg!("Order has builder fee but no escrow account found, in the future this tx will fail."); + } + } + if is_delegate_signer { validate!( verified_message_and_signature.delegate_signed_taker_pubkey == Some(taker_key), @@ -794,6 +874,33 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ..OrderParams::default() }; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use - 1; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add stop loss order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -809,6 +916,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( existing_position_direction_override: Some(matching_taker_order_params.direction), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; } @@ -831,6 +939,33 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ..OrderParams::default() }; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use - 1; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add take profit order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -846,11 +981,39 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( existing_position_direction_override: Some(matching_taker_order_params.direction), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; } signed_msg_order_id.order_id = taker_order_id_to_use; signed_msg_account.add_signed_msg_order_id(signed_msg_order_id)?; + let mut builder_order = if let Some(ref mut escrow) = escrow_zc { + let new_order_id = taker_order_id_to_use; + let new_order_index = taker + .orders + .iter() + .position(|order| order.is_available()) + .ok_or(ErrorCode::MaxNumberOfOrders)?; + match escrow.add_order(RevenueShareOrder::new( + verified_message_and_signature.builder_idx.unwrap(), + taker.sub_account_id, + new_order_id, + builder_fee_bps.unwrap(), + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Open as u8, + new_order_index as u8, + )) { + Ok(order_idx) => escrow.get_order_mut(order_idx).ok(), + Err(_) => { + msg!("Failed to add order, escrow is full"); + None + } + } + } else { + None + }; + controller::orders::place_perp_order( state, taker, @@ -866,6 +1029,7 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( signed_msg_taker_order_slot: Some(order_slot), ..PlaceOrderOptions::default() }, + &mut builder_order, )?; let order_params_hash = @@ -881,6 +1045,10 @@ pub fn place_signed_msg_taker_order<'c: 'info, 'info>( ts: clock.unix_timestamp, }); + if let Some(ref mut escrow) = escrow_zc { + escrow.revoke_completed_orders(taker)?; + }; + Ok(()) } @@ -903,18 +1071,30 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( "user have pool_id 0" )?; + let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); + let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set(market_index), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, Some(state.oracle_guard_rails), )?; + let (mut builder_escrow, maybe_rev_share_map) = + if state.builder_codes_enabled() || state.builder_referral_enabled() { + ( + get_revenue_share_escrow_account(&mut remaining_accounts, &user.authority)?, + load_revenue_share_map(&mut remaining_accounts).ok(), + ) + } else { + (None, None) + }; + let market_in_settlement = perp_market_map.get_ref(&market_index)?.status == MarketStatus::Settlement; @@ -940,8 +1120,7 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( &mut oracle_map, state, &clock, - ) - .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; + )?; controller::pnl::settle_pnl( market_index, @@ -955,8 +1134,27 @@ pub fn handle_settle_pnl<'c: 'info, 'info>( state, None, SettlePnlMode::MustSettle, - ) - .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; + )?; + } + + if state.builder_codes_enabled() || state.builder_referral_enabled() { + if let Some(ref mut escrow) = builder_escrow { + escrow.revoke_completed_orders(user)?; + if let Some(ref builder_map) = maybe_rev_share_map { + controller::revenue_share::sweep_completed_revenue_share_for_market( + market_index, + escrow, + &perp_market_map, + &spot_market_map, + builder_map, + clock.unix_timestamp, + state.builder_codes_enabled(), + state.builder_referral_enabled(), + )?; + } else { + msg!("Builder Users not provided, but RevenueEscrow was provided"); + } + } } let spot_market = spot_market_map.get_quote_spot_market()?; @@ -979,18 +1177,30 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( let user_key = ctx.accounts.user.key(); let user = &mut load_mut!(ctx.accounts.user)?; + let mut remaining_accounts = ctx.remaining_accounts.iter().peekable(); + let AccountMaps { perp_market_map, spot_market_map, mut oracle_map, } = load_maps( - &mut ctx.remaining_accounts.iter().peekable(), + &mut remaining_accounts, &get_writable_perp_market_set_from_vec(&market_indexes), &get_writable_spot_market_set(QUOTE_SPOT_MARKET_INDEX), clock.slot, Some(state.oracle_guard_rails), )?; + let (mut builder_escrow, maybe_rev_share_map) = + if state.builder_codes_enabled() || state.builder_referral_enabled() { + ( + get_revenue_share_escrow_account(&mut remaining_accounts, &user.authority)?, + load_revenue_share_map(&mut remaining_accounts).ok(), + ) + } else { + (None, None) + }; + let meets_margin_requirement = meets_settle_pnl_maintenance_margin_requirement( user, &perp_market_map, @@ -1024,8 +1234,7 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( &mut oracle_map, state, &clock, - ) - .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; + )?; controller::pnl::settle_pnl( *market_index, @@ -1039,8 +1248,27 @@ pub fn handle_settle_multiple_pnls<'c: 'info, 'info>( state, Some(meets_margin_requirement), mode, - ) - .map(|_| ErrorCode::InvalidOracleForSettlePnl)?; + )?; + } + + if state.builder_codes_enabled() || state.builder_referral_enabled() { + if let Some(ref mut escrow) = builder_escrow { + escrow.revoke_completed_orders(user)?; + if let Some(ref builder_map) = maybe_rev_share_map { + controller::revenue_share::sweep_completed_revenue_share_for_market( + *market_index, + escrow, + &perp_market_map, + &spot_market_map, + builder_map, + clock.unix_timestamp, + state.builder_codes_enabled(), + state.builder_referral_enabled(), + )?; + } else { + msg!("Builder Users not provided, but RevenueEscrow was provided"); + } + } } } @@ -1506,11 +1734,13 @@ pub fn handle_liquidate_spot_with_swap_begin<'c: 'info, 'info>( jupiter_mainnet_3::ID, jupiter_mainnet_4::ID, jupiter_mainnet_6::ID, + dflow_mainnet_aggregator_4::ID, + titan_mainnet_argos_v1::ID, ]; validate!( whitelisted_programs.contains(&ix.program_id), ErrorCode::InvalidLiquidateSpotWithSwap, - "only allowed to pass in ixs to token, openbook, and Jupiter v3/v4/v6 programs" + "only allowed to pass in ixs to ATA, openbook, Jupiter v3/v4/v6, dflow, or titan programs" )?; for meta in ix.accounts.iter() { @@ -3083,6 +3313,383 @@ pub fn handle_pause_spot_market_deposit_withdraw( Ok(()) } +pub fn handle_settle_perp_to_lp_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, +) -> Result<()> { + use perp_lp_pool_settlement::*; + + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + let now = Clock::get()?.unix_timestamp; + + if !state.allow_settle_lp_pool() { + msg!("settle lp pool disabled"); + return Err(ErrorCode::SettleLpPoolDisabled.into()); + } + + let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> = + ctx.accounts.amm_cache.load_zc_mut()?; + let quote_market = &mut ctx.accounts.quote_market.load_mut()?; + let mut quote_constituent = ctx.accounts.constituent.load_mut()?; + let lp_pool_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + let tvl_before = quote_market + .get_tvl()? + .safe_add(quote_constituent.vault_token_balance as u128)?; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let AccountMaps { + perp_market_map, + spot_market_map: _, + oracle_map: _, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &MarketSet::new(), + slot, + None, + )?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut *quote_market, + None, + now, + )?; + + for (_, perp_market_loader) in perp_market_map.0.iter() { + let mut perp_market = perp_market_loader.load_mut()?; + if lp_pool.lp_pool_id != perp_market.lp_pool_id { + msg!( + "Perp market {} does not have the same lp pool id as the lp pool being settled to: {} != {}", + perp_market.market_index, + perp_market.lp_pool_id, + lp_pool.lp_pool_id + ); + return Err(ErrorCode::InvalidLpPoolId.into()); + } + + if perp_market.lp_status == 0 + || PerpLpOperation::is_operation_paused( + perp_market.lp_paused_operations, + PerpLpOperation::SettleQuoteOwed, + ) + { + continue; + } + + let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + + // Early validation checks + if slot.saturating_sub(cached_info.oracle_slot) > SETTLE_AMM_ORACLE_MAX_DELAY { + msg!( + "Skipping settling perp market {} to dlp because oracle slot is not up to date", + perp_market.market_index + ); + continue; + } + + validate_market_within_price_band(&perp_market, state, cached_info.oracle_price)?; + + if perp_market.is_operation_paused(PerpOperation::SettlePnl) { + msg!( + "Cannot settle pnl under current market = {} status", + perp_market.market_index + ); + continue; + } + + if cached_info.slot != slot { + msg!("Skipping settling perp market {} to lp pool because amm cache was not updated in the same slot", + perp_market.market_index); + return Err(ErrorCode::AMMCacheStale.into()); + } + + quote_constituent.sync_token_balance(ctx.accounts.constituent_quote_token_account.amount); + + // Create settlement context + let settlement_ctx = SettlementContext { + quote_owed_from_lp: cached_info.quote_owed_from_lp_pool, + quote_constituent_token_balance: quote_constituent.vault_token_balance, + fee_pool_balance: get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + quote_market, + &SpotBalanceType::Deposit, + )?, + pnl_pool_balance: get_token_amount( + perp_market.pnl_pool.scaled_balance, + quote_market, + &SpotBalanceType::Deposit, + )?, + quote_market, + max_settle_quote_amount: lp_pool.max_settle_quote_amount, + }; + + // Calculate settlement + let settlement_result = calculate_settlement_amount(&settlement_ctx)?; + validate_settlement_amount( + &settlement_ctx, + &settlement_result, + &perp_market, + quote_market, + )?; + + if settlement_result.direction == SettlementDirection::None { + continue; + } + + // Execute token transfer + match settlement_result.direction { + SettlementDirection::FromLpPool => { + execute_token_transfer( + &ctx.accounts.token_program, + &ctx.accounts.constituent_quote_token_account, + &ctx.accounts.quote_token_vault, + &ctx.accounts + .constituent_quote_token_account + .to_account_info(), + &Constituent::get_vault_signer_seeds( + "e_constituent.lp_pool, + "e_constituent.spot_market_index, + "e_constituent.vault_bump, + ), + settlement_result.amount_transferred, + Some(remaining_accounts_iter), + )?; + } + SettlementDirection::ToLpPool => { + execute_token_transfer( + &ctx.accounts.token_program, + &ctx.accounts.quote_token_vault, + &ctx.accounts.constituent_quote_token_account, + &ctx.accounts.drift_signer, + &get_signer_seeds(&state.signer_nonce), + settlement_result.amount_transferred, + Some(remaining_accounts_iter), + )?; + } + SettlementDirection::None => unreachable!(), + } + + // Update market pools + update_perp_market_pools_and_quote_market_balance( + &mut perp_market, + &settlement_result, + quote_market, + )?; + + // Emit settle event + let record_id = get_then_update_id!(lp_pool, settle_id); + emit!(LPSettleRecord { + record_id, + last_ts: cached_info.last_settle_ts, + last_slot: cached_info.last_settle_slot, + slot, + ts: now, + perp_market_index: perp_market.market_index, + settle_to_lp_amount: match settlement_result.direction { + SettlementDirection::FromLpPool => settlement_result + .amount_transferred + .cast::()? + .saturating_mul(-1), + SettlementDirection::ToLpPool => + settlement_result.amount_transferred.cast::()?, + SettlementDirection::None => unreachable!(), + }, + perp_amm_pnl_delta: cached_info + .last_net_pnl_pool_token_amount + .safe_sub(cached_info.last_settle_amm_pnl)? + .cast::()?, + perp_amm_ex_fee_delta: cached_info + .last_exchange_fees + .safe_sub(cached_info.last_settle_amm_ex_fees)? + .cast::()?, + lp_aum: lp_pool.last_aum, + lp_price: lp_pool.get_price(lp_pool.token_supply)?, + lp_pool: lp_pool_key, + }); + + // Calculate new quote owed amount + let new_quote_owed = match settlement_result.direction { + SettlementDirection::FromLpPool => cached_info + .quote_owed_from_lp_pool + .safe_sub(settlement_result.amount_transferred as i64)?, + SettlementDirection::ToLpPool => cached_info + .quote_owed_from_lp_pool + .safe_add(settlement_result.amount_transferred as i64)?, + SettlementDirection::None => cached_info.quote_owed_from_lp_pool, + }; + + // Update cache info + update_cache_info(cached_info, &settlement_result, new_quote_owed, slot, now)?; + + // Update LP pool stats + match settlement_result.direction { + SettlementDirection::FromLpPool => { + lp_pool.cumulative_quote_sent_to_perp_markets = lp_pool + .cumulative_quote_sent_to_perp_markets + .saturating_add(settlement_result.amount_transferred as u128); + } + SettlementDirection::ToLpPool => { + lp_pool.cumulative_quote_received_from_perp_markets = lp_pool + .cumulative_quote_received_from_perp_markets + .saturating_add(settlement_result.amount_transferred as u128); + } + SettlementDirection::None => {} + } + + // Sync constituent token balance + let constituent_token_account = &mut ctx.accounts.constituent_quote_token_account; + constituent_token_account.reload()?; + quote_constituent.sync_token_balance(constituent_token_account.amount); + } + + // Final validation + ctx.accounts.quote_token_vault.reload()?; + math::spot_withdraw::validate_spot_market_vault_amount( + quote_market, + ctx.accounts.quote_token_vault.amount, + )?; + + let tvl_after = quote_market + .get_tvl()? + .safe_add(quote_constituent.vault_token_balance as u128)?; + + validate!( + tvl_before.safe_sub(tvl_after)? <= 10, + ErrorCode::LpPoolSettleInvariantBreached, + "LP pool settlement would decrease TVL: {} -> {}", + tvl_before, + tvl_after + )?; + + Ok(()) +} + +pub fn handle_update_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateAmmCache<'info>>, +) -> Result<()> { + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mut amm_cache: AccountZeroCopyMut<'_, CacheInfo, _> = + ctx.accounts.amm_cache.load_zc_mut()?; + + let state = &ctx.accounts.state; + let quote_market = ctx.accounts.quote_market.load()?; + + let AccountMaps { + perp_market_map, + spot_market_map: _, + mut oracle_map, + } = load_maps( + remaining_accounts_iter, + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + let slot = Clock::get()?.slot; + + for (_, perp_market_loader) in perp_market_map.0.iter() { + let perp_market = perp_market_loader.load()?; + if perp_market.lp_status == 0 { + continue; + } + let cached_info = amm_cache.get_mut(perp_market.market_index as u32); + + validate!( + perp_market.oracle_id() == cached_info.oracle_id()?, + ErrorCode::DefaultError, + "oracle id mismatch between amm cache and perp market" + )?; + + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + let mm_oracle_price_data = perp_market.get_mm_oracle_price_data( + *oracle_data, + slot, + &ctx.accounts.state.oracle_guard_rails.validity, + )?; + + cached_info.update_perp_market_fields(&perp_market)?; + cached_info.try_update_oracle_info( + slot, + &mm_oracle_price_data, + &perp_market, + &state.oracle_guard_rails, + )?; + + if perp_market.lp_status != 0 + && !PerpLpOperation::is_operation_paused( + perp_market.lp_paused_operations, + PerpLpOperation::TrackAmmRevenue, + ) + { + amm_cache.update_amount_owed_from_lp_pool(&perp_market, "e_market)?; + } + } + + Ok(()) +} + +#[derive(Accounts)] +pub struct SettleAmmPnlToLp<'info> { + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + constraint = keeper.key() == crate::ids::lp_pool_swap_wallet::id() || keeper.key() == admin_hot_wallet::id() || keeper.key() == state.admin.key(), + )] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, + #[account( + mut, + owner = crate::ID, + seeds = [b"spot_market", QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump, + )] + pub quote_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + owner = crate::ID, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump = constituent.load()?.bump, + constraint = constituent.load()?.mint.eq("e_market.load()?.mint) + )] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + )] + pub constituent_quote_token_account: Box>, + #[account( + mut, + address = quote_market.load()?.vault, + token::authority = drift_signer, + )] + pub quote_token_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateAmmCache<'info> { + #[account(mut)] + pub keeper: Signer<'info>, + pub state: Box>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, + #[account( + owner = crate::ID, + seeds = [b"spot_market", QUOTE_SPOT_MARKET_INDEX.to_le_bytes().as_ref()], + bump, + )] + pub quote_market: AccountLoader<'info, SpotMarket>, +} + #[derive(Accounts)] pub struct FillOrder<'info> { pub state: Box>, diff --git a/programs/drift/src/instructions/lp_admin.rs b/programs/drift/src/instructions/lp_admin.rs new file mode 100644 index 0000000000..28e6026a44 --- /dev/null +++ b/programs/drift/src/instructions/lp_admin.rs @@ -0,0 +1,1392 @@ +use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; +use crate::error::ErrorCode; +use crate::ids::{lp_pool_hot_wallet, lp_pool_swap_wallet, WHITELISTED_SWAP_PROGRAMS}; +use crate::instructions::optional_accounts::{get_token_mint, load_maps, AccountMaps}; +use crate::math::constants::{PRICE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX}; +use crate::math::safe_math::SafeMath; +use crate::state::amm_cache::{AmmCache, CacheInfo, AMM_POSITIONS_CACHE}; +use crate::state::lp_pool::{ + AmmConstituentDatum, AmmConstituentMapping, Constituent, ConstituentCorrelations, + ConstituentTargetBase, LPPool, TargetsDatum, AMM_MAP_PDA_SEED, + CONSTITUENT_CORRELATIONS_PDA_SEED, CONSTITUENT_PDA_SEED, CONSTITUENT_TARGET_BASE_PDA_SEED, + CONSTITUENT_VAULT_PDA_SEED, +}; +use crate::state::perp_market::PerpMarket; +use crate::state::perp_market_map::MarketSet; +use crate::state::spot_market::SpotMarket; +use crate::state::state::State; +use crate::validate; +use crate::{controller, load_mut}; +use anchor_lang::prelude::*; +use anchor_lang::Discriminator; +use anchor_spl::associated_token::AssociatedToken; +use anchor_spl::token::Token; +use anchor_spl::token_2022::Token2022; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::ids::{lighthouse, marinade_mainnet}; + +use crate::state::traits::Size; +use solana_program::sysvar::instructions; + +use super::optional_accounts::get_token_interface; + +pub fn handle_initialize_lp_pool( + ctx: Context, + lp_pool_id: u8, + min_mint_fee: i64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, + whitelist_mint: Pubkey, +) -> Result<()> { + let lp_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_init()?; + let mint = &ctx.accounts.mint; + + validate!( + mint.decimals == 6, + ErrorCode::DefaultError, + "lp mint must have 6 decimals" + )?; + + validate!( + mint.mint_authority == Some(lp_key).into(), + ErrorCode::DefaultError, + "lp mint must have drift_signer as mint authority" + )?; + + *lp_pool = LPPool { + pubkey: ctx.accounts.lp_pool.key(), + mint: mint.key(), + constituent_target_base: ctx.accounts.constituent_target_base.key(), + constituent_correlations: ctx.accounts.constituent_correlations.key(), + constituents: 0, + max_aum, + last_aum: 0, + last_aum_slot: 0, + max_settle_quote_amount: max_settle_quote_amount_per_market, + _padding: 0, + total_mint_redeem_fees_paid: 0, + bump: ctx.bumps.lp_pool, + min_mint_fee, + token_supply: 0, + mint_redeem_id: 1, + settle_id: 1, + quote_consituent_index: 0, + cumulative_quote_sent_to_perp_markets: 0, + cumulative_quote_received_from_perp_markets: 0, + gamma_execution: 2, + volatility: 4, + xi: 2, + target_oracle_delay_fee_bps_per_10_slots: 0, + target_position_delay_fee_bps_per_10_slots: 0, + lp_pool_id, + padding: [0u8; 174], + whitelist_mint, + }; + + let amm_constituent_mapping = &mut ctx.accounts.amm_constituent_mapping; + amm_constituent_mapping.lp_pool = ctx.accounts.lp_pool.key(); + amm_constituent_mapping.bump = ctx.bumps.amm_constituent_mapping; + amm_constituent_mapping + .weights + .resize_with(0 as usize, AmmConstituentDatum::default); + amm_constituent_mapping.validate()?; + + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + constituent_target_base.lp_pool = ctx.accounts.lp_pool.key(); + constituent_target_base.bump = ctx.bumps.constituent_target_base; + constituent_target_base + .targets + .resize_with(0 as usize, TargetsDatum::default); + constituent_target_base.validate()?; + + let consituent_correlations = &mut ctx.accounts.constituent_correlations; + consituent_correlations.lp_pool = ctx.accounts.lp_pool.key(); + consituent_correlations.bump = ctx.bumps.constituent_correlations; + consituent_correlations.correlations.resize(0 as usize, 0); + consituent_correlations.validate()?; + + Ok(()) +} + +pub fn handle_initialize_constituent<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + max_borrow_token_amount: u64, + oracle_staleness_threshold: u64, + cost_to_trade_bps: i32, + constituent_derivative_index: Option, + constituent_derivative_depeg_threshold: u64, + derivative_weight: u64, + volatility: u64, + gamma_execution: u8, + gamma_inventory: u8, + xi: u8, + new_constituent_correlations: Vec, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_init()?; + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + let current_len = constituent_target_base.targets.len(); + + constituent_target_base + .targets + .resize_with((current_len + 1) as usize, TargetsDatum::default); + + let new_target = constituent_target_base + .targets + .get_mut(current_len) + .unwrap(); + new_target.cost_to_trade_bps = cost_to_trade_bps; + constituent_target_base.validate()?; + + msg!( + "initializing constituent {} with spot market index {}", + lp_pool.constituents, + spot_market_index + ); + + validate!( + derivative_weight <= PRICE_PRECISION_U64, + ErrorCode::InvalidConstituent, + "stablecoin_weight must be between 0 and 1", + )?; + + if let Some(constituent_derivative_index) = constituent_derivative_index { + validate!( + constituent_derivative_index < lp_pool.constituents as i16, + ErrorCode::InvalidConstituent, + "constituent_derivative_index must be less than lp_pool.constituents" + )?; + } + + constituent.spot_market_index = spot_market_index; + constituent.constituent_index = lp_pool.constituents; + constituent.decimals = decimals; + constituent.max_weight_deviation = max_weight_deviation; + constituent.swap_fee_min = swap_fee_min; + constituent.swap_fee_max = swap_fee_max; + constituent.oracle_staleness_threshold = oracle_staleness_threshold; + constituent.pubkey = ctx.accounts.constituent.key(); + constituent.mint = ctx.accounts.spot_market_mint.key(); + constituent.vault = ctx.accounts.constituent_vault.key(); + constituent.bump = ctx.bumps.constituent; + constituent.vault_bump = ctx.bumps.constituent_vault; + constituent.max_borrow_token_amount = max_borrow_token_amount; + constituent.lp_pool = lp_pool.pubkey; + constituent.constituent_index = (constituent_target_base.targets.len() - 1) as u16; + constituent.next_swap_id = 1; + constituent.constituent_derivative_index = constituent_derivative_index.unwrap_or(-1); + constituent.constituent_derivative_depeg_threshold = constituent_derivative_depeg_threshold; + constituent.derivative_weight = derivative_weight; + constituent.volatility = volatility; + constituent.gamma_execution = gamma_execution; + constituent.gamma_inventory = gamma_inventory; + constituent.spot_balance.market_index = spot_market_index; + constituent.xi = xi; + lp_pool.constituents += 1; + + if constituent.spot_market_index == QUOTE_SPOT_MARKET_INDEX { + lp_pool.quote_consituent_index = constituent.constituent_index; + } + + let constituent_correlations = &mut ctx.accounts.constituent_correlations; + validate!( + new_constituent_correlations.len() as u16 == lp_pool.constituents - 1, + ErrorCode::InvalidConstituent, + "expected {} correlations, got {}", + lp_pool.constituents, + new_constituent_correlations.len() + )?; + constituent_correlations.add_new_constituent(&new_constituent_correlations)?; + + Ok(()) +} + +pub fn handle_update_constituent_status<'info>( + ctx: Context, + new_status: u8, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + msg!( + "constituent status: {:?} -> {:?}", + constituent.status, + new_status + ); + constituent.status = new_status; + Ok(()) +} + +pub fn handle_update_constituent_paused_operations<'info>( + ctx: Context, + paused_operations: u8, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + msg!( + "constituent paused operations: {:?} -> {:?}", + constituent.paused_operations, + paused_operations + ); + constituent.paused_operations = paused_operations; + Ok(()) +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct ConstituentParams { + pub max_weight_deviation: Option, + pub swap_fee_min: Option, + pub swap_fee_max: Option, + pub max_borrow_token_amount: Option, + pub oracle_staleness_threshold: Option, + pub cost_to_trade_bps: Option, + pub constituent_derivative_index: Option, + pub derivative_weight: Option, + pub volatility: Option, + pub gamma_execution: Option, + pub gamma_inventory: Option, + pub xi: Option, +} + +pub fn handle_update_constituent_params<'info>( + ctx: Context, + constituent_params: ConstituentParams, +) -> Result<()> { + let mut constituent = ctx.accounts.constituent.load_mut()?; + if constituent.spot_balance.market_index != constituent.spot_market_index { + constituent.spot_balance.market_index = constituent.spot_market_index; + } + + if let Some(max_weight_deviation) = constituent_params.max_weight_deviation { + msg!( + "max_weight_deviation: {:?} -> {:?}", + constituent.max_weight_deviation, + max_weight_deviation + ); + constituent.max_weight_deviation = max_weight_deviation; + } + + if let Some(swap_fee_min) = constituent_params.swap_fee_min { + msg!( + "swap_fee_min: {:?} -> {:?}", + constituent.swap_fee_min, + swap_fee_min + ); + constituent.swap_fee_min = swap_fee_min; + } + + if let Some(swap_fee_max) = constituent_params.swap_fee_max { + msg!( + "swap_fee_max: {:?} -> {:?}", + constituent.swap_fee_max, + swap_fee_max + ); + constituent.swap_fee_max = swap_fee_max; + } + + if let Some(oracle_staleness_threshold) = constituent_params.oracle_staleness_threshold { + msg!( + "oracle_staleness_threshold: {:?} -> {:?}", + constituent.oracle_staleness_threshold, + oracle_staleness_threshold + ); + constituent.oracle_staleness_threshold = oracle_staleness_threshold; + } + + if let Some(cost_to_trade_bps) = constituent_params.cost_to_trade_bps { + let constituent_target_base = &mut ctx.accounts.constituent_target_base; + + let target = constituent_target_base + .targets + .get_mut(constituent.constituent_index as usize) + .unwrap(); + + msg!( + "cost_to_trade: {:?} -> {:?}", + target.cost_to_trade_bps, + cost_to_trade_bps + ); + target.cost_to_trade_bps = cost_to_trade_bps; + } + + if let Some(derivative_weight) = constituent_params.derivative_weight { + msg!( + "derivative_weight: {:?} -> {:?}", + constituent.derivative_weight, + derivative_weight + ); + constituent.derivative_weight = derivative_weight; + } + + if let Some(constituent_derivative_index) = constituent_params.constituent_derivative_index { + msg!( + "constituent_derivative_index: {:?} -> {:?}", + constituent.constituent_derivative_index, + constituent_derivative_index + ); + constituent.constituent_derivative_index = constituent_derivative_index; + } + + if let Some(gamma_execution) = constituent_params.gamma_execution { + msg!( + "gamma_execution: {:?} -> {:?}", + constituent.gamma_execution, + gamma_execution + ); + constituent.gamma_execution = gamma_execution; + } + + if let Some(gamma_inventory) = constituent_params.gamma_inventory { + msg!( + "gamma_inventory: {:?} -> {:?}", + constituent.gamma_inventory, + gamma_inventory + ); + constituent.gamma_inventory = gamma_inventory; + } + + if let Some(xi) = constituent_params.xi { + msg!("xi: {:?} -> {:?}", constituent.xi, xi); + constituent.xi = xi; + } + + if let Some(max_borrow_token_amount) = constituent_params.max_borrow_token_amount { + msg!( + "max_borrow_token_amount: {:?} -> {:?}", + constituent.max_borrow_token_amount, + max_borrow_token_amount + ); + constituent.max_borrow_token_amount = max_borrow_token_amount; + } + + if let Some(volatility) = constituent_params.volatility { + msg!( + "volatility: {:?} -> {:?}", + constituent.volatility, + volatility + ); + constituent.volatility = volatility; + } + + Ok(()) +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct LpPoolParams { + pub max_settle_quote_amount: Option, + pub volatility: Option, + pub gamma_execution: Option, + pub xi: Option, + pub max_aum: Option, + pub whitelist_mint: Option, +} + +pub fn handle_update_lp_pool_params<'info>( + ctx: Context, + lp_pool_params: LpPoolParams, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + if let Some(max_settle_quote_amount) = lp_pool_params.max_settle_quote_amount { + msg!( + "max_settle_quote_amount: {:?} -> {:?}", + lp_pool.max_settle_quote_amount, + max_settle_quote_amount + ); + lp_pool.max_settle_quote_amount = max_settle_quote_amount; + } + + if let Some(volatility) = lp_pool_params.volatility { + msg!("volatility: {:?} -> {:?}", lp_pool.volatility, volatility); + lp_pool.volatility = volatility; + } + + if let Some(gamma_execution) = lp_pool_params.gamma_execution { + msg!( + "gamma_execution: {:?} -> {:?}", + lp_pool.gamma_execution, + gamma_execution + ); + lp_pool.gamma_execution = gamma_execution; + } + + if let Some(xi) = lp_pool_params.xi { + msg!("xi: {:?} -> {:?}", lp_pool.xi, xi); + lp_pool.xi = xi; + } + + if let Some(whitelist_mint) = lp_pool_params.whitelist_mint { + msg!( + "whitelist_mint: {:?} -> {:?}", + lp_pool.whitelist_mint, + whitelist_mint + ); + lp_pool.whitelist_mint = whitelist_mint; + } + + if let Some(max_aum) = lp_pool_params.max_aum { + validate!( + max_aum >= lp_pool.max_aum, + ErrorCode::DefaultError, + "new max_aum must be greater than or equal to current max_aum" + )?; + msg!("max_aum: {:?} -> {:?}", lp_pool.max_aum, max_aum); + lp_pool.max_aum = max_aum; + } + + Ok(()) +} + +pub fn handle_update_amm_constituent_mapping_data<'info>( + ctx: Context, + amm_constituent_mapping_data: Vec, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + + for datum in amm_constituent_mapping_data { + let existing_datum = amm_mapping.weights.iter().position(|existing_datum| { + existing_datum.perp_market_index == datum.perp_market_index + && existing_datum.constituent_index == datum.constituent_index + }); + + if existing_datum.is_none() { + msg!( + "AmmConstituentDatum not found for perp_market_index {} and constituent_index {}", + datum.perp_market_index, + datum.constituent_index + ); + return Err(ErrorCode::InvalidAmmConstituentMappingArgument.into()); + } + + amm_mapping.weights[existing_datum.unwrap()] = AmmConstituentDatum { + perp_market_index: datum.perp_market_index, + constituent_index: datum.constituent_index, + weight: datum.weight, + last_slot: Clock::get()?.slot, + ..AmmConstituentDatum::default() + }; + + msg!( + "Updated AmmConstituentDatum for perp_market_index {} and constituent_index {} to {}", + datum.perp_market_index, + datum.constituent_index, + datum.weight + ); + } + + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_remove_amm_constituent_mapping_data<'info>( + ctx: Context, + perp_market_index: u16, + constituent_index: u16, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + + let position = amm_mapping.weights.iter().position(|existing_datum| { + existing_datum.perp_market_index == perp_market_index + && existing_datum.constituent_index == constituent_index + }); + + if position.is_none() { + msg!( + "Not found for perp_market_index {} and constituent_index {}", + perp_market_index, + constituent_index + ); + return Err(ErrorCode::InvalidAmmConstituentMappingArgument.into()); + } + + amm_mapping.weights.remove(position.unwrap()); + amm_mapping.weights.shrink_to_fit(); + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_add_amm_constituent_data<'info>( + ctx: Context, + init_amm_constituent_mapping_data: Vec, +) -> Result<()> { + let amm_mapping = &mut ctx.accounts.amm_constituent_mapping; + let constituent_target_base = &ctx.accounts.constituent_target_base; + let state = &ctx.accounts.state; + let mut current_len = amm_mapping.weights.len(); + + for init_datum in init_amm_constituent_mapping_data { + let perp_market_index = init_datum.perp_market_index; + + validate!( + perp_market_index < state.number_of_markets, + ErrorCode::InvalidAmmConstituentMappingArgument, + "perp_market_index too large compared to number of markets" + )?; + + validate!( + (init_datum.constituent_index as usize) < constituent_target_base.targets.len(), + ErrorCode::InvalidAmmConstituentMappingArgument, + "constituent_index too large compared to number of constituents in target weights" + )?; + + let constituent_index = init_datum.constituent_index; + let mut datum = AmmConstituentDatum::default(); + datum.perp_market_index = perp_market_index; + datum.constituent_index = constituent_index; + datum.weight = init_datum.weight; + datum.last_slot = Clock::get()?.slot; + + // Check if the datum already exists + let exists = amm_mapping.weights.iter().any(|d| { + d.perp_market_index == perp_market_index && d.constituent_index == constituent_index + }); + + validate!( + !exists, + ErrorCode::InvalidAmmConstituentMappingArgument, + "AmmConstituentDatum already exists for perp_market_index {} and constituent_index {}", + perp_market_index, + constituent_index + )?; + + // Add the new datum to the mapping + current_len += 1; + amm_mapping.weights.resize(current_len, datum); + } + amm_mapping.sort(); + + Ok(()) +} + +pub fn handle_update_constituent_correlation_data<'info>( + ctx: Context, + index1: u16, + index2: u16, + corr: i64, +) -> Result<()> { + let constituent_correlations = &mut ctx.accounts.constituent_correlations; + constituent_correlations.set_correlation(index1, index2, corr)?; + + msg!( + "Updated correlation between constituent {} and {} to {}", + index1, + index2, + corr + ); + + constituent_correlations.validate()?; + + Ok(()) +} + +pub fn handle_begin_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + amount_in: u64, +) -> Result<()> { + // Check admin + let admin = &ctx.accounts.admin; + #[cfg(feature = "anchor-test")] + { + let state = &ctx.accounts.state; + validate!( + admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin, + ErrorCode::Unauthorized, + "Wrong signer for lp taker swap" + )?; + } + #[cfg(not(feature = "anchor-test"))] + validate!( + admin.key() == lp_pool_swap_wallet::id(), + ErrorCode::DefaultError, + "Wrong signer for lp taker swap" + )?; + + let ixs = ctx.accounts.instructions.as_ref(); + let current_index = instructions::load_current_index_checked(ixs)? as usize; + + let remaining_accounts_iter = &mut ctx.remaining_accounts.iter().peekable(); + let mint = get_token_mint(remaining_accounts_iter)?; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let current_ix = instructions::load_instruction_at_checked(current_index, ixs)?; + validate!( + current_ix.program_id == *ctx.program_id, + ErrorCode::InvalidSwap, + "SwapBegin must be a top-level instruction (cant be cpi)" + )?; + + validate!( + in_market_index != out_market_index, + ErrorCode::InvalidSwap, + "in and out market the same" + )?; + + validate!( + amount_in != 0, + ErrorCode::InvalidSwap, + "amount_in cannot be zero" + )?; + + // Make sure we have enough balance to do the swap + let constituent_in_token_account = &ctx.accounts.constituent_in_token_account; + validate!( + amount_in <= constituent_in_token_account.amount, + ErrorCode::InvalidSwap, + "trying to swap more than the balance of the constituent in token account" + )?; + + validate!( + out_constituent.flash_loan_initial_token_amount == 0, + ErrorCode::InvalidSwap, + "begin_lp_swap ended in invalid state" + )?; + + in_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_in_token_account.amount; + out_constituent.flash_loan_initial_token_amount = ctx.accounts.signer_out_token_account.amount; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + constituent_in_token_account, + &ctx.accounts.signer_in_token_account, + &constituent_in_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &in_constituent.lp_pool, + &in_constituent.spot_market_index, + &in_constituent.vault_bump, + ), + amount_in, + &mint, + Some(remaining_accounts_iter), + )?; + + drop(in_constituent); + + // The only other drift program allowed is SwapEnd + let mut index = current_index + 1; + let mut found_end = false; + loop { + let ix = match instructions::load_instruction_at_checked(index, ixs) { + Ok(ix) => ix, + Err(ProgramError::InvalidArgument) => break, + Err(e) => return Err(e.into()), + }; + + // Check that the drift program key is not used + if ix.program_id == crate::id() { + // must be the last ix -- this could possibly be relaxed + validate!( + !found_end, + ErrorCode::InvalidSwap, + "the transaction must not contain a Drift instruction after FlashLoanEnd" + )?; + found_end = true; + + // must be the SwapEnd instruction + let discriminator = crate::instruction::EndLpSwap::discriminator(); + validate!( + ix.data[0..8] == discriminator, + ErrorCode::InvalidSwap, + "last drift ix must be end of swap" + )?; + + validate!( + ctx.accounts.signer_out_token_account.key() == ix.accounts[2].pubkey, + ErrorCode::InvalidSwap, + "the out_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.signer_in_token_account.key() == ix.accounts[3].pubkey, + ErrorCode::InvalidSwap, + "the in_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.constituent_out_token_account.key() == ix.accounts[4].pubkey, + ErrorCode::InvalidSwap, + "the constituent out_token_account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.constituent_in_token_account.key() == ix.accounts[5].pubkey, + ErrorCode::InvalidSwap, + "the constituent in token account passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.out_constituent.key() == ix.accounts[6].pubkey, + ErrorCode::InvalidSwap, + "the out constituent passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.in_constituent.key() == ix.accounts[7].pubkey, + ErrorCode::InvalidSwap, + "the in constituent passed to SwapBegin and End must match" + )?; + + validate!( + ctx.accounts.lp_pool.key() == ix.accounts[8].pubkey, + ErrorCode::InvalidSwap, + "the lp pool passed to SwapBegin and End must match" + )?; + } else { + if found_end { + if ix.program_id == lighthouse::ID { + continue; + } + + for meta in ix.accounts.iter() { + validate!( + meta.is_writable == false, + ErrorCode::InvalidSwap, + "instructions after swap end must not have writable accounts" + )?; + } + } else { + let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); + whitelisted_programs.push(AssociatedToken::id()); + whitelisted_programs.push(Token::id()); + whitelisted_programs.push(Token2022::id()); + whitelisted_programs.push(marinade_mainnet::ID); + + validate!( + whitelisted_programs.contains(&ix.program_id), + ErrorCode::InvalidSwap, + "only allowed to pass in ixs to token, openbook, and Jupiter v3/v4/v6 programs" + )?; + + for meta in ix.accounts.iter() { + validate!( + meta.pubkey != crate::id(), + ErrorCode::InvalidSwap, + "instructions between begin and end must not be drift instructions" + )?; + } + } + } + + index += 1; + } + + validate!( + found_end, + ErrorCode::InvalidSwap, + "found no SwapEnd instruction in transaction" + )?; + + Ok(()) +} + +pub fn handle_end_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, +) -> Result<()> { + let signer_in_token_account = &ctx.accounts.signer_in_token_account; + let signer_out_token_account = &ctx.accounts.signer_out_token_account; + + let admin_account_info = ctx.accounts.admin.to_account_info(); + + let constituent_in_token_account = &mut ctx.accounts.constituent_in_token_account; + let constituent_out_token_account = &mut ctx.accounts.constituent_out_token_account; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let out_token_program = get_token_interface(remaining_accounts)?; + + let in_mint = get_token_mint(remaining_accounts)?; + let out_mint = get_token_mint(remaining_accounts)?; + + // Residual of what wasnt swapped + if signer_in_token_account.amount > in_constituent.flash_loan_initial_token_amount { + let residual = signer_in_token_account + .amount + .safe_sub(in_constituent.flash_loan_initial_token_amount)?; + + controller::token::receive( + &ctx.accounts.token_program, + signer_in_token_account, + constituent_in_token_account, + &admin_account_info, + residual, + &in_mint, + Some(remaining_accounts), + )?; + } + + // Whatever was swapped + if signer_out_token_account.amount > out_constituent.flash_loan_initial_token_amount { + let residual = signer_out_token_account + .amount + .safe_sub(out_constituent.flash_loan_initial_token_amount)?; + + if let Some(token_interface) = out_token_program { + receive( + &token_interface, + signer_out_token_account, + constituent_out_token_account, + &admin_account_info, + residual, + &out_mint, + Some(remaining_accounts), + )?; + } else { + receive( + &ctx.accounts.token_program, + signer_out_token_account, + constituent_out_token_account, + &admin_account_info, + residual, + &out_mint, + Some(remaining_accounts), + )?; + } + } + + // Update the balance on the token accounts for after swap + constituent_out_token_account.reload()?; + constituent_in_token_account.reload()?; + out_constituent.sync_token_balance(constituent_out_token_account.amount); + in_constituent.sync_token_balance(constituent_in_token_account.amount); + + out_constituent.flash_loan_initial_token_amount = 0; + in_constituent.flash_loan_initial_token_amount = 0; + + Ok(()) +} + +pub fn handle_update_perp_market_lp_pool_status( + ctx: Context, + lp_status: u8, +) -> Result<()> { + let perp_market = &mut load_mut!(ctx.accounts.perp_market)?; + let amm_cache = &mut ctx.accounts.amm_cache; + + msg!("perp market {}", perp_market.market_index); + perp_market.lp_status = lp_status; + amm_cache.update_perp_market_fields(&perp_market)?; + + Ok(()) +} + +pub fn handle_update_initial_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + + let AccountMaps { + perp_market_map, + spot_market_map: _, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + Clock::get()?.slot, + None, + )?; + + for (_, perp_market_loader) in perp_market_map.0 { + let perp_market = perp_market_loader.load()?; + let oracle_data = oracle_map.get_price_data(&perp_market.oracle_id())?; + let mm_oracle_data = perp_market.get_mm_oracle_price_data( + *oracle_data, + slot, + &ctx.accounts.state.oracle_guard_rails.validity, + )?; + + amm_cache.update_perp_market_fields(&perp_market)?; + amm_cache.update_oracle_info( + slot, + perp_market.market_index, + &mm_oracle_data, + &perp_market, + &state.oracle_guard_rails, + )?; + } + + Ok(()) +} + +pub fn handle_reset_amm_cache(ctx: Context) -> Result<()> { + let state = &ctx.accounts.state; + let amm_cache = &mut ctx.accounts.amm_cache; + + amm_cache.cache.clear(); + amm_cache + .cache + .resize_with(state.number_of_markets as usize, CacheInfo::default); + amm_cache.validate(state)?; + + msg!("AMM cache reset. markets: {}", state.number_of_markets); + Ok(()) +} + +#[derive(Debug, Clone, Copy, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] +pub struct OverrideAmmCacheParams { + pub quote_owed_from_lp_pool: Option, + pub last_settle_slot: Option, + pub last_fee_pool_token_amount: Option, + pub last_net_pnl_pool_token_amount: Option, + pub amm_position_scalar: Option, + pub amm_inventory_limit: Option, +} + +pub fn handle_override_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + market_index: u16, + override_params: OverrideAmmCacheParams, +) -> Result<()> { + let amm_cache = &mut ctx.accounts.amm_cache; + + let cache_entry = amm_cache.cache.get_mut(market_index as usize); + if cache_entry.is_none() { + msg!("No cache entry found for market index {}", market_index); + return Ok(()); + } + + let cache_entry = cache_entry.unwrap(); + if let Some(quote_owed_from_lp_pool) = override_params.quote_owed_from_lp_pool { + cache_entry.quote_owed_from_lp_pool = quote_owed_from_lp_pool; + } + if let Some(last_settle_slot) = override_params.last_settle_slot { + cache_entry.last_settle_slot = last_settle_slot; + } + if let Some(last_fee_pool_token_amount) = override_params.last_fee_pool_token_amount { + cache_entry.last_fee_pool_token_amount = last_fee_pool_token_amount; + } + if let Some(last_net_pnl_pool_token_amount) = override_params.last_net_pnl_pool_token_amount { + cache_entry.last_net_pnl_pool_token_amount = last_net_pnl_pool_token_amount; + } + + if let Some(amm_position_scalar) = override_params.amm_position_scalar { + cache_entry.amm_position_scalar = amm_position_scalar; + } + + if let Some(amm_position_scalar) = override_params.amm_position_scalar { + cache_entry.amm_position_scalar = amm_position_scalar; + } + + if let Some(amm_inventory_limit) = override_params.amm_inventory_limit { + if amm_inventory_limit < 0 { + msg!("amm_inventory_limit must be non-negative"); + return Err(ErrorCode::DefaultError.into()); + } + cache_entry.amm_inventory_limit = amm_inventory_limit; + } + + Ok(()) +} + +#[derive(Accounts)] +#[instruction( + id: u8, +)] +pub struct InitializeLpPool<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + #[account( + init, + seeds = [b"lp_pool", id.to_le_bytes().as_ref()], + space = LPPool::SIZE, + bump, + payer = admin + )] + pub lp_pool: AccountLoader<'info, LPPool>, + pub mint: Account<'info, anchor_spl::token::Mint>, + + #[account( + init, + seeds = [b"LP_POOL_TOKEN_VAULT".as_ref(), lp_pool.key().as_ref()], + bump, + payer = admin, + token::mint = mint, + token::authority = lp_pool + )] + pub lp_pool_token_vault: Box>, + + #[account( + init, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = AmmConstituentMapping::space(0 as usize), + payer = admin, + )] + pub amm_constituent_mapping: Box>, + + #[account( + init, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = ConstituentTargetBase::space(0 as usize), + payer = admin, + )] + pub constituent_target_base: Box>, + + #[account( + init, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + space = ConstituentCorrelations::space(0 as usize), + payer = admin, + )] + pub constituent_correlations: Box>, + + pub state: Box>, + pub token_program: Program<'info, Token>, + + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction( + spot_market_index: u16, +)] +pub struct InitializeConstituent<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_target_base.bump, + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1_usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_base: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_correlations.bump, + realloc = ConstituentCorrelations::space(constituent_target_base.targets.len() + 1_usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_correlations: Box>, + + #[account( + init, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + space = Constituent::SIZE, + payer = admin, + )] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + seeds = [b"spot_market", spot_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + address = spot_market.load()?.mint + )] + pub spot_market_mint: Box>, + #[account( + init, + seeds = [CONSTITUENT_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), spot_market_index.to_le_bytes().as_ref()], + bump, + payer = admin, + token::mint = spot_market_mint, + token::authority = constituent_vault + )] + pub constituent_vault: Box>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentParams<'info> { + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_target_base.bump, + constraint = constituent.load()?.lp_pool == lp_pool.key() + )] + pub constituent_target_base: Box>, + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentStatus<'info> { + #[account( + mut, + constraint = admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentPausedOperations<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, +} + +#[derive(Accounts)] +pub struct UpdateLpPoolParams<'info> { + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)] +pub struct AddAmmConstituentMappingDatum { + pub constituent_index: u16, + pub perp_market_index: u16, + pub weight: i64, +} + +#[derive(Accounts)] +#[instruction( + amm_constituent_mapping_data: Vec, +)] +pub struct AddAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = AmmConstituentMapping::space(amm_constituent_mapping.weights.len() + amm_constituent_mapping_data.len()), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + #[account( + mut, + seeds = [CONSTITUENT_TARGET_BASE_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = ConstituentTargetBase::space(constituent_target_base.targets.len() + 1_usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub constituent_target_base: Box>, + pub state: Box>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction( + amm_constituent_mapping_data: Vec, +)] +pub struct UpdateAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub amm_constituent_mapping: Box>, + pub system_program: Program<'info, System>, + pub state: Box>, +} + +#[derive(Accounts)] +pub struct RemoveAmmConstituentMappingData<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [AMM_MAP_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + realloc = AmmConstituentMapping::space(amm_constituent_mapping.weights.len() - 1), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_constituent_mapping: Box>, + pub system_program: Program<'info, System>, + pub state: Box>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentCorrelation<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, + + #[account( + mut, + seeds = [CONSTITUENT_CORRELATIONS_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump = constituent_correlations.bump, + )] + pub constituent_correlations: Box>, + pub state: Box>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct LPTakerSwap<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == lp_pool_swap_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + /// Signer token accounts + #[account( + mut, + constraint = &constituent_out_token_account.mint.eq(&signer_out_token_account.mint), + token::authority = admin + )] + pub signer_out_token_account: Box>, + #[account( + mut, + constraint = &constituent_in_token_account.mint.eq(&signer_in_token_account.mint), + token::authority = admin + )] + pub signer_in_token_account: Box>, + + /// Constituent token accounts + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + + /// Constituents + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump = out_constituent.load()?.bump, + )] + pub out_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump = in_constituent.load()?.bump, + )] + pub in_constituent: AccountLoader<'info, Constituent>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// Instructions Sysvar for instruction introspection + /// CHECK: fixed instructions sysvar account + #[account(address = instructions::ID)] + pub instructions: UncheckedAccount<'info>, + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +pub struct UpdatePerpMarketLpPoolStatus<'info> { + pub admin: Signer<'info>, + #[account( + has_one = admin + )] + pub state: Box>, + #[account(mut)] + pub perp_market: AccountLoader<'info, PerpMarket>, + #[account(mut, seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump,)] + pub amm_cache: Box>, +} + +#[derive(Accounts)] +pub struct UpdateInitialAmmCacheInfo<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub state: Box>, + pub admin: Signer<'info>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + )] + pub amm_cache: Box>, +} + +#[derive(Accounts)] +pub struct ResetAmmCache<'info> { + #[account( + mut, + constraint = admin.key() == lp_pool_hot_wallet::id() || admin.key() == state.admin + )] + pub admin: Signer<'info>, + pub state: Box>, + #[account( + mut, + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump = amm_cache.bump, + realloc = AmmCache::space(state.number_of_markets as usize), + realloc::payer = admin, + realloc::zero = false, + )] + pub amm_cache: Box>, + pub system_program: Program<'info, System>, +} diff --git a/programs/drift/src/instructions/lp_pool.rs b/programs/drift/src/instructions/lp_pool.rs new file mode 100644 index 0000000000..cfb4856751 --- /dev/null +++ b/programs/drift/src/instructions/lp_pool.rs @@ -0,0 +1,2130 @@ +use anchor_lang::{prelude::*, Accounts, Key, Result}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; + +use crate::ids::lp_pool_swap_wallet; +use crate::math::constants::PRICE_PRECISION_I64; +use crate::state::events::{DepositDirection, LPBorrowLendDepositRecord}; +use crate::state::paused_operations::ConstituentLpOperation; +use crate::validation::whitelist::validate_whitelist_token; +use crate::{ + controller::{ + self, + spot_balance::update_spot_balances, + token::{burn_tokens, mint_tokens}, + }, + error::ErrorCode, + get_then_update_id, + ids::admin_hot_wallet, + math::{ + self, + casting::Cast, + constants::PERCENTAGE_PRECISION_I64, + oracle::{is_oracle_valid_for_action, DriftAction}, + safe_math::SafeMath, + }, + math_error, msg, safe_decrement, safe_increment, + state::{ + amm_cache::{AmmCacheFixed, CacheInfo, AMM_POSITIONS_CACHE}, + constituent_map::{ConstituentMap, ConstituentSet}, + events::{emit_stack, LPMintRedeemRecord, LPSwapRecord}, + lp_pool::{ + update_constituent_target_base_for_derivatives, AmmConstituentDatum, + AmmConstituentMappingFixed, Constituent, ConstituentCorrelationsFixed, + ConstituentTargetBaseFixed, LPPool, TargetsDatum, LP_POOL_SWAP_AUM_UPDATE_DELAY, + }, + oracle_map::OracleMap, + perp_market_map::MarketSet, + spot_market::{SpotBalanceType, SpotMarket}, + spot_market_map::get_writable_spot_market_set_from_many, + state::State, + traits::Size, + user::MarketType, + zero_copy::{AccountZeroCopy, AccountZeroCopyMut, ZeroCopyLoader}, + }, + validate, +}; +use std::iter::Peekable; +use std::slice::Iter; + +use solana_program::sysvar::clock::Clock; + +use super::optional_accounts::{get_whitelist_token, load_maps, AccountMaps}; +use crate::controller::spot_balance::update_spot_market_cumulative_interest; +use crate::controller::token::{receive, send_from_program_vault_with_signature_seeds}; +use crate::instructions::constraints::*; +use crate::state::lp_pool::{ + AmmInventoryAndPricesAndSlots, ConstituentIndexAndDecimalAndPrice, CONSTITUENT_PDA_SEED, + LP_POOL_TOKEN_VAULT_PDA_SEED, +}; + +pub fn handle_update_constituent_target_base<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, +) -> Result<()> { + let slot = Clock::get()?.slot; + + let lp_pool_key: &Pubkey = &ctx.accounts.lp_pool.key(); + let lp_pool = ctx.accounts.lp_pool.load()?; + let constituent_target_base_key: &Pubkey = &ctx.accounts.constituent_target_base.key(); + + let amm_cache: AccountZeroCopy<'_, CacheInfo, AmmCacheFixed> = + ctx.accounts.amm_cache.load_zc()?; + + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let amm_constituent_mapping: AccountZeroCopy< + '_, + AmmConstituentDatum, + AmmConstituentMappingFixed, + > = ctx.accounts.amm_constituent_mapping.load_zc()?; + validate!( + amm_constituent_mapping.fixed.lp_pool.eq(lp_pool_key), + ErrorCode::InvalidPDA, + "Amm constituent mapping lp pool pubkey does not match lp pool pubkey", + )?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool_key, remaining_accounts)?; + + let mut amm_inventories: Vec = + Vec::with_capacity(amm_cache.len() as usize); + for (_, cache_info) in amm_cache.iter().enumerate() { + if cache_info.lp_status_for_perp_market == 0 { + continue; + } + + amm_inventories.push(AmmInventoryAndPricesAndSlots { + inventory: { + let scaled_position = cache_info + .position + .safe_mul(cache_info.amm_position_scalar as i64)? + .safe_div(100)?; + + scaled_position.clamp( + -cache_info.amm_inventory_limit, + cache_info.amm_inventory_limit, + ) + }, + price: cache_info.oracle_price, + last_oracle_slot: cache_info.oracle_slot, + last_position_slot: cache_info.slot, + }); + } + msg!("amm inventories: {:?}", amm_inventories); + + if amm_inventories.is_empty() { + msg!("No valid inventories found for constituent target weights update"); + return Ok(()); + } + + let mut constituent_indexes_and_decimals_and_prices: Vec = + Vec::with_capacity(constituent_map.0.len()); + for (index, loader) in &constituent_map.0 { + let constituent_ref = loader.load()?; + constituent_indexes_and_decimals_and_prices.push(ConstituentIndexAndDecimalAndPrice { + constituent_index: *index, + decimals: constituent_ref.decimals, + price: constituent_ref.last_oracle_price, + }); + } + + constituent_target_base.update_target_base( + &amm_constituent_mapping, + amm_inventories.as_slice(), + constituent_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + )?; + + Ok(()) +} + +pub fn handle_update_lp_pool_aum<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, +) -> Result<()> { + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + let state = &ctx.accounts.state; + + let slot = Clock::get()?.slot; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let constituent_map = + ConstituentMap::load(&ConstituentSet::new(), &lp_pool.pubkey, remaining_accounts)?; + + validate!( + constituent_map.0.len() == lp_pool.constituents as usize, + ErrorCode::WrongNumberOfConstituents, + "Constituent map length does not match lp pool constituent count" + )?; + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let mut constituent_target_base: AccountZeroCopyMut< + '_, + TargetsDatum, + ConstituentTargetBaseFixed, + > = ctx.accounts.constituent_target_base.load_zc_mut()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool.pubkey) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let amm_cache: AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed> = + ctx.accounts.amm_cache.load_zc_mut()?; + + let (aum, crypto_delta, derivative_groups) = lp_pool.update_aum( + slot, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &constituent_target_base, + &amm_cache, + )?; + + // Set USDC stable weight + msg!("aum: {}", aum); + let total_stable_target_base = aum.cast::()?.safe_sub(crypto_delta)?; + constituent_target_base + .get_mut(lp_pool.quote_consituent_index as u32) + .target_base = total_stable_target_base.cast::()?; + + msg!( + "stable target base: {}", + constituent_target_base + .get(lp_pool.quote_consituent_index as u32) + .target_base + ); + msg!("aum: {}, crypto_delta: {}", aum, crypto_delta); + msg!("derivative groups: {:?}", derivative_groups); + + update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + )?; + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + min_out_amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + validate!( + state.allow_swap_lp_pool(), + ErrorCode::DefaultError, + "Swapping with LP Pool is disabled" + )?; + + validate!( + in_market_index != out_market_index, + ErrorCode::InvalidSpotMarketAccount, + "In and out spot market indices cannot be the same" + )?; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let lp_pool_key = ctx.accounts.lp_pool.key(); + let lp_pool = &ctx.accounts.lp_pool.load()?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + + in_constituent.does_constituent_allow_operation(ConstituentLpOperation::Swap)?; + out_constituent.does_constituent_allow_operation(ConstituentLpOperation::Swap)?; + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let constituent_correlations_key = &ctx.accounts.constituent_correlations.key(); + let constituent_correlations: AccountZeroCopy<'_, i64, ConstituentCorrelationsFixed> = + ctx.accounts.constituent_correlations.load_zc()?; + validate!( + constituent_correlations.fixed.lp_pool.eq(&lp_pool_key) + && constituent_correlations_key.eq(&lp_pool.constituent_correlations), + ErrorCode::InvalidPDA, + "Constituent correlations lp pool pubkey does not match lp pool pubkey", + )?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + let out_spot_market = spot_market_map.get_ref(&out_market_index)?; + + if in_constituent.is_reduce_only()? + && !in_constituent.is_operation_reducing(&in_spot_market, true)? + { + msg!("In constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + if out_constituent.is_reduce_only()? + && !out_constituent.is_operation_reducing(&out_spot_market, false)? + { + msg!("Out constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let in_oracle_id = in_spot_market.oracle_id(); + let out_oracle_id = out_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let in_oracle = in_oracle.clone(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let in_target_weight = constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, + )?; + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, + )?; + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + in_target_position_slot_delay, + out_target_position_slot_delay, + in_target_oracle_slot_delay, + out_target_oracle_slot_delay, + &in_oracle, + &out_oracle, + &in_constituent, + &out_constituent, + &in_spot_market, + &out_spot_market, + in_target_weight, + out_target_weight, + in_amount as u128, + constituent_correlations.get_correlation( + in_constituent.constituent_index, + out_constituent.constituent_index, + )?, + )?; + msg!( + "in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}", + in_amount, + out_amount, + in_fee, + out_fee + ); + let out_amount_net_fees = if out_fee > 0 { + out_amount.safe_sub(out_fee.unsigned_abs())? + } else { + out_amount.safe_add(out_fee.unsigned_abs())? + }; + + validate!( + out_amount_net_fees.cast::()? >= min_out_amount, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: out_amount_net_fees({}) < min_out_amount({})", + out_amount_net_fees, min_out_amount + ) + .as_str() + )?; + + validate!( + out_amount_net_fees.cast::()? <= out_constituent.vault_token_balance, + ErrorCode::InsufficientConstituentTokenBalance, + format!( + "Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})", + out_amount_net_fees, out_constituent.vault_token_balance + ) + .as_str() + )?; + + in_constituent.record_swap_fees(in_fee)?; + out_constituent.record_swap_fees(out_fee)?; + + let in_swap_id = get_then_update_id!(in_constituent, next_swap_id); + let out_swap_id = get_then_update_id!(out_constituent, next_swap_id); + + emit_stack::<_, { LPSwapRecord::SIZE }>(LPSwapRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + out_amount: out_amount_net_fees, + in_amount, + out_fee, + in_fee, + out_spot_market_index: out_market_index, + in_spot_market_index: in_market_index, + out_constituent_index: out_constituent.constituent_index, + in_constituent_index: in_constituent.constituent_index, + out_oracle_price: out_oracle.price, + in_oracle_price: in_oracle.price, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: in_constituent.get_weight( + in_oracle.price, + &in_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: in_target_weight, + out_market_current_weight: out_constituent.get_weight( + out_oracle.price, + &out_spot_market, + 0, + lp_pool.last_aum, + )?, + out_market_target_weight: out_target_weight, + in_swap_id, + out_swap_id, + lp_pool: lp_pool_key, + })?; + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_in_token_account, + &ctx.accounts.constituent_in_token_account, + &ctx.accounts.authority, + in_amount.cast::()?, + &Some((*ctx.accounts.in_market_mint).clone()), + Some(remaining_accounts), + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_out_token_account, + &ctx.accounts.user_out_token_account, + &ctx.accounts.constituent_out_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &out_constituent.lp_pool, + &out_constituent.spot_market_index, + &out_constituent.vault_bump, + ), + out_amount_net_fees.cast::()?, + &Some((*ctx.accounts.out_market_mint).clone()), + Some(remaining_accounts), + )?; + + ctx.accounts.constituent_in_token_account.reload()?; + ctx.accounts.constituent_out_token_account.reload()?; + + in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount); + out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount); + + Ok(()) +} + +pub fn handle_view_lp_pool_swap_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolSwapFees<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + in_target_weight: i64, + out_target_weight: i64, +) -> Result<()> { + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + + let lp_pool = &ctx.accounts.lp_pool.load()?; + let in_constituent = ctx.accounts.in_constituent.load()?; + let out_constituent = ctx.accounts.out_constituent.load()?; + let constituent_correlations: AccountZeroCopy<'_, i64, ConstituentCorrelationsFixed> = + ctx.accounts.constituent_correlations.load_zc()?; + + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + &mut ctx.remaining_accounts.iter().peekable(), + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + let out_spot_market = spot_market_map.get_ref(&out_market_index)?; + + let in_oracle_id = in_spot_market.oracle_id(); + let out_oracle_id = out_spot_market.oracle_id(); + + let (in_oracle, _) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let in_oracle = in_oracle.clone(); + + let (out_oracle, _) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool.get_swap_amount( + in_target_position_slot_delay, + out_target_position_slot_delay, + in_target_oracle_slot_delay, + out_target_oracle_slot_delay, + &in_oracle, + &out_oracle, + &in_constituent, + &out_constituent, + &in_spot_market, + &out_spot_market, + in_target_weight, + out_target_weight, + in_amount as u128, + constituent_correlations.get_correlation( + in_constituent.constituent_index, + out_constituent.constituent_index, + )?, + )?; + msg!( + "in_amount: {}, out_amount: {}, in_fee: {}, out_fee: {}", + in_amount, + out_amount, + in_fee, + out_fee + ); + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_add_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, + in_market_index: u16, + in_amount: u128, + min_mint_amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + + validate!( + state.allow_mint_redeem_lp_pool(), + ErrorCode::MintRedeemLpPoolDisabled, + "Mint/redeem LP pool is disabled" + )?; + + let mut in_constituent = ctx.accounts.in_constituent.load_mut()?; + in_constituent.does_constituent_allow_operation(ConstituentLpOperation::Deposit)?; + + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let lp_pool_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + let lp_price_before = lp_pool.get_price(lp_pool.token_supply)?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![in_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let whitelist_mint = &lp_pool.whitelist_mint; + if !whitelist_mint.eq(&Pubkey::default()) { + validate_whitelist_token( + get_whitelist_token(remaining_accounts)?, + whitelist_mint, + &ctx.accounts.authority.key(), + )?; + } + + let mut in_spot_market = spot_market_map.get_ref_mut(&in_market_index)?; + + if in_constituent.is_reduce_only()? + && !in_constituent.is_operation_reducing(&in_spot_market, true)? + { + msg!("In constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let in_oracle_id = in_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let in_oracle = in_oracle.clone(); + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + // TODO: check self.aum validity + + update_spot_market_cumulative_interest(&mut in_spot_market, Some(&in_oracle), now)?; + + msg!("aum: {}", lp_pool.last_aum); + let in_target_weight = if lp_pool.last_aum == 0 { + PERCENTAGE_PRECISION_I64 // 100% weight if no aum + } else { + constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, // TODO: add in_amount * in_oracle to est post add_liquidity aum + )? + }; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + &in_spot_market, + &in_constituent, + in_amount, + &in_oracle, + in_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_amount: {}, in_amount: {}, lp_fee_amount: {}, in_fee_amount: {}", + lp_amount, + in_amount, + lp_fee_amount, + in_fee_amount + ); + + let lp_mint_amount_net_fees = if lp_fee_amount > 0 { + lp_amount.safe_sub(lp_fee_amount.unsigned_abs() as u64)? + } else { + lp_amount.safe_add(lp_fee_amount.unsigned_abs() as u64)? + }; + + validate!( + lp_mint_amount_net_fees >= min_mint_amount, + ErrorCode::SlippageOutsideLimit, + format!( + "Slippage outside limit: lp_mint_amount_net_fees({}) < min_mint_amount({})", + lp_mint_amount_net_fees, min_mint_amount + ) + .as_str() + )?; + + in_constituent.record_swap_fees(in_fee_amount)?; + lp_pool.record_mint_redeem_fees(lp_fee_amount)?; + + let lp_pool_id = lp_pool.lp_pool_id; + let lp_bump = lp_pool.bump; + + let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_pool_id, &lp_bump); + + drop(lp_pool); + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_in_token_account, + &ctx.accounts.constituent_in_token_account, + &ctx.accounts.authority, + in_amount.cast::()?, + &Some((*ctx.accounts.in_market_mint).clone()), + Some(remaining_accounts), + )?; + + mint_tokens( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_amount, + &ctx.accounts.lp_mint, + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.user_lp_token_account, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_mint_amount_net_fees, + &Some((*ctx.accounts.lp_mint).clone()), + Some(remaining_accounts), + )?; + + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.last_aum = lp_pool.last_aum.safe_add( + in_amount + .cast::()? + .safe_mul(in_oracle.price.cast::()?)? + .safe_div(10_u128.pow(in_spot_market.decimals))?, + )?; + + if lp_pool.last_aum > lp_pool.max_aum { + return Err(ErrorCode::MaxDlpAumBreached.into()); + } + + ctx.accounts.constituent_in_token_account.reload()?; + ctx.accounts.lp_mint.reload()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + in_constituent.sync_token_balance(ctx.accounts.constituent_in_token_account.amount); + + ctx.accounts.lp_mint.reload()?; + let lp_price_after = lp_pool.get_price(lp_pool.token_supply)?; + let price_diff = (lp_price_after.cast::()?).safe_sub(lp_price_before.cast::()?)?; + + if lp_price_before > 0 && price_diff.signum() != 0 && in_fee_amount.signum() != 0 { + validate!( + price_diff.signum() == in_fee_amount.signum() || price_diff == 0, + ErrorCode::LpInvariantFailed, + "Adding liquidity resulted in price direction != fee sign, price_diff: {}, in_fee_amount: {}", + price_diff, + in_fee_amount + )?; + } + + let mint_redeem_id = get_then_update_id!(lp_pool, mint_redeem_id); + emit_stack::<_, { LPMintRedeemRecord::SIZE }>(LPMintRedeemRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + description: 1, + amount: in_amount, + fee: in_fee_amount, + spot_market_index: in_market_index, + constituent_index: in_constituent.constituent_index, + oracle_price: in_oracle.price, + mint: in_constituent.mint, + lp_amount, + lp_fee: lp_fee_amount, + lp_price: lp_price_after, + mint_redeem_id, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: in_constituent.get_weight( + in_oracle.price, + &in_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: in_target_weight, + lp_pool: lp_pool_key, + })?; + + Ok(()) +} + +pub fn handle_view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolAddLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u128, +) -> Result<()> { + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + let lp_pool = ctx.accounts.lp_pool.load()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + let in_constituent = ctx.accounts.in_constituent.load()?; + + let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?; + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &MarketSet::new(), + slot, + Some(state.oracle_guard_rails), + )?; + + let in_spot_market = spot_market_map.get_ref(&in_market_index)?; + + let in_oracle_id = in_spot_market.oracle_id(); + + let (in_oracle, in_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + in_spot_market.market_index, + &in_oracle_id, + in_spot_market.historical_oracle_data.last_oracle_price_twap, + in_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let in_oracle = in_oracle.clone(); + + if !is_oracle_valid_for_action(in_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "In oracle data for spot market {} is invalid for lp pool swap.", + in_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + msg!("aum: {}", lp_pool.last_aum); + let in_target_weight = if lp_pool.last_aum == 0 { + PERCENTAGE_PRECISION_I64 // 100% weight if no aum + } else { + constituent_target_base.get_target_weight( + in_constituent.constituent_index, + &in_spot_market, + in_oracle.price, + lp_pool.last_aum, // TODO: add in_amount * in_oracle to est post add_liquidity aum + )? + }; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let in_target_datum = constituent_target_base.get(in_constituent.constituent_index as u32); + let in_target_position_slot_delay = slot.saturating_sub(in_target_datum.last_position_slot); + let in_target_oracle_slot_delay = slot.saturating_sub(in_target_datum.last_oracle_slot); + + let (lp_amount, in_amount, lp_fee_amount, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + &in_spot_market, + &in_constituent, + in_amount, + &in_oracle, + in_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_amount: {}, in_amount: {}, lp_fee_amount: {}, in_fee_amount: {}", + lp_amount, + in_amount, + lp_fee_amount, + in_fee_amount + ); + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_lp_pool_remove_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, + out_market_index: u16, + lp_to_burn: u64, + min_amount_out: u128, +) -> Result<()> { + let slot = Clock::get()?.slot; + let now = Clock::get()?.unix_timestamp; + let state = &ctx.accounts.state; + + validate!( + state.allow_mint_redeem_lp_pool(), + ErrorCode::MintRedeemLpPoolDisabled, + "Mint/redeem LP pool is disabled" + )?; + + let lp_pool_key = ctx.accounts.lp_pool.key(); + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + let lp_price_before = lp_pool.get_price(lp_pool.token_supply)?; + + let mut out_constituent = ctx.accounts.out_constituent.load_mut()?; + out_constituent.does_constituent_allow_operation(ConstituentLpOperation::Withdraw)?; + + // Verify previous settle + let amm_cache: AccountZeroCopy<'_, CacheInfo, _> = ctx.accounts.amm_cache.load_zc()?; + for (i, _) in amm_cache.iter().enumerate() { + let cache_info = amm_cache.get(i as u32); + if cache_info.last_fee_pool_token_amount != 0 && cache_info.last_settle_slot != slot { + msg!( + "Market {} has not been settled in current slot. Last slot: {}", + i, + cache_info.last_settle_slot + ); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let constituent_target_base_key = &ctx.accounts.constituent_target_base.key(); + let constituent_target_base: AccountZeroCopy<'_, TargetsDatum, ConstituentTargetBaseFixed> = + ctx.accounts.constituent_target_base.load_zc()?; + validate!( + constituent_target_base.fixed.lp_pool.eq(&lp_pool_key) + && constituent_target_base_key.eq(&lp_pool.constituent_target_base), + ErrorCode::InvalidPDA, + "Constituent target base lp pool pubkey does not match lp pool pubkey", + )?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![out_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let mut out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?; + + if out_constituent.is_reduce_only()? + && !out_constituent.is_operation_reducing(&out_spot_market, false)? + { + msg!("Out constituent in reduce only mode"); + return Err(ErrorCode::InvalidConstituentOperation.into()); + } + + let out_oracle_id = out_spot_market.oracle_id(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let out_oracle = *out_oracle; + + // TODO: check self.aum validity + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + update_spot_market_cumulative_interest(&mut out_spot_market, Some(&out_oracle), now)?; + + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, // TODO: remove out_amount * out_oracle to est post remove_liquidity aum + )?; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + &out_spot_market, + &out_constituent, + lp_to_burn, + &out_oracle, + out_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_burn_amount: {}, out_amount: {}, lp_fee_amount: {}, out_fee_amount: {}", + lp_burn_amount, + out_amount, + lp_fee_amount, + out_fee_amount + ); + + let lp_burn_amount_net_fees = if lp_fee_amount > 0 { + lp_burn_amount.safe_sub(lp_fee_amount.unsigned_abs() as u64)? + } else { + lp_burn_amount.safe_add(lp_fee_amount.unsigned_abs() as u64)? + }; + + let out_amount_net_fees = if out_fee_amount > 0 { + out_amount.safe_sub(out_fee_amount.unsigned_abs())? + } else { + out_amount.safe_add(out_fee_amount.unsigned_abs())? + }; + + validate!( + out_amount_net_fees >= min_amount_out, + ErrorCode::SlippageOutsideLimit, + "Slippage outside limit: out_amount_net_fees({}) < min_amount_out({})", + out_amount_net_fees, + min_amount_out + )?; + + if out_amount_net_fees > out_constituent.vault_token_balance.cast()? { + let transfer_amount = out_amount_net_fees + .cast::()? + .safe_sub(out_constituent.vault_token_balance)?; + msg!( + "transfering from program vault to constituent vault: {}", + transfer_amount + ); + transfer_from_program_vault( + transfer_amount, + &mut out_spot_market, + &mut out_constituent, + out_oracle.price, + &ctx.accounts.state, + &mut ctx.accounts.spot_market_token_account, + &mut ctx.accounts.constituent_out_token_account, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + &None, + Some(remaining_accounts), + )?; + } + + validate!( + out_amount_net_fees <= out_constituent.vault_token_balance.cast()?, + ErrorCode::InsufficientConstituentTokenBalance, + "Insufficient out constituent balance: out_amount_net_fees({}) > out_constituent.token_balance({})", + out_amount_net_fees, + out_constituent.vault_token_balance + )?; + + out_constituent.record_swap_fees(out_fee_amount)?; + lp_pool.record_mint_redeem_fees(lp_fee_amount)?; + + let lp_pool_id = lp_pool.lp_pool_id; + let lp_bump = lp_pool.bump; + + let lp_vault_signer_seeds = LPPool::get_lp_pool_signer_seeds(&lp_pool_id, &lp_bump); + + drop(lp_pool); + + receive( + &ctx.accounts.token_program, + &ctx.accounts.user_lp_token_account, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.authority, + lp_burn_amount, + &None, + Some(remaining_accounts), + )?; + + burn_tokens( + &ctx.accounts.token_program, + &ctx.accounts.lp_pool_token_vault, + &ctx.accounts.lp_pool.to_account_info(), + &lp_vault_signer_seeds, + lp_burn_amount_net_fees, + &ctx.accounts.lp_mint, + )?; + + send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_out_token_account, + &ctx.accounts.user_out_token_account, + &ctx.accounts.constituent_out_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &out_constituent.lp_pool, + &out_constituent.spot_market_index, + &out_constituent.vault_bump, + ), + out_amount_net_fees.cast::()?, + &None, + Some(remaining_accounts), + )?; + + let mut lp_pool = ctx.accounts.lp_pool.load_mut()?; + + lp_pool.last_aum = lp_pool.last_aum.safe_sub( + out_amount_net_fees + .cast::()? + .safe_mul(out_oracle.price.cast::()?)? + .safe_div(10_u128.pow(out_spot_market.decimals))?, + )?; + + ctx.accounts.constituent_out_token_account.reload()?; + ctx.accounts.lp_mint.reload()?; + lp_pool.sync_token_supply(ctx.accounts.lp_mint.supply); + + out_constituent.sync_token_balance(ctx.accounts.constituent_out_token_account.amount); + + ctx.accounts.lp_mint.reload()?; + let lp_price_after = lp_pool.get_price(lp_pool.token_supply)?; + + let price_diff = (lp_price_after.cast::()?).safe_sub(lp_price_before.cast::()?)?; + if price_diff.signum() != 0 && out_fee_amount.signum() != 0 { + validate!( + price_diff.signum() == out_fee_amount.signum(), + ErrorCode::LpInvariantFailed, + "Removing liquidity resulted in price direction != fee sign, price_diff: {}, out_fee_amount: {}", + price_diff, + out_fee_amount + )?; + } + + let mint_redeem_id = get_then_update_id!(lp_pool, mint_redeem_id); + emit_stack::<_, { LPMintRedeemRecord::SIZE }>(LPMintRedeemRecord { + ts: now, + slot, + authority: ctx.accounts.authority.key(), + description: 0, + amount: out_amount, + fee: out_fee_amount, + spot_market_index: out_market_index, + constituent_index: out_constituent.constituent_index, + oracle_price: out_oracle.price, + mint: out_constituent.mint, + lp_amount: lp_burn_amount, + lp_fee: lp_fee_amount, + lp_price: lp_price_after, + mint_redeem_id, + last_aum: lp_pool.last_aum, + last_aum_slot: lp_pool.last_aum_slot, + in_market_current_weight: out_constituent.get_weight( + out_oracle.price, + &out_spot_market, + 0, + lp_pool.last_aum, + )?, + in_market_target_weight: out_target_weight, + lp_pool: lp_pool_key, + })?; + + Ok(()) +} + +#[access_control( + fill_not_paused(&ctx.accounts.state) +)] +pub fn handle_view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolRemoveLiquidityFees<'info>>, + out_market_index: u16, + lp_to_burn: u64, +) -> Result<()> { + let slot = Clock::get()?.slot; + let state = &ctx.accounts.state; + let lp_pool = ctx.accounts.lp_pool.load()?; + + if slot.saturating_sub(lp_pool.last_aum_slot) > LP_POOL_SWAP_AUM_UPDATE_DELAY { + msg!( + "Must update LP pool AUM before swap, last_aum_slot: {}, current slot: {}", + lp_pool.last_aum_slot, + slot + ); + return Err(ErrorCode::LpPoolAumDelayed.into()); + } + + let out_constituent = ctx.accounts.out_constituent.load_mut()?; + + let constituent_target_base = ctx.accounts.constituent_target_base.load_zc()?; + + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let AccountMaps { + perp_market_map: _, + spot_market_map, + mut oracle_map, + } = load_maps( + remaining_accounts, + &MarketSet::new(), + &get_writable_spot_market_set_from_many(vec![out_market_index]), + slot, + Some(state.oracle_guard_rails), + )?; + + let out_spot_market = spot_market_map.get_ref_mut(&out_market_index)?; + + let out_oracle_id = out_spot_market.oracle_id(); + + let (out_oracle, out_oracle_validity) = oracle_map.get_price_data_and_validity( + MarketType::Spot, + out_spot_market.market_index, + &out_oracle_id, + out_spot_market + .historical_oracle_data + .last_oracle_price_twap, + out_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + let out_oracle = out_oracle.clone(); + + if !is_oracle_valid_for_action(out_oracle_validity, Some(DriftAction::LpPoolSwap))? { + msg!( + "Out oracle data for spot market {} is invalid for lp pool swap.", + out_spot_market.market_index, + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let out_target_weight = constituent_target_base.get_target_weight( + out_constituent.constituent_index, + &out_spot_market, + out_oracle.price, + lp_pool.last_aum, + )?; + + let dlp_total_supply = ctx.accounts.lp_mint.supply; + + let out_target_datum = constituent_target_base.get(out_constituent.constituent_index as u32); + let out_target_position_slot_delay = slot.saturating_sub(out_target_datum.last_position_slot); + let out_target_oracle_slot_delay = slot.saturating_sub(out_target_datum.last_oracle_slot); + + let (lp_burn_amount, out_amount, lp_fee_amount, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + &out_spot_market, + &out_constituent, + lp_to_burn, + &out_oracle, + out_target_weight, + dlp_total_supply, + )?; + msg!( + "lp_burn_amount: {}, out_amount: {}, lp_fee_amount: {}, out_fee_amount: {}", + lp_burn_amount, + out_amount, + lp_fee_amount, + out_fee_amount + ); + + Ok(()) +} + +pub fn handle_update_constituent_oracle_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentOracleInfo<'info>>, +) -> Result<()> { + let clock = Clock::get()?; + let mut constituent = ctx.accounts.constituent.load_mut()?; + let spot_market = ctx.accounts.spot_market.load()?; + + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + let oracle_data_slot = clock.slot - oracle_data.delay.max(0i64).cast::()?; + if constituent.last_oracle_slot < oracle_data_slot { + constituent.last_oracle_price = oracle_data.price; + constituent.last_oracle_slot = oracle_data_slot; + } + + Ok(()) +} + +pub fn handle_deposit_to_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositProgramVault<'info>>, + amount: u64, +) -> Result<()> { + let clock = Clock::get()?; + + let mut spot_market = ctx.accounts.spot_market.load_mut()?; + let spot_market_vault = &ctx.accounts.spot_market_vault; + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let mut constituent = ctx.accounts.constituent.load_mut()?; + let lp_pool_key = constituent.lp_pool; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + let deposit_plus_token_amount_before = amount.safe_add(spot_market_vault.amount)?; + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(&oracle_data), + clock.unix_timestamp, + )?; + let token_balance_after_cumulative_interest_update = constituent + .spot_balance + .get_signed_token_amount(&spot_market)?; + + let interest_accrued_token_amount = token_balance_after_cumulative_interest_update + .cast::()? + .safe_sub(constituent.last_spot_balance_token_amount)?; + + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + let balance_before = constituent.get_full_token_amount(&spot_market)?; + + controller::token::send_from_program_vault_with_signature_seeds( + &ctx.accounts.token_program, + &ctx.accounts.constituent_token_account, + &spot_market_vault, + &ctx.accounts.constituent_token_account.to_account_info(), + &Constituent::get_vault_signer_seeds( + &constituent.lp_pool, + &constituent.spot_market_index, + &constituent.vault_bump, + ), + amount, + &Some(*ctx.accounts.mint.clone()), + Some(remaining_accounts), + )?; + + // Adjust BLPosition for the new deposits + let spot_position = &mut constituent.spot_balance; + update_spot_balances( + amount as u128, + &SpotBalanceType::Deposit, + &mut spot_market, + spot_position, + false, + )?; + + safe_increment!(spot_position.cumulative_deposits, amount.cast()?); + + ctx.accounts.spot_market_vault.reload()?; + ctx.accounts.constituent_token_account.reload()?; + constituent.sync_token_balance(ctx.accounts.constituent_token_account.amount); + spot_market.validate_max_token_deposits_and_borrows(false)?; + + validate!( + ctx.accounts.spot_market_vault.amount == deposit_plus_token_amount_before, + ErrorCode::LpInvariantFailed, + "Spot market vault amount mismatch after deposit" + )?; + + let balance_after = constituent.get_full_token_amount(&spot_market)?; + let balance_diff_notional = if spot_market.decimals > 6 { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(oracle_data.price)? + .safe_div(PRICE_PRECISION_I64)? + .safe_div(10_i64.pow(spot_market.decimals - 6))? + } else { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(10_i64.pow(6 - spot_market.decimals))? + .safe_mul(oracle_data.price)? + .safe_div(PRICE_PRECISION_I64)? + }; + + msg!("Balance difference (notional): {}", balance_diff_notional); + + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 100, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault" + )?; + + let new_token_balance = constituent + .spot_balance + .get_signed_token_amount(&spot_market)? + .cast::()?; + + emit!(LPBorrowLendDepositRecord { + ts: clock.unix_timestamp, + slot: clock.slot, + spot_market_index: spot_market.market_index, + constituent_index: constituent.constituent_index, + direction: DepositDirection::Deposit, + token_balance: new_token_balance, + last_token_balance: constituent.last_spot_balance_token_amount, + interest_accrued_token_amount, + amount_deposit_withdraw: amount, + lp_pool: lp_pool_key, + }); + constituent.last_spot_balance_token_amount = new_token_balance; + constituent.cumulative_spot_interest_accrued_token_amount = constituent + .cumulative_spot_interest_accrued_token_amount + .safe_add(interest_accrued_token_amount)?; + + Ok(()) +} + +pub fn handle_withdraw_from_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawProgramVault<'info>>, + amount: u64, +) -> Result<()> { + let state = &ctx.accounts.state; + let clock = Clock::get()?; + + let mut spot_market = ctx.accounts.spot_market.load_mut()?; + + let oracle_id = spot_market.oracle_id(); + let mut oracle_map = OracleMap::load_one( + &ctx.accounts.oracle, + clock.slot, + Some(ctx.accounts.state.oracle_guard_rails), + )?; + let remaining_accounts = &mut ctx.remaining_accounts.iter().peekable(); + + let mut constituent = ctx.accounts.constituent.load_mut()?; + + if amount == 0 { + return Err(ErrorCode::InsufficientDeposit.into()); + } + + let oracle_data = oracle_map.get_price_data(&oracle_id)?; + + controller::spot_balance::update_spot_market_cumulative_interest( + &mut spot_market, + Some(oracle_data), + clock.unix_timestamp, + )?; + let token_balance_after_cumulative_interest_update = constituent + .spot_balance + .get_signed_token_amount(&spot_market)?; + + let interest_accrued_token_amount = token_balance_after_cumulative_interest_update + .cast::()? + .safe_sub(constituent.last_spot_balance_token_amount)?; + + let mint = &Some(*ctx.accounts.mint.clone()); + transfer_from_program_vault( + amount, + &mut spot_market, + &mut constituent, + oracle_data.price, + &state, + &mut ctx.accounts.spot_market_vault, + &mut ctx.accounts.constituent_token_account, + &ctx.accounts.token_program, + &ctx.accounts.drift_signer, + mint, + Some(remaining_accounts), + )?; + + let new_token_balance = constituent + .spot_balance + .get_signed_token_amount(&spot_market)? + .cast::()?; + + emit!(LPBorrowLendDepositRecord { + ts: clock.unix_timestamp, + slot: clock.slot, + spot_market_index: spot_market.market_index, + constituent_index: constituent.constituent_index, + direction: DepositDirection::Withdraw, + token_balance: new_token_balance, + last_token_balance: constituent.last_spot_balance_token_amount, + interest_accrued_token_amount, + amount_deposit_withdraw: amount, + lp_pool: constituent.lp_pool, + }); + constituent.last_spot_balance_token_amount = new_token_balance; + constituent.cumulative_spot_interest_accrued_token_amount = constituent + .cumulative_spot_interest_accrued_token_amount + .safe_add(interest_accrued_token_amount)?; + + Ok(()) +} + +fn transfer_from_program_vault<'info>( + amount: u64, + spot_market: &mut SpotMarket, + constituent: &mut Constituent, + oracle_price: i64, + state: &State, + spot_market_vault: &mut InterfaceAccount<'info, TokenAccount>, + constituent_token_account: &mut InterfaceAccount<'info, TokenAccount>, + token_program: &Interface<'info, TokenInterface>, + drift_signer: &AccountInfo<'info>, + mint: &Option>, + remaining_accounts: Option<&mut Peekable>>>, +) -> Result<()> { + constituent.sync_token_balance(constituent_token_account.amount); + + let balance_before = constituent.get_full_token_amount(&spot_market)?; + + // Adding some 5% flexibility to max threshold to prevent race conditions + let buffer = constituent + .max_borrow_token_amount + .safe_mul(5)? + .safe_div(100)?; + let max_transfer = constituent + .max_borrow_token_amount + .safe_add(buffer)? + .cast::()? + .safe_add( + constituent + .spot_balance + .get_signed_token_amount(spot_market)?, + )? + .max(0) + .cast::()?; + + validate!( + max_transfer >= amount, + ErrorCode::LpInvariantFailed, + "Max transfer ({}) is less than amount ({})", + max_transfer, + amount + )?; + + // Execute transfer and sync new balance in the constituent account + controller::token::send_from_program_vault( + token_program, + spot_market_vault, + constituent_token_account, + drift_signer, + state.signer_nonce, + amount, + mint, + remaining_accounts, + )?; + constituent_token_account.reload()?; + constituent.sync_token_balance(constituent_token_account.amount); + + // Adjust BLPosition for the new deposits + let spot_position = &mut constituent.spot_balance; + update_spot_balances( + amount as u128, + &SpotBalanceType::Borrow, + spot_market, + spot_position, + true, + )?; + + safe_decrement!(spot_position.cumulative_deposits, amount.cast()?); + + // Re-check spot market invariants + spot_market_vault.reload()?; + spot_market.validate_max_token_deposits_and_borrows(true)?; + math::spot_withdraw::validate_spot_market_vault_amount(&spot_market, spot_market_vault.amount)?; + + // Verify withdraw fully accounted for in BLPosition + let balance_after = constituent.get_full_token_amount(&spot_market)?; + + let balance_diff_notional = if spot_market.decimals > 6 { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(oracle_price)? + .safe_div(PRICE_PRECISION_I64)? + .safe_div(10_i64.pow(spot_market.decimals - 6))? + } else { + balance_after + .abs_diff(balance_before) + .cast::()? + .safe_mul(10_i64.pow(6 - spot_market.decimals))? + .safe_mul(oracle_price)? + .safe_div(PRICE_PRECISION_I64)? + }; + + #[cfg(feature = "mainnet-beta")] + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 100, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault, {}", + balance_diff_notional + )?; + #[cfg(not(feature = "mainnet-beta"))] + validate!( + balance_diff_notional <= PRICE_PRECISION_I64 / 10, + ErrorCode::LpInvariantFailed, + "Constituent balance mismatch after withdraw from program vault, {}", + balance_diff_notional + )?; + + Ok(()) +} + +#[derive(Accounts)] +pub struct DepositProgramVault<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin || admin.key() == lp_pool_swap_wallet::id() + )] + pub admin: Signer<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + constraint = &constituent.load()?.mint.eq(&constituent_token_account.mint), + )] + pub constituent_token_account: Box>, + #[account( + mut, + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + address = spot_market.load()?.vault, + )] + pub spot_market_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + #[account( + address = spot_market.load()?.mint, + )] + pub mint: Box>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct WithdrawProgramVault<'info> { + pub state: Box>, + #[account( + mut, + constraint = admin.key() == admin_hot_wallet::id() || admin.key() == state.admin || admin.key() == lp_pool_swap_wallet::id() + )] + pub admin: Signer<'info>, + /// CHECK: program signer + pub drift_signer: AccountInfo<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + address = constituent.load()?.vault, + constraint = &constituent.load()?.mint.eq(&constituent_token_account.mint), + )] + pub constituent_token_account: Box>, + #[account( + mut, + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + #[account( + mut, + address = spot_market.load()?.vault, + token::authority = drift_signer, + )] + pub spot_market_vault: Box>, + pub token_program: Interface<'info, TokenInterface>, + #[account( + address = spot_market.load()?.mint, + )] + pub mint: Box>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentOracleInfo<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + #[account(mut)] + pub constituent: AccountLoader<'info, Constituent>, + #[account( + owner = crate::ID, + constraint = spot_market.load()?.market_index == constituent.load()?.spot_market_index + )] + pub spot_market: AccountLoader<'info, SpotMarket>, + /// CHECK: checked when loading oracle in oracle map + pub oracle: AccountInfo<'info>, +} + +#[derive(Accounts)] +pub struct UpdateConstituentTargetBase<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + /// CHECK: checked in AmmConstituentMappingZeroCopy checks + pub amm_constituent_mapping: AccountInfo<'info>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + pub amm_cache: AccountInfo<'info>, + pub lp_pool: AccountLoader<'info, LPPool>, +} + +#[derive(Accounts)] +pub struct UpdateLPPoolAum<'info> { + pub state: Box>, + #[account(mut)] + pub keeper: Signer<'info>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + #[account(mut)] + pub constituent_target_base: AccountInfo<'info>, + /// CHECK: checked in AmmCacheZeroCopy checks + #[account(mut)] + pub amm_cache: AccountInfo<'info>, +} + +/// `in`/`out` is in the program's POV for this swap. So `user_in_token_account` is the user owned token account +/// for the `in` token for this swap. +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct LPPoolSwap<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and in ix + pub constituent_target_base: AccountInfo<'info>, + + /// CHECK: checked in ConstituentCorrelationsZeroCopy checks and in ix + pub constituent_correlations: AccountInfo<'info>, + + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + + #[account( + mut, + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) && user_in_token_account.owner == authority.key() + )] + pub user_in_token_account: Box>, + #[account( + mut, + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) && user_out_token_account.owner == authority.key() + )] + pub user_out_token_account: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump=in_constituent.load()?.bump, + constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump=out_constituent.load()?.bump, + constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = in_market_mint.key() == in_constituent.load()?.mint, + )] + pub in_market_mint: Box>, + #[account( + constraint = out_market_mint.key() == out_constituent.load()?.mint, + )] + pub out_market_mint: Box>, + + pub authority: Signer<'info>, + + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, + out_market_index: u16, +)] +pub struct ViewLPPoolSwapFees<'info> { + /// CHECK: forced drift_signer + pub drift_signer: AccountInfo<'info>, + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and in ix + pub constituent_target_base: AccountInfo<'info>, + + /// CHECK: checked in ConstituentCorrelationsZeroCopy checks and in ix + pub constituent_correlations: AccountInfo<'info>, + + #[account( + mut, + address = in_constituent.load()?.vault, + )] + pub constituent_in_token_account: Box>, + #[account( + mut, + address = out_constituent.load()?.vault, + )] + pub constituent_out_token_account: Box>, + + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump=in_constituent.load()?.bump, + constraint = in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump=out_constituent.load()?.bump, + constraint = out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + pub authority: Signer<'info>, + + // TODO: in/out token program + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct LPPoolAddLiquidity<'info> { + pub state: Box>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub in_market_mint: Box>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + constraint = + in_constituent.load()?.mint.eq(&constituent_in_token_account.mint) + )] + pub in_constituent: AccountLoader<'info, Constituent>, + + #[account( + mut, + constraint = user_in_token_account.mint.eq(&constituent_in_token_account.mint) && user_in_token_account.owner == authority.key() + )] + pub user_in_token_account: Box>, + + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub constituent_in_token_account: Box>, + + #[account( + mut, + constraint = user_lp_token_account.mint.eq(&lp_mint.key()) + )] + pub user_lp_token_account: Box>, + + #[account( + mut, + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks + pub constituent_target_base: AccountInfo<'info>, + + #[account( + mut, + seeds = [LP_POOL_TOKEN_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub lp_pool_token_vault: Box>, + + pub token_program: Interface<'info, TokenInterface>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct ViewLPPoolAddLiquidityFees<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub in_market_mint: Box>, + #[account( + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub in_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction( + out_market_index: u16, +)] +pub struct LPPoolRemoveLiquidity<'info> { + pub state: Box>, + #[account( + constraint = drift_signer.key() == state.signer + )] + /// CHECK: drift_signer + pub drift_signer: AccountInfo<'info>, + #[account(mut)] + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub out_market_mint: Box>, + #[account( + mut, + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + constraint = + out_constituent.load()?.mint.eq(&constituent_out_token_account.mint) + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + mut, + constraint = user_out_token_account.mint.eq(&constituent_out_token_account.mint) && user_out_token_account.owner == authority.key() + )] + pub user_out_token_account: Box>, + #[account( + mut, + seeds = ["CONSTITUENT_VAULT".as_ref(), lp_pool.key().as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + )] + pub constituent_out_token_account: Box>, + #[account( + mut, + constraint = user_lp_token_account.mint.eq(&lp_mint.key()) + )] + pub user_lp_token_account: Box>, + #[account( + mut, + seeds = [b"spot_market_vault".as_ref(), out_market_index.to_le_bytes().as_ref()], + bump, + )] + pub spot_market_token_account: Box>, + + #[account( + mut, + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, + + #[account( + mut, + seeds = [LP_POOL_TOKEN_VAULT_PDA_SEED.as_ref(), lp_pool.key().as_ref()], + bump, + )] + pub lp_pool_token_vault: Box>, + + pub token_program: Interface<'info, TokenInterface>, + + #[account( + seeds = [AMM_POSITIONS_CACHE.as_ref()], + bump, + )] + /// CHECK: checked in AmmCacheZeroCopy checks + pub amm_cache: AccountInfo<'info>, +} + +#[derive(Accounts)] +#[instruction( + in_market_index: u16, +)] +pub struct ViewLPPoolRemoveLiquidityFees<'info> { + pub state: Box>, + pub lp_pool: AccountLoader<'info, LPPool>, + pub authority: Signer<'info>, + pub out_market_mint: Box>, + #[account( + seeds = [CONSTITUENT_PDA_SEED.as_ref(), lp_pool.key().as_ref(), in_market_index.to_le_bytes().as_ref()], + bump, + )] + pub out_constituent: AccountLoader<'info, Constituent>, + + #[account( + constraint = lp_mint.key() == lp_pool.load()?.mint, + )] + pub lp_mint: Box>, + + /// CHECK: checked in ConstituentTargetBaseZeroCopy checks and address checked in code + pub constituent_target_base: AccountInfo<'info>, +} diff --git a/programs/drift/src/instructions/mod.rs b/programs/drift/src/instructions/mod.rs index 0caa84f731..d2267d7d5a 100644 --- a/programs/drift/src/instructions/mod.rs +++ b/programs/drift/src/instructions/mod.rs @@ -2,6 +2,8 @@ pub use admin::*; pub use constraints::*; pub use if_staker::*; pub use keeper::*; +pub use lp_admin::*; +pub use lp_pool::*; pub use pyth_lazer_oracle::*; pub use pyth_pull_oracle::*; pub use user::*; @@ -10,6 +12,8 @@ mod admin; mod constraints; mod if_staker; mod keeper; +mod lp_admin; +mod lp_pool; pub mod optional_accounts; mod pyth_lazer_oracle; mod pyth_pull_oracle; diff --git a/programs/drift/src/instructions/optional_accounts.rs b/programs/drift/src/instructions/optional_accounts.rs index 7abe5c33d2..c2365bf0ec 100644 --- a/programs/drift/src/instructions/optional_accounts.rs +++ b/programs/drift/src/instructions/optional_accounts.rs @@ -1,5 +1,8 @@ use crate::error::{DriftResult, ErrorCode}; use crate::state::high_leverage_mode_config::HighLeverageModeConfig; +use crate::state::revenue_share::{ + RevenueShareEscrow, RevenueShareEscrowLoader, RevenueShareEscrowZeroCopyMut, +}; use std::cell::RefMut; use std::convert::TryFrom; @@ -17,7 +20,7 @@ use crate::state::traits::Size; use crate::state::user::{User, UserStats}; use crate::{validate, OracleSource}; use anchor_lang::accounts::account::Account; -use anchor_lang::prelude::{AccountInfo, Interface}; +use anchor_lang::prelude::{AccountInfo, Interface, Pubkey}; use anchor_lang::prelude::{AccountLoader, InterfaceAccount}; use anchor_lang::Discriminator; use anchor_spl::token::TokenAccount; @@ -273,3 +276,40 @@ pub fn get_high_leverage_mode_config<'a>( Ok(Some(high_leverage_mode_config)) } + +pub fn get_revenue_share_escrow_account<'a>( + account_info_iter: &mut Peekable>>, + expected_authority: &Pubkey, +) -> DriftResult>> { + let account_info = account_info_iter.peek(); + if account_info.is_none() { + return Ok(None); + } + + let account_info = account_info.safe_unwrap()?; + + // Check size and discriminator without borrowing + if account_info.data_len() < 80 { + return Ok(None); + } + + let discriminator: [u8; 8] = RevenueShareEscrow::discriminator(); + let borrowed_data = account_info.data.borrow(); + let account_discriminator = array_ref![&borrowed_data, 0, 8]; + if account_discriminator != &discriminator { + return Ok(None); + } + + let account_info = account_info_iter.next().safe_unwrap()?; + + drop(borrowed_data); + let escrow: RevenueShareEscrowZeroCopyMut<'a> = account_info.load_zc_mut()?; + + validate!( + escrow.fixed.authority == *expected_authority, + ErrorCode::RevenueShareEscrowAuthorityMismatch, + "invalid RevenueShareEscrow authority" + )?; + + Ok(Some(escrow)) +} diff --git a/programs/drift/src/instructions/user.rs b/programs/drift/src/instructions/user.rs index 23133b8e87..326aa58bdd 100644 --- a/programs/drift/src/instructions/user.rs +++ b/programs/drift/src/instructions/user.rs @@ -22,19 +22,22 @@ use crate::controller::spot_position::{ update_spot_balances_and_cumulative_deposits_with_limits, }; use crate::error::ErrorCode; +use crate::get_then_update_id; use crate::ids::admin_hot_wallet; -use crate::ids::{ - jupiter_mainnet_3, jupiter_mainnet_4, jupiter_mainnet_6, lighthouse, marinade_mainnet, - serum_program, -}; +use crate::ids::WHITELISTED_SWAP_PROGRAMS; +use crate::ids::{lighthouse, marinade_mainnet}; use crate::instructions::constraints::*; +use crate::instructions::optional_accounts::get_revenue_share_escrow_account; use crate::instructions::optional_accounts::{ get_referrer_and_referrer_stats, get_whitelist_token, load_maps, AccountMaps, }; use crate::instructions::SpotFulfillmentType; +use crate::load; use crate::math::casting::Cast; use crate::math::liquidation::is_cross_margin_being_liquidated; use crate::math::liquidation::is_isolated_margin_being_liquidated; +use crate::math::constants::{QUOTE_SPOT_MARKET_INDEX, THIRTEEN_DAY}; +use crate::math::liquidation::is_user_being_liquidated; use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_liability_info; use crate::math::margin::meets_initial_margin_requirement; use crate::math::margin::{ @@ -43,6 +46,7 @@ use crate::math::margin::{ }; use crate::math::oracle::is_oracle_valid_for_action; use crate::math::oracle::DriftAction; +use crate::math::oracle::LogMode; use crate::math::orders::calculate_existing_position_fields_for_order_action; use crate::math::orders::get_position_delta_for_fill; use crate::math::orders::is_multiple_of_step_size; @@ -81,6 +85,12 @@ use crate::state::paused_operations::{PerpOperation, SpotOperation}; use crate::state::perp_market::MarketStatus; use crate::state::perp_market_map::{get_writable_perp_market_set, MarketSet}; use crate::state::protected_maker_mode_config::ProtectedMakerModeConfig; +use crate::state::revenue_share::BuilderInfo; +use crate::state::revenue_share::RevenueShare; +use crate::state::revenue_share::RevenueShareEscrow; +use crate::state::revenue_share::RevenueShareOrder; +use crate::state::revenue_share::REVENUE_SHARE_ESCROW_PDA_SEED; +use crate::state::revenue_share::REVENUE_SHARE_PDA_SEED; use crate::state::signed_msg_user::SignedMsgOrderId; use crate::state::signed_msg_user::SignedMsgUserOrdersLoader; use crate::state::signed_msg_user::SignedMsgWsDelegates; @@ -107,11 +117,8 @@ use crate::validation::position::validate_perp_position_with_perp_market; use crate::validation::user::validate_user_deletion; use crate::validation::whitelist::validate_whitelist_token; use crate::{controller, math}; -use crate::{get_then_update_id, QUOTE_SPOT_MARKET_INDEX}; -use crate::{load, THIRTEEN_DAY}; use crate::{load_mut, ExchangeStatus}; use anchor_lang::solana_program::sysvar::instructions; -use anchor_spl::associated_token::AssociatedToken; use borsh::{BorshDeserialize, BorshSerialize}; use solana_program::sysvar::instructions::ID as IX_ID; @@ -495,6 +502,146 @@ pub fn handle_reset_fuel_season<'c: 'info, 'info>( Ok(()) } +pub fn handle_initialize_revenue_share<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShare<'info>>, +) -> Result<()> { + let mut revenue_share = ctx + .accounts + .revenue_share + .load_init() + .or(Err(ErrorCode::UnableToLoadAccountLoader))?; + revenue_share.authority = ctx.accounts.authority.key(); + revenue_share.total_referrer_rewards = 0; + revenue_share.total_builder_rewards = 0; + Ok(()) +} + +pub fn handle_initialize_revenue_share_escrow<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShareEscrow<'info>>, + num_orders: u16, +) -> Result<()> { + let escrow = &mut ctx.accounts.escrow; + escrow.authority = ctx.accounts.authority.key(); + escrow + .orders + .resize_with(num_orders as usize, RevenueShareOrder::default); + + let state = &mut ctx.accounts.state; + if state.builder_referral_enabled() { + let mut user_stats = ctx.accounts.user_stats.load_mut()?; + escrow.referrer = user_stats.referrer; + user_stats.update_builder_referral_status(); + } + + escrow.validate()?; + Ok(()) +} + +pub fn handle_migrate_referrer<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, MigrateReferrer<'info>>, +) -> Result<()> { + let state = &mut ctx.accounts.state; + if !state.builder_referral_enabled() { + if state.admin != ctx.accounts.payer.key() + || ctx.accounts.payer.key() == admin_hot_wallet::id() + { + msg!("Only admin can migrate referrer until builder referral feature is enabled"); + return Err(anchor_lang::error::ErrorCode::ConstraintSigner.into()); + } + } + + let escrow = &mut ctx.accounts.escrow; + let mut user_stats = ctx.accounts.user_stats.load_mut()?; + escrow.referrer = user_stats.referrer; + user_stats.update_builder_referral_status(); + + escrow.validate()?; + Ok(()) +} + +pub fn handle_resize_revenue_share_escrow_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeRevenueShareEscrowOrders<'info>>, + num_orders: u16, +) -> Result<()> { + let escrow = &mut ctx.accounts.escrow; + validate!( + num_orders as usize >= escrow.orders.len(), + ErrorCode::InvalidRevenueShareResize, + "Invalid shrinking resize for revenue share escrow" + )?; + + escrow + .orders + .resize_with(num_orders as usize, RevenueShareOrder::default); + escrow.validate()?; + Ok(()) +} + +pub fn handle_change_approved_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeApprovedBuilder<'info>>, + builder: Pubkey, + max_fee_tenth_bps: u16, + add: bool, +) -> Result<()> { + validate!( + ctx.accounts.escrow.authority != builder, + ErrorCode::DefaultError, + "Builder cannot be the same as the escrow authority" + )?; + + let existing_builder_index = ctx + .accounts + .escrow + .approved_builders + .iter() + .position(|b| b.authority == builder); + if let Some(index) = existing_builder_index { + if add { + msg!( + "Updated builder: {} with max fee tenth bps: {} -> {}", + builder, + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps, + max_fee_tenth_bps + ); + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps = max_fee_tenth_bps; + } else { + if ctx + .accounts + .escrow + .orders + .iter() + .any(|o| (o.builder_idx == index as u8) && (!o.is_available())) + { + msg!("Builder has open orders, must cancel orders and settle_pnl before revoking"); + return Err(ErrorCode::CannotRevokeBuilderWithOpenOrders.into()); + } + msg!( + "Revoking builder: {}, max fee tenth bps: {} -> 0", + builder, + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps, + ); + ctx.accounts.escrow.approved_builders[index].max_fee_tenth_bps = 0; + } + } else { + if add { + ctx.accounts.escrow.approved_builders.push(BuilderInfo { + authority: builder, + max_fee_tenth_bps, + ..BuilderInfo::default() + }); + msg!( + "Added builder: {} with max fee tenth bps: {}", + builder, + max_fee_tenth_bps + ); + } else { + msg!("Tried to revoke builder: {}, but it was not found", builder); + } + } + + Ok(()) +} + #[access_control( deposit_not_paused(&ctx.accounts.state) )] @@ -1644,6 +1791,8 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( .last_oracle_price_twap, perp_market.get_max_confidence_interval_multiplier()?, perp_market.amm.oracle_slot_delay_override, + perp_market.amm.oracle_low_risk_slot_delay_override, + Some(LogMode::Margin), )?; step_size = perp_market.amm.order_step_size; tick_size = perp_market.amm.order_tick_size; @@ -1895,6 +2044,8 @@ pub fn handle_transfer_perp_position<'c: 'info, 'info>( maker_existing_quote_entry_amount: from_existing_quote_entry_amount, maker_existing_base_asset_amount: from_existing_base_asset_amount, trigger_price: None, + builder_idx: None, + builder_fee: None, }; emit_stack::<_, { OrderActionRecord::SIZE }>(fill_record)?; @@ -2158,6 +2309,7 @@ pub fn handle_place_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; Ok(()) @@ -2461,6 +2613,7 @@ pub fn handle_place_orders<'c: 'info, 'info>( clock, *params, options, + &mut None, )?; } else { controller::orders::place_spot_order( @@ -2541,6 +2694,7 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( &clock, params, PlaceOrderOptions::default(), + &mut None, )?; drop(user); @@ -2548,6 +2702,14 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( let user = &mut ctx.accounts.user; let order_id = load!(user)?.get_last_order_id(); + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account(remaining_accounts_iter, &load!(user)?.authority)? + } else { + None + }; + let (base_asset_amount_filled, _) = controller::orders::fill_perp_order( order_id, &ctx.accounts.state, @@ -2566,6 +2728,8 @@ pub fn handle_place_and_take_perp_order<'c: 'info, 'info>( is_immediate_or_cancel || optional_params.is_some(), auction_duration_percentage, ), + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_unfilled = load!(ctx.accounts.user)? @@ -2655,6 +2819,7 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2666,6 +2831,17 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( makers_and_referrer.insert(ctx.accounts.user.key(), ctx.accounts.user.clone())?; makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.taker)?.authority, + )? + } else { + None + }; + controller::orders::fill_perp_order( taker_order_id, state, @@ -2681,6 +2857,8 @@ pub fn handle_place_and_make_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_exists = load!(ctx.accounts.user)? @@ -2756,6 +2934,7 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( clock, params, PlaceOrderOptions::default(), + &mut None, )?; let (order_id, authority) = (user.get_last_order_id(), user.authority); @@ -2767,6 +2946,17 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( makers_and_referrer.insert(ctx.accounts.user.key(), ctx.accounts.user.clone())?; makers_and_referrer_stats.insert(authority, ctx.accounts.user_stats.clone())?; + let builder_referral_enabled = state.builder_referral_enabled(); + let builder_codes_enabled = state.builder_codes_enabled(); + let mut escrow = if builder_codes_enabled || builder_referral_enabled { + get_revenue_share_escrow_account( + remaining_accounts_iter, + &load!(ctx.accounts.taker)?.authority, + )? + } else { + None + }; + let taker_signed_msg_account = ctx.accounts.taker_signed_msg_user_orders.load()?; let taker_order_id = taker_signed_msg_account .iter() @@ -2789,6 +2979,8 @@ pub fn handle_place_and_make_signed_msg_perp_order<'c: 'info, 'info>( Some(order_id), clock, FillMode::PlaceAndMake, + &mut escrow.as_mut(), + builder_referral_enabled, )?; let order_exists = load!(ctx.accounts.user)? @@ -3707,13 +3899,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( )?; } } else { - let mut whitelisted_programs = vec![ - serum_program::id(), - AssociatedToken::id(), - jupiter_mainnet_3::ID, - jupiter_mainnet_4::ID, - jupiter_mainnet_6::ID, - ]; + let mut whitelisted_programs = WHITELISTED_SWAP_PROGRAMS.to_vec(); if !delegate_is_signer { whitelisted_programs.push(Token::id()); whitelisted_programs.push(Token2022::id()); @@ -3722,7 +3908,7 @@ pub fn handle_begin_swap<'c: 'info, 'info>( validate!( whitelisted_programs.contains(&ix.program_id), ErrorCode::InvalidSwap, - "only allowed to pass in ixs to token, openbook, and Jupiter v3/v4/v6 programs" + "only allowed to pass in ixs to ATA, openbook, Jupiter v3/v4/v6, dflow, or titan programs" )?; for meta in ix.accounts.iter() { @@ -4227,6 +4413,9 @@ pub struct ResizeSignedMsgUserOrders<'info> { pub signed_msg_user_orders: Box>, /// CHECK: authority pub authority: AccountInfo<'info>, + #[account( + has_one = authority + )] pub user: AccountLoader<'info, User>, #[account(mut)] pub payer: Signer<'info>, @@ -4750,14 +4939,9 @@ pub struct UpdateUser<'info> { } #[derive(Accounts)] -#[instruction( - sub_account_id: u16, -)] pub struct UpdateUserPerpPositionCustomMarginRatio<'info> { #[account( mut, - seeds = [b"user", authority.key.as_ref(), sub_account_id.to_le_bytes().as_ref()], - bump, constraint = can_sign_for_user(&user, &authority)? )] pub user: AccountLoader<'info, User>, @@ -4893,3 +5077,106 @@ pub struct UpdateUserProtectedMakerMode<'info> { #[account(mut)] pub protected_maker_mode_config: AccountLoader<'info, ProtectedMakerModeConfig>, } + +#[derive(Accounts)] +#[instruction()] +pub struct InitializeRevenueShare<'info> { + #[account( + init, + seeds = [REVENUE_SHARE_PDA_SEED.as_ref(), authority.key().as_ref()], + space = RevenueShare::space(), + bump, + payer = payer + )] + pub revenue_share: AccountLoader<'info, RevenueShare>, + /// CHECK: The builder and/or referrer authority, beneficiary of builder/ref fees + pub authority: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(num_orders: u16)] +pub struct InitializeRevenueShareEscrow<'info> { + #[account( + init, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + space = RevenueShareEscrow::space(num_orders as usize, 1), + bump, + payer = payer + )] + pub escrow: Box>, + /// CHECK: The auth owning this account, payer of builder/ref fees + pub authority: AccountInfo<'info>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub state: Box>, + #[account(mut)] + pub payer: Signer<'info>, + pub rent: Sysvar<'info, Rent>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +pub struct MigrateReferrer<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + )] + pub escrow: Box>, + /// CHECK: The auth owning this account, payer of builder/ref fees + pub authority: AccountInfo<'info>, + #[account( + mut, + has_one = authority + )] + pub user_stats: AccountLoader<'info, UserStats>, + pub state: Box>, + pub payer: Signer<'info>, +} + +#[derive(Accounts)] +#[instruction(num_orders: u16)] +pub struct ResizeRevenueShareEscrowOrders<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + realloc = RevenueShareEscrow::space(num_orders as usize, escrow.approved_builders.len()), + realloc::payer = payer, + realloc::zero = false, + has_one = authority + )] + pub escrow: Box>, + /// CHECK: The owner of RevenueShareEscrow + pub authority: AccountInfo<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} + +#[derive(Accounts)] +#[instruction(builder: Pubkey, max_fee_tenth_bps: u16, add: bool)] +pub struct ChangeApprovedBuilder<'info> { + #[account( + mut, + seeds = [REVENUE_SHARE_ESCROW_PDA_SEED.as_ref(), authority.key().as_ref()], + bump, + // revoking a builder does not remove the slot to avoid unintended reuse + realloc = RevenueShareEscrow::space(escrow.orders.len(), if add { escrow.approved_builders.len() + 1 } else { escrow.approved_builders.len() }), + realloc::payer = payer, + realloc::zero = false, + has_one = authority + )] + pub escrow: Box>, + pub authority: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + pub system_program: Program<'info, System>, +} diff --git a/programs/drift/src/lib.rs b/programs/drift/src/lib.rs index 07a3462069..ff3b2c298d 100644 --- a/programs/drift/src/lib.rs +++ b/programs/drift/src/lib.rs @@ -6,7 +6,6 @@ use anchor_lang::prelude::*; use instructions::*; #[cfg(test)] -use math::amm; use math::{bn, constants::*}; use state::oracle::OracleSource; @@ -455,13 +454,13 @@ pub mod drift { handle_update_user_reduce_only(ctx, _sub_account_id, reduce_only) } - pub fn update_user_advanced_lp( - ctx: Context, - _sub_account_id: u16, - advanced_lp: bool, - ) -> Result<()> { - handle_update_user_advanced_lp(ctx, _sub_account_id, advanced_lp) - } + // pub fn update_user_advanced_lp( + // ctx: Context, + // _sub_account_id: u16, + // advanced_lp: bool, + // ) -> Result<()> { + // handle_update_user_advanced_lp(ctx, _sub_account_id, advanced_lp) + // } pub fn update_user_protected_maker_orders( ctx: Context, @@ -567,9 +566,9 @@ pub mod drift { handle_update_user_stats_referrer_info(ctx) } - pub fn update_user_open_orders_count(ctx: Context) -> Result<()> { - handle_update_user_open_orders_count(ctx) - } + // pub fn update_user_open_orders_count(ctx: Context) -> Result<()> { + // handle_update_user_open_orders_count(ctx) + // } pub fn admin_disable_update_perp_bid_ask_twap( ctx: Context, @@ -767,6 +766,7 @@ pub mod drift { handle_update_spot_market_expiry(ctx, expiry_ts) } + // IF stakers pub fn update_user_quote_asset_insurance_stake( ctx: Context, ) -> Result<()> { @@ -779,7 +779,11 @@ pub mod drift { handle_update_user_gov_token_insurance_stake(ctx) } - // IF stakers + pub fn update_delegate_user_gov_token_insurance_stake( + ctx: Context, + ) -> Result<()> { + handle_update_delegate_user_gov_token_insurance_stake(ctx) + } pub fn initialize_insurance_fund_stake( ctx: Context, @@ -818,13 +822,14 @@ pub mod drift { handle_remove_insurance_fund_stake(ctx, market_index) } - pub fn transfer_protocol_if_shares( - ctx: Context, - market_index: u16, - shares: u128, - ) -> Result<()> { - handle_transfer_protocol_if_shares(ctx, market_index, shares) - } + // pub fn transfer_protocol_if_shares( + // ctx: Context, + // market_index: u16, + // shares: u128, + // ) -> Result<()> { + // handle_transfer_protocol_if_shares(ctx, market_index, shares) + // } + pub fn begin_insurance_fund_swap<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InsuranceFundSwap<'info>>, in_market_index: u16, @@ -850,6 +855,14 @@ pub mod drift { handle_transfer_protocol_if_shares_to_revenue_pool(ctx, market_index, amount) } + pub fn deposit_into_insurance_fund_stake<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositIntoInsuranceFundStake<'info>>, + market_index: u16, + amount: u64, + ) -> Result<()> { + handle_deposit_into_insurance_fund_stake(ctx, market_index, amount) + } + pub fn update_pyth_pull_oracle( ctx: Context, feed_id: [u8; 32], @@ -979,9 +992,9 @@ pub mod drift { handle_update_phoenix_fulfillment_config_status(ctx, status) } - pub fn update_serum_vault(ctx: Context) -> Result<()> { - handle_update_serum_vault(ctx) - } + // pub fn update_serum_vault(ctx: Context) -> Result<()> { + // handle_update_serum_vault(ctx) + // } pub fn initialize_perp_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, InitializePerpMarket<'info>>, @@ -1010,6 +1023,7 @@ pub mod drift { curve_update_intensity: u8, amm_jit_intensity: u8, name: [u8; 32], + lp_pool_id: u8, ) -> Result<()> { handle_initialize_perp_market( ctx, @@ -1038,9 +1052,28 @@ pub mod drift { curve_update_intensity, amm_jit_intensity, name, + lp_pool_id, ) } + pub fn initialize_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeAmmCache<'info>>, + ) -> Result<()> { + handle_initialize_amm_cache(ctx) + } + + pub fn resize_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeAmmCache<'info>>, + ) -> Result<()> { + handle_resize_amm_cache(ctx) + } + + pub fn update_initial_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + ) -> Result<()> { + handle_update_initial_amm_cache_info(ctx) + } + pub fn initialize_prediction_market<'c: 'info, 'info>( ctx: Context<'_, '_, 'c, 'info, AdminUpdatePerpMarket<'info>>, ) -> Result<()> { @@ -1092,6 +1125,32 @@ pub mod drift { handle_update_perp_market_expiry(ctx, expiry_ts) } + pub fn update_perp_market_lp_pool_paused_operations( + ctx: Context, + lp_paused_operations: u8, + ) -> Result<()> { + handle_update_perp_market_lp_pool_paused_operations(ctx, lp_paused_operations) + } + + pub fn update_perp_market_lp_pool_status( + ctx: Context, + lp_status: u8, + ) -> Result<()> { + handle_update_perp_market_lp_pool_status(ctx, lp_status) + } + + pub fn update_perp_market_lp_pool_fee_transfer_scalar( + ctx: Context, + optional_lp_fee_transfer_scalar: Option, + optional_lp_net_pnl_transfer_scalar: Option, + ) -> Result<()> { + handle_update_perp_market_lp_pool_fee_transfer_scalar( + ctx, + optional_lp_fee_transfer_scalar, + optional_lp_net_pnl_transfer_scalar, + ) + } + pub fn settle_expired_market_pools_to_revenue_pool( ctx: Context, ) -> Result<()> { @@ -1191,6 +1250,13 @@ pub mod drift { handle_update_perp_liquidation_fee(ctx, liquidator_fee, if_liquidation_fee) } + pub fn update_perp_market_lp_pool_id( + ctx: Context, + lp_pool_id: u8, + ) -> Result<()> { + handle_update_perp_lp_pool_id(ctx, lp_pool_id) + } + pub fn update_insurance_fund_unstaking_period( ctx: Context, insurance_fund_unstaking_period: i64, @@ -1368,7 +1434,7 @@ pub mod drift { } pub fn update_perp_market_paused_operations( - ctx: Context, + ctx: Context, paused_operations: u8, ) -> Result<()> { handle_update_perp_market_paused_operations(ctx, paused_operations) @@ -1415,6 +1481,16 @@ pub mod drift { handle_update_perp_market_curve_update_intensity(ctx, curve_update_intensity) } + pub fn update_perp_market_reference_price_offset_deadband_pct( + ctx: Context, + reference_price_offset_deadband_pct: u8, + ) -> Result<()> { + handle_update_perp_market_reference_price_offset_deadband_pct( + ctx, + reference_price_offset_deadband_pct, + ) + } + pub fn update_lp_cooldown_time( ctx: Context, lp_cooldown_time: u64, @@ -1509,7 +1585,7 @@ pub mod drift { } pub fn update_perp_market_max_spread( - ctx: Context, + ctx: Context, max_spread: u32, ) -> Result<()> { handle_update_perp_market_max_spread(ctx, max_spread) @@ -1601,11 +1677,14 @@ pub mod drift { ) } - pub fn update_perp_market_taker_speed_bump_override( + pub fn update_perp_market_oracle_low_risk_slot_delay_override( ctx: Context, - taker_speed_bump_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> Result<()> { - handle_update_perp_market_taker_speed_bump_override(ctx, taker_speed_bump_override) + handle_update_perp_market_oracle_low_risk_slot_delay_override( + ctx, + oracle_low_risk_slot_delay_override, + ) } pub fn update_perp_market_amm_spread_adjustment( @@ -1704,23 +1783,23 @@ pub mod drift { handle_update_spot_auction_duration(ctx, default_spot_auction_duration) } - pub fn initialize_protocol_if_shares_transfer_config( - ctx: Context, - ) -> Result<()> { - handle_initialize_protocol_if_shares_transfer_config(ctx) - } - - pub fn update_protocol_if_shares_transfer_config( - ctx: Context, - whitelisted_signers: Option<[Pubkey; 4]>, - max_transfer_per_epoch: Option, - ) -> Result<()> { - handle_update_protocol_if_shares_transfer_config( - ctx, - whitelisted_signers, - max_transfer_per_epoch, - ) - } + // pub fn initialize_protocol_if_shares_transfer_config( + // ctx: Context, + // ) -> Result<()> { + // handle_initialize_protocol_if_shares_transfer_config(ctx) + // } + + // pub fn update_protocol_if_shares_transfer_config( + // ctx: Context, + // whitelisted_signers: Option<[Pubkey; 4]>, + // max_transfer_per_epoch: Option, + // ) -> Result<()> { + // handle_update_protocol_if_shares_transfer_config( + // ctx, + // whitelisted_signers, + // max_transfer_per_epoch, + // ) + // } pub fn initialize_prelaunch_oracle( ctx: Context, @@ -1835,6 +1914,336 @@ pub mod drift { ) -> Result<()> { handle_update_feature_bit_flags_median_trigger_price(ctx, enable) } + + // pub fn update_feature_bit_flags_builder_referral( + // ctx: Context, + // enable: bool, + // ) -> Result<()> { + // handle_update_feature_bit_flags_builder_referral(ctx, enable) + // } + + pub fn update_feature_bit_flags_builder_codes( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_builder_codes(ctx, enable) + } + + pub fn initialize_revenue_share<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShare<'info>>, + ) -> Result<()> { + handle_initialize_revenue_share(ctx) + } + + pub fn initialize_revenue_share_escrow<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, InitializeRevenueShareEscrow<'info>>, + num_orders: u16, + ) -> Result<()> { + handle_initialize_revenue_share_escrow(ctx, num_orders) + } + + // pub fn migrate_referrer<'c: 'info, 'info>( + // ctx: Context<'_, '_, 'c, 'info, MigrateReferrer<'info>>, + // ) -> Result<()> { + // handle_migrate_referrer(ctx) + // } + + pub fn resize_revenue_share_escrow_orders<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResizeRevenueShareEscrowOrders<'info>>, + num_orders: u16, + ) -> Result<()> { + handle_resize_revenue_share_escrow_orders(ctx, num_orders) + } + + pub fn change_approved_builder<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ChangeApprovedBuilder<'info>>, + builder: Pubkey, + max_fee_bps: u16, + add: bool, + ) -> Result<()> { + handle_change_approved_builder(ctx, builder, max_fee_bps, add) + } + + pub fn initialize_lp_pool( + ctx: Context, + lp_pool_id: u8, + min_mint_fee: i64, + max_aum: u128, + max_settle_quote_amount_per_market: u64, + whitelist_mint: Pubkey, + ) -> Result<()> { + handle_initialize_lp_pool( + ctx, + lp_pool_id, + min_mint_fee, + max_aum, + max_settle_quote_amount_per_market, + whitelist_mint, + ) + } + + pub fn update_feature_bit_flags_settle_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_settle_lp_pool(ctx, enable) + } + + pub fn update_feature_bit_flags_swap_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_swap_lp_pool(ctx, enable) + } + + pub fn update_feature_bit_flags_mint_redeem_lp_pool( + ctx: Context, + enable: bool, + ) -> Result<()> { + handle_update_feature_bit_flags_mint_redeem_lp_pool(ctx, enable) + } + + pub fn initialize_constituent<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeConstituent<'info>>, + spot_market_index: u16, + decimals: u8, + max_weight_deviation: i64, + swap_fee_min: i64, + swap_fee_max: i64, + max_borrow_token_amount: u64, + oracle_staleness_threshold: u64, + cost_to_trade: i32, + constituent_derivative_index: Option, + constituent_derivative_depeg_threshold: u64, + derivative_weight: u64, + volatility: u64, + gamma_execution: u8, + gamma_inventory: u8, + xi: u8, + new_constituent_correlations: Vec, + ) -> Result<()> { + handle_initialize_constituent( + ctx, + spot_market_index, + decimals, + max_weight_deviation, + swap_fee_min, + swap_fee_max, + max_borrow_token_amount, + oracle_staleness_threshold, + cost_to_trade, + constituent_derivative_index, + constituent_derivative_depeg_threshold, + derivative_weight, + volatility, + gamma_execution, + gamma_inventory, + xi, + new_constituent_correlations, + ) + } + + pub fn update_constituent_status<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateConstituentStatus<'info>>, + new_status: u8, + ) -> Result<()> { + handle_update_constituent_status(ctx, new_status) + } + + pub fn update_constituent_paused_operations<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateConstituentPausedOperations<'info>>, + paused_operations: u8, + ) -> Result<()> { + handle_update_constituent_paused_operations(ctx, paused_operations) + } + + pub fn update_constituent_params( + ctx: Context, + constituent_params: ConstituentParams, + ) -> Result<()> { + handle_update_constituent_params(ctx, constituent_params) + } + + pub fn update_lp_pool_params( + ctx: Context, + lp_pool_params: LpPoolParams, + ) -> Result<()> { + handle_update_lp_pool_params(ctx, lp_pool_params) + } + + pub fn add_amm_constituent_mapping_data( + ctx: Context, + amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_add_amm_constituent_data(ctx, amm_constituent_mapping_data) + } + + pub fn update_amm_constituent_mapping_data( + ctx: Context, + amm_constituent_mapping_data: Vec, + ) -> Result<()> { + handle_update_amm_constituent_mapping_data(ctx, amm_constituent_mapping_data) + } + + pub fn remove_amm_constituent_mapping_data<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, RemoveAmmConstituentMappingData<'info>>, + perp_market_index: u16, + constituent_index: u16, + ) -> Result<()> { + handle_remove_amm_constituent_mapping_data(ctx, perp_market_index, constituent_index) + } + + pub fn update_constituent_correlation_data( + ctx: Context, + index1: u16, + index2: u16, + correlation: i64, + ) -> Result<()> { + handle_update_constituent_correlation_data(ctx, index1, index2, correlation) + } + + pub fn update_lp_constituent_target_base<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentTargetBase<'info>>, + ) -> Result<()> { + handle_update_constituent_target_base(ctx) + } + + pub fn update_lp_pool_aum<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateLPPoolAum<'info>>, + ) -> Result<()> { + handle_update_lp_pool_aum(ctx) + } + + pub fn update_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateAmmCache<'info>>, + ) -> Result<()> { + handle_update_amm_cache(ctx) + } + + pub fn override_amm_cache_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateInitialAmmCacheInfo<'info>>, + market_index: u16, + override_params: OverrideAmmCacheParams, + ) -> Result<()> { + handle_override_amm_cache_info(ctx, market_index, override_params) + } + + pub fn reset_amm_cache<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ResetAmmCache<'info>>, + ) -> Result<()> { + handle_reset_amm_cache(ctx) + } + + pub fn lp_pool_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + min_out_amount: u64, + ) -> Result<()> { + handle_lp_pool_swap( + ctx, + in_market_index, + out_market_index, + in_amount, + min_out_amount, + ) + } + + pub fn view_lp_pool_swap_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolSwapFees<'info>>, + in_market_index: u16, + out_market_index: u16, + in_amount: u64, + in_target_weight: i64, + out_target_weight: i64, + ) -> Result<()> { + handle_view_lp_pool_swap_fees( + ctx, + in_market_index, + out_market_index, + in_amount, + in_target_weight, + out_target_weight, + ) + } + + pub fn lp_pool_add_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolAddLiquidity<'info>>, + in_market_index: u16, + in_amount: u128, + min_mint_amount: u64, + ) -> Result<()> { + handle_lp_pool_add_liquidity(ctx, in_market_index, in_amount, min_mint_amount) + } + + pub fn lp_pool_remove_liquidity<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPPoolRemoveLiquidity<'info>>, + in_market_index: u16, + in_amount: u64, + min_out_amount: u128, + ) -> Result<()> { + handle_lp_pool_remove_liquidity(ctx, in_market_index, in_amount, min_out_amount) + } + + pub fn view_lp_pool_add_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolAddLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u128, + ) -> Result<()> { + handle_view_lp_pool_add_liquidity_fees(ctx, in_market_index, in_amount) + } + + pub fn view_lp_pool_remove_liquidity_fees<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, ViewLPPoolRemoveLiquidityFees<'info>>, + in_market_index: u16, + in_amount: u64, + ) -> Result<()> { + handle_view_lp_pool_remove_liquidity_fees(ctx, in_market_index, in_amount) + } + + pub fn begin_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + in_market_index: u16, + out_market_index: u16, + amount_in: u64, + ) -> Result<()> { + handle_begin_lp_swap(ctx, in_market_index, out_market_index, amount_in) + } + + pub fn end_lp_swap<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, LPTakerSwap<'info>>, + _in_market_index: u16, + _out_market_index: u16, + ) -> Result<()> { + handle_end_lp_swap(ctx) + } + + pub fn update_constituent_oracle_info<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, UpdateConstituentOracleInfo<'info>>, + ) -> Result<()> { + handle_update_constituent_oracle_info(ctx) + } + + pub fn deposit_to_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, DepositProgramVault<'info>>, + amount: u64, + ) -> Result<()> { + handle_deposit_to_program_vault(ctx, amount) + } + + pub fn withdraw_from_program_vault<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, WithdrawProgramVault<'info>>, + amount: u64, + ) -> Result<()> { + handle_withdraw_from_program_vault(ctx, amount) + } + + pub fn settle_perp_to_lp_pool<'c: 'info, 'info>( + ctx: Context<'_, '_, 'c, 'info, SettleAmmPnlToLp<'info>>, + ) -> Result<()> { + handle_settle_perp_to_lp_pool(ctx) + } } #[cfg(not(feature = "no-entrypoint"))] diff --git a/programs/drift/src/math/amm.rs b/programs/drift/src/math/amm.rs index 0407f627cf..914dffe621 100644 --- a/programs/drift/src/math/amm.rs +++ b/programs/drift/src/math/amm.rs @@ -10,8 +10,8 @@ use crate::math::casting::Cast; use crate::math::constants::{ BID_ASK_SPREAD_PRECISION_I128, CONCENTRATION_PRECISION, DEFAULT_MAX_TWAP_UPDATE_PRICE_BAND_DENOMINATOR, FIVE_MINUTE, ONE_HOUR, ONE_MINUTE, - PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, - PRICE_TO_PEG_PRECISION_RATIO, + PERCENTAGE_PRECISION_U64, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, + PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, PRICE_TO_PEG_PRECISION_RATIO, }; use crate::math::orders::standardize_base_asset_amount; use crate::math::quote_asset::reserve_to_asset_amount; @@ -19,7 +19,7 @@ use crate::math::stats::{calculate_new_twap, calculate_rolling_sum, calculate_we use crate::state::oracle::{MMOraclePriceData, OraclePriceData}; use crate::state::perp_market::AMM; use crate::state::state::PriceDivergenceGuardRails; -use crate::{validate, PERCENTAGE_PRECISION_U64}; +use crate::validate; use super::helpers::get_proportion_u128; use crate::math::safe_math::SafeMath; diff --git a/programs/drift/src/math/amm_spread.rs b/programs/drift/src/math/amm_spread.rs index 80915b83b9..7007722932 100644 --- a/programs/drift/src/math/amm_spread.rs +++ b/programs/drift/src/math/amm_spread.rs @@ -193,6 +193,35 @@ pub fn calculate_inventory_liquidity_ratio( Ok(amm_inventory_pct) } +pub fn calculate_inventory_liquidity_ratio_for_reference_price_offset( + base_asset_amount_with_amm: i128, + base_asset_reserve: u128, + min_base_asset_reserve: u128, + max_base_asset_reserve: u128, +) -> DriftResult { + // inventory scale + let (max_bids, max_asks) = _calculate_market_open_bids_asks( + base_asset_reserve, + min_base_asset_reserve, + max_base_asset_reserve, + )?; + + let avg_liquidity = (max_bids.safe_add(max_asks.abs())?).safe_div(2)?; + + let amm_inventory_pct = if base_asset_amount_with_amm.abs() < avg_liquidity { + base_asset_amount_with_amm + .abs() + .safe_mul(PERCENTAGE_PRECISION_I128) + .unwrap_or(i128::MAX) + .safe_div(avg_liquidity.max(1))? + .min(PERCENTAGE_PRECISION_I128) + } else { + PERCENTAGE_PRECISION_I128 // 100% + }; + + Ok(amm_inventory_pct) +} + pub fn calculate_spread_inventory_scale( base_asset_amount_with_amm: i128, base_asset_reserve: u128, @@ -570,7 +599,7 @@ pub fn calculate_reference_price_offset( mark_twap_slow: u64, max_offset_pct: i64, ) -> DriftResult { - if last_24h_avg_funding_rate == 0 { + if last_24h_avg_funding_rate == 0 || liquidity_fraction == 0 { return Ok(0); } diff --git a/programs/drift/src/math/amm_spread/tests.rs b/programs/drift/src/math/amm_spread/tests.rs index 5953db1e04..ac57fb45df 100644 --- a/programs/drift/src/math/amm_spread/tests.rs +++ b/programs/drift/src/math/amm_spread/tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod test { + use crate::controller::amm::update_spreads; use crate::math::amm::calculate_price; use crate::math::amm_spread::*; use crate::math::constants::{ @@ -189,6 +190,73 @@ mod test { assert_eq!(res, 0); } + #[test] + fn calculate_reference_price_offset_deadband_tests() { + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: AMM_RESERVE_PRECISION * 11, + quote_asset_reserve: AMM_RESERVE_PRECISION * 10, + sqrt_k: AMM_RESERVE_PRECISION * 10, + peg_multiplier: 34_000_000, + min_base_asset_reserve: AMM_RESERVE_PRECISION * 7, + max_base_asset_reserve: AMM_RESERVE_PRECISION * 14, + base_spread: 1000, + max_spread: 20_000, + curve_update_intensity: 110, + last_24h_avg_funding_rate: 1, + last_mark_price_twap_5min: 4216 * 10000 + 2 * 10000, + last_mark_price_twap: 4216 * 10000 + 2 * 10000, + historical_oracle_data: { + let mut hod: crate::state::oracle::HistoricalOracleData = Default::default(); + hod.last_oracle_price_twap_5min = 4216 * 10000; + hod.last_oracle_price_twap = 4216 * 10000; + hod + }, + ..AMM::default() + }, + ..PerpMarket::default() + }; + + let reserve_price = 4216 * 10000; + + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 7 / 20) as i128; + let inventory_ratio = calculate_inventory_liquidity_ratio_for_reference_price_offset( + market.amm.base_asset_amount_with_amm, + market.amm.base_asset_reserve, + market.amm.min_base_asset_reserve, + market.amm.max_base_asset_reserve, + ) + .unwrap(); + assert_eq!(inventory_ratio, 100000); // 10% + + market.amm.reference_price_offset_deadband_pct = 10; // 10% + + // If inventory exceeds threshold positive ref price offset + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 8 / 20) as i128; + let (_l, _s) = update_spreads(&mut market, reserve_price as u64, None).unwrap(); + assert!(market.amm.reference_price_offset > 0); + + // If inventory is small, goes to 0 + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 6 / 20) as i128; + let (_l, _s) = update_spreads(&mut market, reserve_price as u64, None).unwrap(); + assert_eq!(market.amm.reference_price_offset, 0); + + // Same for short pos + // Make sure that the premium is also short + market.amm.last_24h_avg_funding_rate = -1; + market.amm.last_mark_price_twap_5min = 4216 * 10000 - 2 * 10000; + market.amm.last_mark_price_twap = 4216 * 10000 - 2 * 10000; + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 8 / 20) as i128 * -1; + let (_l, _s) = update_spreads(&mut market, reserve_price as u64, None).unwrap(); + println!("ref offset: {}", market.amm.reference_price_offset); + assert!(market.amm.reference_price_offset < 0); + + // Same for short pos + market.amm.base_asset_amount_with_amm = (AMM_RESERVE_PRECISION * 6 / 20) as i128 * -1; + let (_l, _s) = update_spreads(&mut market, reserve_price as u64, None).unwrap(); + assert_eq!(market.amm.reference_price_offset, 0); + } + #[test] fn calculate_spread_tests() { let base_spread = 1000; // .1% diff --git a/programs/drift/src/math/auction.rs b/programs/drift/src/math/auction.rs index 88049e246f..476dd539d0 100644 --- a/programs/drift/src/math/auction.rs +++ b/programs/drift/src/math/auction.rs @@ -1,7 +1,7 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; -use crate::math::constants::AUCTION_DERIVE_PRICE_FRACTION; +use crate::math::constants::{AUCTION_DERIVE_PRICE_FRACTION, MAX_PREDICTION_MARKET_PRICE}; use crate::math::orders::standardize_price; use crate::math::safe_math::SafeMath; use crate::msg; @@ -9,13 +9,10 @@ use crate::state::oracle::OraclePriceData; use crate::state::perp_market::ContractTier; use crate::state::user::{Order, OrderBitFlag, OrderType}; -use crate::state::fill_mode::FillMode; -use crate::state::perp_market::{AMMAvailability, PerpMarket}; -use crate::{OrderParams, MAX_PREDICTION_MARKET_PRICE}; +use crate::state::perp_market::PerpMarket; +use crate::OrderParams; use std::cmp::min; -use super::orders::get_posted_slot_from_clock_slot; - #[cfg(test)] mod tests; @@ -234,42 +231,6 @@ pub fn is_auction_complete(order_slot: u64, auction_duration: u8, slot: u64) -> Ok(slots_elapsed > auction_duration.cast()?) } -pub fn can_fill_with_amm( - amm_availability: AMMAvailability, - valid_oracle_price: Option, - order: &Order, - min_auction_duration: u8, - slot: u64, - fill_mode: FillMode, -) -> DriftResult { - Ok(amm_availability != AMMAvailability::Unavailable - && valid_oracle_price.is_some() - && (amm_availability == AMMAvailability::Immediate - || is_amm_available_liquidity_source(order, min_auction_duration, slot, fill_mode)?)) -} - -pub fn is_amm_available_liquidity_source( - order: &Order, - min_auction_duration: u8, - slot: u64, - fill_mode: FillMode, -) -> DriftResult { - if fill_mode.is_liquidation() { - return Ok(true); - } - - if order.is_bit_flag_set(OrderBitFlag::SafeTriggerOrder) { - return Ok(true); - } - - if order.is_signed_msg() { - let clock_slot_tail = get_posted_slot_from_clock_slot(slot); - return Ok(clock_slot_tail.wrapping_sub(order.posted_slot_tail) >= min_auction_duration); - } - - Ok(is_auction_complete(order.slot, min_auction_duration, slot)?) -} - pub fn calculate_auction_params_for_trigger_order( order: &Order, oracle_price_data: &OraclePriceData, diff --git a/programs/drift/src/math/constants.rs b/programs/drift/src/math/constants.rs index 1c127315ae..cf9148aff6 100644 --- a/programs/drift/src/math/constants.rs +++ b/programs/drift/src/math/constants.rs @@ -53,6 +53,8 @@ pub const PERCENTAGE_PRECISION: u128 = 1_000_000; // expo -6 (represents 100%) pub const PERCENTAGE_PRECISION_I128: i128 = PERCENTAGE_PRECISION as i128; pub const PERCENTAGE_PRECISION_U64: u64 = PERCENTAGE_PRECISION as u64; pub const PERCENTAGE_PRECISION_I64: i64 = PERCENTAGE_PRECISION as i64; +pub const PERCENTAGE_PRECISION_I32: i32 = PERCENTAGE_PRECISION as i32; + pub const TEN_BPS: i128 = PERCENTAGE_PRECISION_I128 / 1000; pub const TEN_BPS_I64: i64 = TEN_BPS as i64; pub const TWO_PT_TWO_PCT: i128 = 22_000; @@ -168,7 +170,7 @@ pub const MAX_K_BPS_INCREASE: i128 = TEN_BPS; pub const MAX_K_BPS_DECREASE: i128 = TWO_PT_TWO_PCT; pub const MAX_UPDATE_K_PRICE_CHANGE: u128 = HUNDRENTH_OF_CENT; pub const MAX_SQRT_K: u128 = 1000000000000000000000; // 1e21 (count 'em!) -pub const MAX_BASE_ASSET_AMOUNT_WITH_AMM: u128 = 100000000000000000; // 1e17 (count 'em!) +pub const MAX_BASE_ASSET_AMOUNT_WITH_AMM: u128 = 400000000000000000; // 4e17 (count 'em!) pub const MAX_PEG_BPS_INCREASE: u128 = TEN_BPS as u128; // 10 bps increase pub const MAX_PEG_BPS_DECREASE: u128 = TEN_BPS as u128; // 10 bps decrease diff --git a/programs/drift/src/math/cp_curve/tests.rs b/programs/drift/src/math/cp_curve/tests.rs index ca7de7e987..b6c8b667b7 100644 --- a/programs/drift/src/math/cp_curve/tests.rs +++ b/programs/drift/src/math/cp_curve/tests.rs @@ -1,14 +1,10 @@ use crate::controller::amm::update_spreads; use crate::controller::position::PositionDirection; -use crate::math::amm::calculate_bid_ask_bounds; -use crate::math::constants::BASE_PRECISION; -use crate::math::constants::CONCENTRATION_PRECISION; use crate::math::constants::{ - BASE_PRECISION_U64, MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, QUOTE_PRECISION_I64, + MAX_CONCENTRATION_COEFFICIENT, MAX_K_BPS_INCREASE, QUOTE_PRECISION_I64, }; use crate::math::cp_curve::*; use crate::state::perp_market::AMM; -use crate::state::user::PerpPosition; #[test] fn k_update_results_bound_flag() { @@ -331,10 +327,6 @@ fn amm_spread_adj_logic() { // let (t_price, _t_qar, _t_bar) = calculate_terminal_price_and_reserves(&market.amm).unwrap(); // market.amm.terminal_quote_asset_reserve = _t_qar; - let mut position = PerpPosition { - ..PerpPosition::default() - }; - // todo fix this market.amm.base_asset_amount_per_lp = 1; diff --git a/programs/drift/src/math/fees.rs b/programs/drift/src/math/fees.rs index 3e23e718f3..8431be24bf 100644 --- a/programs/drift/src/math/fees.rs +++ b/programs/drift/src/math/fees.rs @@ -16,8 +16,8 @@ use crate::math::safe_math::SafeMath; use crate::state::state::{FeeStructure, FeeTier, OrderFillerRewardStructure}; use crate::state::user::{MarketType, UserStats}; +use crate::math::constants::{FEE_ADJUSTMENT_MAX, QUOTE_PRECISION_U64}; use crate::msg; -use crate::{FEE_ADJUSTMENT_MAX, QUOTE_PRECISION_U64}; #[cfg(test)] mod tests; @@ -30,6 +30,7 @@ pub struct FillFees { pub filler_reward: u64, pub referrer_reward: u64, pub referee_discount: u64, + pub builder_fee: Option, } pub fn calculate_fee_for_fulfillment_with_amm( @@ -45,6 +46,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( is_post_only: bool, fee_adjustment: i16, user_high_leverage_mode: bool, + builder_fee_bps: Option, ) -> DriftResult { let fee_tier = determine_user_fee_tier( user_stats, @@ -92,6 +94,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( filler_reward, referrer_reward: 0, referee_discount: 0, + builder_fee: None, }) } else { let mut fee = calculate_taker_fee(quote_asset_amount, &fee_tier, fee_adjustment)?; @@ -131,6 +134,16 @@ pub fn calculate_fee_for_fulfillment_with_amm( let fee_to_market_for_lp = fee_to_market.safe_sub(quote_asset_amount_surplus)?; + let builder_fee = if let Some(builder_fee_bps) = builder_fee_bps { + Some( + quote_asset_amount + .safe_mul(builder_fee_bps.cast()?)? + .safe_div(100_000)?, + ) + } else { + None + }; + // must be non-negative Ok(FillFees { user_fee: fee, @@ -140,6 +153,7 @@ pub fn calculate_fee_for_fulfillment_with_amm( filler_reward, referrer_reward, referee_discount, + builder_fee, }) } } @@ -286,6 +300,7 @@ pub fn calculate_fee_for_fulfillment_with_match( market_type: &MarketType, fee_adjustment: i16, user_high_leverage_mode: bool, + builder_fee_bps: Option, ) -> DriftResult { let taker_fee_tier = determine_user_fee_tier( taker_stats, @@ -337,6 +352,16 @@ pub fn calculate_fee_for_fulfillment_with_match( .safe_sub(maker_rebate)? .cast::()?; + let builder_fee = if let Some(builder_fee_bps) = builder_fee_bps { + Some( + quote_asset_amount + .safe_mul(builder_fee_bps.cast()?)? + .safe_div(100_000)?, + ) + } else { + None + }; + Ok(FillFees { user_fee: taker_fee, maker_rebate, @@ -345,6 +370,7 @@ pub fn calculate_fee_for_fulfillment_with_match( referrer_reward, fee_to_market_for_lp: 0, referee_discount, + builder_fee, }) } diff --git a/programs/drift/src/math/fees/tests.rs b/programs/drift/src/math/fees/tests.rs index 82188b62b9..296f3bfce5 100644 --- a/programs/drift/src/math/fees/tests.rs +++ b/programs/drift/src/math/fees/tests.rs @@ -31,6 +31,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -75,6 +76,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -118,6 +120,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -161,6 +164,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -202,6 +206,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 0, false, + None, ) .unwrap(); @@ -240,6 +245,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -271,6 +277,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, 50, false, + None, ) .unwrap(); @@ -303,6 +310,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -335,6 +343,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, false, + None, ) .unwrap(); @@ -373,6 +382,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -404,6 +414,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -436,6 +447,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, true, + None, ) .unwrap(); @@ -468,6 +480,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -500,6 +513,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -100, false, + None, ) .unwrap(); @@ -538,6 +552,7 @@ mod calculate_fee_for_taker_and_maker { &MarketType::Perp, -50, true, + None, ) .unwrap(); @@ -583,6 +598,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, 0, false, + None, ) .unwrap(); @@ -620,6 +636,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -649,6 +666,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, 50, false, + None, ) .unwrap(); @@ -679,6 +697,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -709,6 +728,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, false, + None, ) .unwrap(); @@ -746,6 +766,7 @@ mod calculate_fee_for_order_fulfill_against_amm { false, -50, true, + None, ) .unwrap(); diff --git a/programs/drift/src/math/fuel.rs b/programs/drift/src/math/fuel.rs index 2cd139d9f3..5069761c98 100644 --- a/programs/drift/src/math/fuel.rs +++ b/programs/drift/src/math/fuel.rs @@ -1,9 +1,9 @@ use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{FUEL_WINDOW_U128, QUOTE_PRECISION, QUOTE_PRECISION_U64}; use crate::math::safe_math::SafeMath; use crate::state::perp_market::PerpMarket; use crate::state::spot_market::SpotMarket; -use crate::{FUEL_WINDOW_U128, QUOTE_PRECISION, QUOTE_PRECISION_U64}; #[cfg(test)] mod tests; diff --git a/programs/drift/src/math/fulfillment.rs b/programs/drift/src/math/fulfillment.rs index c4892bedf8..15103bddcd 100644 --- a/programs/drift/src/math/fulfillment.rs +++ b/programs/drift/src/math/fulfillment.rs @@ -1,12 +1,10 @@ use crate::controller::position::PositionDirection; use crate::error::DriftResult; -use crate::math::auction::can_fill_with_amm; use crate::math::casting::Cast; use crate::math::matching::do_orders_cross; use crate::math::safe_unwrap::SafeUnwrap; -use crate::state::fill_mode::FillMode; use crate::state::fulfillment::{PerpFulfillmentMethod, SpotFulfillmentMethod}; -use crate::state::perp_market::{AMMAvailability, AMM}; +use crate::state::perp_market::AMM; use crate::state::user::Order; use solana_program::pubkey::Pubkey; @@ -18,38 +16,21 @@ pub fn determine_perp_fulfillment_methods( maker_orders_info: &[(Pubkey, usize, u64)], amm: &AMM, amm_reserve_price: u64, - valid_oracle_price: Option, limit_price: Option, - amm_availability: AMMAvailability, - slot: u64, - min_auction_duration: u8, - fill_mode: FillMode, + amm_is_available: bool, ) -> DriftResult> { if order.post_only { return determine_perp_fulfillment_methods_for_maker( order, amm, amm_reserve_price, - valid_oracle_price, limit_price, - amm_availability, - slot, - min_auction_duration, - fill_mode, + amm_is_available, ); } let mut fulfillment_methods = Vec::with_capacity(8); - let can_fill_with_amm = can_fill_with_amm( - amm_availability, - valid_oracle_price, - order, - min_auction_duration, - slot, - fill_mode, - )?; - let maker_direction = order.direction.opposite(); let mut amm_price = match maker_direction { @@ -67,7 +48,7 @@ pub fn determine_perp_fulfillment_methods( break; } - if can_fill_with_amm { + if amm_is_available { let maker_better_than_amm = match order.direction { PositionDirection::Long => *maker_price <= amm_price, PositionDirection::Short => *maker_price >= amm_price, @@ -90,7 +71,7 @@ pub fn determine_perp_fulfillment_methods( } } - if can_fill_with_amm { + if amm_is_available { let taker_crosses_amm = match limit_price { Some(taker_price) => do_orders_cross(maker_direction, amm_price, taker_price), None => true, @@ -108,25 +89,12 @@ fn determine_perp_fulfillment_methods_for_maker( order: &Order, amm: &AMM, amm_reserve_price: u64, - valid_oracle_price: Option, limit_price: Option, - amm_availability: AMMAvailability, - slot: u64, - min_auction_duration: u8, - fill_mode: FillMode, + amm_is_available: bool, ) -> DriftResult> { let maker_direction = order.direction; - let can_fill_with_amm = can_fill_with_amm( - amm_availability, - valid_oracle_price, - order, - min_auction_duration, - slot, - fill_mode, - )?; - - if !can_fill_with_amm { + if !amm_is_available { return Ok(vec![]); } diff --git a/programs/drift/src/math/fulfillment/tests.rs b/programs/drift/src/math/fulfillment/tests.rs index 23e32cb923..005bbefa63 100644 --- a/programs/drift/src/math/fulfillment/tests.rs +++ b/programs/drift/src/math/fulfillment/tests.rs @@ -1,11 +1,9 @@ mod determine_perp_fulfillment_methods { use crate::controller::position::PositionDirection; use crate::math::constants::{ - AMM_RESERVE_PRECISION, PEG_PRECISION, PRICE_PRECISION, PRICE_PRECISION_I64, - PRICE_PRECISION_U64, + AMM_RESERVE_PRECISION, PEG_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64, }; use crate::math::fulfillment::determine_perp_fulfillment_methods; - use crate::state::fill_mode::FillMode; use crate::state::fulfillment::PerpFulfillmentMethod; use crate::state::oracle::HistoricalOracleData; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; @@ -52,8 +50,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -61,12 +57,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 103 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -113,8 +105,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -122,12 +112,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 99 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -186,8 +172,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -195,12 +179,8 @@ mod determine_perp_fulfillment_methods { &[(Pubkey::default(), 0, 101 * PRICE_PRECISION_U64)], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -254,8 +234,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -266,12 +244,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -326,8 +300,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -342,12 +314,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -405,8 +373,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -417,12 +383,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -478,8 +440,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -490,12 +450,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -550,8 +506,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -562,12 +516,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -621,8 +571,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -633,12 +581,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -694,8 +638,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -706,12 +648,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -758,8 +696,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -770,12 +706,8 @@ mod determine_perp_fulfillment_methods { ], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -823,8 +755,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -832,12 +762,8 @@ mod determine_perp_fulfillment_methods { &[], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); @@ -885,8 +811,6 @@ mod determine_perp_fulfillment_methods { ..Order::default() }; - let oracle_price = 100 * PRICE_PRECISION_I64; - let taker_price = Some(taker_order.price); let fulfillment_methods = determine_perp_fulfillment_methods( @@ -894,12 +818,8 @@ mod determine_perp_fulfillment_methods { &[], &market.amm, market.amm.reserve_price().unwrap(), - Some(oracle_price), taker_price, - crate::state::perp_market::AMMAvailability::AfterMinDuration, - 0, - 0, - FillMode::Fill, + true, ) .unwrap(); diff --git a/programs/drift/src/math/insurance.rs b/programs/drift/src/math/insurance.rs index 1232a19451..4658f97db6 100644 --- a/programs/drift/src/math/insurance.rs +++ b/programs/drift/src/math/insurance.rs @@ -1,7 +1,8 @@ -use crate::{msg, PRICE_PRECISION}; +use crate::msg; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::PRICE_PRECISION; use crate::math::helpers::{get_proportion_u128, log10_iter}; use crate::math::safe_math::SafeMath; diff --git a/programs/drift/src/math/liquidation.rs b/programs/drift/src/math/liquidation.rs index c1adc5ff2d..f624b89092 100644 --- a/programs/drift/src/math/liquidation.rs +++ b/programs/drift/src/math/liquidation.rs @@ -12,6 +12,7 @@ use crate::math::margin::calculate_margin_requirement_and_total_collateral_and_l use crate::math::safe_math::SafeMath; use crate::math::spot_balance::get_token_amount; +use crate::math::constants::{BASE_PRECISION, LIQUIDATION_FEE_INCREASE_PER_SLOT}; use crate::math::spot_swap::calculate_swap_price; use crate::msg; use crate::state::margin_calculation::MarginContext; @@ -21,10 +22,7 @@ use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; use crate::state::user::{OrderType, User}; -use crate::{ - validate, MarketType, OrderParams, PositionDirection, BASE_PRECISION, - LIQUIDATION_FEE_INCREASE_PER_SLOT, -}; +use crate::{validate, MarketType, OrderParams, PositionDirection}; pub const LIQUIDATION_FEE_ADJUST_GRACE_PERIOD_SLOTS: u64 = 1_500; // ~10 minutes diff --git a/programs/drift/src/math/lp_pool.rs b/programs/drift/src/math/lp_pool.rs new file mode 100644 index 0000000000..cb35bf743d --- /dev/null +++ b/programs/drift/src/math/lp_pool.rs @@ -0,0 +1,268 @@ +pub mod perp_lp_pool_settlement { + use core::slice::Iter; + use std::iter::Peekable; + + use crate::error::ErrorCode; + use crate::math::casting::Cast; + use crate::math::constants::QUOTE_PRECISION_U64; + use crate::math::spot_balance::get_token_amount; + use crate::state::spot_market::{SpotBalance, SpotBalanceType}; + use crate::{ + math::safe_math::SafeMath, + state::{amm_cache::CacheInfo, perp_market::PerpMarket, spot_market::SpotMarket}, + *, + }; + use anchor_spl::token_interface::{TokenAccount, TokenInterface}; + + #[derive(Debug, Clone, Copy)] + pub struct SettlementResult { + pub amount_transferred: u64, + pub direction: SettlementDirection, + pub fee_pool_used: u128, + pub pnl_pool_used: u128, + } + + #[derive(Debug, Clone, Copy, PartialEq)] + pub enum SettlementDirection { + ToLpPool, + FromLpPool, + None, + } + + pub struct SettlementContext<'a> { + pub quote_owed_from_lp: i64, + pub quote_constituent_token_balance: u64, + pub fee_pool_balance: u128, + pub pnl_pool_balance: u128, + pub quote_market: &'a SpotMarket, + pub max_settle_quote_amount: u64, + } + + pub fn calculate_settlement_amount(ctx: &SettlementContext) -> Result { + if ctx.quote_owed_from_lp > 0 { + calculate_lp_to_perp_settlement(ctx) + } else if ctx.quote_owed_from_lp < 0 { + calculate_perp_to_lp_settlement(ctx) + } else { + Ok(SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }) + } + } + + pub fn validate_settlement_amount( + ctx: &SettlementContext, + result: &SettlementResult, + perp_market: &PerpMarket, + quote_spot_market: &SpotMarket, + ) -> Result<()> { + if result.amount_transferred > ctx.max_settle_quote_amount { + msg!( + "Amount to settle exceeds maximum allowed, {} > {}", + result.amount_transferred, + ctx.max_settle_quote_amount + ); + return Err(ErrorCode::LpPoolSettleInvariantBreached.into()); + } + + if result.direction == SettlementDirection::ToLpPool { + if result.fee_pool_used > 0 { + let fee_pool_token_amount = get_token_amount( + perp_market.amm.fee_pool.balance(), + quote_spot_market, + &SpotBalanceType::Deposit, + )?; + validate!( + fee_pool_token_amount >= result.fee_pool_used, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Fee pool balance insufficient for settlement: {} < {}", + fee_pool_token_amount, + result.fee_pool_used + )?; + } + + if result.pnl_pool_used > 0 { + let pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.balance(), + quote_spot_market, + &SpotBalanceType::Deposit, + )?; + validate!( + pnl_pool_token_amount >= result.pnl_pool_used, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Pnl pool balance insufficient for settlement: {} < {}", + pnl_pool_token_amount, + result.pnl_pool_used + )?; + } + } + if result.direction == SettlementDirection::FromLpPool { + validate!( + ctx.quote_constituent_token_balance + .saturating_sub(result.amount_transferred) + >= QUOTE_PRECISION_U64, + ErrorCode::LpPoolSettleInvariantBreached.into(), + "Quote constituent token balance insufficient for settlement: {} < {}", + ctx.quote_constituent_token_balance, + result.amount_transferred + )?; + } + Ok(()) + } + + fn calculate_lp_to_perp_settlement(ctx: &SettlementContext) -> Result { + if ctx.quote_constituent_token_balance == 0 { + return Ok(SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }); + } + + let amount_to_send = ctx + .quote_owed_from_lp + .cast::()? + .min( + ctx.quote_constituent_token_balance + .saturating_sub(QUOTE_PRECISION_U64), + ) + .min(ctx.max_settle_quote_amount); + + Ok(SettlementResult { + amount_transferred: amount_to_send, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }) + } + + fn calculate_perp_to_lp_settlement(ctx: &SettlementContext) -> Result { + let amount_to_send = + (ctx.quote_owed_from_lp.abs().cast::()?).min(ctx.max_settle_quote_amount); + + if ctx.fee_pool_balance >= amount_to_send as u128 { + // Fee pool can cover entire amount + Ok(SettlementResult { + amount_transferred: amount_to_send, + direction: SettlementDirection::ToLpPool, + fee_pool_used: amount_to_send as u128, + pnl_pool_used: 0, + }) + } else { + // Need to use both fee pool and pnl pool + let remaining_amount = (amount_to_send as u128).safe_sub(ctx.fee_pool_balance)?; + let pnl_pool_used = remaining_amount.min(ctx.pnl_pool_balance); + let actual_transfer = ctx.fee_pool_balance.safe_add(pnl_pool_used)?; + + Ok(SettlementResult { + amount_transferred: actual_transfer as u64, + direction: SettlementDirection::ToLpPool, + fee_pool_used: ctx.fee_pool_balance, + pnl_pool_used, + }) + } + } + + pub fn execute_token_transfer<'info>( + token_program: &Interface<'info, TokenInterface>, + from_vault: &InterfaceAccount<'info, TokenAccount>, + to_vault: &InterfaceAccount<'info, TokenAccount>, + signer: &AccountInfo<'info>, + signer_seed: &[&[u8]], + amount: u64, + remaining_accounts: Option<&mut Peekable>>>, + ) -> Result<()> { + controller::token::send_from_program_vault_with_signature_seeds( + token_program, + from_vault, + to_vault, + signer, + signer_seed, + amount, + &None, + remaining_accounts, + ) + } + + // Market state updates + pub fn update_perp_market_pools_and_quote_market_balance( + perp_market: &mut PerpMarket, + result: &SettlementResult, + quote_spot_market: &mut SpotMarket, + ) -> Result<()> { + match result.direction { + SettlementDirection::FromLpPool => { + controller::spot_balance::update_spot_balances( + result.amount_transferred as u128, + &SpotBalanceType::Deposit, + quote_spot_market, + &mut perp_market.amm.fee_pool, + false, + )?; + } + SettlementDirection::ToLpPool => { + if result.fee_pool_used > 0 { + controller::spot_balance::update_spot_balances( + result.fee_pool_used, + &SpotBalanceType::Borrow, + quote_spot_market, + &mut perp_market.amm.fee_pool, + true, + )?; + } + if result.pnl_pool_used > 0 { + controller::spot_balance::update_spot_balances( + result.pnl_pool_used, + &SpotBalanceType::Borrow, + quote_spot_market, + &mut perp_market.pnl_pool, + true, + )?; + } + } + SettlementDirection::None => {} + } + Ok(()) + } + + pub fn update_cache_info( + cache_info: &mut CacheInfo, + result: &SettlementResult, + new_quote_owed: i64, + slot: u64, + now: i64, + ) -> Result<()> { + cache_info.quote_owed_from_lp_pool = new_quote_owed; + cache_info.last_settle_amount = result.amount_transferred; + cache_info.last_settle_slot = slot; + cache_info.last_settle_ts = now; + cache_info.last_settle_amm_ex_fees = cache_info.last_exchange_fees; + cache_info.last_settle_amm_pnl = cache_info.last_net_pnl_pool_token_amount; + + match result.direction { + SettlementDirection::FromLpPool => { + cache_info.last_fee_pool_token_amount = cache_info + .last_fee_pool_token_amount + .safe_add(result.amount_transferred as u128)?; + } + SettlementDirection::ToLpPool => { + if result.fee_pool_used > 0 { + cache_info.last_fee_pool_token_amount = cache_info + .last_fee_pool_token_amount + .safe_sub(result.fee_pool_used)?; + } + if result.pnl_pool_used > 0 { + cache_info.last_net_pnl_pool_token_amount = cache_info + .last_net_pnl_pool_token_amount + .safe_sub(result.pnl_pool_used as i128)?; + } + } + SettlementDirection::None => {} + } + Ok(()) + } +} diff --git a/programs/drift/src/math/margin.rs b/programs/drift/src/math/margin.rs index 65863c4a8a..9f759f4822 100644 --- a/programs/drift/src/math/margin.rs +++ b/programs/drift/src/math/margin.rs @@ -1,22 +1,23 @@ use crate::error::DriftResult; use crate::error::ErrorCode; use crate::math::constants::{ - MARGIN_PRECISION_U128, MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN, PRICE_PRECISION, - SPOT_IMF_PRECISION_U128, SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_U128, + MARGIN_PRECISION_U128, MAX_POSITIVE_UPNL_FOR_INITIAL_MARGIN, PERCENTAGE_PRECISION, + PRICE_PRECISION, SPOT_IMF_PRECISION_U128, SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_U128, }; +use crate::math::oracle::LogMode; use crate::math::position::calculate_base_asset_value_and_pnl_with_oracle_price; -use crate::MARGIN_PRECISION; -use crate::{validate, PRICE_PRECISION_I128}; -use crate::{validation, PRICE_PRECISION_I64}; +use crate::math::constants::{MARGIN_PRECISION, PRICE_PRECISION_I128, PRICE_PRECISION_I64}; +use crate::validate; +use crate::validation; use crate::math::casting::Cast; use crate::math::funding::calculate_funding_payment; use crate::math::oracle::{is_oracle_valid_for_action, DriftAction}; -use crate::math::spot_balance::{get_strict_token_value, get_token_value}; - +use crate::math::helpers::get_proportion_u128; use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::{get_strict_token_value, get_token_value}; use crate::msg; use crate::state::margin_calculation::{MarginCalculation, MarginContext, MarketIdentifier}; use crate::state::oracle::{OraclePriceData, StrictOraclePrice}; @@ -47,6 +48,7 @@ pub fn calculate_size_premium_liability_weight( imf_factor: u32, liability_weight: u32, precision: u128, + is_bounded: bool, ) -> DriftResult { if imf_factor == 0 { return Ok(liability_weight); @@ -68,8 +70,46 @@ pub fn calculate_size_premium_liability_weight( )? .cast::()?; - let max_liability_weight = max(liability_weight, size_premium_liability_weight); - Ok(max_liability_weight) + if is_bounded { + let max_liability_weight = max(liability_weight, size_premium_liability_weight); + return Ok(max_liability_weight); + } + + Ok(size_premium_liability_weight) +} + +pub fn calc_high_leverage_mode_initial_margin_ratio_from_size( + pre_size_adj_margin_ratio: u32, + size_adj_margin_ratio: u32, + default_margin_ratio: u32, +) -> DriftResult { + let result = if size_adj_margin_ratio < pre_size_adj_margin_ratio { + let size_pct_discount_factor = PERCENTAGE_PRECISION.saturating_sub( + pre_size_adj_margin_ratio + .cast::()? + .safe_sub(size_adj_margin_ratio.cast::()?)? + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div((pre_size_adj_margin_ratio.safe_div(5)?).cast::()?)?, + ); + + let hlm_margin_delta = pre_size_adj_margin_ratio + .saturating_sub(default_margin_ratio) + .max(1); + + let hlm_margin_delta_proportion = get_proportion_u128( + hlm_margin_delta.cast()?, + size_pct_discount_factor, + PERCENTAGE_PRECISION, + )? + .cast::()?; + hlm_margin_delta_proportion.safe_add(default_margin_ratio)? + } else if size_adj_margin_ratio == pre_size_adj_margin_ratio { + default_margin_ratio + } else { + size_adj_margin_ratio + }; + + Ok(result) } pub fn calculate_size_discount_asset_weight( @@ -255,6 +295,8 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, + Some(LogMode::Margin), )?; let mut skip_token_value = false; @@ -510,6 +552,8 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( .last_oracle_price_twap, quote_spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, + Some(LogMode::Margin), )?; let strict_quote_price = StrictOraclePrice::new( @@ -527,7 +571,9 @@ pub fn calculate_margin_requirement_and_total_collateral_and_liability_info( &market.oracle_id(), market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier()?, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + Some(LogMode::Margin), )?; let perp_position_custom_margin_ratio = @@ -955,6 +1001,8 @@ pub fn calculate_user_equity( spot_market.historical_oracle_data.last_oracle_price_twap, spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, + Some(LogMode::Margin), )?; all_oracles_valid &= is_oracle_valid_for_action(oracle_validity, Some(DriftAction::MarginCalc))?; @@ -985,6 +1033,8 @@ pub fn calculate_user_equity( .last_oracle_price_twap, quote_spot_market.get_max_confidence_interval_multiplier()?, 0, + 0, + Some(LogMode::Margin), )?; all_oracles_valid &= @@ -1012,7 +1062,9 @@ pub fn calculate_user_equity( &market.oracle_id(), market.amm.historical_oracle_data.last_oracle_price_twap, market.get_max_confidence_interval_multiplier()?, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, + Some(LogMode::Margin), )?; all_oracles_valid &= diff --git a/programs/drift/src/math/margin/tests.rs b/programs/drift/src/math/margin/tests.rs index 9f997e11f3..3f73436cfd 100644 --- a/programs/drift/src/math/margin/tests.rs +++ b/programs/drift/src/math/margin/tests.rs @@ -1,9 +1,5 @@ #[cfg(test)] mod test { - use num_integer::Roots; - - use crate::amm::calculate_swap_output; - use crate::controller::amm::SwapDirection; use crate::math::constants::{ AMM_RESERVE_PRECISION, PRICE_PRECISION, PRICE_PRECISION_U64, QUOTE_PRECISION, QUOTE_PRECISION_I64, SPOT_IMF_PRECISION, @@ -18,6 +14,7 @@ mod test { PRICE_PRECISION_I64, QUOTE_PRECISION_U64, SPOT_BALANCE_PRECISION, SPOT_CUMULATIVE_INTEREST_PRECISION, }; + use num_integer::Roots; #[test] fn asset_tier_checks() { diff --git a/programs/drift/src/math/mod.rs b/programs/drift/src/math/mod.rs index 89edbdafc5..17a972d826 100644 --- a/programs/drift/src/math/mod.rs +++ b/programs/drift/src/math/mod.rs @@ -16,6 +16,7 @@ pub mod funding; pub mod helpers; pub mod insurance; pub mod liquidation; +pub mod lp_pool; pub mod margin; pub mod matching; pub mod oracle; diff --git a/programs/drift/src/math/oracle.rs b/programs/drift/src/math/oracle.rs index 78be9ce782..92bf2a47e8 100644 --- a/programs/drift/src/math/oracle.rs +++ b/programs/drift/src/math/oracle.rs @@ -11,6 +11,7 @@ use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::PerpMarket; use crate::state::state::{OracleGuardRails, ValidityGuardRails}; use crate::state::user::MarketType; +use std::convert::TryFrom; use std::fmt; #[cfg(test)] @@ -24,7 +25,10 @@ pub enum OracleValidity { TooUncertain, StaleForMargin, InsufficientDataPoints, - StaleForAMM, + StaleForAMM { + immediate: bool, + low_risk: bool, + }, #[default] Valid, } @@ -37,7 +41,7 @@ impl OracleValidity { OracleValidity::TooUncertain => ErrorCode::OracleTooUncertain, OracleValidity::StaleForMargin => ErrorCode::OracleStaleForMargin, OracleValidity::InsufficientDataPoints => ErrorCode::OracleInsufficientDataPoints, - OracleValidity::StaleForAMM => ErrorCode::OracleStaleForAMM, + OracleValidity::StaleForAMM { .. } => ErrorCode::OracleStaleForAMM, OracleValidity::Valid => unreachable!(), } } @@ -51,25 +55,93 @@ impl fmt::Display for OracleValidity { OracleValidity::TooUncertain => write!(f, "TooUncertain"), OracleValidity::StaleForMargin => write!(f, "StaleForMargin"), OracleValidity::InsufficientDataPoints => write!(f, "InsufficientDataPoints"), - OracleValidity::StaleForAMM => write!(f, "StaleForAMM"), + OracleValidity::StaleForAMM { + immediate, + low_risk, + } => { + if *immediate { + write!(f, "StaleForAMM (immediate)") + } else if *low_risk { + write!(f, "StaleForAMM (low risk)") + } else { + write!(f, "StaleForAMM") + } + } OracleValidity::Valid => write!(f, "Valid"), } } } +impl TryFrom for OracleValidity { + type Error = ErrorCode; + + fn try_from(v: u8) -> DriftResult { + match v { + 0 => Ok(OracleValidity::NonPositive), + 1 => Ok(OracleValidity::TooVolatile), + 2 => Ok(OracleValidity::TooUncertain), + 3 => Ok(OracleValidity::StaleForMargin), + 4 => Ok(OracleValidity::InsufficientDataPoints), + 5 => Ok(OracleValidity::StaleForAMM { + immediate: true, + low_risk: true, + }), + 6 => Ok(OracleValidity::StaleForAMM { + immediate: true, + low_risk: false, + }), + 7 => Ok(OracleValidity::Valid), + _ => panic!("Invalid OracleValidity"), + } + } +} + +impl From for u8 { + fn from(src: OracleValidity) -> u8 { + match src { + OracleValidity::NonPositive => 0, + OracleValidity::TooVolatile => 1, + OracleValidity::TooUncertain => 2, + OracleValidity::StaleForMargin => 3, + OracleValidity::InsufficientDataPoints => 4, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: true, + } => 5, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: false, + } => 6, + OracleValidity::Valid + | OracleValidity::StaleForAMM { + immediate: false, + low_risk: false, + } => 7, + OracleValidity::StaleForAMM { + immediate: false, + low_risk: true, + } => unreachable!(), + } + } +} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum DriftAction { UpdateFunding, SettlePnl, TriggerOrder, FillOrderMatch, - FillOrderAmm, + FillOrderAmmLowRisk, + FillOrderAmmImmediate, Liquidate, MarginCalc, UpdateTwap, UpdateAMMCurve, OracleOrderPrice, UseMMOraclePrice, + UpdateAmmCache, + UpdateLpPoolAum, + LpPoolSwap, } pub fn is_oracle_valid_for_action( @@ -78,15 +150,25 @@ pub fn is_oracle_valid_for_action( ) -> DriftResult { let is_ok = match action { Some(action) => match action { - DriftAction::FillOrderAmm => { + DriftAction::FillOrderAmmImmediate => { matches!(oracle_validity, OracleValidity::Valid) } + DriftAction::FillOrderAmmLowRisk => { + matches!( + oracle_validity, + OracleValidity::Valid + | OracleValidity::StaleForAMM { + immediate: _, + low_risk: false + } + ) + } // relax oracle staleness, later checks for sufficiently recent amm slot update for funding update DriftAction::UpdateFunding => { matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints | OracleValidity::StaleForMargin ) @@ -95,7 +177,7 @@ pub fn is_oracle_valid_for_action( matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints ) } @@ -113,11 +195,14 @@ pub fn is_oracle_valid_for_action( DriftAction::SettlePnl => matches!( oracle_validity, OracleValidity::Valid - | OracleValidity::StaleForAMM + | OracleValidity::StaleForAMM { .. } | OracleValidity::InsufficientDataPoints | OracleValidity::StaleForMargin ), - DriftAction::FillOrderMatch => !matches!( + DriftAction::FillOrderMatch + | DriftAction::UpdateAmmCache + | DriftAction::UpdateLpPoolAum + | DriftAction::LpPoolSwap => !matches!( oracle_validity, OracleValidity::NonPositive | OracleValidity::TooVolatile @@ -184,6 +269,7 @@ pub fn get_oracle_status( guard_rails: &OracleGuardRails, reserve_price: u64, ) -> DriftResult { + let slot_delay_override = guard_rails.validity.slots_before_stale_for_amm.cast()?; let oracle_validity = oracle_validity( MarketType::Perp, market.market_index, @@ -193,7 +279,8 @@ pub fn get_oracle_status( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, LogMode::None, - 0, + slot_delay_override, + slot_delay_override, )?; let oracle_reserve_price_spread_pct = amm::calculate_oracle_twap_5min_price_spread_pct(&market.amm, reserve_price)?; @@ -216,6 +303,7 @@ pub enum LogMode { ExchangeOracle, MMOracle, SafeMMOracle, + Margin, } pub fn oracle_validity( @@ -227,7 +315,8 @@ pub fn oracle_validity( max_confidence_interval_multiplier: u64, oracle_source: &OracleSource, log_mode: LogMode, - slots_before_stale_for_amm_override: i8, + slots_before_stale_for_amm_immdiate_override: i8, + oracle_low_risk_slot_delay_override: i8, ) -> DriftResult { let OraclePriceData { price: oracle_price, @@ -252,15 +341,21 @@ pub fn oracle_validity( .confidence_interval_max_size .safe_mul(max_confidence_interval_multiplier)?); - let is_stale_for_amm = if slots_before_stale_for_amm_override != 0 { - oracle_delay.gt(&slots_before_stale_for_amm_override.max(0).cast()?) + let is_stale_for_amm_immediate = if slots_before_stale_for_amm_immdiate_override != 0 { + oracle_delay.gt(&slots_before_stale_for_amm_immdiate_override.max(0).cast()?) + } else { + true + }; + + let is_stale_for_amm_low_risk = if oracle_low_risk_slot_delay_override != 0 { + oracle_delay.gt(&oracle_low_risk_slot_delay_override.max(0).cast()?) } else { oracle_delay.gt(&valid_oracle_guard_rails.slots_before_stale_for_amm) }; let is_stale_for_margin = if matches!( oracle_source, - OracleSource::PythStableCoinPull | OracleSource::PythStableCoin + OracleSource::PythStableCoinPull | OracleSource::PythLazerStableCoin ) { oracle_delay.gt(&(valid_oracle_guard_rails .slots_before_stale_for_margin @@ -279,14 +374,17 @@ pub fn oracle_validity( OracleValidity::StaleForMargin } else if !has_sufficient_number_of_data_points { OracleValidity::InsufficientDataPoints - } else if is_stale_for_amm { - OracleValidity::StaleForAMM + } else if is_stale_for_amm_immediate || is_stale_for_amm_low_risk { + OracleValidity::StaleForAMM { + immediate: is_stale_for_amm_immediate, + low_risk: is_stale_for_amm_low_risk, + } } else { OracleValidity::Valid }; if log_mode != LogMode::None { - let oracle_type = if log_mode == LogMode::ExchangeOracle { + let oracle_type = if log_mode == LogMode::ExchangeOracle || log_mode == LogMode::Margin { "Exchange" } else if log_mode == LogMode::SafeMMOracle { "SafeMM" @@ -332,15 +430,29 @@ pub fn oracle_validity( ); } - if is_stale_for_amm || is_stale_for_margin { + if is_stale_for_margin { crate::msg!( - "Invalid {} {} {} Oracle: Stale (oracle_delay={:?})", + "Invalid {} {} {} Oracle: Stale for Margin (oracle_delay={:?})", market_type, market_index, oracle_type, oracle_delay ); } + + if (is_stale_for_amm_immediate || is_stale_for_amm_low_risk) && log_mode != LogMode::Margin + { + crate::msg!( + "Invalid {} {} {} Oracle: Stale (oracle_delay={:?}), (stale_for_amm_immediate={}, stale_for_amm_low_risk={}, stale_for_margin={})", + market_type, + market_index, + oracle_type, + oracle_delay, + is_stale_for_amm_immediate, + is_stale_for_amm_low_risk, + is_stale_for_margin + ); + } } Ok(oracle_validity) diff --git a/programs/drift/src/math/orders.rs b/programs/drift/src/math/orders.rs index df05e6a118..879bfe8321 100644 --- a/programs/drift/src/math/orders.rs +++ b/programs/drift/src/math/orders.rs @@ -8,17 +8,16 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::amm::calculate_amm_available_liquidity; use crate::math::casting::Cast; -use crate::state::protected_maker_mode_config::ProtectedMakerParams; -use crate::state::user::OrderBitFlag; -use crate::PERCENTAGE_PRECISION_I128; -use crate::{ - load, math, FeeTier, BASE_PRECISION_I128, FEE_ADJUSTMENT_MAX, MARGIN_PRECISION_I128, +use crate::math::constants::{ + BASE_PRECISION_I128, FEE_ADJUSTMENT_MAX, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, MAX_PREDICTION_MARKET_PRICE, MAX_PREDICTION_MARKET_PRICE_I64, OPEN_ORDER_MARGIN_REQUIREMENT, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I128, PRICE_PRECISION_U64, QUOTE_PRECISION_I128, - SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, + PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I128, PRICE_PRECISION_U64, + QUOTE_PRECISION_I128, SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, }; +use crate::state::protected_maker_mode_config::ProtectedMakerParams; +use crate::state::user::OrderBitFlag; +use crate::{load, math, FeeTier}; -use crate::math::constants::MARGIN_PRECISION_U128; use crate::math::margin::{ calculate_margin_requirement_and_total_collateral_and_liability_info, MarginRequirementType, }; @@ -806,6 +805,7 @@ pub fn calculate_max_perp_order_size( )?; let user_custom_margin_ratio = user.max_margin_ratio; + let perp_position_margin_ratio = user.perp_positions[position_index].max_margin_ratio as u32; let user_high_leverage_mode = user.is_high_leverage_mode(MarginRequirementType::Initial); let is_isolated_position = user.perp_positions[position_index].is_isolated(); @@ -844,7 +844,8 @@ pub fn calculate_max_perp_order_size( MarginRequirementType::Initial, user_high_leverage_mode, )? - .max(user_custom_margin_ratio); + .max(user_custom_margin_ratio) + .max(perp_position_margin_ratio); let mut order_size_to_reduce_position = 0_u64; let mut free_collateral_released = 0_i128; @@ -921,7 +922,8 @@ pub fn calculate_max_perp_order_size( MarginRequirementType::Initial, user_high_leverage_mode, )? - .max(user_custom_margin_ratio); + .max(user_custom_margin_ratio) + .max(perp_position_margin_ratio); Ok((new_order_size, new_margin_ratio)) }; diff --git a/programs/drift/src/math/orders/tests.rs b/programs/drift/src/math/orders/tests.rs index 112144451d..339aa20361 100644 --- a/programs/drift/src/math/orders/tests.rs +++ b/programs/drift/src/math/orders/tests.rs @@ -1956,7 +1956,7 @@ mod calculate_max_perp_order_size { use crate::state::perp_market_map::PerpMarketMap; use crate::state::spot_market::{SpotBalanceType, SpotMarket}; use crate::state::spot_market_map::SpotMarketMap; - use crate::state::user::{Order, PerpPosition, SpotPosition, User}; + use crate::state::user::{MarginMode, Order, PerpPosition, SpotPosition, User}; use crate::test_utils::get_pyth_price; use crate::test_utils::*; use crate::{ @@ -2791,6 +2791,252 @@ mod calculate_max_perp_order_size { assert!(total_collateral.unsigned_abs() - margin_requirement < 100 * QUOTE_PRECISION); } + #[test] + pub fn sol_perp_5x_bid_perp_position_margin_ratio() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Long, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 49999950000); + + user.perp_positions[0].open_orders = 1; + user.perp_positions[0].open_bids = max_order_size as i64; + + let MarginCalculation { + margin_requirement, + total_collateral, + .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial).strict(true), + ) + .unwrap(); + + assert_eq!(total_collateral.unsigned_abs(), margin_requirement); + } + + #[test] + pub fn sol_perp_5x_ask_perp_position_margin_ratio() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 2000, + margin_ratio_maintenance: 1000, + status: MarketStatus::Initialized, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + max_margin_ratio: 2 * MARGIN_PRECISION as u16, + ..PerpPosition::default() + }), + spot_positions, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + + assert_eq!(max_order_size, 49999950000); + + user.perp_positions[0].open_orders = 1; + user.perp_positions[0].open_asks = -(max_order_size as i64); + + let MarginCalculation { + margin_requirement, + total_collateral, + .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial).strict(true), + ) + .unwrap(); + + assert_eq!(total_collateral.unsigned_abs(), margin_requirement); + } + #[test] pub fn sol_perp_10x_ask_with_imf() { let slot = 0_u64; @@ -2914,6 +3160,191 @@ mod calculate_max_perp_order_size { assert!(total_collateral.unsigned_abs() - margin_requirement < 100 * QUOTE_PRECISION); } + #[test] + pub fn sol_perp_hlm_with_imf() { + let slot = 0_u64; + + let mut oracle_price = get_pyth_price(100, 6); + let oracle_price_key = + Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap(); + let pyth_program = crate::ids::pyth_program::id(); + create_account_info!( + oracle_price, + &oracle_price_key, + &pyth_program, + oracle_account_info + ); + let mut oracle_map = OracleMap::load_one(&oracle_account_info, slot, None).unwrap(); + + let mut market = PerpMarket { + amm: AMM { + base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + bid_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_base_asset_reserve: 100 * AMM_RESERVE_PRECISION, + ask_quote_asset_reserve: 100 * AMM_RESERVE_PRECISION, + sqrt_k: 100 * AMM_RESERVE_PRECISION, + peg_multiplier: 100 * PEG_PRECISION, + max_slippage_ratio: 50, + max_fill_reserve_fraction: 100, + order_step_size: 1000, + order_tick_size: 1, + oracle: oracle_price_key, + base_spread: 0, // 1 basis point + historical_oracle_data: HistoricalOracleData { + last_oracle_price: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap: (100 * PRICE_PRECISION) as i64, + last_oracle_price_twap_5min: (100 * PRICE_PRECISION) as i64, + + ..HistoricalOracleData::default() + }, + ..AMM::default() + }, + margin_ratio_initial: 1000, + margin_ratio_maintenance: 500, + high_leverage_margin_ratio_initial: 100, + high_leverage_margin_ratio_maintenance: 66, + imf_factor: 50, + status: MarketStatus::Active, + ..PerpMarket::default_test() + }; + market.amm.max_base_asset_reserve = u128::MAX; + market.amm.min_base_asset_reserve = 0; + + create_anchor_account_info!(market, PerpMarket, market_account_info); + let market_map = PerpMarketMap::load_one(&market_account_info, true).unwrap(); + + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + initial_asset_weight: SPOT_WEIGHT_PRECISION, + maintenance_asset_weight: SPOT_WEIGHT_PRECISION, + deposit_balance: 10000 * SPOT_BALANCE_PRECISION, + liquidator_fee: 0, + historical_oracle_data: HistoricalOracleData { + last_oracle_price_twap: PRICE_PRECISION_I64, + last_oracle_price_twap_5min: PRICE_PRECISION_I64, + ..HistoricalOracleData::default() + }, + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + let spot_market_account_infos = Vec::from([&usdc_spot_market_account_info]); + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10000 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + ..PerpPosition::default() + }), + spot_positions, + margin_mode: MarginMode::HighLeverage, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + assert_eq!(max_order_size, 4098356557000); // 4098 + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 100 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + ..PerpPosition::default() + }), + spot_positions, + margin_mode: MarginMode::HighLeverage, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + assert_eq!(max_order_size, 84737288000); // 84 + + let mut spot_positions = [SpotPosition::default(); 8]; + spot_positions[0] = SpotPosition { + market_index: 0, + balance_type: SpotBalanceType::Deposit, + scaled_balance: 10 * SPOT_BALANCE_PRECISION_U64, + ..SpotPosition::default() + }; + let mut user = User { + orders: [Order::default(); 32], + perp_positions: get_positions(PerpPosition { + market_index: 0, + ..PerpPosition::default() + }), + spot_positions, + margin_mode: MarginMode::HighLeverage, + ..User::default() + }; + + let max_order_size = calculate_max_perp_order_size( + &user, + 0, + 0, + PositionDirection::Short, + &market_map, + &spot_market_map, + &mut oracle_map, + ) + .unwrap(); + assert_eq!(max_order_size, 9605769000); // 9.6 + + user.perp_positions[0].open_orders = 1; + user.perp_positions[0].open_asks = -(max_order_size as i64); + + let MarginCalculation { + margin_requirement, + total_collateral, + .. + } = calculate_margin_requirement_and_total_collateral_and_liability_info( + &user, + &market_map, + &spot_market_map, + &mut oracle_map, + MarginContext::standard(MarginRequirementType::Initial).strict(true), + ) + .unwrap(); + + assert!(total_collateral.unsigned_abs() - margin_requirement < QUOTE_PRECISION); + } + #[test] pub fn swift_failure() { let clock_slot = 0_u64; @@ -3102,7 +3533,7 @@ mod calculate_max_perp_order_size { &lazer_program, ); - let mut account_infos = vec![ + let account_infos = vec![ usdc_oracle_info, sol_oracle_info, eth_oracle_info, @@ -3139,6 +3570,8 @@ mod calculate_max_perp_order_size { ) .unwrap(); + assert_eq!(max_order_size, 1600000); + user.perp_positions[0].open_orders += 1; user.perp_positions[0].open_bids += max_order_size as i64; @@ -3155,7 +3588,10 @@ mod calculate_max_perp_order_size { ) .unwrap(); - assert!(total_collateral.unsigned_abs() - margin_requirement < QUOTE_PRECISION); + assert_eq!(total_collateral.unsigned_abs(), 2199358529); // ~$2200 + assert_eq!(margin_requirement, 2186678676); + + assert!(total_collateral.unsigned_abs() - margin_requirement < 13 * QUOTE_PRECISION); } } diff --git a/programs/drift/src/math/position.rs b/programs/drift/src/math/position.rs index 8201123f09..e40c75205d 100644 --- a/programs/drift/src/math/position.rs +++ b/programs/drift/src/math/position.rs @@ -5,15 +5,14 @@ use crate::math::amm; use crate::math::amm::calculate_quote_asset_amount_swapped; use crate::math::casting::Cast; use crate::math::constants::{ - AMM_RESERVE_PRECISION_I128, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, - PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, + AMM_RESERVE_PRECISION_I128, BASE_PRECISION, MAX_PREDICTION_MARKET_PRICE_U128, + PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO, PRICE_TIMES_AMM_TO_QUOTE_PRECISION_RATIO_I128, }; use crate::math::pnl::calculate_pnl; use crate::math::safe_math::SafeMath; use crate::state::perp_market::{ContractType, AMM}; use crate::state::user::PerpPosition; -use crate::{BASE_PRECISION, MAX_PREDICTION_MARKET_PRICE_U128}; pub fn calculate_base_asset_value_and_pnl( base_asset_amount: i128, diff --git a/programs/drift/src/math/repeg.rs b/programs/drift/src/math/repeg.rs index 4cab184297..995e425f64 100644 --- a/programs/drift/src/math/repeg.rs +++ b/programs/drift/src/math/repeg.rs @@ -46,7 +46,8 @@ pub fn calculate_repeg_validity_from_oracle_account( market.get_max_confidence_interval_multiplier()?, &market.amm.oracle_source, oracle::LogMode::ExchangeOracle, - 0, + market.amm.oracle_slot_delay_override, + market.amm.oracle_low_risk_slot_delay_override, )? == OracleValidity::Valid; let (oracle_is_valid, direction_valid, profitability_valid, price_impact_valid) = diff --git a/programs/drift/src/math/repeg/tests.rs b/programs/drift/src/math/repeg/tests.rs index 4beb1c7bfb..0619d0b35f 100644 --- a/programs/drift/src/math/repeg/tests.rs +++ b/programs/drift/src/math/repeg/tests.rs @@ -256,7 +256,7 @@ fn calculate_optimal_peg_and_budget_2_test() { let oracle_price_data = OraclePriceData { price: (17_800 * PRICE_PRECISION) as i64, confidence: 10233, - delay: 2, + delay: 0, has_sufficient_number_of_data_points: true, sequence_id: None, }; diff --git a/programs/drift/src/math/spot_swap.rs b/programs/drift/src/math/spot_swap.rs index 10bcc87e16..8087786c05 100644 --- a/programs/drift/src/math/spot_swap.rs +++ b/programs/drift/src/math/spot_swap.rs @@ -1,12 +1,13 @@ use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{PRICE_PRECISION, SPOT_WEIGHT_PRECISION_U128}; use crate::math::margin::MarginRequirementType; use crate::math::orders::{calculate_fill_price, validate_fill_price_within_price_bands}; use crate::math::safe_math::SafeMath; use crate::math::spot_balance::{get_strict_token_value, get_token_value}; use crate::state::oracle::StrictOraclePrice; use crate::state::spot_market::SpotMarket; -use crate::{PositionDirection, PRICE_PRECISION, SPOT_WEIGHT_PRECISION_U128}; +use crate::PositionDirection; #[cfg(test)] mod tests; diff --git a/programs/drift/src/state/amm_cache.rs b/programs/drift/src/state/amm_cache.rs new file mode 100644 index 0000000000..59982ce15c --- /dev/null +++ b/programs/drift/src/state/amm_cache.rs @@ -0,0 +1,359 @@ +use std::convert::TryFrom; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::amm::calculate_net_user_pnl; +use crate::math::casting::Cast; +use crate::math::oracle::{is_oracle_valid_for_action, oracle_validity, DriftAction, LogMode}; +use crate::math::safe_math::SafeMath; +use crate::math::spot_balance::get_token_amount; +use crate::state::oracle::MMOraclePriceData; +use crate::state::oracle_map::OracleIdentifier; +use crate::state::perp_market::PerpMarket; +use crate::state::spot_market::{SpotBalance, SpotMarket}; +use crate::state::state::State; +use crate::state::traits::Size; +use crate::state::zero_copy::HasLen; +use crate::state::zero_copy::{AccountZeroCopy, AccountZeroCopyMut}; +use crate::validate; +use crate::OracleSource; +use crate::{impl_zero_copy_loader, OracleGuardRails}; + +use anchor_lang::prelude::*; + +use super::user::MarketType; + +pub const AMM_POSITIONS_CACHE: &str = "amm_cache_seed"; + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct AmmCache { + pub bump: u8, + _padding: [u8; 3], + pub cache: Vec, +} + +#[zero_copy] +#[derive(AnchorSerialize, AnchorDeserialize, Debug)] +#[repr(C)] +pub struct CacheInfo { + pub oracle: Pubkey, + pub last_fee_pool_token_amount: u128, + pub last_net_pnl_pool_token_amount: i128, + pub last_exchange_fees: u128, + pub last_settle_amm_ex_fees: u128, + pub last_settle_amm_pnl: i128, + /// BASE PRECISION + pub position: i64, + pub slot: u64, + pub last_settle_amount: u64, + pub last_settle_slot: u64, + pub last_settle_ts: i64, + pub quote_owed_from_lp_pool: i64, + pub amm_inventory_limit: i64, + pub oracle_price: i64, + pub oracle_slot: u64, + pub oracle_source: u8, + pub oracle_validity: u8, + pub lp_status_for_perp_market: u8, + pub amm_position_scalar: u8, + pub _padding: [u8; 36], +} + +impl Size for CacheInfo { + const SIZE: usize = 230; +} + +impl Default for CacheInfo { + fn default() -> Self { + CacheInfo { + position: 0i64, + slot: 0u64, + oracle_price: 0i64, + oracle_slot: 0u64, + oracle_validity: 0u8, + oracle: Pubkey::default(), + last_fee_pool_token_amount: 0u128, + last_net_pnl_pool_token_amount: 0i128, + last_exchange_fees: 0u128, + last_settle_amount: 0u64, + last_settle_slot: 0u64, + last_settle_ts: 0i64, + last_settle_amm_pnl: 0i128, + last_settle_amm_ex_fees: 0u128, + amm_inventory_limit: 0i64, + oracle_source: 0u8, + quote_owed_from_lp_pool: 0i64, + lp_status_for_perp_market: 0u8, + amm_position_scalar: 0u8, + _padding: [0u8; 36], + } + } +} + +impl CacheInfo { + pub fn get_oracle_source(&self) -> DriftResult { + Ok(OracleSource::try_from(self.oracle_source)?) + } + + pub fn oracle_id(&self) -> DriftResult { + let oracle_source = self.get_oracle_source()?; + Ok((self.oracle, oracle_source)) + } + + pub fn get_last_available_amm_token_amount(&self) -> DriftResult { + let last_available_balance = self + .last_fee_pool_token_amount + .cast::()? + .safe_add(self.last_net_pnl_pool_token_amount)?; + Ok(last_available_balance) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + self.oracle = perp_market.amm.oracle; + self.oracle_source = u8::from(perp_market.amm.oracle_source); + self.position = perp_market + .amm + .get_protocol_owned_position()? + .safe_mul(-1)?; + self.lp_status_for_perp_market = perp_market.lp_status; + Ok(()) + } + + pub fn try_update_oracle_info( + &mut self, + clock_slot: u64, + oracle_price_data: &MMOraclePriceData, + perp_market: &PerpMarket, + oracle_guard_rails: &OracleGuardRails, + ) -> DriftResult<()> { + let safe_oracle_data = oracle_price_data.get_safe_oracle_price_data(); + let validity = oracle_validity( + MarketType::Perp, + perp_market.market_index, + perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap, + &safe_oracle_data, + &oracle_guard_rails.validity, + perp_market.get_max_confidence_interval_multiplier()?, + &perp_market.amm.oracle_source, + LogMode::SafeMMOracle, + perp_market.amm.oracle_slot_delay_override, + perp_market.amm.oracle_low_risk_slot_delay_override, + )?; + if is_oracle_valid_for_action(validity, Some(DriftAction::UpdateAmmCache))? { + self.oracle_price = safe_oracle_data.price; + self.oracle_slot = clock_slot.safe_sub(safe_oracle_data.delay.max(0) as u64)?; + self.oracle_validity = u8::from(validity); + } else { + msg!( + "Not updating oracle price for perp market {}. Oracle data is invalid", + perp_market.market_index + ); + } + self.slot = clock_slot; + + Ok(()) + } +} + +#[zero_copy] +#[derive(Default, Debug)] +#[repr(C)] +pub struct AmmCacheFixed { + pub bump: u8, + _pad: [u8; 3], + pub len: u32, +} + +impl HasLen for AmmCacheFixed { + fn len(&self) -> u32 { + self.len + } +} + +impl AmmCache { + pub fn init_space() -> usize { + 8 + 8 + 4 + } + + pub fn space(num_markets: usize) -> usize { + 8 + 8 + 4 + num_markets * CacheInfo::SIZE + } + + pub fn validate(&self, state: &State) -> DriftResult<()> { + validate!( + self.cache.len() <= state.number_of_markets as usize, + ErrorCode::DefaultError, + "Number of amm positions is no larger than number of markets" + )?; + Ok(()) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + let cache_info = self.cache.get_mut(perp_market.market_index as usize); + if let Some(cache_info) = cache_info { + cache_info.update_perp_market_fields(perp_market)?; + } else { + msg!( + "Updating amm cache from admin with perp market index not found in cache: {}", + perp_market.market_index + ); + return Err(ErrorCode::DefaultError.into()); + } + + Ok(()) + } + + pub fn update_oracle_info( + &mut self, + clock_slot: u64, + market_index: u16, + oracle_price_data: &MMOraclePriceData, + perp_market: &PerpMarket, + oracle_guard_rails: &OracleGuardRails, + ) -> DriftResult<()> { + let cache_info = self.cache.get_mut(market_index as usize); + if let Some(cache_info) = cache_info { + cache_info.try_update_oracle_info( + clock_slot, + oracle_price_data, + perp_market, + oracle_guard_rails, + )?; + } else { + msg!( + "Updating amm cache from admin with perp market index not found in cache: {}", + market_index + ); + return Err(ErrorCode::DefaultError.into()); + } + + Ok(()) + } +} + +impl_zero_copy_loader!(AmmCache, crate::id, AmmCacheFixed, CacheInfo); + +impl<'a> AccountZeroCopy<'a, CacheInfo, AmmCacheFixed> { + pub fn check_settle_staleness(&self, slot: u64, threshold_slot_diff: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.last_settle_slot < slot.saturating_sub(threshold_slot_diff) { + msg!("AMM settle data is stale for perp market {}", i); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } + + pub fn check_perp_market_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.slot < slot.saturating_sub(threshold) { + msg!("Perp market cache info is stale for perp market {}", i); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } + + pub fn check_oracle_staleness(&self, slot: u64, threshold: u64) -> DriftResult<()> { + for (i, cache_info) in self.iter().enumerate() { + if cache_info.slot == 0 { + continue; + } + if cache_info.oracle_slot < slot.saturating_sub(threshold) { + msg!( + "Perp market cache info is stale for perp market {}. oracle slot: {}, slot: {}", + i, + cache_info.oracle_slot, + slot + ); + return Err(ErrorCode::AMMCacheStale.into()); + } + } + Ok(()) + } +} + +impl<'a> AccountZeroCopyMut<'a, CacheInfo, AmmCacheFixed> { + pub fn update_amount_owed_from_lp_pool( + &mut self, + perp_market: &PerpMarket, + quote_market: &SpotMarket, + ) -> DriftResult<()> { + let cached_info = self.get_mut(perp_market.market_index as u32); + + let fee_pool_token_amount = get_token_amount( + perp_market.amm.fee_pool.scaled_balance, + "e_market, + perp_market.amm.fee_pool.balance_type(), + )?; + + let net_pnl_pool_token_amount = get_token_amount( + perp_market.pnl_pool.scaled_balance, + "e_market, + perp_market.pnl_pool.balance_type(), + )? + .cast::()? + .safe_sub(calculate_net_user_pnl( + &perp_market.amm, + cached_info.oracle_price, + )?)?; + + let amm_amount_available = + net_pnl_pool_token_amount.safe_add(fee_pool_token_amount.cast::()?)?; + + if cached_info.last_net_pnl_pool_token_amount == 0 + && cached_info.last_fee_pool_token_amount == 0 + && cached_info.last_exchange_fees == 0 + { + cached_info.last_fee_pool_token_amount = fee_pool_token_amount; + cached_info.last_net_pnl_pool_token_amount = net_pnl_pool_token_amount; + cached_info.last_exchange_fees = perp_market.amm.total_exchange_fee; + cached_info.last_settle_amm_ex_fees = perp_market.amm.total_exchange_fee; + cached_info.last_settle_amm_pnl = net_pnl_pool_token_amount; + return Ok(()); + } + + let exchange_fee_delta = perp_market + .amm + .total_exchange_fee + .saturating_sub(cached_info.last_exchange_fees); + + let amount_to_send_to_lp_pool = amm_amount_available + .safe_sub(cached_info.get_last_available_amm_token_amount()?)? + .safe_mul(perp_market.lp_fee_transfer_scalar as i128)? + .safe_div_ceil(100)? + .safe_sub( + exchange_fee_delta + .cast::()? + .safe_mul(perp_market.lp_exchange_fee_excluscion_scalar as i128)? + .safe_div_ceil(100)?, + )?; + + cached_info.quote_owed_from_lp_pool = cached_info + .quote_owed_from_lp_pool + .safe_sub(amount_to_send_to_lp_pool.cast::()?)?; + + cached_info.last_fee_pool_token_amount = fee_pool_token_amount; + cached_info.last_net_pnl_pool_token_amount = net_pnl_pool_token_amount; + cached_info.last_exchange_fees = perp_market.amm.total_exchange_fee; + + Ok(()) + } + + pub fn update_perp_market_fields(&mut self, perp_market: &PerpMarket) -> DriftResult<()> { + let cache_info = self.get_mut(perp_market.market_index as u32); + cache_info.update_perp_market_fields(perp_market)?; + + Ok(()) + } +} diff --git a/programs/drift/src/state/constituent_map.rs b/programs/drift/src/state/constituent_map.rs new file mode 100644 index 0000000000..57518c0d4b --- /dev/null +++ b/programs/drift/src/state/constituent_map.rs @@ -0,0 +1,253 @@ +use anchor_lang::accounts::account_loader::AccountLoader; +use std::cell::{Ref, RefMut}; +use std::collections::{BTreeMap, BTreeSet}; +use std::iter::Peekable; +use std::slice::Iter; + +use anchor_lang::prelude::{AccountInfo, Pubkey}; + +use anchor_lang::Discriminator; +use arrayref::array_ref; + +use crate::error::{DriftResult, ErrorCode}; + +use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::traits::Size; +use crate::{msg, validate}; +use std::panic::Location; + +use super::lp_pool::Constituent; + +pub struct ConstituentMap<'a>(pub BTreeMap>); + +impl<'a> ConstituentMap<'a> { + #[track_caller] + #[inline(always)] + pub fn get_ref(&self, constituent_index: &u16) -> DriftResult> { + let loader = match self.0.get(constituent_index) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + constituent_index, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load() { + Ok(constituent) => Ok(constituent), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + constituent_index, + caller.file(), + caller.line() + ); + Err(ErrorCode::ConstituentCouldNotLoad) + } + } + } + + #[track_caller] + #[inline(always)] + pub fn get_ref_mut(&self, market_index: &u16) -> DriftResult> { + let loader = match self.0.get(market_index) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find constituent {} at {}:{}", + market_index, + caller.file(), + caller.line() + ); + return Err(ErrorCode::ConstituentNotFound); + } + }; + + match loader.load_mut() { + Ok(perp_market) => Ok(perp_market), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load constituent {} at {}:{}", + market_index, + caller.file(), + caller.line() + ); + Err(ErrorCode::ConstituentCouldNotLoad) + } + } + } + + pub fn load<'b, 'c>( + writable_constituents: &'b ConstituentSet, + lp_pool_key: &Pubkey, + account_info_iter: &'c mut Peekable>>, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + while let Some(account_info) = account_info_iter.peek() { + if account_info.owner != &crate::ID { + break; + } + + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + msg!( + "didnt match constituent size, {}, {}", + data.len(), + expected_data_len + ); + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + msg!( + "didnt match account discriminator {:?}, {:?}", + account_discriminator, + constituent_discriminator + ); + break; + } + + // Pubkey + let constituent_lp_key = Pubkey::from(*array_ref![data, 72, 32]); + validate!( + &constituent_lp_key == lp_pool_key, + ErrorCode::InvalidConstituent, + "Constituent lp pool pubkey does not match lp pool pubkey" + )?; + + // constituent index 308 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); + if constituent_map.0.contains_key(&constituent_index) { + msg!( + "Can not include same constituent index twice {}", + constituent_index + ); + return Err(ErrorCode::InvalidConstituent); + } + + let account_info = account_info_iter.next().safe_unwrap()?; + + let is_writable = account_info.is_writable; + if writable_constituents.contains(&constituent_index) && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + let account_loader: AccountLoader = AccountLoader::try_from(account_info) + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + constituent_map.0.insert(constituent_index, account_loader); + } + + Ok(constituent_map) + } +} + +#[cfg(test)] +impl<'a> ConstituentMap<'a> { + pub fn load_one<'c: 'a>( + account_info: &'c AccountInfo<'a>, + must_be_writable: bool, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + // market index 1160 bytes from front of account + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); + + let is_writable = account_info.is_writable; + let account_loader: AccountLoader = + AccountLoader::try_from(account_info).or(Err(ErrorCode::InvalidMarketAccount))?; + + if must_be_writable && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + constituent_map.0.insert(constituent_index, account_loader); + + Ok(constituent_map) + } + + pub fn load_multiple<'c: 'a>( + account_info: Vec<&'c AccountInfo<'a>>, + must_be_writable: bool, + ) -> DriftResult> { + let mut constituent_map: ConstituentMap = ConstituentMap(BTreeMap::new()); + + let account_info_iter = account_info.into_iter(); + for account_info in account_info_iter { + let constituent_discriminator: [u8; 8] = Constituent::discriminator(); + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + let expected_data_len = Constituent::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let account_discriminator = array_ref![data, 0, 8]; + if account_discriminator != &constituent_discriminator { + return Err(ErrorCode::ConstituentCouldNotLoad); + } + + let constituent_index = u16::from_le_bytes(*array_ref![data, 308, 2]); + + if constituent_map.0.contains_key(&constituent_index) { + msg!( + "Can not include same constituent index twice {}", + constituent_index + ); + return Err(ErrorCode::InvalidConstituent); + } + + let is_writable = account_info.is_writable; + let account_loader: AccountLoader = AccountLoader::try_from(account_info) + .or(Err(ErrorCode::ConstituentCouldNotLoad))?; + + if must_be_writable && !is_writable { + return Err(ErrorCode::ConstituentWrongMutability); + } + + constituent_map.0.insert(constituent_index, account_loader); + } + + Ok(constituent_map) + } + + pub fn empty() -> Self { + ConstituentMap(BTreeMap::new()) + } +} + +pub(crate) type ConstituentSet = BTreeSet; diff --git a/programs/drift/src/state/events.rs b/programs/drift/src/state/events.rs index 20b71598e0..5bcb2492aa 100644 --- a/programs/drift/src/state/events.rs +++ b/programs/drift/src/state/events.rs @@ -256,10 +256,15 @@ pub struct OrderActionRecord { pub maker_existing_base_asset_amount: Option, /// precision: PRICE_PRECISION pub trigger_price: Option, + + /// the idx of the builder in the taker's [`RevenueShareEscrow`] account + pub builder_idx: Option, + /// precision: QUOTE_PRECISION builder fee paid by the taker + pub builder_fee: Option, } impl Size for OrderActionRecord { - const SIZE: usize = 464; + const SIZE: usize = 480; } pub fn get_order_action_record( @@ -288,6 +293,8 @@ pub fn get_order_action_record( maker_existing_quote_entry_amount: Option, maker_existing_base_asset_amount: Option, trigger_price: Option, + builder_idx: Option, + builder_fee: Option, ) -> DriftResult { Ok(OrderActionRecord { ts, @@ -341,6 +348,8 @@ pub fn get_order_action_record( maker_existing_quote_entry_amount, maker_existing_base_asset_amount, trigger_price, + builder_idx, + builder_fee, }) } @@ -586,6 +595,7 @@ pub enum StakeAction { Unstake, UnstakeTransfer, StakeTransfer, + AdminDeposit, } #[event] @@ -703,6 +713,23 @@ pub struct FuelSeasonRecord { pub fuel_total: u128, } +#[event] +pub struct RevenueShareSettleRecord { + pub ts: i64, + pub builder: Option, + pub referrer: Option, + pub fee_settled: u64, + pub market_index: u16, + pub market_type: MarketType, + pub builder_sub_account_id: u16, + pub builder_total_referrer_rewards: u64, + pub builder_total_builder_rewards: u64, +} + +impl Size for RevenueShareSettleRecord { + const SIZE: usize = 140; +} + pub fn emit_stack(event: T) -> DriftResult { #[cfg(not(feature = "drift-rs"))] { @@ -768,6 +795,8 @@ pub struct LPSettleRecord { pub lp_aum: u128, // current mint price of lp pub lp_price: u128, + // lp pool pubkey + pub lp_pool: Pubkey, } #[event] @@ -809,10 +838,12 @@ pub struct LPSwapRecord { pub out_market_target_weight: i64, pub in_swap_id: u64, pub out_swap_id: u64, + // lp pool pubkey + pub lp_pool: Pubkey, } impl Size for LPSwapRecord { - const SIZE: usize = 376; + const SIZE: usize = 408; } #[event] @@ -847,8 +878,29 @@ pub struct LPMintRedeemRecord { /// PERCENTAGE_PRECISION pub in_market_current_weight: i64, pub in_market_target_weight: i64, + // lp pool pubkey + pub lp_pool: Pubkey, } impl Size for LPMintRedeemRecord { - const SIZE: usize = 328; + const SIZE: usize = 360; +} + +#[event] +#[derive(Default)] +pub struct LPBorrowLendDepositRecord { + pub ts: i64, + pub slot: u64, + pub spot_market_index: u16, + pub constituent_index: u16, + pub direction: DepositDirection, + pub token_balance: i64, + pub last_token_balance: i64, + pub interest_accrued_token_amount: i64, + pub amount_deposit_withdraw: u64, + pub lp_pool: Pubkey, +} + +impl Size for LPBorrowLendDepositRecord { + const SIZE: usize = 104; } diff --git a/programs/drift/src/state/insurance_fund_stake.rs b/programs/drift/src/state/insurance_fund_stake.rs index bd92019969..25d81f4b0d 100644 --- a/programs/drift/src/state/insurance_fund_stake.rs +++ b/programs/drift/src/state/insurance_fund_stake.rs @@ -1,12 +1,13 @@ use crate::error::DriftResult; use crate::error::ErrorCode; +use crate::math::constants::EPOCH_DURATION; use crate::math::safe_math::SafeMath; +use crate::math_error; use crate::safe_decrement; use crate::safe_increment; use crate::state::spot_market::SpotMarket; use crate::state::traits::Size; use crate::validate; -use crate::{math_error, EPOCH_DURATION}; use anchor_lang::prelude::*; #[cfg(test)] diff --git a/programs/drift/src/state/lp_pool.rs b/programs/drift/src/state/lp_pool.rs new file mode 100644 index 0000000000..106945fa5a --- /dev/null +++ b/programs/drift/src/state/lp_pool.rs @@ -0,0 +1,1891 @@ +use std::collections::BTreeMap; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::constants::{ + BASE_PRECISION_I128, PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I128, PERCENTAGE_PRECISION_I64, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION, QUOTE_PRECISION_I128, QUOTE_PRECISION_U64, +}; +use crate::math::oracle::{is_oracle_valid_for_action, DriftAction}; +use crate::math::safe_math::SafeMath; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::math::spot_balance::{get_signed_token_amount, get_token_amount}; +use crate::state::amm_cache::{AmmCacheFixed, CacheInfo}; +use crate::state::constituent_map::ConstituentMap; +use crate::state::oracle_map::OracleMap; +use crate::state::paused_operations::ConstituentLpOperation; +use crate::state::spot_market_map::SpotMarketMap; +use crate::state::user::MarketType; +use anchor_lang::prelude::*; +use borsh::{BorshDeserialize, BorshSerialize}; +use enumflags2::BitFlags; + +use super::oracle::OraclePriceData; +use super::spot_market::SpotMarket; +use super::zero_copy::{AccountZeroCopy, AccountZeroCopyMut, HasLen}; +use crate::state::spot_market::{SpotBalance, SpotBalanceType}; +use crate::state::traits::Size; +use crate::{impl_zero_copy_loader, validate}; + +pub const LP_POOL_PDA_SEED: &str = "lp_pool"; +pub const AMM_MAP_PDA_SEED: &str = "AMM_MAP"; +pub const CONSTITUENT_PDA_SEED: &str = "CONSTITUENT"; +pub const CONSTITUENT_TARGET_BASE_PDA_SEED: &str = "constituent_target_base_seed"; +pub const CONSTITUENT_CORRELATIONS_PDA_SEED: &str = "constituent_correlations"; +pub const CONSTITUENT_VAULT_PDA_SEED: &str = "CONSTITUENT_VAULT"; +pub const LP_POOL_TOKEN_VAULT_PDA_SEED: &str = "LP_POOL_TOKEN_VAULT"; + +pub const BASE_SWAP_FEE: i128 = 300; // 0.3% in PERCENTAGE_PRECISION +pub const MAX_SWAP_FEE: i128 = 37_500; // 37.5% in PERCENTAGE_PRECISION + +pub const MIN_AUM_EXECUTION_FEE: u128 = 10_000_000_000_000; + +// Delay constants +#[cfg(feature = "anchor-test")] +pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 100; +#[cfg(not(feature = "anchor-test"))] +pub const SETTLE_AMM_ORACLE_MAX_DELAY: u64 = 10; +pub const LP_POOL_SWAP_AUM_UPDATE_DELAY: u64 = 0; +#[cfg(feature = "anchor-test")] +pub const MAX_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +#[cfg(not(feature = "anchor-test"))] +pub const MAX_STALENESS_FOR_TARGET_CALC: u64 = 0u64; + +#[cfg(feature = "anchor-test")] +pub const MAX_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10000u64; +#[cfg(not(feature = "anchor-test"))] +pub const MAX_ORACLE_STALENESS_FOR_TARGET_CALC: u64 = 10u64; + +#[cfg(test)] +mod tests; + +#[account(zero_copy(unsafe))] +#[derive(Debug)] +#[repr(C)] +pub struct LPPool { + /// address of the vault. + pub pubkey: Pubkey, + // vault token mint + pub mint: Pubkey, // 32, 96 + // whitelist mint + pub whitelist_mint: Pubkey, + // constituent target base pubkey + pub constituent_target_base: Pubkey, + // constituent correlations pubkey + pub constituent_correlations: Pubkey, + + /// The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index) + /// which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0) + /// pub quote_constituent_index: u16, + + /// QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this + pub max_aum: u128, + + /// QUOTE_PRECISION: AUM of the vault in USD, updated lazily + pub last_aum: u128, + + /// QUOTE PRECISION: Cumulative quotes from settles + pub cumulative_quote_sent_to_perp_markets: u128, + pub cumulative_quote_received_from_perp_markets: u128, + + /// QUOTE_PRECISION: Total fees paid for minting and redeeming LP tokens + pub total_mint_redeem_fees_paid: i128, + + /// timestamp of last AUM slot + pub last_aum_slot: u64, + + pub max_settle_quote_amount: u64, + + /// timestamp of last vAMM revenue rebalance + pub _padding: u64, + + /// Every mint/redeem has a monotonically increasing id. This is the next id to use + pub mint_redeem_id: u64, + pub settle_id: u64, + + /// PERCENTAGE_PRECISION + pub min_mint_fee: i64, + + pub token_supply: u64, + + // PERCENTAGE_PRECISION: percentage precision const = 100% + pub volatility: u64, + + pub constituents: u16, + pub quote_consituent_index: u16, + + pub bump: u8, + + // No precision - just constant + pub gamma_execution: u8, + // No precision - just constant + pub xi: u8, + + // Bps of fees for target delays + pub target_oracle_delay_fee_bps_per_10_slots: u8, + pub target_position_delay_fee_bps_per_10_slots: u8, + + pub lp_pool_id: u8, + + pub padding: [u8; 174], +} + +impl Default for LPPool { + fn default() -> Self { + Self { + pubkey: Pubkey::default(), + mint: Pubkey::default(), + whitelist_mint: Pubkey::default(), + constituent_target_base: Pubkey::default(), + constituent_correlations: Pubkey::default(), + max_aum: 0, + last_aum: 0, + cumulative_quote_sent_to_perp_markets: 0, + cumulative_quote_received_from_perp_markets: 0, + total_mint_redeem_fees_paid: 0, + last_aum_slot: 0, + max_settle_quote_amount: 0, + _padding: 0, + mint_redeem_id: 0, + settle_id: 0, + min_mint_fee: 0, + token_supply: 0, + volatility: 0, + constituents: 0, + quote_consituent_index: 0, + bump: 0, + gamma_execution: 0, + xi: 0, + target_oracle_delay_fee_bps_per_10_slots: 0, + target_position_delay_fee_bps_per_10_slots: 0, + lp_pool_id: 0, + padding: [0u8; 174], + } + } +} + +impl Size for LPPool { + const SIZE: usize = 504; +} + +impl LPPool { + pub fn sync_token_supply(&mut self, supply: u64) { + self.token_supply = supply; + } + + pub fn get_price(&self, mint_supply: u64) -> Result { + match mint_supply { + 0 => Ok(0), + supply => { + // TODO: assuming mint decimals = quote decimals = 6 + Ok(self + .last_aum + .safe_mul(PRICE_PRECISION)? + .safe_div(supply as u128)?) + } + } + } + + /// Get the swap price between two (non-LP token) constituents. + /// Accounts for precision differences between in and out constituents + /// returns swap price in PRICE_PRECISION + pub fn get_swap_price( + &self, + in_decimals: u32, + out_decimals: u32, + in_oracle: &OraclePriceData, + out_oracle: &OraclePriceData, + ) -> DriftResult<(u128, u128)> { + let in_price = in_oracle.price.cast::()?; + let out_price = out_oracle.price.cast::()?; + + let (prec_diff_numerator, prec_diff_denominator) = if out_decimals > in_decimals { + (10_u128.pow(out_decimals - in_decimals), 1) + } else { + (1, 10_u128.pow(in_decimals - out_decimals)) + }; + + let swap_price_num = in_price.safe_mul(prec_diff_numerator)?; + let swap_price_denom = out_price.safe_mul(prec_diff_denominator)?; + + Ok((swap_price_num, swap_price_denom)) + } + + /// in the respective token units. Amounts are gross fees and in + /// token mint precision. + /// Positive fees are paid, negative fees are rebated + /// Returns (in_amount out_amount, in_fee, out_fee) + pub fn get_swap_amount( + &self, + in_target_position_slot_delay: u64, + out_target_position_slot_delay: u64, + in_target_oracle_slot_delay: u64, + out_target_oracle_slot_delay: u64, + in_oracle: &OraclePriceData, + out_oracle: &OraclePriceData, + in_constituent: &Constituent, + out_constituent: &Constituent, + in_spot_market: &SpotMarket, + out_spot_market: &SpotMarket, + in_target_weight: i64, + out_target_weight: i64, + in_amount: u128, + correlation: i64, + ) -> DriftResult<(u128, u128, i128, i128)> { + let (swap_price_num, swap_price_denom) = self.get_swap_price( + in_spot_market.decimals, + out_spot_market.decimals, + in_oracle, + out_oracle, + )?; + + let (mut in_fee, mut out_fee) = self.get_swap_fees( + in_spot_market, + in_oracle.price, + in_constituent, + in_amount.cast::()?, + in_target_weight, + Some(out_spot_market), + Some(out_oracle.price), + Some(out_constituent), + Some(out_target_weight), + correlation, + )?; + + in_fee = in_fee.safe_add(self.get_target_uncertainty_fees( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + )?)?; + out_fee = out_fee.safe_add(self.get_target_uncertainty_fees( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + )?)?; + + in_fee = in_fee.min(MAX_SWAP_FEE); + out_fee = out_fee.min(MAX_SWAP_FEE); + + let in_fee_amount = in_amount + .cast::()? + .safe_mul(in_fee)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let out_amount = in_amount + .cast::()? + .safe_sub(in_fee_amount)? + .cast::()? + .safe_mul(swap_price_num)? + .safe_div(swap_price_denom)?; + + let out_fee_amount = out_amount + .cast::()? + .safe_mul(out_fee)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + Ok((in_amount, out_amount, in_fee_amount, out_fee_amount)) + } + + /// Calculates the amount of LP tokens to mint for a given input of constituent tokens. + /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision + pub fn get_add_liquidity_mint_amount( + &self, + in_target_position_slot_delay: u64, + in_target_oracle_slot_delay: u64, + in_spot_market: &SpotMarket, + in_constituent: &Constituent, + in_amount: u128, + in_oracle: &OraclePriceData, + in_target_weight: i64, + dlp_total_supply: u64, + ) -> DriftResult<(u64, u128, i64, i128)> { + let (mut in_fee_pct, out_fee_pct) = if self.last_aum == 0 { + (0, 0) + } else { + self.get_swap_fees( + in_spot_market, + in_oracle.price, + in_constituent, + in_amount.cast::()?, + in_target_weight, + None, + None, + None, + None, + 0, + )? + }; + in_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + in_fee_pct += self.get_target_uncertainty_fees( + in_target_position_slot_delay, + in_target_oracle_slot_delay, + )?; + in_fee_pct = in_fee_pct.min(MAX_SWAP_FEE * 2); + + let in_fee_amount = in_amount + .cast::()? + .safe_mul(in_fee_pct)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let in_amount_less_fees = in_amount + .cast::()? + .safe_sub(in_fee_amount)? + .max(0) + .cast::()?; + + let token_precision_denominator = 10_u128.pow(in_spot_market.decimals); + let token_amount_usd = in_oracle + .price + .cast::()? + .safe_mul(in_amount_less_fees)?; + let lp_amount = if dlp_total_supply == 0 { + token_amount_usd.safe_div(token_precision_denominator)? + } else { + token_amount_usd + .safe_mul(dlp_total_supply as u128)? + .safe_div(self.last_aum)? + .safe_div(token_precision_denominator)? + }; + + let lp_fee_to_charge_pct = self.min_mint_fee; + // let lp_fee_to_charge_pct = self.get_mint_redeem_fee(now, true)?; + let lp_fee_to_charge = lp_amount + .safe_mul(lp_fee_to_charge_pct as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .cast::()?; + + Ok(( + lp_amount.cast::()?, + in_amount, + lp_fee_to_charge, + in_fee_amount, + )) + } + + /// Calculates the amount of constituent tokens to receive for a given amount of LP tokens to burn + /// Returns the mint_amount in lp token precision and fee to charge in constituent mint precision + pub fn get_remove_liquidity_amount( + &self, + out_target_position_slot_delay: u64, + out_target_oracle_slot_delay: u64, + out_spot_market: &SpotMarket, + out_constituent: &Constituent, + lp_to_burn: u64, + out_oracle: &OraclePriceData, + out_target_weight: i64, + dlp_total_supply: u64, + ) -> DriftResult<(u64, u128, i64, i128)> { + let lp_fee_to_charge_pct = self.min_mint_fee; + let mut lp_burn_amount = lp_to_burn; + if dlp_total_supply.saturating_sub(lp_burn_amount) <= QUOTE_PRECISION_U64 { + lp_burn_amount = dlp_total_supply.saturating_sub(QUOTE_PRECISION_U64); + } + + let lp_fee_to_charge = lp_burn_amount + .cast::()? + .safe_mul(lp_fee_to_charge_pct.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .cast::()?; + + let lp_amount_less_fees = (lp_burn_amount as i128).safe_sub(lp_fee_to_charge as i128)?; + + let token_precision_denominator = 10_u128.pow(out_spot_market.decimals); + + // Calculate proportion of LP tokens being burned + let proportion = lp_amount_less_fees + .cast::()? + .safe_mul(10u128.pow(3))? + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(dlp_total_supply as u128)?; + + // Apply proportion to AUM and convert to token amount + let out_amount = self + .last_aum + .safe_mul(proportion)? + .safe_mul(token_precision_denominator)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(10u128.pow(3))? + .safe_div(out_oracle.price.cast::()?)?; + + let (in_fee_pct, mut out_fee_pct) = self.get_swap_fees( + out_spot_market, + out_oracle.price, + out_constituent, + out_amount.cast::()?.safe_mul(-1_i128)?, + out_target_weight, + None, + None, + None, + None, + 0, + )?; + + out_fee_pct += self.get_target_uncertainty_fees( + out_target_position_slot_delay, + out_target_oracle_slot_delay, + )?; + out_fee_pct = in_fee_pct.safe_add(out_fee_pct)?; + out_fee_pct = out_fee_pct.min(MAX_SWAP_FEE * 2); + + let out_fee_amount = out_amount + .cast::()? + .safe_mul(out_fee_pct)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + Ok((lp_burn_amount, out_amount, lp_fee_to_charge, out_fee_amount)) + } + + pub fn get_quadratic_fee_inventory( + &self, + gamma_covar: [[i128; 2]; 2], + pre_notional_errors: [i128; 2], + post_notional_errors: [i128; 2], + trade_notional: u128, + ) -> DriftResult<(i128, i128)> { + let gamma_covar_error_pre_in = gamma_covar[0][0] + .safe_mul(pre_notional_errors[0])? + .safe_add(gamma_covar[0][1].safe_mul(pre_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let gamma_covar_error_pre_out = gamma_covar[1][0] + .safe_mul(pre_notional_errors[0])? + .safe_add(gamma_covar[1][1].safe_mul(pre_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let gamma_covar_error_post_in = gamma_covar[0][0] + .safe_mul(post_notional_errors[0])? + .safe_add(gamma_covar[0][1].safe_mul(post_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let gamma_covar_error_post_out = gamma_covar[1][0] + .safe_mul(post_notional_errors[0])? + .safe_add(gamma_covar[1][1].safe_mul(post_notional_errors[1])?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + let c_pre_in: i128 = gamma_covar_error_pre_in + .safe_mul(pre_notional_errors[0])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + let c_pre_out = gamma_covar_error_pre_out + .safe_mul(pre_notional_errors[1])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + + let c_post_in: i128 = gamma_covar_error_post_in + .safe_mul(post_notional_errors[0])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + let c_post_out = gamma_covar_error_post_out + .safe_mul(post_notional_errors[1])? + .safe_div(2)? + .safe_div(QUOTE_PRECISION_I128)?; + + let in_fee = c_post_in + .safe_sub(c_pre_in)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(trade_notional.cast::()?)? + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(self.last_aum.cast::()?)?; + let out_fee = c_post_out + .safe_sub(c_pre_out)? + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(trade_notional.cast::()?)? + .safe_mul(QUOTE_PRECISION_I128)? + .safe_div(self.last_aum.cast::()?)?; + + Ok((in_fee, out_fee)) + } + + pub fn get_linear_fee_execution( + &self, + trade_ratio: i128, + kappa_execution: u128, + xi: u8, + ) -> DriftResult { + trade_ratio + .safe_mul(kappa_execution.safe_mul(xi as u128)?.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128) + } + + pub fn get_quadratic_fee_execution( + &self, + trade_ratio: i128, + kappa_execution: u128, + xi: u8, + ) -> DriftResult { + kappa_execution + .cast::()? + .safe_mul(xi.safe_mul(xi)?.cast::()?)? + .safe_mul(trade_ratio.safe_mul(trade_ratio)?)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_div(PERCENTAGE_PRECISION_I128) + } + + /// returns fee in PERCENTAGE_PRECISION + pub fn get_swap_fees( + &self, + in_spot_market: &SpotMarket, + in_oracle_price: i64, + in_constituent: &Constituent, + in_amount: i128, + in_target_weight: i64, + out_spot_market: Option<&SpotMarket>, + out_oracle_price: Option, + out_constituent: Option<&Constituent>, + out_target_weight: Option, + correlation: i64, + ) -> DriftResult<(i128, i128)> { + let notional_trade_size = in_constituent.get_notional(in_oracle_price, in_amount)?; + let in_volatility = in_constituent.volatility; + + let ( + mint_redeem, + out_volatility, + out_gamma_execution, + out_gamma_inventory, + out_xi, + out_notional_target, + out_notional_pre, + out_notional_post, + ) = if let Some(out_constituent) = out_constituent { + let out_spot_market = out_spot_market.unwrap(); + let out_oracle_price = out_oracle_price.unwrap(); + let out_amount = notional_trade_size + .safe_mul(10_i128.pow(out_spot_market.decimals))? + .safe_div(out_oracle_price.cast::()?)?; + ( + false, + out_constituent.volatility, + out_constituent.gamma_execution, + out_constituent.gamma_inventory, + out_constituent.xi, + out_target_weight + .unwrap() + .cast::()? + .safe_mul(self.last_aum.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?, + out_constituent.get_notional_with_delta(out_oracle_price, out_spot_market, 0)?, + out_constituent.get_notional_with_delta( + out_oracle_price, + out_spot_market, + out_amount.safe_mul(-1)?, + )?, + ) + } else { + ( + true, + self.volatility, + self.gamma_execution, + 0, + self.xi, + 0, + 0, + 0, + ) + }; + + let in_kappa_execution: u128 = (in_volatility as u128) + .safe_mul(in_volatility as u128)? + .safe_mul(in_constituent.gamma_execution as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(2u128)?; + + let out_kappa_execution: u128 = (out_volatility as u128) + .safe_mul(out_volatility as u128)? + .safe_mul(out_gamma_execution as u128)? + .safe_div(PERCENTAGE_PRECISION)? + .safe_div(2u128)?; + + // Compute notional targets and errors + let in_notional_target = in_target_weight + .cast::()? + .safe_mul(self.last_aum.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + let in_notional_pre = + in_constituent.get_notional_with_delta(in_oracle_price, in_spot_market, 0)?; + let in_notional_post = + in_constituent.get_notional_with_delta(in_oracle_price, in_spot_market, in_amount)?; + let in_notional_error_pre = in_notional_pre.safe_sub(in_notional_target)?; + + // keep aum fixed if it's a swap for calculating post error, othwerise + // increase aum first + let in_notional_error_post = if !mint_redeem { + in_notional_post.safe_sub(in_notional_target)? + } else { + let adjusted_aum = self + .last_aum + .cast::()? + .safe_add(notional_trade_size)?; + let in_notional_target_post_mint_redeem = in_target_weight + .cast::()? + .safe_mul(adjusted_aum)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + in_notional_post.safe_sub(in_notional_target_post_mint_redeem)? + }; + + let out_notional_error_pre = out_notional_pre.safe_sub(out_notional_target)?; + let out_notional_error_post = out_notional_post.safe_sub(out_notional_target)?; + + let trade_ratio: i128 = notional_trade_size + .abs() + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(self.last_aum.max(MIN_AUM_EXECUTION_FEE).cast::()?)?; + + // Linear fee computation amount + let in_fee_execution_linear = + self.get_linear_fee_execution(trade_ratio, in_kappa_execution, in_constituent.xi)?; + + let out_fee_execution_linear = + self.get_linear_fee_execution(trade_ratio, out_kappa_execution, out_xi)?; + + // Quadratic fee components + let in_fee_execution_quadratic = + self.get_quadratic_fee_execution(trade_ratio, in_kappa_execution, in_constituent.xi)?; + let out_fee_execution_quadratic = + self.get_quadratic_fee_execution(trade_ratio, out_kappa_execution, out_xi)?; + let (in_quadratic_inventory_fee, out_quadratic_inventory_fee) = self + .get_quadratic_fee_inventory( + get_gamma_covar_matrix( + correlation, + in_constituent.gamma_inventory, + out_gamma_inventory, + in_constituent.volatility, + out_volatility, + )?, + [in_notional_error_pre, out_notional_error_pre], + [in_notional_error_post, out_notional_error_post], + notional_trade_size.abs().cast::()?, + )?; + + msg!( + "fee breakdown - in_exec_linear: {}, in_exec_quad: {}, in_inv_quad: {}, out_exec_linear: {}, out_exec_quad: {}, out_inv_quad: {}", + in_fee_execution_linear, + in_fee_execution_quadratic, + in_quadratic_inventory_fee, + out_fee_execution_linear, + out_fee_execution_quadratic, + out_quadratic_inventory_fee + ); + let total_in_fee = in_fee_execution_linear + .safe_add(in_fee_execution_quadratic)? + .safe_add(in_quadratic_inventory_fee)? + .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; + let total_out_fee = out_fee_execution_linear + .safe_add(out_fee_execution_quadratic)? + .safe_add(out_quadratic_inventory_fee)? + .safe_add(BASE_SWAP_FEE.safe_div(2)?)?; + + Ok((total_in_fee, total_out_fee)) + } + + pub fn get_target_uncertainty_fees( + self, + target_position_slot_delay: u64, + target_oracle_slot_delay: u64, + ) -> DriftResult { + // Gives an uncertainty fee in bps if the oracle or position was stale when calcing target. + // Uses a step function that goes up every 10 slots beyond a threshold where we consider it okay + // - delay 0 (<= threshold) = 0 bps + // - delay 1..10 = 10 bps (1 block) + // - delay 11..20 = 20 bps (2 blocks) + fn step_fee(delay: u64, threshold: u64, per_10_slot_bps: u8) -> DriftResult { + if delay <= threshold || per_10_slot_bps == 0 { + return Ok(0); + } + let elapsed = delay.saturating_sub(threshold); + let blocks = elapsed.safe_add(9)?.safe_div(10)?; + let fee_bps = (blocks as u128).safe_mul(per_10_slot_bps as u128)?; + let fee = fee_bps + .safe_mul(PERCENTAGE_PRECISION)? + .safe_div(10_000u128)?; + Ok(fee) + } + + let oracle_uncertainty_fee = step_fee( + target_oracle_slot_delay, + MAX_ORACLE_STALENESS_FOR_TARGET_CALC, + self.target_oracle_delay_fee_bps_per_10_slots, + )?; + let position_uncertainty_fee = step_fee( + target_position_slot_delay, + MAX_STALENESS_FOR_TARGET_CALC, + self.target_position_delay_fee_bps_per_10_slots, + )?; + + Ok(oracle_uncertainty_fee + .safe_add(position_uncertainty_fee)? + .cast::()?) + } + + pub fn record_mint_redeem_fees(&mut self, amount: i64) -> DriftResult { + self.total_mint_redeem_fees_paid = self + .total_mint_redeem_fees_paid + .safe_add(amount.cast::()?)?; + Ok(()) + } + + pub fn update_aum( + &mut self, + slot: u64, + constituent_map: &ConstituentMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + constituent_target_base: &AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, + amm_cache: &AccountZeroCopyMut<'_, CacheInfo, AmmCacheFixed>, + ) -> DriftResult<(u128, i128, BTreeMap>)> { + let mut aum: i128 = 0; + let mut crypto_delta = 0_i128; + let mut derivative_groups: BTreeMap> = BTreeMap::new(); + for i in 0..self.constituents as usize { + let constituent = constituent_map.get_ref(&(i as u16))?; + if slot.saturating_sub(constituent.last_oracle_slot) + > constituent.oracle_staleness_threshold + { + msg!( + "Constituent {} oracle slot is too stale: {}, current slot: {}", + constituent.constituent_index, + constituent.last_oracle_slot, + slot + ); + return Err(ErrorCode::ConstituentOracleStale.into()); + } + + if constituent.constituent_derivative_index >= 0 && constituent.derivative_weight != 0 { + if !derivative_groups + .contains_key(&(constituent.constituent_derivative_index as u16)) + { + derivative_groups.insert( + constituent.constituent_derivative_index as u16, + vec![constituent.constituent_index], + ); + } else { + derivative_groups + .get_mut(&(constituent.constituent_derivative_index as u16)) + .unwrap() + .push(constituent.constituent_index); + } + } + + let spot_market = spot_market_map.get_ref(&constituent.spot_market_index)?; + let oracle_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + constituent.spot_market_index, + &spot_market.oracle_id(), + spot_market.historical_oracle_data.last_oracle_price_twap, + spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + if !is_oracle_valid_for_action( + oracle_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Constituent {} oracle is not valid for action", + constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle.into()); + } + + let constituent_aum = constituent + .get_full_token_amount(&spot_market)? + .safe_mul(oracle_and_validity.0.price as i128)? + .safe_div(10_i128.pow(spot_market.decimals))?; + msg!( + "constituent: {}, balance: {}, aum: {}, deriv index: {}, bl token balance {}, bl balance type {}, vault balance: {}", + constituent.constituent_index, + constituent.get_full_token_amount(&spot_market)?, + constituent_aum, + constituent.constituent_derivative_index, + constituent.spot_balance.get_token_amount(&spot_market)?, + constituent.spot_balance.balance_type, + constituent.vault_token_balance + ); + + // sum up crypto deltas (notional exposures for all non-stablecoins) + if constituent.constituent_index != self.quote_consituent_index + && constituent.constituent_derivative_index != self.quote_consituent_index as i16 + { + let constituent_target_notional = constituent_target_base + .get(constituent.constituent_index as u32) + .target_base + .cast::()? + .safe_mul(oracle_and_validity.0.price.cast::()?)? + .safe_div(10_i128.pow(constituent.decimals as u32))? + .cast::()?; + crypto_delta = crypto_delta.safe_add(constituent_target_notional.cast()?)?; + } + aum = aum.saturating_add(constituent_aum); + } + + msg!("Aum before quote owed from lp pool: {}", aum); + + let mut total_quote_owed: i128 = 0; + for cache_datum in amm_cache.iter() { + total_quote_owed = + total_quote_owed.safe_add(cache_datum.quote_owed_from_lp_pool as i128)?; + } + + if total_quote_owed > 0 { + aum = aum + .saturating_sub(total_quote_owed) + .max(QUOTE_PRECISION_I128); + } else if total_quote_owed < 0 { + aum = aum.saturating_add(-total_quote_owed); + } + + let aum_u128 = aum.max(0).cast::()?; + self.last_aum = aum_u128; + self.last_aum_slot = slot; + + Ok((aum_u128, crypto_delta, derivative_groups)) + } + + pub fn get_lp_pool_signer_seeds<'a>(lp_pool_id: &'a u8, bump: &'a u8) -> [&'a [u8]; 3] { + [ + LP_POOL_PDA_SEED.as_ref(), + bytemuck::bytes_of(lp_pool_id), + bytemuck::bytes_of(bump), + ] + } +} + +#[zero_copy(unsafe)] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct ConstituentSpotBalance { + /// The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow + /// interest of corresponding market. + /// precision: token precision + pub scaled_balance: u128, + /// The cumulative deposits/borrows a user has made into a market + /// precision: token mint precision + pub cumulative_deposits: i64, + /// The market index of the corresponding spot market + pub market_index: u16, + /// Whether the position is deposit or borrow + pub balance_type: SpotBalanceType, + pub padding: [u8; 5], +} + +impl SpotBalance for ConstituentSpotBalance { + fn market_index(&self) -> u16 { + self.market_index + } + + fn balance_type(&self) -> &SpotBalanceType { + &self.balance_type + } + + fn balance(&self) -> u128 { + self.scaled_balance + } + + fn increase_balance(&mut self, delta: u128) -> DriftResult { + self.scaled_balance = self.scaled_balance.safe_add(delta)?; + Ok(()) + } + + fn decrease_balance(&mut self, delta: u128) -> DriftResult { + self.scaled_balance = self.scaled_balance.safe_sub(delta)?; + Ok(()) + } + + fn update_balance_type(&mut self, balance_type: SpotBalanceType) -> DriftResult { + self.balance_type = balance_type; + Ok(()) + } +} + +impl ConstituentSpotBalance { + pub fn get_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + get_token_amount(self.scaled_balance, spot_market, &self.balance_type) + } + + pub fn get_signed_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + let token_amount = self.get_token_amount(spot_market)?; + get_signed_token_amount(token_amount, &self.balance_type) + } +} + +#[account(zero_copy(unsafe))] +#[derive(Debug)] +#[repr(C)] +pub struct Constituent { + /// address of the constituent + pub pubkey: Pubkey, + pub mint: Pubkey, + pub lp_pool: Pubkey, + pub vault: Pubkey, + + /// total fees received by the constituent. Positive = fees received, Negative = fees paid + pub total_swap_fees: i128, + + /// spot borrow-lend balance for constituent + pub spot_balance: ConstituentSpotBalance, // should be in constituent base asset + + pub last_spot_balance_token_amount: i64, // token precision + pub cumulative_spot_interest_accrued_token_amount: i64, // token precision + + /// max deviation from target_weight allowed for the constituent + /// precision: PERCENTAGE_PRECISION + pub max_weight_deviation: i64, + /// min fee charged on swaps to/from this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_min: i64, + /// max fee charged on swaps to/from this constituent + /// precision: PERCENTAGE_PRECISION + pub swap_fee_max: i64, + + /// Max Borrow amount: + /// precision: token precision + pub max_borrow_token_amount: u64, + + /// ata token balance in token precision + pub vault_token_balance: u64, + + pub last_oracle_price: i64, + pub last_oracle_slot: u64, + + /// Delay allowed for valid AUM calculation + pub oracle_staleness_threshold: u64, + + pub flash_loan_initial_token_amount: u64, + /// Every swap to/from this constituent has a monotonically increasing id. This is the next id to use + pub next_swap_id: u64, + + /// percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight + pub derivative_weight: u64, + + pub volatility: u64, // volatility in PERCENTAGE_PRECISION 1=1% + + // depeg threshold in relation top parent in PERCENTAGE_PRECISION + pub constituent_derivative_depeg_threshold: u64, + + /// The `constituent_index` of the parent constituent. -1 if it is a parent index + /// Example: if in a pool with SOL (parent) and dSOL (derivative), + /// SOL.constituent_index = 1, SOL.constituent_derivative_index = -1, + /// dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1 + pub constituent_derivative_index: i16, + + pub spot_market_index: u16, + pub constituent_index: u16, + + pub decimals: u8, + pub bump: u8, + pub vault_bump: u8, + + // Fee params + pub gamma_inventory: u8, + pub gamma_execution: u8, + pub xi: u8, + + // Status + pub status: u8, + pub paused_operations: u8, + pub _padding: [u8; 162], +} + +impl Default for Constituent { + fn default() -> Self { + Self { + pubkey: Pubkey::default(), + mint: Pubkey::default(), + lp_pool: Pubkey::default(), + vault: Pubkey::default(), + total_swap_fees: 0, + spot_balance: ConstituentSpotBalance::default(), + last_spot_balance_token_amount: 0, + cumulative_spot_interest_accrued_token_amount: 0, + max_weight_deviation: 0, + swap_fee_min: 0, + swap_fee_max: 0, + max_borrow_token_amount: 0, + vault_token_balance: 0, + last_oracle_price: 0, + last_oracle_slot: 0, + oracle_staleness_threshold: 0, + flash_loan_initial_token_amount: 0, + next_swap_id: 0, + derivative_weight: 0, + volatility: 0, + constituent_derivative_depeg_threshold: 0, + constituent_derivative_index: -1, + spot_market_index: 0, + constituent_index: 0, + decimals: 0, + bump: 0, + vault_bump: 0, + gamma_inventory: 0, + gamma_execution: 0, + xi: 0, + status: 0, + paused_operations: 0, + _padding: [0; 162], + } + } +} + +impl Size for Constituent { + const SIZE: usize = 480; +} + +#[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] +pub enum ConstituentStatus { + /// fills only able to reduce liability + ReduceOnly = 0b00000001, + /// market has no remaining participants + Decommissioned = 0b00000010, +} + +impl Constituent { + pub fn get_status(&self) -> DriftResult> { + BitFlags::::from_bits(usize::from(self.status)).safe_unwrap() + } + + pub fn is_decommissioned(&self) -> DriftResult { + Ok(self + .get_status()? + .contains(ConstituentStatus::Decommissioned)) + } + + pub fn is_reduce_only(&self) -> DriftResult { + Ok(self.get_status()?.contains(ConstituentStatus::ReduceOnly)) + } + + pub fn does_constituent_allow_operation( + &self, + operation: ConstituentLpOperation, + ) -> DriftResult<()> { + if self.is_decommissioned()? { + msg!( + "Constituent {:?}, spot market {}, is decommissioned", + self.pubkey, + self.spot_market_index + ); + Err(ErrorCode::InvalidConstituentOperation) + } else if ConstituentLpOperation::is_operation_paused(self.paused_operations, operation) { + msg!( + "Constituent {:?}, spot market {}, is paused for operation {:?}", + self.pubkey, + self.spot_market_index, + operation + ); + Err(ErrorCode::InvalidConstituentOperation) + } else { + Ok(()) + } + } + + pub fn is_operation_reducing( + &self, + spot_market: &SpotMarket, + is_increasing: bool, + ) -> DriftResult { + let current_balance_sign = self.get_full_token_amount(spot_market)?.signum(); + if current_balance_sign > 0 { + Ok(!is_increasing) + } else { + Ok(is_increasing) + } + } + + /// Returns the full balance of the Constituent, the total of the amount in Constituent's token + /// account and in Drift Borrow-Lend. + pub fn get_full_token_amount(&self, spot_market: &SpotMarket) -> DriftResult { + let token_amount = self.spot_balance.get_signed_token_amount(spot_market)?; + let vault_balance = self.vault_token_balance.cast::()?; + token_amount.safe_add(vault_balance) + } + + pub fn record_swap_fees(&mut self, amount: i128) -> DriftResult { + self.total_swap_fees = self.total_swap_fees.safe_add(amount)?; + Ok(()) + } + + /// Current weight of this constituent = price * token_balance / lp_pool_aum + /// Note: lp_pool_aum is from LPPool.last_aum, which is a lagged value updated via crank + pub fn get_weight( + &self, + price: i64, + spot_market: &SpotMarket, + token_amount_delta: i128, + lp_pool_aum: u128, + ) -> DriftResult { + if lp_pool_aum == 0 { + return Ok(0); + } + let value_usd = self.get_notional_with_delta(price, spot_market, token_amount_delta)?; + + value_usd + .safe_mul(PERCENTAGE_PRECISION_I64.cast::()?)? + .safe_div(lp_pool_aum.cast::()?)? + .cast::() + } + + pub fn get_notional(&self, price: i64, token_amount: i128) -> DriftResult { + let token_precision = 10_i128.pow(self.decimals as u32); + let value_usd = token_amount.safe_mul(price.cast::()?)?; + value_usd.safe_div(token_precision) + } + + pub fn get_notional_with_delta( + &self, + price: i64, + spot_market: &SpotMarket, + token_amount: i128, + ) -> DriftResult { + let token_precision = 10_i128.pow(self.decimals as u32); + let balance = self.get_full_token_amount(spot_market)?.cast::()?; + let amount = balance.safe_add(token_amount)?; + let value_usd = amount.safe_mul(price.cast::()?)?; + value_usd.safe_div(token_precision) + } + + pub fn sync_token_balance(&mut self, token_account_amount: u64) { + self.vault_token_balance = token_account_amount; + } + + pub fn get_vault_signer_seeds<'a>( + lp_pool: &'a Pubkey, + spot_market_index: &'a u16, + bump: &'a u8, + ) -> [&'a [u8]; 4] { + [ + CONSTITUENT_VAULT_PDA_SEED.as_ref(), + lp_pool.as_ref(), + bytemuck::bytes_of(spot_market_index), + bytemuck::bytes_of(bump), + ] + } +} + +#[zero_copy] +#[derive(Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct AmmConstituentDatum { + pub perp_market_index: u16, + pub constituent_index: u16, + pub _padding: [u8; 4], + pub last_slot: u64, + /// PERCENTAGE_PRECISION. The weight this constituent has on the perp market + pub weight: i64, +} + +impl Default for AmmConstituentDatum { + fn default() -> Self { + AmmConstituentDatum { + perp_market_index: u16::MAX, + constituent_index: u16::MAX, + _padding: [0; 4], + last_slot: 0, + weight: 0, + } + } +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct AmmConstituentMappingFixed { + pub lp_pool: Pubkey, + pub bump: u8, + pub _pad: [u8; 3], + pub len: u32, +} + +impl HasLen for AmmConstituentMappingFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct AmmConstituentMapping { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. Each datum represents the target weight for a single (AMM, Constituent) pair. + // An AMM may be partially backed by multiple Constituents + pub weights: Vec, +} + +impl AmmConstituentMapping { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * 24 + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.weights.len() <= 128, + ErrorCode::DefaultError, + "Number of constituents len must be between 1 and 128" + )?; + Ok(()) + } + + pub fn sort(&mut self) { + self.weights.sort_by_key(|datum| datum.constituent_index); + } +} + +impl_zero_copy_loader!( + AmmConstituentMapping, + crate::id, + AmmConstituentMappingFixed, + AmmConstituentDatum +); + +#[zero_copy] +#[derive(Debug, Default, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct TargetsDatum { + pub cost_to_trade_bps: i32, + pub _padding: [u8; 4], + pub target_base: i64, + pub last_oracle_slot: u64, + pub last_position_slot: u64, +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct ConstituentTargetBaseFixed { + pub lp_pool: Pubkey, + pub bump: u8, + _pad: [u8; 3], + /// total elements in the flattened `data` vec + pub len: u32, +} + +impl HasLen for ConstituentTargetBaseFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct ConstituentTargetBase { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. The weights of the target weight matrix. Updated async + pub targets: Vec, +} + +impl ConstituentTargetBase { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * 32 + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.targets.len() <= 128, + ErrorCode::DefaultError, + "Number of constituents len must be between 1 and 128" + )?; + + validate!( + !self.targets.iter().any(|t| t.cost_to_trade_bps == 0), + ErrorCode::DefaultError, + "cost_to_trade_bps must be non-zero" + )?; + + Ok(()) + } +} + +impl_zero_copy_loader!( + ConstituentTargetBase, + crate::id, + ConstituentTargetBaseFixed, + TargetsDatum +); + +impl Default for ConstituentTargetBase { + fn default() -> Self { + ConstituentTargetBase { + lp_pool: Pubkey::default(), + bump: 0, + _padding: [0; 3], + targets: Vec::with_capacity(0), + } + } +} + +impl<'a> AccountZeroCopy<'a, TargetsDatum, ConstituentTargetBaseFixed> { + pub fn get_target_weight( + &self, + constituent_index: u16, + spot_market: &SpotMarket, + price: i64, + aum: u128, + ) -> DriftResult { + validate!( + constituent_index < self.len() as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index = {}, ConstituentTargetBase len = {}", + constituent_index, + self.len() + )?; + + // TODO: validate spot market + let datum = self.get(constituent_index as u32); + let target_weight = calculate_target_weight(datum.target_base, spot_market, price, aum)?; + Ok(target_weight) + } +} + +pub fn calculate_target_weight( + target_base: i64, + spot_market: &SpotMarket, + price: i64, + lp_pool_aum: u128, +) -> DriftResult { + if lp_pool_aum == 0 { + return Ok(0); + } + let notional: i128 = (target_base as i128) + .safe_mul(price as i128)? + .safe_div(10_i128.pow(spot_market.decimals))?; + + let target_weight = notional + .safe_mul(PERCENTAGE_PRECISION_I128)? + .safe_div(lp_pool_aum.cast::()?)? + .cast::()? + .clamp(-PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_I64); + + Ok(target_weight) +} + +/// Update target base based on amm_inventory and mapping +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AmmInventoryAndPricesAndSlots { + pub inventory: i64, + pub price: i64, + pub last_oracle_slot: u64, + pub last_position_slot: u64, +} + +pub struct ConstituentIndexAndDecimalAndPrice { + pub constituent_index: u16, + pub decimals: u8, + pub price: i64, +} + +impl<'a> AccountZeroCopyMut<'a, TargetsDatum, ConstituentTargetBaseFixed> { + pub fn update_target_base( + &mut self, + mapping: &AccountZeroCopy<'a, AmmConstituentDatum, AmmConstituentMappingFixed>, + amm_inventory_and_prices: &[AmmInventoryAndPricesAndSlots], + constituents_indexes_and_decimals_and_prices: &mut [ConstituentIndexAndDecimalAndPrice], + slot: u64, + ) -> DriftResult<()> { + // Sorts by constituent index + constituents_indexes_and_decimals_and_prices.sort_by_key(|c| c.constituent_index); + + // Precompute notional by perp market index + let mut notionals_and_slots: Vec<(i128, u64, u64)> = + Vec::with_capacity(amm_inventory_and_prices.len()); + for &AmmInventoryAndPricesAndSlots { + inventory, + price, + last_oracle_slot, + last_position_slot, + } in amm_inventory_and_prices.iter() + { + let notional = (inventory as i128) + .safe_mul(price as i128)? + .safe_div(BASE_PRECISION_I128)?; + notionals_and_slots.push((notional, last_oracle_slot, last_position_slot)); + } + + let mut mapping_index = 0; + for ( + i, + &ConstituentIndexAndDecimalAndPrice { + constituent_index, + decimals, + price, + }, + ) in constituents_indexes_and_decimals_and_prices + .iter() + .enumerate() + { + let mut target_notional = 0i128; + + let mut j = mapping_index; + let mut oldest_oracle_slot = u64::MAX; + let mut oldest_position_slot = u64::MAX; + while j < mapping.len() { + let d = mapping.get(j); + if d.constituent_index != constituent_index { + while j < mapping.len() && mapping.get(j).constituent_index < constituent_index + { + j += 1; + } + break; + } + if let Some((perp_notional, perp_last_oracle_slot, perp_last_position_slot)) = + notionals_and_slots.get(d.perp_market_index as usize) + { + target_notional = target_notional + .saturating_add(perp_notional.saturating_mul(d.weight as i128)); + + oldest_oracle_slot = oldest_oracle_slot.min(*perp_last_oracle_slot); + oldest_position_slot = oldest_position_slot.min(*perp_last_position_slot); + } + j += 1; + } + mapping_index = j; + + let cell = self.get_mut(i as u32); + let target_base = -target_notional + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_mul(10_i128.pow(decimals as u32))? + .safe_div(price as i128)?; // Want to target opposite sign of total scaled notional inventory + + msg!( + "updating constituent index {} target base to {} from aggregated perp notional {}", + constituent_index, + target_base, + target_notional, + ); + cell.target_base = target_base.cast::()?; + + if slot.saturating_sub(oldest_position_slot) == MAX_STALENESS_FOR_TARGET_CALC { + cell.last_position_slot = slot; + } else { + msg!( + "not updating last_position_slot for target base constituent_index {}: oldest_position_slot {}, current slot {}", + constituent_index, + oldest_position_slot, + slot + ); + } + + if slot.saturating_sub(oldest_oracle_slot) <= MAX_ORACLE_STALENESS_FOR_TARGET_CALC { + cell.last_oracle_slot = slot; + } else { + msg!( + "not updating last_oracle_slot for target base constituent_index {}: oldest_oracle_slot {}, current slot {}", + constituent_index, + oldest_oracle_slot, + slot + ); + } + } + + Ok(()) + } +} + +impl<'a> AccountZeroCopyMut<'a, AmmConstituentDatum, AmmConstituentMappingFixed> { + #[cfg(test)] + pub fn add_amm_constituent_datum(&mut self, datum: AmmConstituentDatum) -> DriftResult<()> { + let len = self.len(); + + let mut open_slot_index: Option = None; + for i in 0..len { + let cell = self.get(i); + if cell.constituent_index == datum.constituent_index + && cell.perp_market_index == datum.perp_market_index + { + return Err(ErrorCode::DefaultError); + } + if cell.last_slot == 0 && open_slot_index.is_none() { + open_slot_index = Some(i); + } + } + let open_slot = open_slot_index.ok_or(ErrorCode::DefaultError)?; + + let cell = self.get_mut(open_slot); + *cell = datum; + + self.sort()?; + Ok(()) + } + + #[cfg(test)] + pub fn sort(&mut self) -> DriftResult<()> { + let len = self.len(); + let mut data: Vec = Vec::with_capacity(len as usize); + for i in 0..len { + data.push(*self.get(i)); + } + data.sort_by_key(|datum| datum.constituent_index); + for i in 0..len { + let cell = self.get_mut(i); + *cell = data[i as usize]; + } + Ok(()) + } +} + +#[zero_copy] +#[derive(Debug, Default)] +#[repr(C)] +pub struct ConstituentCorrelationsFixed { + pub lp_pool: Pubkey, + pub bump: u8, + _pad: [u8; 3], + /// total elements in the flattened `data` vec + pub len: u32, +} + +impl HasLen for ConstituentCorrelationsFixed { + fn len(&self) -> u32 { + self.len + } +} + +#[account] +#[derive(Debug)] +#[repr(C)] +pub struct ConstituentCorrelations { + pub lp_pool: Pubkey, + pub bump: u8, + _padding: [u8; 3], + // PERCENTAGE_PRECISION. The weights of the target weight matrix. Updated async + pub correlations: Vec, +} + +impl HasLen for ConstituentCorrelations { + fn len(&self) -> u32 { + self.correlations.len() as u32 + } +} + +impl_zero_copy_loader!( + ConstituentCorrelations, + crate::id, + ConstituentCorrelationsFixed, + i64 +); + +impl ConstituentCorrelations { + pub fn space(num_constituents: usize) -> usize { + 8 + 40 + num_constituents * num_constituents * 8 + } + + pub fn validate(&self) -> DriftResult<()> { + let len = self.correlations.len(); + let num_constituents = (len as f32).sqrt() as usize; // f32 is plenty precise for matrix dims < 2^16 + validate!( + num_constituents * num_constituents == self.correlations.len(), + ErrorCode::DefaultError, + "ConstituentCorrelation correlations len must be a perfect square" + )?; + + for i in 0..num_constituents { + for j in 0..num_constituents { + let corr = self.correlations[i * num_constituents + j]; + validate!( + corr <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" + )?; + let corr_ji = self.correlations[j * num_constituents + i]; + validate!( + corr == corr_ji, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be symmetric" + )?; + } + let corr_ii = self.correlations[i * num_constituents + i]; + validate!( + corr_ii == PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations diagonal must be PERCENTAGE_PRECISION" + )?; + } + + Ok(()) + } + + pub fn add_new_constituent(&mut self, new_constituent_correlations: &[i64]) -> DriftResult { + // Add a new constituent at index N (where N = old size), + // given a slice `new_corrs` of length `N` such that + // new_corrs[i] == correlation[i, N]. + // + // On entry: + // self.correlations.len() == N*N + // + // After: + // self.correlations.len() == (N+1)*(N+1) + let len = self.correlations.len(); + let n = (len as f64).sqrt() as usize; + validate!( + n * n == len, + ErrorCode::DefaultError, + "existing correlations len must be a perfect square" + )?; + validate!( + new_constituent_correlations.len() == n, + ErrorCode::DefaultError, + "new_corrs length must equal number of number of other constituents ({})", + n + )?; + for &c in new_constituent_correlations { + validate!( + c <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "correlation must be ≤ PERCENTAGE_PRECISION" + )?; + } + + let new_n = n + 1; + let mut buf = Vec::with_capacity(new_n * new_n); + + for i in 0..n { + buf.extend_from_slice(&self.correlations[i * n..i * n + n]); + buf.push(new_constituent_correlations[i]); + } + + buf.extend_from_slice(new_constituent_correlations); + buf.push(PERCENTAGE_PRECISION_I64); + + self.correlations = buf; + + debug_assert_eq!(self.correlations.len(), new_n * new_n); + + Ok(()) + } + + pub fn set_correlation(&mut self, i: u16, j: u16, corr: i64) -> DriftResult { + let num_constituents = (self.correlations.len() as f64).sqrt() as usize; + validate!( + i < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index i = {}, ConstituentCorrelation len = {}", + i, + num_constituents + )?; + validate!( + j < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index j = {}, ConstituentCorrelation len = {}", + j, + num_constituents + )?; + validate!( + corr <= PERCENTAGE_PRECISION_I64, + ErrorCode::DefaultError, + "ConstituentCorrelation correlations must be between 0 and PERCENTAGE_PRECISION" + )?; + + self.correlations[i as usize * num_constituents + j as usize] = corr; + self.correlations[j as usize * num_constituents + i as usize] = corr; + + self.validate()?; + + Ok(()) + } +} + +impl<'a> AccountZeroCopy<'a, i64, ConstituentCorrelationsFixed> { + pub fn get_correlation(&self, i: u16, j: u16) -> DriftResult { + let num_constituents = (self.len() as f64).sqrt() as usize; + validate!( + i < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index i = {}, ConstituentCorrelation len = {}", + i, + num_constituents + )?; + validate!( + j < num_constituents as u16, + ErrorCode::InvalidConstituent, + "Invalid constituent_index j = {}, ConstituentCorrelation len = {}", + j, + num_constituents + )?; + + let corr = self.get((i as usize * num_constituents + j as usize) as u32); + Ok(*corr) + } +} + +pub fn get_gamma_covar_matrix( + correlation_ij: i64, + gamma_i: u8, + gamma_j: u8, + vol_i: u64, + vol_j: u64, +) -> DriftResult<[[i128; 2]; 2]> { + // Build the covariance matrix + let mut covar_matrix = [[0i128; 2]; 2]; + let scaled_vol_i = vol_i as i128; + let scaled_vol_j = vol_j as i128; + covar_matrix[0][0] = scaled_vol_i + .safe_mul(scaled_vol_i)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[1][1] = scaled_vol_j + .safe_mul(scaled_vol_j)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[0][1] = scaled_vol_i + .safe_mul(scaled_vol_j)? + .safe_mul(correlation_ij as i128)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + covar_matrix[1][0] = covar_matrix[0][1]; + + // Build the gamma matrix as a diagonal matrix + let gamma_matrix = [[gamma_i as i128, 0i128], [0i128, gamma_j as i128]]; + + // Multiply gamma_matrix with covar_matrix: product = gamma_matrix * covar_matrix + let mut product = [[0i128; 2]; 2]; + for i in 0..2 { + for j in 0..2 { + for k in 0..2 { + product[i][j] = product[i][j] + .checked_add( + gamma_matrix[i][k] + .checked_mul(covar_matrix[k][j]) + .ok_or(ErrorCode::MathError)?, + ) + .ok_or(ErrorCode::MathError)?; + } + } + } + + Ok(product) +} + +pub fn update_constituent_target_base_for_derivatives( + aum: u128, + derivative_groups: &BTreeMap>, + constituent_map: &ConstituentMap, + spot_market_map: &SpotMarketMap, + oracle_map: &mut OracleMap, + constituent_target_base: &mut AccountZeroCopyMut<'_, TargetsDatum, ConstituentTargetBaseFixed>, +) -> DriftResult<()> { + for (parent_index, constituent_indexes) in derivative_groups.iter() { + let parent_constituent = constituent_map.get_ref(parent_index)?; + + let parent_spot_market = spot_market_map.get_ref(&parent_constituent.spot_market_index)?; + let parent_oracle_price_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + parent_spot_market.market_index, + &parent_spot_market.oracle_id(), + parent_spot_market + .historical_oracle_data + .last_oracle_price_twap, + parent_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + if !is_oracle_valid_for_action( + parent_oracle_price_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Parent constituent {} oracle is invalid", + parent_constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle); + } + let parent_constituent_price = parent_oracle_price_and_validity.0.price; + + let parent_target_base = constituent_target_base + .get(*parent_index as u32) + .target_base; + let target_parent_weight = calculate_target_weight( + parent_target_base, + &*spot_market_map.get_ref(&parent_constituent.spot_market_index)?, + parent_oracle_price_and_validity.0.price, + aum, + )?; + let mut derivative_weights_sum: u64 = 0; + for constituent_index in constituent_indexes { + let constituent = constituent_map.get_ref(constituent_index)?; + let constituent_spot_market = + spot_market_map.get_ref(&constituent.spot_market_index)?; + let constituent_oracle_price_and_validity = oracle_map.get_price_data_and_validity( + MarketType::Spot, + constituent.spot_market_index, + &constituent_spot_market.oracle_id(), + constituent_spot_market + .historical_oracle_data + .last_oracle_price_twap, + constituent_spot_market.get_max_confidence_interval_multiplier()?, + 0, + 0, + None, + )?; + if !is_oracle_valid_for_action( + constituent_oracle_price_and_validity.1, + Some(DriftAction::UpdateLpPoolAum), + )? { + msg!( + "Constituent {} oracle is invalid", + constituent.constituent_index + ); + return Err(ErrorCode::InvalidOracle); + } + + if constituent_oracle_price_and_validity.0.price + < parent_constituent_price + .safe_mul(constituent.constituent_derivative_depeg_threshold as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)? + { + msg!( + "Constituent {} last oracle price {} is too low compared to parent constituent {} last oracle price {}. Assuming depegging and setting target base to 0.", + constituent.constituent_index, + constituent_oracle_price_and_validity.0.price, + parent_constituent.constituent_index, + parent_constituent_price + ); + constituent_target_base + .get_mut(*constituent_index as u32) + .target_base = 0_i64; + continue; + } + + derivative_weights_sum = + derivative_weights_sum.saturating_add(constituent.derivative_weight); + + let target_weight = (target_parent_weight as i128) + .safe_mul(constituent.derivative_weight.cast::()?)? + .safe_div(PERCENTAGE_PRECISION_I128)?; + + msg!( + "constituent: {}, target weight: {}", + constituent_index, + target_weight, + ); + let target_base = aum + .cast::()? + .safe_mul(target_weight)? + .safe_div(PERCENTAGE_PRECISION_I128)? + .safe_mul(10_i128.pow(constituent.decimals as u32))? + .safe_div(constituent_oracle_price_and_validity.0.price as i128)?; + + msg!( + "constituent: {}, target base: {}", + constituent_index, + target_base + ); + constituent_target_base + .get_mut(*constituent_index as u32) + .target_base = target_base.cast::()?; + } + + validate!( + derivative_weights_sum <= PERCENTAGE_PRECISION_U64, + ErrorCode::InvalidConstituentDerivativeWeights, + "derivative_weights_sum for parent constituent {} must be less than or equal to 100%", + parent_index + )?; + + constituent_target_base + .get_mut(*parent_index as u32) + .target_base = parent_target_base + .safe_mul(PERCENTAGE_PRECISION_U64.safe_sub(derivative_weights_sum)? as i64)? + .safe_div(PERCENTAGE_PRECISION_I64)?; + } + + Ok(()) +} diff --git a/programs/drift/src/state/lp_pool/tests.rs b/programs/drift/src/state/lp_pool/tests.rs new file mode 100644 index 0000000000..ef9153b723 --- /dev/null +++ b/programs/drift/src/state/lp_pool/tests.rs @@ -0,0 +1,3860 @@ +#[cfg(test)] +mod tests { + use crate::math::constants::{ + BASE_PRECISION_I64, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I64, QUOTE_PRECISION, + }; + use crate::state::lp_pool::*; + use std::{cell::RefCell, marker::PhantomData, vec}; + + fn amm_const_datum( + perp_market_index: u16, + constituent_index: u16, + weight: i64, + last_slot: u64, + ) -> AmmConstituentDatum { + AmmConstituentDatum { + perp_market_index, + constituent_index, + weight, + last_slot, + ..AmmConstituentDatum::default() + } + } + + #[test] + fn test_complex_implementation() { + // Constituents are BTC, SOL, ETH, USDC + + let slot = 20202020 as u64; + let amm_data = [ + amm_const_datum(0, 0, PERCENTAGE_PRECISION_I64, slot), // BTC-PERP + amm_const_datum(1, 1, PERCENTAGE_PRECISION_I64, slot), // SOL-PERP + amm_const_datum(2, 2, PERCENTAGE_PRECISION_I64, slot), // ETH-PERP + amm_const_datum(3, 0, 46 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for BTC + amm_const_datum(3, 1, 132 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for SOL + amm_const_datum(3, 2, 35 * (PERCENTAGE_PRECISION_I64 / 100), slot), // FARTCOIN-PERP for ETH + ]; + + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 6, + ..AmmConstituentMappingFixed::default() + }); + const LEN: usize = 6; + const DATA_SIZE: usize = std::mem::size_of::() * LEN; + let defaults: [AmmConstituentDatum; LEN] = [AmmConstituentDatum::default(); LEN]; + let mapping_data = RefCell::new(unsafe { + std::mem::transmute::<[AmmConstituentDatum; LEN], [u8; DATA_SIZE]>(defaults) + }); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + for amm_datum in amm_data { + println!("Adding AMM Constituent Datum: {:?}", amm_datum); + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_price: Vec = vec![ + AmmInventoryAndPricesAndSlots { + inventory: 4 * BASE_PRECISION_I64, + price: 100_000 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, + }, // $400k BTC + AmmInventoryAndPricesAndSlots { + inventory: 2000 * BASE_PRECISION_I64, + price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, + }, // $400k SOL + AmmInventoryAndPricesAndSlots { + inventory: 200 * BASE_PRECISION_I64, + price: 1500 * PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, + }, // $300k ETH + AmmInventoryAndPricesAndSlots { + inventory: 16500 * BASE_PRECISION_I64, + price: PRICE_PRECISION_I64, + last_oracle_slot: slot, + last_position_slot: slot, + }, // $16.5k FARTCOIN + ]; + let mut constituents_indexes_and_decimals_and_prices = vec![ + ConstituentIndexAndDecimalAndPrice { + constituent_index: 0, + decimals: 6, + price: 100_000 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 200 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 2, + decimals: 6, + price: 1500 * PRICE_PRECISION_I64, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 3, + decimals: 6, + price: PRICE_PRECISION_I64, + }, // USDC + ]; + let aum = 2_000_000 * QUOTE_PRECISION; // $2M AUM + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 4, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 4 * 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_price, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + + let target_weights: Vec = target_zc_mut + .iter() + .enumerate() + .map(|(index, datum)| { + calculate_target_weight( + datum.target_base.cast::().unwrap(), + &SpotMarket::default_quote_market(), + amm_inventory_and_price.get(index).unwrap().price, + aum, + ) + .unwrap() + }) + .collect(); + + println!("Target Weights: {:?}", target_weights); + assert_eq!(target_weights.len(), 4); + assert_eq!(target_weights[0], -203795); // 20.3% BTC + assert_eq!(target_weights[1], -210890); // 21.1% SOL + assert_eq!(target_weights[2], -152887); // 15.3% ETH + assert_eq!(target_weights[3], 0); // USDC not set if it's not in AUM update + } + + #[test] + fn test_single_zero_weight() { + let slot = 20202020 as u64; + let amm_datum = amm_const_datum(0, 1, 0, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000, + price: 1_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 1_000_000, + }]; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + + assert!(target_zc_mut.iter().all(|&x| x.target_base == 0)); + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).target_base, 0); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); + } + + #[test] + fn test_single_full_weight() { + let slot = 20202020 as u64; + let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let price = PRICE_PRECISION_I64; + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: BASE_PRECISION_I64, + price, + last_oracle_slot: slot, + last_position_slot: slot, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price, + }]; + let aum = 1_000_000; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Should succeed but not update the target's slot if clock slot is too recent + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + 4040404040440404404, // far future slot + ) + .unwrap(); + + let weight = calculate_target_weight( + target_zc_mut.get(0).target_base as i64, + &SpotMarket::default(), + price, + aum, + ) + .unwrap(); + + assert_eq!( + target_zc_mut.get(0).target_base as i128, + -1 * 10_i128.pow(6_u32) + ); + assert_eq!(weight, -1000000); + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, 0); + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); // still not updated + } + + #[test] + fn test_multiple_constituents_partial_weights() { + let slot = 20202020 as u64; + let amm_mapping_data = vec![ + amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64 / 2, 111), + amm_const_datum(0, 2, PERCENTAGE_PRECISION_I64 / 2, 111), + ]; + + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: amm_mapping_data.len() as u32, + ..AmmConstituentMappingFixed::default() + }); + + // 48 = size_of::() * amm_mapping_data.len() + let mapping_data = RefCell::new([0u8; 48]); + + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + for amm_datum in &amm_mapping_data { + mapping_zc_mut + .add_amm_constituent_datum(*amm_datum) + .unwrap(); + } + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000_000, + price: 1_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; + let mut constituents_indexes_and_decimals_and_prices = vec![ + ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 6, + price: 1_000_000, + }, + ConstituentIndexAndDecimalAndPrice { + constituent_index: 2, + decimals: 6, + price: 1_000_000, + }, + ]; + + let aum = 1_000_000; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: amm_mapping_data.len() as u32, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 2 * 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + + assert_eq!(target_zc_mut.len(), 2); + + for i in 0..target_zc_mut.len() { + assert_eq!( + calculate_target_weight( + target_zc_mut.get(i).target_base, + &SpotMarket::default_quote_market(), + constituents_indexes_and_decimals_and_prices + .get(i as usize) + .unwrap() + .price, + aum, + ) + .unwrap(), + -1 * PERCENTAGE_PRECISION_I64 / 2 + ); + assert_eq!(target_zc_mut.get(i).last_oracle_slot, slot); + } + } + + #[test] + fn test_zero_aum_safe() { + let slot = 20202020 as u64; + let amm_datum = amm_const_datum(0, 1, PERCENTAGE_PRECISION_I64, 0); + let mapping_fixed = RefCell::new(AmmConstituentMappingFixed { + len: 1, + ..AmmConstituentMappingFixed::default() + }); + let mapping_data = RefCell::new([0u8; 24]); + { + let mut mapping_zc_mut = + AccountZeroCopyMut::<'_, AmmConstituentDatum, AmmConstituentMappingFixed> { + fixed: mapping_fixed.borrow_mut(), + data: mapping_data.borrow_mut(), + _marker: PhantomData::, + }; + mapping_zc_mut.add_amm_constituent_datum(amm_datum).unwrap(); + } + + let mapping_zc = { + let fixed_ref = mapping_fixed.borrow(); + let data_ref = mapping_data.borrow(); + AccountZeroCopy { + fixed: fixed_ref, + data: data_ref, + _marker: PhantomData::, + } + }; + + let amm_inventory_and_prices: Vec = + vec![AmmInventoryAndPricesAndSlots { + inventory: 1_000_000, + price: 142_000_000, + last_oracle_slot: slot, + last_position_slot: slot, + }]; + let mut constituents_indexes_and_decimals_and_prices = + vec![ConstituentIndexAndDecimalAndPrice { + constituent_index: 1, + decimals: 9, + price: 142_000_000, + }]; + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 1, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 32]); + let mut target_zc_mut = AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + target_zc_mut + .update_target_base( + &mapping_zc, + &amm_inventory_and_prices, + constituents_indexes_and_decimals_and_prices.as_mut_slice(), + slot, + ) + .unwrap(); + + assert_eq!(target_zc_mut.len(), 1); + assert_eq!(target_zc_mut.get(0).target_base, -1_000_000); // despite no aum, desire to reach target + assert_eq!(target_zc_mut.get(0).last_oracle_slot, slot); + } +} + +#[cfg(test)] +mod swap_tests { + use crate::math::constants::{ + PERCENTAGE_PRECISION, PERCENTAGE_PRECISION_I64, PRICE_PRECISION_I128, PRICE_PRECISION_I64, + SPOT_BALANCE_PRECISION, + }; + use crate::state::lp_pool::*; + + #[test] + fn test_get_swap_price() { + let lp_pool = LPPool::default(); + + let in_oracle = OraclePriceData { + price: 1_000_000, + ..OraclePriceData::default() + }; + let out_oracle = OraclePriceData { + price: 233_400_000, + ..OraclePriceData::default() + }; + + // same decimals + let (price_num, price_denom) = lp_pool + .get_swap_price(6, 6, &in_oracle, &out_oracle) + .unwrap(); + assert_eq!(price_num, 1_000_000); + assert_eq!(price_denom, 233_400_000); + + let (price_num, price_denom) = lp_pool + .get_swap_price(6, 6, &out_oracle, &in_oracle) + .unwrap(); + assert_eq!(price_num, 233_400_000); + assert_eq!(price_denom, 1_000_000); + } + + fn get_swap_amount_decimals_scenario( + in_target_position_delay: u64, + out_target_position_delay: u64, + in_target_oracle_delay: u64, + out_target_oracle_delay: u64, + in_current_weight: u64, + out_current_weight: u64, + in_decimals: u32, + out_decimals: u32, + in_amount: u64, + expected_in_amount: u128, + expected_out_amount: u128, + expected_in_fee: i128, + expected_out_fee: i128, + in_xi: u8, + out_xi: u8, + in_gamma_inventory: u8, + out_gamma_inventory: u8, + in_gamma_execution: u8, + out_gamma_execution: u8, + in_volatility: u64, + out_volatility: u64, + ) { + let lp_pool = LPPool { + last_aum: 1_000_000_000_000, + target_oracle_delay_fee_bps_per_10_slots: 2, + target_position_delay_fee_bps_per_10_slots: 10, + ..LPPool::default() + }; + + let oracle_0 = OraclePriceData { + price: 1_000_000, + ..OraclePriceData::default() + }; + let oracle_1 = OraclePriceData { + price: 233_400_000, + ..OraclePriceData::default() + }; + + let in_notional = (in_current_weight as u128) * lp_pool.last_aum / PERCENTAGE_PRECISION; + let in_token_amount = in_notional * 10_u128.pow(in_decimals) / oracle_0.price as u128; + + let out_notional = (out_current_weight as u128) * lp_pool.last_aum / PERCENTAGE_PRECISION; + let out_token_amount = out_notional * 10_u128.pow(out_decimals) / oracle_1.price as u128; + + let constituent_0 = Constituent { + decimals: in_decimals as u8, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: in_gamma_execution, + gamma_inventory: in_gamma_inventory, + xi: in_xi, + volatility: in_volatility, + vault_token_balance: in_token_amount as u64, + // max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + ..Constituent::default() + }; + let constituent_1 = Constituent { + decimals: out_decimals as u8, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: out_gamma_execution, + gamma_inventory: out_gamma_inventory, + xi: out_xi, + volatility: out_volatility, + vault_token_balance: out_token_amount as u64, + // max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + ..Constituent::default() + }; + let spot_market_0 = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + let spot_market_1 = SpotMarket { + decimals: out_decimals, + ..SpotMarket::default() + }; + + let (in_amount, out_amount, in_fee, out_fee) = lp_pool + .get_swap_amount( + in_target_position_delay, + out_target_position_delay, + in_target_oracle_delay, + out_target_oracle_delay, + &oracle_0, + &oracle_1, + &constituent_0, + &constituent_1, + &spot_market_0, + &spot_market_1, + 500_000, + 500_000, + in_amount.cast::().unwrap(), + 0, + ) + .unwrap(); + assert_eq!(in_amount, expected_in_amount); + assert_eq!(out_amount, expected_out_amount); + assert_eq!(in_fee, expected_in_fee); + assert_eq!(out_fee, expected_out_fee); + } + + #[test] + fn test_get_swap_amount_in_6_out_6() { + get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, + 500_000, + 500_000, + 6, + 6, + 150_000_000_000, + 150_000_000_000, + 642577120, + 22500000, // 1 bps + 281448, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_swap_amount_in_6_out_9() { + get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, + 500_000, + 500_000, + 6, + 9, + 150_000_000_000, + 150_000_000_000, + 642577120822, + 22500000, + 282091356, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_swap_amount_in_9_out_6() { + get_swap_amount_decimals_scenario( + 0, + 0, + 0, + 0, + 500_000, + 500_000, + 9, + 6, + 150_000_000_000_000, + 150_000_000_000_000, + 642577120, + 22500000000, // 1 bps + 281448, + 1, + 2, + 1, + 2, + 1, + 2, + 0u64, + PERCENTAGE_PRECISION_U64 * 4 / 100, + ); + } + + #[test] + fn test_get_weight() { + let c = Constituent { + swap_fee_min: -1 * PERCENTAGE_PRECISION_I64 / 10000, // -1 bps (rebate) + swap_fee_max: PERCENTAGE_PRECISION_I64 / 100, // 100 bps + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, // 10% + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 500_000, + cumulative_deposits: 1_000_000, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: 500_000, + decimals: 6, + ..Constituent::default() + }; + + let spot_market = SpotMarket { + market_index: 0, + decimals: 6, + cumulative_deposit_interest: 10_000_000_000_000, + ..SpotMarket::default() + }; + + let full_balance = c.get_full_token_amount(&spot_market).unwrap(); + assert_eq!(full_balance, 1_000_000); + + // 1/10 = 10% + let weight = c + .get_weight( + 1_000_000, // $1 + &spot_market, + 0, + 10_000_000, + ) + .unwrap(); + assert_eq!(weight, 100_000); + + // (1+1)/10 = 20% + let weight = c + .get_weight(1_000_000, &spot_market, 1_000_000, 10_000_000) + .unwrap(); + assert_eq!(weight, 200_000); + + // (1-0.5)/10 = 0.5% + let weight = c + .get_weight(1_000_000, &spot_market, -500_000, 10_000_000) + .unwrap(); + assert_eq!(weight, 50_000); + } + + fn get_add_liquidity_mint_amount_scenario( + in_target_position_delay: u64, + in_target_oracle_delay: u64, + last_aum: u128, + _now: i64, + in_decimals: u32, + in_amount: u128, + dlp_total_supply: u64, + expected_lp_amount: u64, + expected_lp_fee: i64, + expected_in_fee_amount: i128, + xi: u8, + gamma_inventory: u8, + gamma_execution: u8, + volatility: u64, + ) { + let lp_pool = LPPool { + last_aum, + _padding: 0, + min_mint_fee: 0, + ..LPPool::default() + }; + + let spot_market = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + + let token_balance = if in_decimals > 6 { + last_aum.safe_mul(10_u128.pow(in_decimals - 6)).unwrap() + } else { + last_aum.safe_div(10_u128.pow(6 - in_decimals)).unwrap() + }; + + let constituent = Constituent { + decimals: in_decimals as u8, + swap_fee_min: 0, + swap_fee_max: 0, + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 0, + cumulative_deposits: 0, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: token_balance as u64, + xi, + gamma_inventory, + gamma_execution, + volatility, + ..Constituent::default() + }; + + let oracle = OraclePriceData { + price: PRICE_PRECISION_I64, // $1 + ..OraclePriceData::default() + }; + + let (lp_amount, in_amount_1, lp_fee, in_fee_amount) = lp_pool + .get_add_liquidity_mint_amount( + in_target_position_delay, + in_target_oracle_delay, + &spot_market, + &constituent, + in_amount, + &oracle, + PERCENTAGE_PRECISION_I64, // 100% target weight, to minimize fee for this test + dlp_total_supply, + ) + .unwrap(); + + assert_eq!(lp_amount, expected_lp_amount); + assert_eq!(lp_fee, expected_lp_fee); + assert_eq!(in_amount_1, in_amount); + assert_eq!(in_fee_amount, expected_in_fee_amount); + } + + // test with 6 decimal constituent (matches dlp precision) + #[test] + fn test_get_add_liquidity_mint_amount_zero_aum() { + get_add_liquidity_mint_amount_scenario( + 0, 0, 0, // last_aum + 0, // now + 6, // in_decimals + 1_000_000, // in_amount + 0, // dlp_total_supply (non-zero to avoid MathError) + 1_000_000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, 2, 2, 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum() { + get_add_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 6, // in_decimals + 1_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount + 0, // expected_lp_fee + 300, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + // test with 8 decimal constituent + #[test] + fn test_get_add_liquidity_mint_amount_with_zero_aum_8_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, + 0, + 0, // last_aum + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 0, // dlp_total_supply + 1_000_000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum_8_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount in lp decimals + 0, // expected_lp_fee + 30000, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + // test with 4 decimal constituent + #[test] + fn test_get_add_liquidity_mint_amount_with_zero_aum_4_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, 0, 0, // last_aum + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = $1 + 0, // dlp_total_supply + 1000000, // expected_lp_amount + 0, // expected_lp_fee + 0, // expected_in_fee_amount + 1, 2, 2, 0, + ); + } + + #[test] + fn test_get_add_liquidity_mint_amount_with_existing_aum_4_decimals() { + get_add_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999700, // expected_lp_amount + 0, // expected_lp_fee + 3, // expected_in_fee_amount + 1, + 2, + 2, + 0, + ); + } + + fn get_remove_liquidity_mint_amount_scenario( + out_target_position_delay: u64, + _out_target_oracle_delay: u64, + last_aum: u128, + _now: i64, + in_decimals: u32, + lp_burn_amount: u64, + dlp_total_supply: u64, + expected_out_amount: u128, + expected_lp_fee: i64, + expected_out_fee_amount: i128, + xi: u8, + gamma_inventory: u8, + gamma_execution: u8, + volatility: u64, + ) { + let lp_pool = LPPool { + last_aum, + _padding: 0, + min_mint_fee: 100, // 1 bps + ..LPPool::default() + }; + + let spot_market = SpotMarket { + decimals: in_decimals, + ..SpotMarket::default() + }; + + let token_balance = if in_decimals > 6 { + last_aum.safe_mul(10_u128.pow(in_decimals - 6)).unwrap() + } else { + last_aum.safe_div(10_u128.pow(6 - in_decimals)).unwrap() + }; + + let constituent = Constituent { + decimals: in_decimals as u8, + swap_fee_min: 0, + swap_fee_max: 0, + max_weight_deviation: PERCENTAGE_PRECISION_I64 / 10, + spot_market_index: 0, + spot_balance: ConstituentSpotBalance { + scaled_balance: 0, + cumulative_deposits: 0, + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + }, + vault_token_balance: token_balance as u64, + xi, + gamma_inventory, + gamma_execution, + volatility, + ..Constituent::default() + }; + + let oracle = OraclePriceData { + price: PRICE_PRECISION_I64, // $1 + ..OraclePriceData::default() + }; + + let (lp_amount_1, out_amount, lp_fee, out_fee_amount) = lp_pool + .get_remove_liquidity_amount( + out_target_position_delay, + out_target_position_delay, + &spot_market, + &constituent, + lp_burn_amount, + &oracle, + PERCENTAGE_PRECISION_I64, // 100% target weight, to minimize fee for this test + dlp_total_supply, + ) + .unwrap(); + + assert_eq!(lp_amount_1, lp_burn_amount); + assert_eq!(lp_fee, expected_lp_fee); + assert_eq!(out_amount, expected_out_amount); + assert_eq!(out_fee_amount, expected_out_fee_amount); + } + + // test with 6 decimal constituent (matches dlp precision) + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 6, // in_decimals + 1_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 999900, // expected_out_amount + 100, // expected_lp_fee + 299, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + // test with 8 decimal constituent + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 8, // in_decimals + 100_000_000, // in_amount (1 token) = $1 + 10_000_000_000, // dlp_total_supply + 9999000000, // expected_out_amount + 10000, // expected_lp_fee + 2999700, + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + // test with 4 decimal constituent + // there will be a problem with 4 decimal constituents with aum ~10M + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_4_decimals() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000, // last_aum ($10,000) + 0, // now + 4, // in_decimals + 10_000, // in_amount (1 token) = 1/10000 + 10_000_000_000, // dlp_total_supply + 99, // expected_out_amount + 1, // expected_lp_fee + 0, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_5_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 5, // in_decimals + 100_000_000_000 * 100_000, // in_amount + 100_000_000_000 * 1_000_000, // dlp_total_supply + 999900000000000, // expected_out_amount + 1000000000000, // expected_lp_fee + 473952600000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_6_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 100_000_000_000 * 1_000_000, // last_aum ($100,000,000,000) + 0, // now + 6, // in_decimals + 100_000_000_000 * 1_000_000 - 1_000_000, // Leave in QUOTE AMOUNT + 100_000_000_000 * 1_000_000, // dlp_total_supply + 99989999900000000, // expected_out_amount + 9999999999900, // expected_lp_fee + 349765019650200, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + #[test] + fn test_get_remove_liquidity_mint_amount_with_existing_aum_8_decimals_large_aum() { + get_remove_liquidity_mint_amount_scenario( + 0, + 0, + 10_000_000_000_000_000, // last_aum ($10,000,000,000) + 0, // now + 8, // in_decimals + 10_000_000_000 * 1_000_000 - 1_000_000, // in_amount + 10_000_000_000 * 1_000_000, // dlp_total_supply + 999899999000000000, // expected_out_amount + (10_000_000_000 * 1_000_000 - 1_000_000) / 10000, // expected_lp_fee + 3497650196502000, // expected_out_fee_amount + 1, + 2, + 2, + PERCENTAGE_PRECISION_U64 * 4 / 100, // volatility + ); + } + + fn round_to_sig(x: i128, sig: u32) -> i128 { + if x == 0 { + return 0; + } + let digits = (x.abs() as f64).log10().floor() as u32 + 1; + let factor = 10_i128.pow(digits - sig); + ((x + factor / 2) / factor) * factor + } + + fn get_swap_amounts( + in_target_position_delay: u64, + out_target_position_delay: u64, + in_target_oracle_delay: u64, + out_target_oracle_delay: u64, + in_oracle_price: i64, + out_oracle_price: i64, + in_current_weight: i64, + out_current_weight: i64, + in_amount: u64, + in_volatility: u64, + out_volatility: u64, + in_target_weight: i64, + out_target_weight: i64, + ) -> (u128, u128, i128, i128, i128, i128) { + let lp_pool = LPPool { + last_aum: 1_000_000_000_000, + ..LPPool::default() + }; + + let oracle_0 = OraclePriceData { + price: in_oracle_price, + ..OraclePriceData::default() + }; + let oracle_1 = OraclePriceData { + price: out_oracle_price, + ..OraclePriceData::default() + }; + + let in_notional = (in_current_weight as i128) * lp_pool.last_aum.cast::().unwrap() + / PERCENTAGE_PRECISION_I128; + let in_token_amount = in_notional * 10_i128.pow(6) / oracle_0.price as i128; + let in_spot_balance = if in_token_amount > 0 { + ConstituentSpotBalance { + scaled_balance: (in_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + } else { + ConstituentSpotBalance { + scaled_balance: (in_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Borrow, + market_index: 0, + ..ConstituentSpotBalance::default() + } + }; + + let out_notional = (out_current_weight as i128) * lp_pool.last_aum.cast::().unwrap() + / PERCENTAGE_PRECISION_I128; + let out_token_amount = out_notional * 10_i128.pow(6) / oracle_1.price as i128; + let out_spot_balance = if out_token_amount > 0 { + ConstituentSpotBalance { + scaled_balance: (out_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + } else { + ConstituentSpotBalance { + scaled_balance: (out_token_amount.abs() as u128) + * (SPOT_BALANCE_PRECISION / 10_u128.pow(6)), + balance_type: SpotBalanceType::Deposit, + market_index: 0, + ..ConstituentSpotBalance::default() + } + }; + + let constituent_0 = Constituent { + decimals: 6, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: 1, + gamma_inventory: 1, + xi: 1, + volatility: in_volatility, + spot_balance: in_spot_balance, + ..Constituent::default() + }; + let constituent_1 = Constituent { + decimals: 6, + swap_fee_min: PERCENTAGE_PRECISION_I64 / 10000, + swap_fee_max: PERCENTAGE_PRECISION_I64 / 1000, + gamma_execution: 2, + gamma_inventory: 2, + xi: 2, + volatility: out_volatility, + spot_balance: out_spot_balance, + ..Constituent::default() + }; + let spot_market_0 = SpotMarket { + decimals: 6, + ..SpotMarket::default() + }; + let spot_market_1 = SpotMarket { + decimals: 6, + ..SpotMarket::default() + }; + + let (in_amount_result, out_amount, in_fee, out_fee) = lp_pool + .get_swap_amount( + in_target_position_delay, + out_target_position_delay, + in_target_oracle_delay, + out_target_oracle_delay, + &oracle_0, + &oracle_1, + &constituent_0, + &constituent_1, + &spot_market_0, + &spot_market_1, + in_target_weight, + out_target_weight, + in_amount.cast::().unwrap(), + 0, + ) + .unwrap(); + + return ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount, + out_token_amount, + ); + } + + #[test] + fn grid_search_swap() { + let weights: [i64; 20] = [ + -100_000, -200_000, -300_000, -400_000, -500_000, -600_000, -700_000, -800_000, + -900_000, -1_000_000, 100_000, 200_000, 300_000, 400_000, 500_000, 600_000, 700_000, + 800_000, 900_000, 1_000_000, + ]; + let in_amounts: Vec = (0..=10) + .map(|i| (1000 + i * 20000) * 10_u64.pow(6)) + .collect(); + + let volatilities: Vec = (1..=10) + .map(|i| PERCENTAGE_PRECISION_U64 * i / 100) + .collect(); + + let in_oracle_price = PRICE_PRECISION_I64; // $1 + let out_oracle_price = 233_400_000; // $233.4 + + // Assert monotonically increasing fees in in_amounts + for in_current_weight in weights.iter() { + let out_current_weight = 1_000_000 - *in_current_weight; + for out_volatility in volatilities.iter() { + let mut prev_in_fee_bps = 0_i128; + let mut prev_out_fee_bps = 0_i128; + for in_amount in in_amounts.iter() { + let (in_amount_result, out_amount, in_fee, out_fee, _, _) = get_swap_amounts( + 0, + 0, + 0, + 0, + in_oracle_price, + out_oracle_price, + *in_current_weight, + out_current_weight, + *in_amount, + 0, + *out_volatility, + PERCENTAGE_PRECISION_I64, // 100% target weight + PERCENTAGE_PRECISION_I64, // 100% target weight + ); + + // Calculate fee in basis points with precision + let in_fee_bps = if in_amount_result > 0 { + (in_fee * 10_000 * 1_000_000) / in_amount_result as i128 + } else { + 0 + }; + + let out_fee_bps = if out_amount > 0 { + (out_fee * 10_000 * 1_000_000) / out_amount as i128 + } else { + 0 + }; + + // Assert monotonically increasing fees + if in_amounts.iter().position(|&x| x == *in_amount).unwrap() > 0 { + assert!( + in_fee_bps >= prev_in_fee_bps, + "in_fee should be monotonically increasing. Current: {} bps, Previous: {} bps, weight: {}, amount: {}, volatility: {}", + in_fee_bps as f64 / 1_000_000.0, + prev_in_fee_bps as f64 / 1_000_000.0, + in_current_weight, + in_amount, + out_volatility + ); + assert!( + out_fee_bps >= prev_out_fee_bps, + "out_fee should be monotonically increasing. Current: {} bps, Previous: {} bps, weight: {}, amount: {}, volatility: {}", + out_fee_bps as f64 / 1_000_000.0, + prev_out_fee_bps as f64 / 1_000_000.0, + out_current_weight, + in_amount, + out_volatility + ); + } + + println!( + "in_weight: {}, out_weight: {}, in_amount: {}, out_amount: {}, in_fee: {:.6} bps, out_fee: {:.6} bps", + in_current_weight, + out_current_weight, + in_amount_result, + out_amount, + in_fee_bps as f64 / 1_000_000.0, + out_fee_bps as f64 / 1_000_000.0 + ); + + prev_in_fee_bps = in_fee_bps; + prev_out_fee_bps = out_fee_bps; + } + } + } + + // Assert monotonically increasing fees based on error improvement + for in_amount in in_amounts.iter() { + for in_current_weight in weights.iter() { + let out_current_weight = 1_000_000 - *in_current_weight; + let fixed_volatility = PERCENTAGE_PRECISION_U64 * 5 / 100; + let target_weights: Vec = (1..=20).map(|i| i * 50_000).collect(); + + let mut results: Vec<(i128, i128, i128, i128, i128, i128)> = Vec::new(); + + for target_weight in target_weights.iter() { + let in_target_weight = *target_weight; + let out_target_weight = 1_000_000 - in_target_weight; + + let ( + in_amount_result, + out_amount, + in_fee, + out_fee, + in_token_amount_pre, + out_token_amount_pre, + ) = get_swap_amounts( + 0, + 0, + 0, + 0, + in_oracle_price, + out_oracle_price, + *in_current_weight, + out_current_weight, + *in_amount, + fixed_volatility, + fixed_volatility, + in_target_weight, + out_target_weight, + ); + + // Calculate weights after swap + + let out_token_after = out_token_amount_pre - out_amount as i128 + out_fee; + let in_token_after = in_token_amount_pre + in_amount_result as i128; + + let out_notional_after = + out_token_after * (out_oracle_price as i128) / PRICE_PRECISION_I128; + let in_notional_after = + in_token_after * (in_oracle_price as i128) / PRICE_PRECISION_I128; + let total_notional_after = in_notional_after + out_notional_after; + + let out_weight_after = + (out_notional_after * PERCENTAGE_PRECISION_I128) / (total_notional_after); + let in_weight_after = + (in_notional_after * PERCENTAGE_PRECISION_I128) / (total_notional_after); + + // Calculate error improvement (positive means improvement) + let in_error_before = (*in_current_weight - in_target_weight).abs() as i128; + let out_error_before = (out_current_weight - out_target_weight).abs() as i128; + + let in_error_after = (in_weight_after - in_target_weight as i128).abs(); + let out_error_after = (out_weight_after - out_target_weight as i128).abs(); + + let in_error_improvement = round_to_sig(in_error_before - in_error_after, 2); + let out_error_improvement = round_to_sig(out_error_before - out_error_after, 2); + + let in_fee_bps = if in_amount_result > 0 { + (in_fee * 10_000 * 1_000_000) / in_amount_result as i128 + } else { + 0 + }; + + let out_fee_bps = if out_amount > 0 { + (out_fee * 10_000 * 1_000_000) / out_amount as i128 + } else { + 0 + }; + + results.push(( + in_error_improvement, + out_error_improvement, + in_fee_bps, + out_fee_bps, + in_target_weight as i128, + out_target_weight as i128, + )); + + println!( + "in_weight: {}, out_weight: {}, in_target: {}, out_target: {}, in_error_improvement: {}, out_error_improvement: {}, in_fee: {:.6} bps, out_fee: {:.6} bps", + in_current_weight, + out_current_weight, + in_target_weight, + out_target_weight, + in_error_improvement, + out_error_improvement, + in_fee_bps as f64 / 1_000_000.0, + out_fee_bps as f64 / 1_000_000.0 + ); + } + + // Sort by in_error_improvement and check monotonicity + results.sort_by_key(|&(in_error_improvement, _, _, _, _, _)| -in_error_improvement); + + for i in 1..results.len() { + let (prev_in_improvement, _, prev_in_fee_bps, _, _, _) = results[i - 1]; + let (curr_in_improvement, _, curr_in_fee_bps, _, in_target, _) = results[i]; + + // Less improvement should mean higher fees + if curr_in_improvement < prev_in_improvement { + assert!( + curr_in_fee_bps >= prev_in_fee_bps, + "in_fee should increase as error improvement decreases. Current improvement: {}, Previous improvement: {}, Current fee: {:.6} bps, Previous fee: {:.6} bps, in_weight: {}, in_target: {}", + curr_in_improvement, + prev_in_improvement, + curr_in_fee_bps as f64 / 1_000_000.0, + prev_in_fee_bps as f64 / 1_000_000.0, + in_current_weight, + in_target + ); + } + } + + // Sort by out_error_improvement and check monotonicity + results + .sort_by_key(|&(_, out_error_improvement, _, _, _, _)| -out_error_improvement); + + for i in 1..results.len() { + let (_, prev_out_improvement, _, prev_out_fee_bps, _, _) = results[i - 1]; + let (_, curr_out_improvement, _, curr_out_fee_bps, _, out_target) = results[i]; + + // Less improvement should mean higher fees + if curr_out_improvement < prev_out_improvement { + assert!( + curr_out_fee_bps >= prev_out_fee_bps, + "out_fee should increase as error improvement decreases. Current improvement: {}, Previous improvement: {}, Current fee: {:.6} bps, Previous fee: {:.6} bps, out_weight: {}, out_target: {}", + curr_out_improvement, + prev_out_improvement, + curr_out_fee_bps as f64 / 1_000_000.0, + prev_out_fee_bps as f64 / 1_000_000.0, + out_current_weight, + out_target + ); + } + } + } + } + } +} + +#[cfg(test)] +mod swap_fee_tests { + use crate::math::constants::{ + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, QUOTE_PRECISION, + }; + use crate::state::lp_pool::*; + + #[test] + fn test_get_gamma_covar_matrix() { + // in = sol, out = btc + let covar_matrix = get_gamma_covar_matrix( + PERCENTAGE_PRECISION_I64, + 2, // gamma sol + 2, // gamma btc + 4 * PERCENTAGE_PRECISION_U64 / 100, // vol sol + 3 * PERCENTAGE_PRECISION_U64 / 100, // vol btc + ) + .unwrap(); + assert_eq!(covar_matrix, [[3200, 2400], [2400, 1800]]); + } + + #[test] + fn test_lp_pool_get_linear_fee_execution() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let trade_ratio = 5_000_000 * QUOTE_PRECISION_I128 * PERCENTAGE_PRECISION_I128 + / (15_000_000 * QUOTE_PRECISION_I128); + + let fee_execution_linear = lp_pool + .get_linear_fee_execution( + trade_ratio, + 1600, // 0.0016 + 2, + ) + .unwrap(); + + assert_eq!(fee_execution_linear, 1066); // 10.667 bps + } + + #[test] + fn test_lp_pool_get_quadratic_fee_execution() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let trade_ratio = 5_000_000 * QUOTE_PRECISION_I128 * PERCENTAGE_PRECISION_I128 + / (15_000_000 * QUOTE_PRECISION_I128); + + let fee_execution_quadratic = lp_pool + .get_quadratic_fee_execution( + trade_ratio, + 1600, // 0.0016 + 2, + ) + .unwrap(); + + assert_eq!(fee_execution_quadratic, 711); // 7.1 bps + } + + #[test] + fn test_lp_pool_get_quadratic_fee_inventory() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + ..LPPool::default() + }; + + let (fee_in, fee_out) = lp_pool + .get_quadratic_fee_inventory( + [[3200, 2400], [2400, 1800]], + [ + 1_000_000 * QUOTE_PRECISION_I128, + -500_000 * QUOTE_PRECISION_I128, + ], + [ + -4_000_000 * QUOTE_PRECISION_I128, + 4_500_000 * QUOTE_PRECISION_I128, + ], + 5_000_000 * QUOTE_PRECISION, + ) + .unwrap(); + + assert_eq!(fee_in, 6 * PERCENTAGE_PRECISION_I128 / 100000); // 0.6 bps + assert_eq!(fee_out, -6 * PERCENTAGE_PRECISION_I128 / 100000); // -0.6 bps + } + + #[test] + fn test_target_delays() { + let lp_pool = LPPool { + last_aum: 10_000_000 * QUOTE_PRECISION, // $10,000,000 + target_oracle_delay_fee_bps_per_10_slots: 2, + target_position_delay_fee_bps_per_10_slots: 10, + ..LPPool::default() + }; + + // Even a small delay in the position incurs a larger fee + let uncertainty_fee = lp_pool.get_target_uncertainty_fees(1, 0).unwrap(); + assert_eq!( + uncertainty_fee, + PERCENTAGE_PRECISION_I128 / 10000i128 + * lp_pool.target_position_delay_fee_bps_per_10_slots as i128 + ); + } +} + +#[cfg(test)] +mod settle_tests { + use crate::math::constants::{QUOTE_PRECISION, QUOTE_PRECISION_I64, QUOTE_PRECISION_U64}; + use crate::math::lp_pool::perp_lp_pool_settlement::{ + calculate_settlement_amount, update_cache_info, SettlementContext, SettlementDirection, + SettlementResult, + }; + use crate::state::amm_cache::CacheInfo; + use crate::state::spot_market::SpotMarket; + + fn create_mock_spot_market() -> SpotMarket { + SpotMarket::default() + } + + #[test] + fn test_calculate_settlement_no_amount_owed() { + let ctx = SettlementContext { + quote_owed_from_lp: 0, + quote_constituent_token_balance: 1000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::None); + assert_eq!(result.amount_transferred, 0); + } + + #[test] + fn test_lp_to_perp_settlement_sufficient_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 500 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 300 * QUOTE_PRECISION, + pnl_pool_balance: 200 * QUOTE_PRECISION, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 500 * QUOTE_PRECISION_U64); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_lp_to_perp_settlement_insufficient_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 1500 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 300 * QUOTE_PRECISION, + pnl_pool_balance: 200 * QUOTE_PRECISION, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!( + result.amount_transferred, + 1000 * QUOTE_PRECISION_U64 - QUOTE_PRECISION_U64 + ); // Limited by LP balance + } + + #[test] + fn test_lp_to_perp_settlement_no_lp_balance() { + let ctx = SettlementContext { + quote_owed_from_lp: 500, + quote_constituent_token_balance: 0, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::None); + assert_eq!(result.amount_transferred, 0); + } + + #[test] + fn test_perp_to_lp_settlement_fee_pool_sufficient() { + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 800, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_perp_to_lp_settlement_needs_both_pools() { + let ctx = SettlementContext { + quote_owed_from_lp: -1000, + quote_constituent_token_balance: 2000, + fee_pool_balance: 300, + pnl_pool_balance: 800, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 700); + } + + #[test] + fn test_perp_to_lp_settlement_insufficient_pools() { + let ctx = SettlementContext { + quote_owed_from_lp: -1500, + quote_constituent_token_balance: 2000, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 500); // Limited by pool balances + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 200); + } + + #[test] + fn test_settlement_edge_cases() { + // Test with zero fee pool + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 0, + pnl_pool_balance: 800, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.amount_transferred, 500); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 500); + + // Test with zero pnl pool + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 1000, + fee_pool_balance: 300, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.amount_transferred, 300); + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_update_cache_info_to_lp_pool() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: -400, + last_fee_pool_token_amount: 2_000, + last_net_pnl_pool_token_amount: 500, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 200, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 120, + pnl_pool_used: 80, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool + result.amount_transferred as i64; + let ts = 99; + let slot = 100; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed updated + assert_eq!(cache.quote_owed_from_lp_pool, new_quote_owed); + // settle fields updated + assert_eq!(cache.last_settle_amount, 200); + assert_eq!(cache.last_settle_slot, slot); + // fee pool decreases by fee_pool_used + assert_eq!(cache.last_fee_pool_token_amount, 2_000 - 120); + // pnl pool decreases by pnl_pool_used + assert_eq!(cache.last_net_pnl_pool_token_amount, 500 - 80); + } + + #[test] + fn test_update_cache_info_from_lp_pool() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 500, + last_fee_pool_token_amount: 1_000, + last_net_pnl_pool_token_amount: 200, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 150, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool - result.amount_transferred as i64; + let ts = 42; + let slot = 100; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed updated + assert_eq!(cache.quote_owed_from_lp_pool, new_quote_owed); + // settle fields updated + assert_eq!(cache.last_settle_amount, 150); + assert_eq!(cache.last_settle_slot, slot); + // fee pool increases by amount_transferred + assert_eq!(cache.last_fee_pool_token_amount, 1_000 + 150); + // pnl pool untouched + assert_eq!(cache.last_net_pnl_pool_token_amount, 200); + } + + #[test] + fn test_large_settlement_amounts() { + // Test with very large amounts to check for overflow + let ctx = SettlementContext { + quote_owed_from_lp: i64::MAX / 2, + quote_constituent_token_balance: u64::MAX / 2, + fee_pool_balance: u128::MAX / 4, + pnl_pool_balance: u128::MAX / 4, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert!(result.amount_transferred > 0); + } + + #[test] + fn test_negative_large_settlement_amounts() { + let ctx = SettlementContext { + quote_owed_from_lp: i64::MIN / 2, + quote_constituent_token_balance: u64::MAX / 2, + fee_pool_balance: u128::MAX / 4, + pnl_pool_balance: u128::MAX / 4, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert!(result.amount_transferred > 0); + } + + #[test] + fn test_exact_boundary_settlements() { + // Test when quote_owed exactly equals LP balance + let ctx = SettlementContext { + quote_owed_from_lp: 1000 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 1000 * QUOTE_PRECISION_U64, + fee_pool_balance: 500 * QUOTE_PRECISION, + pnl_pool_balance: 300 * QUOTE_PRECISION, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000 * QUOTE_PRECISION_U64, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!( + result.amount_transferred, + 1000 * QUOTE_PRECISION_U64 - QUOTE_PRECISION_U64 + ); // Leave QUOTE PRECISION + + // Test when negative quote_owed exactly equals total pool balance + let ctx = SettlementContext { + quote_owed_from_lp: -800, + quote_constituent_token_balance: 2000, + fee_pool_balance: 500, + pnl_pool_balance: 300, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 300); + } + + #[test] + fn test_minimal_settlement_amounts() { + // Test with minimal positive amount + let ctx = SettlementContext { + quote_owed_from_lp: 1, + quote_constituent_token_balance: 1, + fee_pool_balance: 1, + pnl_pool_balance: 1, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 0); // Cannot transfer if less than QUOTE_PRECISION + + // Test with minimal negative amount + let ctx = SettlementContext { + quote_owed_from_lp: -1, + quote_constituent_token_balance: 1, + fee_pool_balance: 1, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1); + assert_eq!(result.fee_pool_used, 1); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_all_zero_balances() { + let ctx = SettlementContext { + quote_owed_from_lp: -500, + quote_constituent_token_balance: 0, + fee_pool_balance: 0, + pnl_pool_balance: 0, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 0); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_cache_info_update_none_direction() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 100, + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: 500, + last_settle_amount: 50, + last_settle_slot: 12345, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 0, + direction: SettlementDirection::None, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = 100; // No change + let ts = 67890; + let slot = 100000000; + + update_cache_info(&mut cache, &result, new_quote_owed, slot, ts).unwrap(); + + // quote_owed unchanged + assert_eq!(cache.quote_owed_from_lp_pool, 100); + // settle fields updated with new timestamp but zero amount + assert_eq!(cache.last_settle_amount, 0); + assert_eq!(cache.last_settle_slot, slot); + // pool amounts unchanged + assert_eq!(cache.last_fee_pool_token_amount, 1000); + assert_eq!(cache.last_net_pnl_pool_token_amount, 500); + } + + #[test] + fn test_cache_info_update_maximum_values() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: i64::MAX / 2, + last_fee_pool_token_amount: u128::MAX / 2, + last_net_pnl_pool_token_amount: i128::MAX / 2, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: u64::MAX / 4, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool - (result.amount_transferred as i64); + let slot = u64::MAX / 2; + let ts = i64::MAX / 2; + + let update_result = update_cache_info(&mut cache, &result, new_quote_owed, slot, ts); + assert!(update_result.is_ok()); + } + + #[test] + fn test_cache_info_update_minimum_values() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: i64::MIN / 2, + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: i128::MIN / 2, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + let result = SettlementResult { + amount_transferred: 500, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 200, + pnl_pool_used: 300, + }; + let new_quote_owed = cache.quote_owed_from_lp_pool + (result.amount_transferred as i64); + let slot = u64::MAX / 2; + let ts = 42; + + let update_result = update_cache_info(&mut cache, &result, new_quote_owed, slot, ts); + assert!(update_result.is_ok()); + } + + #[test] + fn test_sequential_settlement_updates() { + let mut cache = CacheInfo { + quote_owed_from_lp_pool: 1000, + last_fee_pool_token_amount: 5000, + last_net_pnl_pool_token_amount: 3000, + last_settle_amount: 0, + last_settle_slot: 0, + ..Default::default() + }; + + // First settlement: From LP pool + let result1 = SettlementResult { + amount_transferred: 300, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + let new_quote_owed1 = cache.quote_owed_from_lp_pool - (result1.amount_transferred as i64); + update_cache_info(&mut cache, &result1, new_quote_owed1, 101010101, 100).unwrap(); + + assert_eq!(cache.quote_owed_from_lp_pool, 700); + assert_eq!(cache.last_fee_pool_token_amount, 5300); + assert_eq!(cache.last_net_pnl_pool_token_amount, 3000); + + // Second settlement: To LP pool + let result2 = SettlementResult { + amount_transferred: 400, + direction: SettlementDirection::ToLpPool, + fee_pool_used: 250, + pnl_pool_used: 150, + }; + let new_quote_owed2 = cache.quote_owed_from_lp_pool + (result2.amount_transferred as i64); + update_cache_info(&mut cache, &result2, new_quote_owed2, 10101010, 200).unwrap(); + + assert_eq!(cache.quote_owed_from_lp_pool, 1100); + assert_eq!(cache.last_fee_pool_token_amount, 5050); + assert_eq!(cache.last_net_pnl_pool_token_amount, 2850); + assert_eq!(cache.last_settle_slot, 10101010); + } + + #[test] + fn test_perp_to_lp_with_only_pnl_pool() { + let ctx = SettlementContext { + quote_owed_from_lp: -1000, + quote_constituent_token_balance: 2000, + fee_pool_balance: 0, // No fee pool + pnl_pool_balance: 1200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 1000); + } + + #[test] + fn test_perp_to_lp_capped_with_max() { + let ctx = SettlementContext { + quote_owed_from_lp: -1100, + quote_constituent_token_balance: 2000, + fee_pool_balance: 500, // No fee pool + pnl_pool_balance: 700, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 1000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 1000); + assert_eq!(result.fee_pool_used, 500); + assert_eq!(result.pnl_pool_used, 500); + } + + #[test] + fn test_lp_to_perp_capped_with_max() { + let ctx = SettlementContext { + quote_owed_from_lp: 1100 * QUOTE_PRECISION_I64, + quote_constituent_token_balance: 2000 * QUOTE_PRECISION_U64, + fee_pool_balance: 0, // No fee pool + pnl_pool_balance: 1200 * QUOTE_PRECISION, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 1000 * QUOTE_PRECISION_U64, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::FromLpPool); + assert_eq!(result.amount_transferred, 1000 * QUOTE_PRECISION_U64); // Leave QUOTE PRECISION in the balance + assert_eq!(result.fee_pool_used, 0); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_perp_to_lp_with_only_fee_pool() { + let ctx = SettlementContext { + quote_owed_from_lp: -800, + quote_constituent_token_balance: 1500, + fee_pool_balance: 1000, + pnl_pool_balance: 0, // No PnL pool + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); + assert_eq!(result.fee_pool_used, 800); + assert_eq!(result.pnl_pool_used, 0); + } + + #[test] + fn test_fractional_settlement_coverage() { + // Test when pools can only partially cover the needed amount + let ctx = SettlementContext { + quote_owed_from_lp: -2000, + quote_constituent_token_balance: 5000, + fee_pool_balance: 300, + pnl_pool_balance: 500, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert_eq!(result.direction, SettlementDirection::ToLpPool); + assert_eq!(result.amount_transferred, 800); // Only what pools can provide + assert_eq!(result.fee_pool_used, 300); + assert_eq!(result.pnl_pool_used, 500); + } + + #[test] + fn test_settlement_direction_consistency() { + // Positive quote_owed should always result in FromLpPool or None + for quote_owed in [1, 100, 1000, 10000] { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: 500, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert!( + result.direction == SettlementDirection::FromLpPool + || result.direction == SettlementDirection::None + ); + } + + // Negative quote_owed should always result in ToLpPool or None + for quote_owed in [-1, -100, -1000, -10000] { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: 500, + fee_pool_balance: 300, + pnl_pool_balance: 200, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + assert!( + result.direction == SettlementDirection::ToLpPool + || result.direction == SettlementDirection::None + ); + } + } + + #[test] + fn test_cache_info_timestamp_progression() { + let mut cache = CacheInfo::default(); + + let timestamps = [1000, 2000, 3000, 1500, 5000]; // Including out-of-order + + for (_, &ts) in timestamps.iter().enumerate() { + let result = SettlementResult { + amount_transferred: 100, + direction: SettlementDirection::FromLpPool, + fee_pool_used: 0, + pnl_pool_used: 0, + }; + + update_cache_info(&mut cache, &result, 0, 1010101, ts).unwrap(); + assert_eq!(cache.last_settle_ts, ts); + assert_eq!(cache.last_settle_amount, 100); + } + } + + #[test] + fn test_settlement_amount_conservation() { + // Test that fee_pool_used + pnl_pool_used = amount_transferred for ToLpPool + let test_cases = [ + (-500, 1000, 300, 400), // Normal case + (-1000, 2000, 600, 500), // Uses both pools + (-200, 500, 0, 300), // Only PnL pool + (-150, 400, 200, 0), // Only fee pool + ]; + + for (quote_owed, lp_balance, fee_pool, pnl_pool) in test_cases { + let ctx = SettlementContext { + quote_owed_from_lp: quote_owed, + quote_constituent_token_balance: lp_balance, + fee_pool_balance: fee_pool, + pnl_pool_balance: pnl_pool, + quote_market: &create_mock_spot_market(), + max_settle_quote_amount: 10000, + }; + + let result = calculate_settlement_amount(&ctx).unwrap(); + + if result.direction == SettlementDirection::ToLpPool { + assert_eq!( + result.amount_transferred as u128, + result.fee_pool_used + result.pnl_pool_used, + "Amount transferred should equal sum of pool usage for case: {:?}", + (quote_owed, lp_balance, fee_pool, pnl_pool) + ); + } + } + } + + #[test] + fn test_cache_pool_balance_tracking() { + let mut cache = CacheInfo { + last_fee_pool_token_amount: 1000, + last_net_pnl_pool_token_amount: 500, + ..Default::default() + }; + + // Multiple settlements that should maintain balance consistency + let settlements = [ + (SettlementDirection::ToLpPool, 200, 120, 80), // Uses both pools + (SettlementDirection::FromLpPool, 150, 0, 0), // Adds to fee pool + (SettlementDirection::ToLpPool, 100, 100, 0), // Uses only fee pool + (SettlementDirection::ToLpPool, 50, 30, 20), // Uses both pools again + ]; + + let mut expected_fee_pool = cache.last_fee_pool_token_amount; + let mut expected_pnl_pool = cache.last_net_pnl_pool_token_amount; + + for (direction, amount, fee_used, pnl_used) in settlements { + let result = SettlementResult { + amount_transferred: amount, + direction, + fee_pool_used: fee_used, + pnl_pool_used: pnl_used, + }; + + match direction { + SettlementDirection::FromLpPool => { + expected_fee_pool += amount as u128; + } + SettlementDirection::ToLpPool => { + expected_fee_pool -= fee_used; + expected_pnl_pool -= pnl_used as i128; + } + SettlementDirection::None => {} + } + + update_cache_info(&mut cache, &result, 0, 1000, 0).unwrap(); + + assert_eq!(cache.last_fee_pool_token_amount, expected_fee_pool); + assert_eq!(cache.last_net_pnl_pool_token_amount, expected_pnl_pool); + } + } +} + +#[cfg(test)] +mod update_aum_tests { + use crate::{ + create_anchor_account_info, + math::constants::SPOT_CUMULATIVE_INTEREST_PRECISION, + math::constants::{PRICE_PRECISION_I64, QUOTE_PRECISION}, + state::amm_cache::{AmmCacheFixed, CacheInfo}, + state::lp_pool::*, + state::oracle::HistoricalOracleData, + state::oracle::OracleSource, + state::oracle_map::OracleMap, + state::pyth_lazer_oracle::PythLazerOracle, + state::spot_market::SpotMarket, + state::spot_market_map::SpotMarketMap, + state::zero_copy::AccountZeroCopyMut, + test_utils::{create_account_info, get_anchor_account_bytes}, + }; + use anchor_lang::prelude::Pubkey; + use std::{cell::RefCell, marker::PhantomData}; + + fn test_aum_with_balances( + usdc_balance: u64, // USDC balance in tokens (6 decimals) + sol_balance: u64, // SOL balance in tokens (9 decimals) + btc_balance: u64, // BTC balance in tokens (8 decimals) + bonk_balance: u64, // BONK balance in tokens (5 decimals) + expected_aum_usd: u64, + test_name: &str, + ) { + let mut lp_pool = LPPool::default(); + lp_pool.constituents = 4; + lp_pool.quote_consituent_index = 0; + + // Create constituents with specified token balances + let mut constituent_usdc = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 0, + constituent_index: 0, + last_oracle_price: PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 6, + vault_token_balance: usdc_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_usdc, Constituent, constituent_usdc_account_info); + + let mut constituent_sol = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 1, + constituent_index: 1, + last_oracle_price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + vault_token_balance: sol_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_sol, Constituent, constituent_sol_account_info); + + let mut constituent_btc = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 2, + constituent_index: 2, + last_oracle_price: 100_000 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 8, + vault_token_balance: btc_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_btc, Constituent, constituent_btc_account_info); + + let mut constituent_bonk = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: 3, + constituent_index: 3, + last_oracle_price: 22, // $0.000022 in PRICE_PRECISION_I64 + last_oracle_slot: 100, + decimals: 5, + vault_token_balance: bonk_balance, + oracle_staleness_threshold: 10, + ..Constituent::default() + }; + create_anchor_account_info!(constituent_bonk, Constituent, constituent_bonk_account_info); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &constituent_usdc_account_info, + &constituent_sol_account_info, + &constituent_btc_account_info, + &constituent_bonk_account_info, + ], + true, + ) + .unwrap(); + + // Create simple PythLazer oracle accounts for non-quote assets with prices matching constituents + // Use exponent -6 so values are already in PRICE_PRECISION units + let sol_oracle_pubkey = Pubkey::new_unique(); + let mut sol_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + sol_oracle, + &sol_oracle_pubkey, + PythLazerOracle, + sol_oracle_account_info + ); + + let btc_oracle_pubkey = Pubkey::new_unique(); + let mut btc_oracle = PythLazerOracle { + price: 100_000 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + btc_oracle, + &btc_oracle_pubkey, + PythLazerOracle, + btc_oracle_account_info + ); + + let bonk_oracle_pubkey = Pubkey::new_unique(); + let mut bonk_oracle = PythLazerOracle { + price: 22, // $0.000022 in PRICE_PRECISION + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + bonk_oracle, + &bonk_oracle_pubkey, + PythLazerOracle, + bonk_oracle_account_info + ); + + // Create spot markets + let mut usdc_spot_market = SpotMarket { + market_index: 0, + oracle_source: OracleSource::QuoteAsset, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 6, + historical_oracle_data: HistoricalOracleData::default_quote_oracle(), + ..SpotMarket::default() + }; + create_anchor_account_info!(usdc_spot_market, SpotMarket, usdc_spot_market_account_info); + + let mut sol_spot_market = SpotMarket { + market_index: 1, + oracle_source: OracleSource::PythLazer, + oracle: sol_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(200 * PRICE_PRECISION_I64), + ..SpotMarket::default() + }; + create_anchor_account_info!(sol_spot_market, SpotMarket, sol_spot_market_account_info); + + let mut btc_spot_market = SpotMarket { + market_index: 2, + oracle_source: OracleSource::PythLazer, + oracle: btc_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 8, + historical_oracle_data: HistoricalOracleData::default_price( + 100_000 * PRICE_PRECISION_I64, + ), + ..SpotMarket::default() + }; + create_anchor_account_info!(btc_spot_market, SpotMarket, btc_spot_market_account_info); + + let mut bonk_spot_market = SpotMarket { + market_index: 3, + oracle_source: OracleSource::PythLazer, + oracle: bonk_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 5, + historical_oracle_data: HistoricalOracleData::default_price(22), + ..SpotMarket::default() + }; + create_anchor_account_info!(bonk_spot_market, SpotMarket, bonk_spot_market_account_info); + + let spot_market_account_infos = vec![ + &usdc_spot_market_account_info, + &sol_spot_market_account_info, + &btc_spot_market_account_info, + &bonk_spot_market_account_info, + ]; + let spot_market_map = + SpotMarketMap::load_multiple(spot_market_account_infos, true).unwrap(); + // Build an oracle map containing the three non-quote oracles + let oracle_accounts = vec![ + sol_oracle_account_info.clone(), + btc_oracle_account_info.clone(), + bonk_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + msg!( + "oracle map entry 0 {:?}", + oracle_map + .get_price_data(&sol_spot_market.oracle_id()) + .unwrap() + ); + + // Create constituent target base + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 4, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 128]); // 4 * 32 bytes per TargetsDatum + let constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Create AMM cache + let mut cache_fixed_default = AmmCacheFixed::default(); + cache_fixed_default.len = 0; // No perp markets for this test + let cache_fixed = RefCell::new(cache_fixed_default); + let cache_data = RefCell::new([0u8; 0]); // Empty cache data + let amm_cache = AccountZeroCopyMut::<'_, CacheInfo, AmmCacheFixed> { + fixed: cache_fixed.borrow_mut(), + data: cache_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Call update_aum + let result = lp_pool.update_aum( + 101, // slot + &constituent_map, + &spot_market_map, + &mut oracle_map, + &constituent_target_base, + &amm_cache, + ); + + assert!(result.is_ok(), "{}: update_aum should succeed", test_name); + let (aum, crypto_delta, derivative_groups) = result.unwrap(); + + // Convert expected USD to quote precision + let expected_aum = expected_aum_usd as u128 * QUOTE_PRECISION; + + println!( + "{}: AUM = ${}, Expected = ${}", + test_name, + aum / QUOTE_PRECISION, + expected_aum / QUOTE_PRECISION + ); + + // Verify the results (allow small rounding differences) + let aum_diff = if aum > expected_aum { + aum - expected_aum + } else { + expected_aum - aum + }; + assert!( + aum_diff <= QUOTE_PRECISION, // Allow up to $1 difference for rounding + "{}: AUM mismatch. Got: ${}, Expected: ${}, Diff: ${}", + test_name, + aum / QUOTE_PRECISION, + expected_aum / QUOTE_PRECISION, + aum_diff / QUOTE_PRECISION + ); + + assert_eq!(crypto_delta, 0, "{}: crypto_delta should be 0", test_name); + assert!( + derivative_groups.is_empty(), + "{}: derivative_groups should be empty", + test_name + ); + + // Verify LP pool state was updated + assert_eq!( + lp_pool.last_aum, aum, + "{}: last_aum should match calculated AUM", + test_name + ); + assert_eq!( + lp_pool.last_aum_slot, 101, + "{}: last_aum_slot should be updated", + test_name + ); + } + + #[test] + fn test_aum_zero() { + test_aum_with_balances( + 0, // 0 USDC + 0, // 0 SOL + 0, // 0 BTC + 0, // 0 BONK + 0, // $0 expected AUM + "Zero AUM", + ); + } + + #[test] + fn test_aum_low_1k() { + test_aum_with_balances( + 1_000_000_000, // 1,000 USDC (6 decimals) = $1,000 + 0, // 0 SOL + 0, // 0 BTC + 0, // 0 BONK + 1_000, // $1,000 expected AUM + "Low AUM (~$1k)", + ); + } + + #[test] + fn test_aum_reasonable() { + test_aum_with_balances( + 1_000_000_000_000, // 1M USDC (6 decimals) = $1M + 5_000_000_000_000, // 5k SOL (9 decimals) = $1M at $200/SOL + 800_000_000, // 8 BTC (8 decimals) = $800k at $100k/BTC + 0, // 0 BONK + 2_800_000, // Expected AUM based on actual calculation + "Reasonable AUM (~$2.8M)", + ); + } + + #[test] + fn test_aum_high() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 0, // 0 BONK + 210_000_000_000, // Expected AUM based on actual calculation + "High AUM (~$210b)", + ); + } + + #[test] + fn test_aum_with_small_bonk_balance() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 100_000_000_000_000, // 1B BONK (5 decimals) = $22k at $0.000022/BONK + 210_000_022_000, // Expected AUM based on actual calculation + "High AUM (~$210b) with BONK", + ); + } + + #[test] + fn test_aum_with_large_bonk_balance() { + test_aum_with_balances( + 10_000_000_000_000_000, // 10B USDC (6 decimals) = $10B + 500_000_000_000_000_000, // 500M SOL (9 decimals) = $100B at $200/SOL + 100_000_000_000_000, // 1M BTC (8 decimals) = $100B at $100k/BTC + 100_000_000_000_000_000, // 1T BONK (5 decimals) = $22M at $0.000022/BONK + 210_022_000_000, // Expected AUM based on actual calculation + "High AUM (~$210b) with BONK", + ); + } +} + +#[cfg(test)] +mod update_constituent_target_base_for_derivatives_tests { + use super::super::update_constituent_target_base_for_derivatives; + use crate::create_anchor_account_info; + use crate::math::constants::{ + PERCENTAGE_PRECISION_I64, PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, QUOTE_PRECISION, + SPOT_CUMULATIVE_INTEREST_PRECISION, + }; + use crate::state::constituent_map::ConstituentMap; + use crate::state::lp_pool::{Constituent, ConstituentTargetBaseFixed, TargetsDatum}; + use crate::state::oracle::{HistoricalOracleData, OracleSource}; + use crate::state::oracle_map::OracleMap; + use crate::state::pyth_lazer_oracle::PythLazerOracle; + use crate::state::spot_market::SpotMarket; + use crate::state::spot_market_map::SpotMarketMap; + use crate::state::zero_copy::AccountZeroCopyMut; + use crate::test_utils::{create_account_info, get_anchor_account_bytes}; + use anchor_lang::prelude::Pubkey; + use anchor_lang::Owner; + use std::collections::BTreeMap; + use std::{cell::RefCell, marker::PhantomData}; + + fn test_derivative_weights_scenario( + derivative_weights: Vec, + test_name: &str, + should_succeed: bool, + ) { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + // Create parent constituent (SOL) - parent_index must not be 0 + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 SOL + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, // Parent index + derivative_weight: 0, // Parent doesn't have derivative weight + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // Create first derivative constituent + let derivative1_index = parent_index + 1; // 2 + let mut derivative1_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative1_index, + constituent_index: derivative1_index, + last_oracle_price: 195 * PRICE_PRECISION_I64, // $195 (slightly below parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(0).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative1_constituent, + Constituent, + derivative1_constituent_account_info + ); + + // Create second derivative constituent + let derivative2_index = parent_index + 2; // 3 + let mut derivative2_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative2_index, + constituent_index: derivative2_index, + last_oracle_price: 205 * PRICE_PRECISION_I64, // $205 (slightly above parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(1).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative2_constituent, + Constituent, + derivative2_constituent_account_info + ); + + // Create third derivative constituent + let derivative3_index = parent_index + 3; // 4 + let mut derivative3_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative3_index, + constituent_index: derivative3_index, + last_oracle_price: 210 * PRICE_PRECISION_I64, // $210 (slightly above parent) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: derivative_weights.get(2).map(|w| *w).unwrap_or(0), + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative3_constituent, + Constituent, + derivative3_constituent_account_info + ); + + let constituents_list = vec![ + &parent_constituent_account_info, + &derivative1_constituent_account_info, + &derivative2_constituent_account_info, + &derivative3_constituent_account_info, + ]; + let constituent_map = ConstituentMap::load_multiple(constituents_list, true).unwrap(); + + // Create oracles for parent and derivatives, with prices matching their last_oracle_price + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative1_oracle_pubkey = Pubkey::new_unique(); + let mut derivative1_oracle = PythLazerOracle { + price: 195 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative1_oracle, + &derivative1_oracle_pubkey, + PythLazerOracle, + derivative1_oracle_account_info + ); + + let derivative2_oracle_pubkey = Pubkey::new_unique(); + let mut derivative2_oracle = PythLazerOracle { + price: 205 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative2_oracle, + &derivative2_oracle_pubkey, + PythLazerOracle, + derivative2_oracle_account_info + ); + + let derivative3_oracle_pubkey = Pubkey::new_unique(); + let mut derivative3_oracle = PythLazerOracle { + price: 210 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative3_oracle, + &derivative3_oracle_pubkey, + PythLazerOracle, + derivative3_oracle_account_info + ); + + // Create spot markets bound to the above oracles + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative1_spot_market = SpotMarket { + market_index: derivative1_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative1_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative1_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative1_spot_market, + SpotMarket, + derivative1_spot_market_account_info + ); + + let mut derivative2_spot_market = SpotMarket { + market_index: derivative2_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative2_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative2_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative2_spot_market, + SpotMarket, + derivative2_spot_market_account_info + ); + + let mut derivative3_spot_market = SpotMarket { + market_index: derivative3_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative3_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative3_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative3_spot_market, + SpotMarket, + derivative3_spot_market_account_info + ); + + let spot_market_list = vec![ + &parent_spot_market_account_info, + &derivative1_spot_market_account_info, + &derivative2_spot_market_account_info, + &derivative3_spot_market_account_info, + ]; + let spot_market_map = SpotMarketMap::load_multiple(spot_market_list, true).unwrap(); + + // Build an oracle map for parent and derivatives + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative1_oracle_account_info.clone(), + derivative2_oracle_account_info.clone(), + derivative3_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + // Create constituent target base + let num_constituents = 4; // Fixed: parent + 3 derivatives + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: num_constituents as u32, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 5 * 32]); // 4+1 constituents * 32 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set initial parent target base (targeting 10% of total AUM worth of SOL tokens) + // For 10M AUM and $200 SOL price with 9 decimals: (10M * 0.1) / 200 * 10^9 = 5,000,000,000,000 tokens + let initial_parent_target_base = 5_000_000_000_000i64; // ~$1M worth of SOL tokens + constituent_target_base + .get_mut(parent_index as u32) + .target_base = initial_parent_target_base; + constituent_target_base + .get_mut(parent_index as u32) + .last_oracle_slot = 100; + + // Initialize derivative target bases to 0 + constituent_target_base + .get_mut(derivative1_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative1_index as u32) + .last_oracle_slot = 100; + constituent_target_base + .get_mut(derivative2_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative2_index as u32) + .last_oracle_slot = 100; + constituent_target_base + .get_mut(derivative3_index as u32) + .target_base = 0; + constituent_target_base + .get_mut(derivative3_index as u32) + .last_oracle_slot = 100; + + // Create derivative groups + let mut derivative_groups = BTreeMap::new(); + let mut active_derivatives = Vec::new(); + for (i, _) in derivative_weights.iter().enumerate() { + // Add all derivatives regardless of weight (they may have zero weight for testing) + let derivative_index = match i { + 0 => derivative1_index, + 1 => derivative2_index, + 2 => derivative3_index, + _ => continue, + }; + active_derivatives.push(derivative_index); + } + if !active_derivatives.is_empty() { + derivative_groups.insert(parent_index, active_derivatives); + } + + // Call the function + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok() == should_succeed, + "{}: update_constituent_target_base_for_derivatives should succeed", + test_name + ); + + if !should_succeed { + return; + } + + // Verify results + let parent_target_base_after = constituent_target_base.get(parent_index as u32).target_base; + let total_derivative_weight: u64 = derivative_weights.iter().sum(); + let remaining_parent_weight = PERCENTAGE_PRECISION_U64 - total_derivative_weight; + + // Expected parent target base after scaling down + let expected_parent_target_base = initial_parent_target_base + * (remaining_parent_weight as i64) + / (PERCENTAGE_PRECISION_I64); + + println!( + "{}: Original parent target base: {}, After: {}, Expected: {}", + test_name, + initial_parent_target_base, + parent_target_base_after, + expected_parent_target_base + ); + + assert_eq!( + parent_target_base_after, expected_parent_target_base, + "{}: Parent target base should be scaled down correctly", + test_name + ); + + // Verify derivative target bases + for (i, derivative_weight) in derivative_weights.iter().enumerate() { + let derivative_index = match i { + 0 => derivative1_index, + 1 => derivative2_index, + 2 => derivative3_index, + _ => continue, + }; + + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + + if *derivative_weight == 0 { + // If derivative weight is 0, target base should remain 0 + assert_eq!( + derivative_target_base, 0, + "{}: Derivative {} with zero weight should have target base 0", + test_name, derivative_index + ); + continue; + } + + // For simplicity, just verify that the derivative target base is positive and reasonable + // The exact calculation is complex and depends on the internal implementation + println!( + "{}: Derivative {} target base: {}, Weight: {}", + test_name, derivative_index, derivative_target_base, derivative_weight + ); + + assert!( + derivative_target_base > 0, + "{}: Derivative {} target base should be positive", + test_name, + derivative_index + ); + + // Verify that target base is reasonable (not too large or too small) + assert!( + derivative_target_base < 10_000_000_000_000i64, + "{}: Derivative {} target base should be reasonable", + test_name, + derivative_index + ); + } + } + + fn test_depeg_scenario() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + // Create parent constituent (SOL) - parent_index must not be 0 + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 SOL + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, // Parent index + derivative_weight: 0, + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // Create derivative constituent that's depegged - must have different index than parent + let derivative_index = parent_index + 1; // 2 + let mut derivative_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative_index, + constituent_index: derivative_index, + last_oracle_price: 180 * PRICE_PRECISION_I64, // $180 (below 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 500_000, // 50% weight + constituent_derivative_depeg_threshold: 950_000, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + derivative_constituent, + Constituent, + derivative_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative_constituent_account_info, + ], + true, + ) + .unwrap(); + + // Create PythLazer oracles corresponding to prices + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative_oracle_pubkey = Pubkey::new_unique(); + let mut derivative_oracle = PythLazerOracle { + price: 180 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative_oracle, + &derivative_oracle_pubkey, + PythLazerOracle, + derivative_oracle_account_info + ); + + // Create spot markets + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative_spot_market = SpotMarket { + market_index: derivative_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative_spot_market, + SpotMarket, + derivative_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative_spot_market_account_info, + ], + true, + ) + .unwrap(); + + // Build oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + // Create constituent target base + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 2, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 3 * 32]); // 2+1 constituents * 24 bytes per TargetsDatum + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set initial values + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 2_500_000_000_000i64; // ~$500k worth of SOL + constituent_target_base + .get_mut(derivative_index as u32) + .target_base = 1_250_000_000_000i64; // ~$250k worth + + // Create derivative groups + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative_index]); + + // Call the function + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "depeg scenario: update_constituent_target_base_for_derivatives should succeed" + ); + + // Verify that depegged derivative has target base set to 0 + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + assert_eq!( + derivative_target_base, 0, + "depeg scenario: Depegged derivative should have target base 0" + ); + + // Verify that parent target base is unchanged since derivative weight is 0 now + let parent_target_base = constituent_target_base.get(parent_index as u32).target_base; + assert_eq!( + parent_target_base, 2_500_000_000_000i64, + "depeg scenario: Parent target base should remain unchanged" + ); + } + + #[test] + fn test_derivative_depeg_scenario() { + // Test case: Test depeg scenario + test_depeg_scenario(); + } + + #[test] + fn test_derivative_weights_sum_to_110_percent() { + // Test case: Derivative constituents with weights that sum to 1.1 (110%) + test_derivative_weights_scenario( + vec![ + 500_000, // 50% weight + 300_000, // 30% weight + 300_000, // 30% weight + ], + "weights sum to 110%", + false, + ); + } + + #[test] + fn test_derivative_weights_sum_to_100_percent() { + // Test case: Derivative constituents with weights that sum to 1 (100%) + test_derivative_weights_scenario( + vec![ + 500_000, // 50% weight + 300_000, // 30% weight + 200_000, // 20% weight + ], + "weights sum to 100%", + true, + ); + } + + #[test] + fn test_derivative_weights_sum_to_75_percent() { + // Test case: Derivative constituents with weights that sum to < 1 (75%) + test_derivative_weights_scenario( + vec![ + 400_000, // 40% weight + 200_000, // 20% weight + 150_000, // 15% weight + ], + "weights sum to 75%", + true, + ); + } + + #[test] + fn test_single_derivative_60_percent_weight() { + // Test case: Single derivative with partial weight + test_derivative_weights_scenario( + vec![ + 600_000, // 60% weight + ], + "single derivative 60% weight", + true, + ); + } + + #[test] + fn test_single_derivative_100_percent_weight() { + // Test case: Single derivative with 100% weight - parent should become 0 + test_derivative_weights_scenario( + vec![ + 1_000_000, // 100% weight + ], + "single derivative 100% weight", + true, + ); + } + + #[test] + fn test_mixed_zero_and_nonzero_weights() { + // Test case: Mix of zero and non-zero weights + test_derivative_weights_scenario( + vec![ + 0, // 0% weight + 400_000, // 40% weight + 0, // 0% weight + ], + "mixed zero and non-zero weights", + true, + ); + } + + #[test] + fn test_very_small_weights() { + // Test case: Very small weights (1 basis point = 0.01%) + test_derivative_weights_scenario( + vec![ + 100, // 0.01% weight + 200, // 0.02% weight + 300, // 0.03% weight + ], + "very small weights", + true, + ); + } + + #[test] + fn test_zero_parent_target_base() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, + derivative_weight: 0, + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + let derivative_index = parent_index + 1; + let mut derivative_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative_index, + constituent_index: derivative_index, + last_oracle_price: 195 * PRICE_PRECISION_I64, + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 500_000, // 50% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative_constituent, + Constituent, + derivative_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative_constituent_account_info, + ], + true, + ) + .unwrap(); + + // Create PythLazer oracles so update_constituent_target_base_for_derivatives can fetch current prices + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + + let derivative_oracle_pubkey = Pubkey::new_unique(); + let mut derivative_oracle = PythLazerOracle { + price: 195 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative_oracle, + &derivative_oracle_pubkey, + PythLazerOracle, + derivative_oracle_account_info + ); + + // Spot markets bound to the test oracles + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative_spot_market = SpotMarket { + market_index: derivative_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative_spot_market, + SpotMarket, + derivative_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative_spot_market_account_info, + ], + true, + ) + .unwrap(); + + // Build oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 2, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 3 * 32]); + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + // Set parent target base to 0 + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 0i64; + constituent_target_base + .get_mut(derivative_index as u32) + .target_base = 0i64; + + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative_index]); + + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "zero parent target base scenario should succeed" + ); + + // With zero parent target base, derivative should also be 0 + let derivative_target_base = constituent_target_base + .get(derivative_index as u32) + .target_base; + assert_eq!( + derivative_target_base, 0, + "zero parent target base: derivative target base should be 0" + ); + } + + #[test] + fn test_mixed_depegged_and_valid_derivatives() { + let aum = 10_000_000 * QUOTE_PRECISION; // $10M AUM + + let parent_index = 1u16; + let mut parent_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: parent_index, + constituent_index: parent_index, + last_oracle_price: 200 * PRICE_PRECISION_I64, // $200 + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: -1, + derivative_weight: 0, + constituent_derivative_depeg_threshold: 949_999, // 95% threshold + ..Constituent::default() + }; + create_anchor_account_info!( + parent_constituent, + Constituent, + parent_constituent_account_info + ); + + // First derivative - depegged + let derivative1_index = parent_index + 1; + let mut derivative1_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative1_index, + constituent_index: derivative1_index, + last_oracle_price: 180 * PRICE_PRECISION_I64, // $180 (below 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 300_000, // 30% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative1_constituent, + Constituent, + derivative1_constituent_account_info + ); + + // Second derivative - valid + let derivative2_index = parent_index + 2; + let mut derivative2_constituent = Constituent { + mint: Pubkey::new_unique(), + spot_market_index: derivative2_index, + constituent_index: derivative2_index, + last_oracle_price: 198 * PRICE_PRECISION_I64, // $198 (above 95% threshold) + last_oracle_slot: 100, + decimals: 9, + constituent_derivative_index: parent_index as i16, + derivative_weight: 400_000, // 40% weight + constituent_derivative_depeg_threshold: 950_000, + ..Constituent::default() + }; + create_anchor_account_info!( + derivative2_constituent, + Constituent, + derivative2_constituent_account_info + ); + + let constituent_map = ConstituentMap::load_multiple( + vec![ + &parent_constituent_account_info, + &derivative1_constituent_account_info, + &derivative2_constituent_account_info, + ], + true, + ) + .unwrap(); + + // Oracles + let parent_oracle_pubkey = Pubkey::new_unique(); + let mut parent_oracle = PythLazerOracle { + price: 200 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + parent_oracle, + &parent_oracle_pubkey, + PythLazerOracle, + parent_oracle_account_info + ); + let derivative1_oracle_pubkey = Pubkey::new_unique(); + let mut derivative1_oracle = PythLazerOracle { + price: 180 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative1_oracle, + &derivative1_oracle_pubkey, + PythLazerOracle, + derivative1_oracle_account_info + ); + let derivative2_oracle_pubkey = Pubkey::new_unique(); + let mut derivative2_oracle = PythLazerOracle { + price: 198 * PRICE_PRECISION_I64, + publish_time: 1, + posted_slot: 100, + exponent: -6, + _padding: [0; 4], + conf: 0, + }; + create_anchor_account_info!( + derivative2_oracle, + &derivative2_oracle_pubkey, + PythLazerOracle, + derivative2_oracle_account_info + ); + + let mut parent_spot_market = SpotMarket { + market_index: parent_index, + oracle_source: OracleSource::PythLazer, + oracle: parent_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(parent_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + parent_spot_market, + SpotMarket, + parent_spot_market_account_info + ); + + let mut derivative1_spot_market = SpotMarket { + market_index: derivative1_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative1_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative1_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative1_spot_market, + SpotMarket, + derivative1_spot_market_account_info + ); + + let mut derivative2_spot_market = SpotMarket { + market_index: derivative2_index, + oracle_source: OracleSource::PythLazer, + oracle: derivative2_oracle_pubkey, + cumulative_deposit_interest: SPOT_CUMULATIVE_INTEREST_PRECISION, + decimals: 9, + historical_oracle_data: HistoricalOracleData::default_price(derivative2_oracle.price), + ..SpotMarket::default() + }; + create_anchor_account_info!( + derivative2_spot_market, + SpotMarket, + derivative2_spot_market_account_info + ); + + let spot_market_map = SpotMarketMap::load_multiple( + vec![ + &parent_spot_market_account_info, + &derivative1_spot_market_account_info, + &derivative2_spot_market_account_info, + ], + true, + ) + .unwrap(); + + // Oracle map + let oracle_accounts = vec![ + parent_oracle_account_info.clone(), + derivative1_oracle_account_info.clone(), + derivative2_oracle_account_info.clone(), + ]; + let mut oracle_iter = oracle_accounts.iter().peekable(); + let mut oracle_map = OracleMap::load(&mut oracle_iter, 101, None).unwrap(); + + let target_fixed = RefCell::new(ConstituentTargetBaseFixed { + len: 3, + ..ConstituentTargetBaseFixed::default() + }); + let target_data = RefCell::new([0u8; 4 * 32]); + let mut constituent_target_base = + AccountZeroCopyMut::<'_, TargetsDatum, ConstituentTargetBaseFixed> { + fixed: target_fixed.borrow_mut(), + data: target_data.borrow_mut(), + _marker: PhantomData::, + }; + + constituent_target_base + .get_mut(parent_index as u32) + .target_base = 5_000_000_000_000i64; + constituent_target_base + .get_mut(derivative1_index as u32) + .target_base = 0i64; + constituent_target_base + .get_mut(derivative2_index as u32) + .target_base = 0i64; + + let mut derivative_groups = BTreeMap::new(); + derivative_groups.insert(parent_index, vec![derivative1_index, derivative2_index]); + + let result = update_constituent_target_base_for_derivatives( + aum, + &derivative_groups, + &constituent_map, + &spot_market_map, + &mut oracle_map, + &mut constituent_target_base, + ); + + assert!( + result.is_ok(), + "mixed depegged and valid derivatives scenario should succeed" + ); + + // First derivative should be depegged (target base = 0) + let derivative1_target_base = constituent_target_base + .get(derivative1_index as u32) + .target_base; + assert_eq!( + derivative1_target_base, 0, + "mixed scenario: depegged derivative should have target base 0" + ); + + // Second derivative should have positive target base + let derivative2_target_base = constituent_target_base + .get(derivative2_index as u32) + .target_base; + assert!( + derivative2_target_base > 0, + "mixed scenario: valid derivative should have positive target base" + ); + + // Parent should be scaled down by only the valid derivative's weight (40%) + let parent_target_base = constituent_target_base.get(parent_index as u32).target_base; + let expected_parent_target_base = 5_000_000_000_000i64 * (1_000_000 - 400_000) / 1_000_000; + assert_eq!( + parent_target_base, expected_parent_target_base, + "mixed scenario: parent should be scaled by valid derivative weight only" + ); + } +} diff --git a/programs/drift/src/state/margin_calculation.rs b/programs/drift/src/state/margin_calculation.rs index d4b19ad69f..4069a11bf4 100644 --- a/programs/drift/src/state/margin_calculation.rs +++ b/programs/drift/src/state/margin_calculation.rs @@ -2,6 +2,9 @@ use std::collections::BTreeMap; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::{ + AMM_RESERVE_PRECISION_I128, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, +}; use crate::math::fuel::{calculate_perp_fuel_bonus, calculate_spot_fuel_bonus}; use crate::math::margin::MarginRequirementType; use crate::math::safe_math::SafeMath; @@ -11,9 +14,7 @@ use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::PerpMarket; use crate::state::spot_market::SpotMarket; use crate::state::user::{PerpPosition, User}; -use crate::{ - validate, MarketType, AMM_RESERVE_PRECISION_I128, MARGIN_PRECISION_I128, MARGIN_PRECISION_U128, -}; +use crate::{validate, MarketType}; use anchor_lang::{prelude::*, solana_program::msg}; #[derive(Clone, Copy, Debug)] diff --git a/programs/drift/src/state/mod.rs b/programs/drift/src/state/mod.rs index 65fdacf16d..73b57392f4 100644 --- a/programs/drift/src/state/mod.rs +++ b/programs/drift/src/state/mod.rs @@ -1,3 +1,5 @@ +pub mod amm_cache; +pub mod constituent_map; pub mod events; pub mod fill_mode; pub mod fulfillment; @@ -7,6 +9,7 @@ pub mod if_rebalance_config; pub mod insurance_fund_stake; pub mod liquidation_mode; pub mod load_ref; +pub mod lp_pool; pub mod margin_calculation; pub mod oracle; pub mod oracle_map; @@ -16,6 +19,8 @@ pub mod perp_market; pub mod perp_market_map; pub mod protected_maker_mode_config; pub mod pyth_lazer_oracle; +pub mod revenue_share; +pub mod revenue_share_map; pub mod settle_pnl_mode; pub mod signed_msg_user; pub mod spot_fulfillment_params; @@ -26,3 +31,4 @@ pub mod state; pub mod traits; pub mod user; pub mod user_map; +pub mod zero_copy; diff --git a/programs/drift/src/state/oracle.rs b/programs/drift/src/state/oracle.rs index 3becf945f6..0275a5549a 100644 --- a/programs/drift/src/state/oracle.rs +++ b/programs/drift/src/state/oracle.rs @@ -1,5 +1,6 @@ use anchor_lang::prelude::*; use std::cell::Ref; +use std::convert::TryFrom; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; @@ -172,6 +173,55 @@ impl OracleSource { } } +impl TryFrom for OracleSource { + type Error = ErrorCode; + + fn try_from(v: u8) -> DriftResult { + match v { + 0 => Ok(OracleSource::Pyth), + 1 => Ok(OracleSource::Switchboard), + 2 => Ok(OracleSource::QuoteAsset), + 3 => Ok(OracleSource::Pyth1K), + 4 => Ok(OracleSource::Pyth1M), + 5 => Ok(OracleSource::PythStableCoin), + 6 => Ok(OracleSource::Prelaunch), + 7 => Ok(OracleSource::PythPull), + 8 => Ok(OracleSource::Pyth1KPull), + 9 => Ok(OracleSource::Pyth1MPull), + 10 => Ok(OracleSource::PythStableCoinPull), + 11 => Ok(OracleSource::SwitchboardOnDemand), + 12 => Ok(OracleSource::PythLazer), + 13 => Ok(OracleSource::PythLazer1K), + 14 => Ok(OracleSource::PythLazer1M), + 15 => Ok(OracleSource::PythLazerStableCoin), + _ => Err(ErrorCode::InvalidOracle), + } + } +} + +impl From for u8 { + fn from(src: OracleSource) -> u8 { + match src { + OracleSource::Pyth => 0, + OracleSource::Switchboard => 1, + OracleSource::QuoteAsset => 2, + OracleSource::Pyth1K => 3, + OracleSource::Pyth1M => 4, + OracleSource::PythStableCoin => 5, + OracleSource::Prelaunch => 6, + OracleSource::PythPull => 7, + OracleSource::Pyth1KPull => 8, + OracleSource::Pyth1MPull => 9, + OracleSource::PythStableCoinPull => 10, + OracleSource::SwitchboardOnDemand => 11, + OracleSource::PythLazer => 12, + OracleSource::PythLazer1K => 13, + OracleSource::PythLazer1M => 14, + OracleSource::PythLazerStableCoin => 15, + } + } +} + const MM_EXCHANGE_FALLBACK_THRESHOLD: u128 = PERCENTAGE_PRECISION / 100; // 1% #[derive(Default, Clone, Copy, Debug)] pub struct MMOraclePriceData { diff --git a/programs/drift/src/state/oracle_map.rs b/programs/drift/src/state/oracle_map.rs index dc574b8d98..acbe7c618c 100644 --- a/programs/drift/src/state/oracle_map.rs +++ b/programs/drift/src/state/oracle_map.rs @@ -89,11 +89,19 @@ impl<'a> OracleMap<'a> { last_oracle_price_twap: i64, max_confidence_interval_multiplier: u64, slots_before_stale_for_amm_override: i8, + oracle_low_risk_slot_delay_override_override: i8, + log_mode: Option, ) -> DriftResult<(&OraclePriceData, OracleValidity)> { if self.should_get_quote_asset_price_data(&oracle_id.0) { return Ok((&self.quote_asset_price_data, OracleValidity::Valid)); } + let log_mode = if let Some(lm) = log_mode { + lm + } else { + LogMode::ExchangeOracle + }; + if self.price_data.contains_key(oracle_id) { let oracle_price_data = self.price_data.get(oracle_id).safe_unwrap()?; @@ -108,8 +116,9 @@ impl<'a> OracleMap<'a> { &self.oracle_guard_rails.validity, max_confidence_interval_multiplier, &oracle_id.1, - LogMode::ExchangeOracle, + log_mode, slots_before_stale_for_amm_override, + oracle_low_risk_slot_delay_override_override, )?; self.validity.insert(*oracle_id, oracle_validity); oracle_validity @@ -138,8 +147,9 @@ impl<'a> OracleMap<'a> { &self.oracle_guard_rails.validity, max_confidence_interval_multiplier, &oracle_id.1, - LogMode::ExchangeOracle, + log_mode, slots_before_stale_for_amm_override, + oracle_low_risk_slot_delay_override_override, )?; self.validity.insert(*oracle_id, oracle_validity); diff --git a/programs/drift/src/state/order_params.rs b/programs/drift/src/state/order_params.rs index 69e1c6710d..a5b81b8bb6 100644 --- a/programs/drift/src/state/order_params.rs +++ b/programs/drift/src/state/order_params.rs @@ -1,15 +1,15 @@ use crate::controller::position::PositionDirection; use crate::error::DriftResult; use crate::math::casting::Cast; +use crate::math::constants::{ + MAX_PREDICTION_MARKET_PRICE_I64, ONE_HUNDRED_THOUSAND_QUOTE, PERCENTAGE_PRECISION_I64, + PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, +}; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; use crate::state::events::OrderActionExplanation; use crate::state::perp_market::{ContractTier, PerpMarket}; use crate::state::user::{MarketType, OrderTriggerCondition, OrderType}; -use crate::{ - MAX_PREDICTION_MARKET_PRICE_I64, ONE_HUNDRED_THOUSAND_QUOTE, PERCENTAGE_PRECISION_I64, - PERCENTAGE_PRECISION_U64, PRICE_PRECISION_I64, -}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; use std::ops::Div; @@ -702,13 +702,6 @@ impl OrderParams { } .cast::()?; - let baseline_start_price_offset_slow = mark_twap_slow.safe_sub( - perp_market - .amm - .historical_oracle_data - .last_oracle_price_twap, - )?; - let baseline_start_price_offset_fast = perp_market .amm .last_mark_price_twap_5min @@ -720,27 +713,42 @@ impl OrderParams { .last_oracle_price_twap_5min, )?; - let frac_of_long_spread_in_price: i64 = perp_market - .amm - .long_spread - .cast::()? - .safe_mul(mark_twap_slow)? - .safe_div(PRICE_PRECISION_I64 * 10)?; + let baseline_start_price_offset_slow = mark_twap_slow.safe_sub( + perp_market + .amm + .historical_oracle_data + .last_oracle_price_twap, + )?; - let frac_of_short_spread_in_price: i64 = perp_market - .amm - .short_spread - .cast::()? - .safe_mul(mark_twap_slow)? - .safe_div(PRICE_PRECISION_I64 * 10)?; - - let baseline_start_price_offset = match direction { - PositionDirection::Long => baseline_start_price_offset_slow - .safe_add(frac_of_long_spread_in_price)? - .min(baseline_start_price_offset_fast.safe_sub(frac_of_short_spread_in_price)?), - PositionDirection::Short => baseline_start_price_offset_slow - .safe_sub(frac_of_short_spread_in_price)? - .max(baseline_start_price_offset_fast.safe_add(frac_of_long_spread_in_price)?), + let baseline_start_price_offset = if baseline_start_price_offset_slow + .abs_diff(baseline_start_price_offset_fast) + <= perp_market.amm.last_mark_price_twap_5min / 200 + { + let frac_of_long_spread_in_price: i64 = perp_market + .amm + .long_spread + .cast::()? + .safe_mul(mark_twap_slow)? + .safe_div(PRICE_PRECISION_I64 * 10)?; + + let frac_of_short_spread_in_price: i64 = perp_market + .amm + .short_spread + .cast::()? + .safe_mul(mark_twap_slow)? + .safe_div(PRICE_PRECISION_I64 * 10)?; + + match direction { + PositionDirection::Long => baseline_start_price_offset_slow + .safe_add(frac_of_long_spread_in_price)? + .min(baseline_start_price_offset_fast.safe_sub(frac_of_short_spread_in_price)?), + PositionDirection::Short => baseline_start_price_offset_slow + .safe_sub(frac_of_short_spread_in_price)? + .max(baseline_start_price_offset_fast.safe_add(frac_of_long_spread_in_price)?), + } + } else { + // more than 50bps different of fast/slow twap, use fast only + baseline_start_price_offset_fast }; Ok(baseline_start_price_offset) @@ -865,6 +873,9 @@ pub struct SignedMsgOrderParamsMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, + pub isolated_position_deposit: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -876,6 +887,9 @@ pub struct SignedMsgOrderParamsDelegateMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, + pub isolated_position_deposit: Option, } #[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq, Debug)] @@ -891,15 +905,15 @@ fn get_auction_duration( ) -> DriftResult { let percent_diff = price_diff.safe_mul(PERCENTAGE_PRECISION_U64)?.div(price); - let slots_per_bp = if contract_tier.is_as_safe_as_contract(&ContractTier::B) { + let slots_per_pct = if contract_tier.is_as_safe_as_contract(&ContractTier::B) { 100 } else { 60 }; Ok(percent_diff - .safe_mul(slots_per_bp)? - .safe_div_ceil(PERCENTAGE_PRECISION_U64 / 100)? // 1% = 60 slots + .safe_mul(slots_per_pct)? + .safe_div_ceil(PERCENTAGE_PRECISION_U64 / 100)? // 1% = 40 slots .clamp(1, 180) as u8) // 180 slots max } diff --git a/programs/drift/src/state/order_params/tests.rs b/programs/drift/src/state/order_params/tests.rs index 5f59a6c3f3..d2d54af789 100644 --- a/programs/drift/src/state/order_params/tests.rs +++ b/programs/drift/src/state/order_params/tests.rs @@ -409,10 +409,11 @@ mod update_perp_auction_params { ..AMM::default() }; amm.last_bid_price_twap = (oracle_price * 15 / 10 - 192988) as u64; - amm.last_mark_price_twap_5min = (oracle_price * 16 / 10) as u64; + amm.last_mark_price_twap_5min = (oracle_price * 155 / 100) as u64; amm.last_ask_price_twap = (oracle_price * 16 / 10 + 192988) as u64; amm.historical_oracle_data.last_oracle_price_twap = oracle_price; amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; amm.historical_oracle_data.last_oracle_price = oracle_price; let perp_market = PerpMarket { @@ -436,7 +437,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); - assert_eq!(order_params_after.auction_start_price, Some(72_524_319)); + assert_eq!(order_params_after.auction_start_price, Some(79_750_000)); assert_eq!(order_params_after.auction_end_price, Some(90_092_988)); assert_eq!(order_params_after.auction_duration, Some(180)); } @@ -456,13 +457,13 @@ mod update_perp_auction_params { ..AMM::default() }; amm.last_bid_price_twap = (oracle_price * 99 / 100) as u64; - amm.last_mark_price_twap_5min = oracle_price as u64; amm.last_ask_price_twap = (oracle_price * 101 / 100) as u64; amm.historical_oracle_data.last_oracle_price_twap = oracle_price; amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; amm.historical_oracle_data.last_oracle_price = oracle_price; - let perp_market = PerpMarket { + let mut perp_market = PerpMarket { amm, ..PerpMarket::default() }; @@ -563,10 +564,10 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_duration, Some(175)); + assert_eq!(order_params_after.auction_duration, Some(120)); assert_eq!( order_params_after.auction_start_price, - Some(100 * PRICE_PRECISION_I64 - 901000) + Some(100 * PRICE_PRECISION_I64) ); assert_eq!( order_params_after.auction_end_price, @@ -599,20 +600,24 @@ mod update_perp_auction_params { direction: PositionDirection::Short, ..OrderParams::default() }; + + // tighten bid/ask to mark twap 5min to activate buffer + perp_market.amm.last_bid_price_twap = amm.last_mark_price_twap_5min - 100000; + perp_market.amm.last_ask_price_twap = amm.last_mark_price_twap_5min + 100000; let mut order_params_after = order_params_before; order_params_after .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_duration, Some(174)); assert_eq!( order_params_after.auction_start_price, - Some(100 * PRICE_PRECISION_I64 + 899000) // %1 / 10 = 10 bps aggression + Some(100 * PRICE_PRECISION_I64 + 100100) // a bit more passive than mid ); assert_eq!( order_params_after.auction_end_price, Some(98 * PRICE_PRECISION_I64) ); + assert_eq!(order_params_after.auction_duration, Some(127)); } #[test] @@ -786,10 +791,12 @@ mod update_perp_auction_params { }; amm.historical_oracle_data.last_oracle_price = oracle_price; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let mut perp_market = PerpMarket { amm, @@ -812,7 +819,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 98901080); + assert_eq!(order_params_after.auction_start_price.unwrap(), 99018698); let amm_bid_price = amm.bid_price(amm.reserve_price().unwrap()).unwrap(); assert_eq!(amm_bid_price, 98010000); assert!(order_params_after.auction_start_price.unwrap() as u64 > amm_bid_price); @@ -832,7 +839,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 99118879); + assert_eq!(order_params_after.auction_start_price.unwrap(), 99216738); // skip for prelaunch oracle perp_market.amm.oracle_source = OracleSource::Prelaunch; @@ -893,12 +900,13 @@ mod update_perp_auction_params { ..AMM::default() }; amm.historical_oracle_data.last_oracle_price = oracle_price; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; - + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let perp_market = PerpMarket { amm, contract_tier: ContractTier::B, @@ -920,7 +928,8 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), -98920); + assert_eq!(order_params_after.auction_start_price.unwrap(), 18698); + assert_eq!(order_params_after.auction_end_price.unwrap(), 2196053); let order_params_before = OrderParams { order_type: OrderType::Oracle, @@ -985,10 +994,12 @@ mod update_perp_auction_params { }; amm.historical_oracle_data.last_oracle_price = oracle_price; amm.historical_oracle_data.last_oracle_price_twap = oracle_price - 97238; + amm.historical_oracle_data.last_oracle_price_twap_5min = oracle_price - 97238; amm.last_ask_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 217999; amm.last_bid_price_twap = (amm.historical_oracle_data.last_oracle_price_twap as u64) + 17238; + amm.last_mark_price_twap_5min = (amm.last_ask_price_twap + amm.last_bid_price_twap) / 2; let perp_market = PerpMarket { amm, @@ -1011,7 +1022,7 @@ mod update_perp_auction_params { .update_perp_auction_params(&perp_market, oracle_price, false) .unwrap(); assert_ne!(order_params_before, order_params_after); - assert_eq!(order_params_after.auction_start_price.unwrap(), 98653580); + assert_eq!(order_params_after.auction_start_price.unwrap(), 98771198); let order_params_before = OrderParams { order_type: OrderType::Market, @@ -1030,7 +1041,7 @@ mod update_perp_auction_params { assert_ne!(order_params_before, order_params_after); assert_eq!( order_params_after.auction_start_price.unwrap(), - (99 * PRICE_PRECISION_I64 - oracle_price / 400) - 98920 // approx equal with some noise + (99 * PRICE_PRECISION_I64 - oracle_price / 400 + 18698) // approx equal with some noise ); let order_params_before = OrderParams { @@ -1049,7 +1060,7 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 99118879 + oracle_price / 400 + 99118879 + oracle_price / 400 + 97859 ); let order_params_before = OrderParams { @@ -1069,7 +1080,7 @@ mod update_perp_auction_params { assert_ne!(order_params_before, order_params_after); assert_eq!( order_params_after.auction_start_price.unwrap(), - (99 * PRICE_PRECISION_U64 + 100000) as i64 + oracle_price / 400 + 18879 // use limit price and oracle buffer with some noise + (99 * PRICE_PRECISION_U64 + 100000) as i64 + oracle_price / 400 + 116738 // use limit price and oracle buffer with some noise ); let order_params_before = OrderParams { @@ -1088,11 +1099,11 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 99118879 + oracle_price / 400 + 99118879 + oracle_price / 400 + 97859 ); assert_eq!(order_params_after.auction_end_price.unwrap(), 98028211); - assert_eq!(order_params_after.auction_duration, Some(82)); + assert_eq!(order_params_after.auction_duration, Some(88)); let order_params_before = OrderParams { order_type: OrderType::Market, @@ -1110,11 +1121,11 @@ mod update_perp_auction_params { .unwrap(); assert_eq!( order_params_after.auction_start_price.unwrap(), - 98901080 - oracle_price / 400 + 98901080 - oracle_price / 400 + 117618 ); assert_eq!(order_params_after.auction_end_price.unwrap(), 100207026); - assert_eq!(order_params_after.auction_duration, Some(95)); + assert_eq!(order_params_after.auction_duration, Some(88)); } #[test] @@ -1325,8 +1336,11 @@ mod get_close_perp_params { let amm = AMM { last_ask_price_twap: 101 * PRICE_PRECISION_U64, last_bid_price_twap: 99 * PRICE_PRECISION_U64, + last_mark_price_twap_5min: 99 * PRICE_PRECISION_U64, historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + last_oracle_price_twap_5min: 100 * PRICE_PRECISION_I64, + ..HistoricalOracleData::default() }, mark_std: PRICE_PRECISION_U64, @@ -1385,7 +1399,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, PRICE_PRECISION_I64); + assert_eq!(auction_start_price, 2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 4 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, 4 * PRICE_PRECISION_I64 as i32); @@ -1420,7 +1434,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, -3 * PRICE_PRECISION_I64); + assert_eq!(auction_start_price, -2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 0); assert_eq!(oracle_price_offset, 0); @@ -1436,8 +1450,11 @@ mod get_close_perp_params { let amm = AMM { last_ask_price_twap: 101 * PRICE_PRECISION_U64, last_bid_price_twap: 99 * PRICE_PRECISION_U64, + last_mark_price_twap_5min: 100 * PRICE_PRECISION_U64, + historical_oracle_data: HistoricalOracleData { last_oracle_price_twap: 100 * PRICE_PRECISION_I64, + last_oracle_price_twap_5min: 100 * PRICE_PRECISION_I64, ..HistoricalOracleData::default() }, mark_std: PRICE_PRECISION_U64, @@ -1462,7 +1479,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 1000000); + assert_eq!(auction_start_price, 0); assert_eq!(auction_end_price, -2 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, -2 * PRICE_PRECISION_I64 as i32); @@ -1498,7 +1515,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 3 * PRICE_PRECISION_I64); + assert_eq!(auction_start_price, 2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, 0); assert_eq!(oracle_price_offset, 0); @@ -1536,7 +1553,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, -PRICE_PRECISION_I64); + assert_eq!(auction_start_price, -2 * PRICE_PRECISION_I64); assert_eq!(auction_end_price, -4 * PRICE_PRECISION_I64); assert_eq!(oracle_price_offset, -4 * PRICE_PRECISION_I64 as i32); @@ -1613,7 +1630,7 @@ mod get_close_perp_params { let auction_start_price = params.auction_start_price.unwrap(); let auction_end_price = params.auction_end_price.unwrap(); let oracle_price_offset = params.oracle_price_offset.unwrap(); - assert_eq!(auction_start_price, 641); + assert_eq!(auction_start_price, 284); assert_eq!(auction_end_price, -1021); assert_eq!(oracle_price_offset, -1021); diff --git a/programs/drift/src/state/paused_operations.rs b/programs/drift/src/state/paused_operations.rs index 81a6ec2a3a..516470460a 100644 --- a/programs/drift/src/state/paused_operations.rs +++ b/programs/drift/src/state/paused_operations.rs @@ -97,3 +97,55 @@ impl InsuranceFundOperation { } } } + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum PerpLpOperation { + TrackAmmRevenue = 0b00000001, + SettleQuoteOwed = 0b00000010, +} + +const ALL_PERP_LP_OPERATIONS: [PerpLpOperation; 2] = [ + PerpLpOperation::TrackAmmRevenue, + PerpLpOperation::SettleQuoteOwed, +]; + +impl PerpLpOperation { + pub fn is_operation_paused(current: u8, operation: PerpLpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_PERP_LP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum ConstituentLpOperation { + Swap = 0b00000001, + Deposit = 0b00000010, + Withdraw = 0b00000100, +} + +const ALL_CONSTITUENT_LP_OPERATIONS: [ConstituentLpOperation; 3] = [ + ConstituentLpOperation::Swap, + ConstituentLpOperation::Deposit, + ConstituentLpOperation::Withdraw, +]; + +impl ConstituentLpOperation { + pub fn is_operation_paused(current: u8, operation: ConstituentLpOperation) -> bool { + current & operation as u8 != 0 + } + + pub fn log_all_operations_paused(current: u8) { + for operation in ALL_CONSTITUENT_LP_OPERATIONS.iter() { + if Self::is_operation_paused(current, *operation) { + msg!("{:?} is paused", operation); + } + } + } +} diff --git a/programs/drift/src/state/perp_market.rs b/programs/drift/src/state/perp_market.rs index c373794967..48e8f26501 100644 --- a/programs/drift/src/state/perp_market.rs +++ b/programs/drift/src/state/perp_market.rs @@ -1,5 +1,7 @@ +use crate::msg; +use crate::state::fill_mode::FillMode; use crate::state::pyth_lazer_oracle::PythLazerOracle; -use crate::state::user::MarketType; +use crate::state::user::{MarketType, Order}; use anchor_lang::prelude::*; use crate::state::state::{State, ValidityGuardRails}; @@ -7,7 +9,7 @@ use std::cmp::max; use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; -use crate::math::amm; +use crate::math::amm::{self}; use crate::math::casting::Cast; #[cfg(test)] use crate::math::constants::{AMM_RESERVE_PRECISION, MAX_CONCENTRATION_COEFFICIENT}; @@ -21,8 +23,8 @@ use crate::math::constants::{ SPOT_WEIGHT_PRECISION, TWENTY_FOUR_HOUR, }; use crate::math::margin::{ - calculate_size_discount_asset_weight, calculate_size_premium_liability_weight, - MarginRequirementType, + calc_high_leverage_mode_initial_margin_ratio_from_size, calculate_size_discount_asset_weight, + calculate_size_premium_liability_weight, MarginRequirementType, }; use crate::math::safe_math::SafeMath; use crate::math::stats; @@ -42,7 +44,9 @@ use static_assertions::const_assert_eq; use super::oracle_map::OracleIdentifier; use super::protected_maker_mode_config::ProtectedMakerParams; -use crate::math::oracle::{oracle_validity, LogMode, OracleValidity}; +use crate::math::oracle::{ + is_oracle_valid_for_action, oracle_validity, DriftAction, LogMode, OracleValidity, +}; #[cfg(test)] mod tests; @@ -87,6 +91,23 @@ impl MarketStatus { } } +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] +pub enum LpStatus { + /// Not considered + #[default] + Uncollateralized, + /// all operations allowed + Active, + /// Decommissioning + Decommissioning, +} + +impl LpStatus { + pub fn is_collateralized(&self) -> bool { + !matches!(self, LpStatus::Uncollateralized) + } +} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] pub enum ContractType { #[default] @@ -133,13 +154,6 @@ impl ContractTier { } } -#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, PartialOrd, Ord)] -pub enum AMMAvailability { - Immediate, - AfterMinDuration, - Unavailable, -} - #[account(zero_copy(unsafe))] #[derive(Eq, PartialEq, Debug)] #[repr(C)] @@ -232,9 +246,13 @@ pub struct PerpMarket { pub high_leverage_margin_ratio_maintenance: u16, pub protected_maker_limit_price_divisor: u8, pub protected_maker_dynamic_divisor: u8, - pub padding1: u32, + pub lp_fee_transfer_scalar: u8, + pub lp_status: u8, + pub lp_paused_operations: u8, + pub lp_exchange_fee_excluscion_scalar: u8, pub last_fill_price: u64, - pub padding: [u8; 24], + pub lp_pool_id: u8, + pub padding: [u8; 23], } impl Default for PerpMarket { @@ -276,9 +294,13 @@ impl Default for PerpMarket { high_leverage_margin_ratio_maintenance: 0, protected_maker_limit_price_divisor: 0, protected_maker_dynamic_divisor: 0, - padding1: 0, + lp_fee_transfer_scalar: 0, + lp_status: 0, + lp_exchange_fee_excluscion_scalar: 0, + lp_paused_operations: 0, last_fill_price: 0, - padding: [0; 24], + lp_pool_id: 0, + padding: [0; 23], } } } @@ -325,16 +347,12 @@ impl PerpMarket { let amm_low_inventory_and_profitable = self.amm.net_revenue_since_last_funding >= DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT && amm_has_low_enough_inventory; - let amm_oracle_no_latency = self.amm.oracle_source == OracleSource::Prelaunch - || (self.amm.historical_oracle_data.last_oracle_delay == 0 - && self.amm.oracle_source == OracleSource::PythLazer); - let can_skip = amm_low_inventory_and_profitable || amm_oracle_no_latency; - if can_skip { + if amm_low_inventory_and_profitable { msg!("market {} amm skipping auction duration", self.market_index); } - Ok(can_skip) + Ok(amm_low_inventory_and_profitable) } pub fn has_too_much_drawdown(&self) -> DriftResult { @@ -436,18 +454,19 @@ impl PerpMarket { user_high_leverage_mode: bool, ) -> DriftResult { if self.status == MarketStatus::Settlement { - return Ok(0); // no liability weight on size + return Ok(0); } - let (margin_ratio_initial, margin_ratio_maintenance) = - if user_high_leverage_mode && self.is_high_leverage_mode_enabled() { - ( - self.high_leverage_margin_ratio_initial.cast::()?, - self.high_leverage_margin_ratio_maintenance.cast::()?, - ) - } else { - (self.margin_ratio_initial, self.margin_ratio_maintenance) - }; + let is_high_leverage_user = user_high_leverage_mode && self.is_high_leverage_mode_enabled(); + + let (margin_ratio_initial, margin_ratio_maintenance) = if is_high_leverage_user { + ( + self.high_leverage_margin_ratio_initial.cast::()?, + self.high_leverage_margin_ratio_maintenance.cast::()?, + ) + } else { + (self.margin_ratio_initial, self.margin_ratio_maintenance) + }; let default_margin_ratio = match margin_type { MarginRequirementType::Initial => margin_ratio_initial, @@ -457,14 +476,43 @@ impl PerpMarket { MarginRequirementType::Maintenance => margin_ratio_maintenance, }; - let size_adj_margin_ratio = calculate_size_premium_liability_weight( - size, - self.imf_factor, - default_margin_ratio, - MARGIN_PRECISION_U128, - )?; - - let margin_ratio = default_margin_ratio.max(size_adj_margin_ratio); + let margin_ratio = + if is_high_leverage_user && margin_type != MarginRequirementType::Maintenance { + // use HLM maintenance margin but ordinary mode initial/fill margin for size adj calculation + let pre_size_adj_margin_ratio = match margin_type { + MarginRequirementType::Initial => self.margin_ratio_initial, + MarginRequirementType::Fill => { + self.margin_ratio_initial + .safe_add(self.margin_ratio_maintenance)? + / 2 + } + MarginRequirementType::Maintenance => margin_ratio_maintenance, + }; + + let size_adj_margin_ratio = calculate_size_premium_liability_weight( + size, + self.imf_factor, + pre_size_adj_margin_ratio, + MARGIN_PRECISION_U128, + false, + )?; + + calc_high_leverage_mode_initial_margin_ratio_from_size( + pre_size_adj_margin_ratio, + size_adj_margin_ratio, + default_margin_ratio, + )? + } else { + let size_adj_margin_ratio = calculate_size_premium_liability_weight( + size, + self.imf_factor, + default_margin_ratio, + MARGIN_PRECISION_U128, + true, + )?; + + default_margin_ratio.max(size_adj_margin_ratio) + }; Ok(margin_ratio) } @@ -702,14 +750,6 @@ impl PerpMarket { } } - pub fn get_min_perp_auction_duration(&self, default_min_auction_duration: u8) -> u8 { - if self.amm.taker_speed_bump_override != 0 { - self.amm.taker_speed_bump_override.max(0).unsigned_abs() - } else { - default_min_auction_duration - } - } - pub fn get_trigger_price( &self, oracle_price: i64, @@ -737,9 +777,11 @@ impl PerpMarket { let oracle_plus_funding_basis = oracle_price.safe_add(last_funding_basis)?.cast::()?; let median_price = if last_fill_price > 0 { - println!( + msg!( "last_fill_price: {} oracle_plus_funding_basis: {} oracle_plus_basis_5min: {}", - last_fill_price, oracle_plus_funding_basis, oracle_plus_basis_5min + last_fill_price, + oracle_plus_funding_basis, + oracle_plus_basis_5min ); let mut prices = [ last_fill_price, @@ -846,6 +888,7 @@ impl PerpMarket { &self.amm.oracle_source, LogMode::MMOracle, self.amm.oracle_slot_delay_override, + self.amm.oracle_low_risk_slot_delay_override, )? }; Ok(MMOraclePriceData::new( @@ -856,6 +899,83 @@ impl PerpMarket { oracle_price_data, )?) } + + pub fn amm_can_fill_order( + &self, + order: &Order, + clock_slot: u64, + fill_mode: FillMode, + state: &State, + safe_oracle_validity: OracleValidity, + user_can_skip_auction_duration: bool, + mm_oracle_price_data: &MMOraclePriceData, + ) -> DriftResult { + if self.is_operation_paused(PerpOperation::AmmFill) { + return Ok(false); + } + + if self.has_too_much_drawdown()? { + return Ok(false); + } + + // We are already using safe oracle data from MM oracle. + // But AMM isnt available if we could have used MM oracle but fell back due to price diff + // This is basically early volatility protection + let mm_oracle_not_too_volatile = + if mm_oracle_price_data.is_enabled() && mm_oracle_price_data.is_mm_oracle_as_recent() { + let amm_available = !mm_oracle_price_data.is_mm_exchange_diff_bps_high(); + amm_available + } else { + true + }; + + if !mm_oracle_not_too_volatile { + msg!("AMM cannot fill order: MM oracle too volatile compared to exchange oracle"); + return Ok(false); + } + + // Determine if order is fillable with low risk + let oracle_valid_for_amm_fill_low_risk = is_oracle_valid_for_action( + safe_oracle_validity, + Some(DriftAction::FillOrderAmmLowRisk), + )?; + if !oracle_valid_for_amm_fill_low_risk { + msg!("AMM cannot fill order: oracle not valid for low risk fills"); + return Ok(false); + } + let safe_oracle_price_data = mm_oracle_price_data.get_safe_oracle_price_data(); + let can_fill_low_risk = order.is_low_risk_for_amm( + safe_oracle_price_data.delay, + clock_slot, + fill_mode.is_liquidation(), + )?; + + // Proceed if order is low risk and we can fill it. Otherwise check if we can higher risk order immediately + let can_fill_order = if can_fill_low_risk { + true + } else { + let oracle_valid_for_can_fill_immediately = is_oracle_valid_for_action( + safe_oracle_validity, + Some(DriftAction::FillOrderAmmImmediate), + )?; + if !oracle_valid_for_can_fill_immediately { + msg!("AMM cannot fill order: oracle not valid for immediate fills"); + return Ok(false); + } + let amm_wants_to_jit_make = self.amm.amm_wants_to_jit_make(order.direction)?; + let amm_has_low_enough_inventory = self + .amm + .amm_has_low_enough_inventory(amm_wants_to_jit_make)?; + let amm_can_skip_duration = + self.can_skip_auction_duration(&state, amm_has_low_enough_inventory)?; + + amm_can_skip_duration + && oracle_valid_for_can_fill_immediately + && user_can_skip_auction_duration + }; + + Ok(can_fill_order) + } } #[cfg(test)] @@ -1155,7 +1275,7 @@ pub struct AMM { pub per_lp_base: i8, /// the override for the state.min_perp_auction_duration /// 0 is no override, -1 is disable speed bump, 1-100 is literal speed bump - pub taker_speed_bump_override: i8, + pub oracle_low_risk_slot_delay_override: i8, /// signed scale amm_spread similar to fee_adjustment logic (-100 = 0, 100 = double) pub amm_spread_adjustment: i8, pub oracle_slot_delay_override: i8, @@ -1165,7 +1285,8 @@ pub struct AMM { pub reference_price_offset: i32, /// signed scale amm_spread similar to fee_adjustment logic (-100 = 0, 100 = double) pub amm_inventory_spread_adjustment: i8, - pub padding: [u8; 3], + pub reference_price_offset_deadband_pct: u8, + pub padding: [u8; 2], pub last_funding_oracle_twap: i64, } @@ -1248,21 +1369,26 @@ impl Default for AMM { last_oracle_valid: false, target_base_asset_amount_per_lp: 0, per_lp_base: 0, - taker_speed_bump_override: 0, + oracle_low_risk_slot_delay_override: 0, amm_spread_adjustment: 0, - oracle_slot_delay_override: 0, + oracle_slot_delay_override: -1, mm_oracle_sequence_id: 0, net_unsettled_funding_pnl: 0, quote_asset_amount_with_unsettled_lp: 0, reference_price_offset: 0, amm_inventory_spread_adjustment: 0, - padding: [0; 3], + reference_price_offset_deadband_pct: 0, + padding: [0; 2], last_funding_oracle_twap: 0, } } } impl AMM { + pub fn get_reference_price_offset_deadband_pct(&self) -> DriftResult { + let pct = self.reference_price_offset_deadband_pct as u128; + Ok(PERCENTAGE_PRECISION.safe_mul(pct)?.safe_div(100_u128)?) + } pub fn get_fallback_price( self, direction: &PositionDirection, @@ -1624,6 +1750,8 @@ impl AMM { #[cfg(test)] impl AMM { pub fn default_test() -> Self { + use crate::math::constants::PRICE_PRECISION_I64; + let default_reserves = 100 * AMM_RESERVE_PRECISION; // make sure tests dont have the default sqrt_k = 0 AMM { @@ -1649,6 +1777,8 @@ impl AMM { } pub fn default_btc_test() -> Self { + use crate::math::constants::PRICE_PRECISION_I64; + AMM { base_asset_reserve: 65 * AMM_RESERVE_PRECISION, quote_asset_reserve: 63015384615, diff --git a/programs/drift/src/state/perp_market/tests.rs b/programs/drift/src/state/perp_market/tests.rs index 46de2248b1..3f09e5c58c 100644 --- a/programs/drift/src/state/perp_market/tests.rs +++ b/programs/drift/src/state/perp_market/tests.rs @@ -84,76 +84,137 @@ mod get_margin_ratio { let margin_ratio_initial = perp_market .get_margin_ratio(BASE_PRECISION, MarginRequirementType::Initial, true) .unwrap(); + assert_eq!(margin_ratio_initial, MARGIN_PRECISION / 50); let margin_ratio_maintenance = perp_market .get_margin_ratio(BASE_PRECISION, MarginRequirementType::Maintenance, true) .unwrap(); + assert_eq!(margin_ratio_maintenance, MARGIN_PRECISION / 100); + let margin_ratio_fill = perp_market .get_margin_ratio(BASE_PRECISION, MarginRequirementType::Fill, true) .unwrap(); - assert_eq!(margin_ratio_initial, MARGIN_PRECISION / 50); assert_eq!( margin_ratio_fill, (MARGIN_PRECISION / 50 + MARGIN_PRECISION / 100) / 2 ); - assert_eq!(margin_ratio_maintenance, MARGIN_PRECISION / 100); } -} - -mod get_min_perp_auction_duration { - use crate::state::perp_market::{PerpMarket, AMM}; - use crate::State; #[test] - fn test_get_speed_bump() { + fn new_hlm_imf_size_loop() { let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: 0, - ..AMM::default() - }, + margin_ratio_initial: MARGIN_PRECISION / 20, + margin_ratio_maintenance: MARGIN_PRECISION / 33, + high_leverage_margin_ratio_initial: (MARGIN_PRECISION / 100) as u16, + high_leverage_margin_ratio_maintenance: (MARGIN_PRECISION / 151) as u16, + imf_factor: 50, ..PerpMarket::default() }; - let state = State { - min_perp_auction_duration: 10, - ..State::default() + let mut cnt = 0; + + for i in 1..1_000 { + let hlm_margin_ratio_initial = perp_market + .get_margin_ratio( + BASE_PRECISION * i * 1000, + MarginRequirementType::Initial, + true, + ) + .unwrap(); + + let margin_ratio_initial = perp_market + .get_margin_ratio( + BASE_PRECISION * i * 1000, + MarginRequirementType::Initial, + false, + ) + .unwrap(); + + if margin_ratio_initial != perp_market.margin_ratio_initial { + // crate::msg!("{}", BASE_PRECISION * i); + assert_eq!(hlm_margin_ratio_initial, margin_ratio_initial); + cnt += 1; + } + } + + assert_eq!(cnt, 959_196 / 1_000); + } + + #[test] + fn new_hlm_imf_size() { + let perp_market = PerpMarket { + margin_ratio_initial: MARGIN_PRECISION / 10, + margin_ratio_maintenance: MARGIN_PRECISION / 20, + high_leverage_margin_ratio_initial: (MARGIN_PRECISION / 100) as u16, + high_leverage_margin_ratio_maintenance: (MARGIN_PRECISION / 200) as u16, + imf_factor: 50, + ..PerpMarket::default() }; - // no override uses state value + let normal_margin_ratio_initial = perp_market + .get_margin_ratio( + BASE_PRECISION * 1000000, + MarginRequirementType::Initial, + false, + ) + .unwrap(); + + assert_eq!(normal_margin_ratio_initial, 1300); + + let hlm_margin_ratio_initial = perp_market + .get_margin_ratio(BASE_PRECISION / 10, MarginRequirementType::Initial, true) + .unwrap(); + assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 10 + hlm_margin_ratio_initial, + perp_market.high_leverage_margin_ratio_initial as u32 ); - let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: -1, - ..AMM::default() - }, - ..PerpMarket::default() - }; + let hlm_margin_ratio_initial = perp_market + .get_margin_ratio(BASE_PRECISION, MarginRequirementType::Initial, true) + .unwrap(); - // -1 override disables speed bump assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 0 + hlm_margin_ratio_initial, + perp_market.high_leverage_margin_ratio_initial as u32 ); - let perp_market = PerpMarket { - amm: AMM { - taker_speed_bump_override: 20, - ..AMM::default() - }, - ..PerpMarket::default() - }; + let hlm_margin_ratio_initial = perp_market + .get_margin_ratio(BASE_PRECISION * 10, MarginRequirementType::Initial, true) + .unwrap(); - // positive override uses override value assert_eq!( - perp_market.get_min_perp_auction_duration(state.min_perp_auction_duration), - 20 + hlm_margin_ratio_initial, + 104 // slightly under 100x at 10 base ); + + let hlm_margin_ratio_initial_sized = perp_market + .get_margin_ratio(BASE_PRECISION * 3000, MarginRequirementType::Initial, true) + .unwrap(); + assert_eq!(hlm_margin_ratio_initial_sized, 221); + assert!( + hlm_margin_ratio_initial_sized > perp_market.high_leverage_margin_ratio_initial as u32 + ); + + let hlm_margin_ratio_maint = perp_market + .get_margin_ratio( + BASE_PRECISION * 3000, + MarginRequirementType::Maintenance, + true, + ) + .unwrap(); + assert_eq!(hlm_margin_ratio_maint, 67); // hardly changed + + let hlm_margin_ratio_maint = perp_market + .get_margin_ratio( + BASE_PRECISION * 300000, + MarginRequirementType::Maintenance, + true, + ) + .unwrap(); + assert_eq!(hlm_margin_ratio_maint, 313); // changed more at large size } } @@ -336,3 +397,243 @@ mod get_trigger_price { assert_eq!(clamped_price, large_oracle_price + max_oracle_diff_large); } } + +mod amm_can_fill_order_tests { + use crate::controller::position::PositionDirection; + use crate::math::oracle::OracleValidity; + use crate::state::fill_mode::FillMode; + use crate::state::oracle::{MMOraclePriceData, OraclePriceData}; + use crate::state::paused_operations::PerpOperation; + use crate::state::perp_market::{PerpMarket, AMM}; + use crate::state::state::{State, ValidityGuardRails}; + use crate::state::user::{Order, OrderStatus}; + use crate::PRICE_PRECISION_I64; + + fn base_state() -> State { + State { + min_perp_auction_duration: 10, + ..State::default() + } + } + + fn base_market() -> PerpMarket { + PerpMarket { + amm: AMM { + mm_oracle_price: PRICE_PRECISION_I64, + mm_oracle_slot: 0, + order_step_size: 1, + amm_jit_intensity: 100, + ..AMM::default() + }, + ..PerpMarket::default() + } + } + + fn base_order() -> Order { + Order { + status: OrderStatus::Open, + slot: 0, + base_asset_amount: 1, + direction: PositionDirection::Long, + ..Order::default() + } + } + + fn mm_oracle_ok_and_as_recent() -> (MMOraclePriceData, ValidityGuardRails) { + let exchange = OraclePriceData { + price: PRICE_PRECISION_I64, + confidence: 1, + delay: 5, + has_sufficient_number_of_data_points: true, + sequence_id: Some(100), + }; + let mm = + MMOraclePriceData::new(PRICE_PRECISION_I64, 5, 100, OracleValidity::Valid, exchange) + .unwrap(); + (mm, ValidityGuardRails::default()) + } + + #[test] + fn paused_operation_returns_false() { + let mut market = base_market(); + // Pause AMM fill + market.paused_operations = PerpOperation::AmmFill as u8; + let order = base_order(); + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + let can = market + .amm_can_fill_order( + &order, + 10, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } + + #[test] + fn mm_oracle_too_volatile_blocks() { + let market = base_market(); + let order = base_order(); + let state = base_state(); + + // Create MM oracle data with >1% diff vs exchange to force fallback and block + let exchange = OraclePriceData { + price: PRICE_PRECISION_I64, // 1.0 + confidence: 1, + delay: 1, + has_sufficient_number_of_data_points: true, + sequence_id: Some(100), + }; + // 3% higher than exchange + let mm = MMOraclePriceData::new( + PRICE_PRECISION_I64 + (PRICE_PRECISION_I64 / 33), + 1, + 99, + OracleValidity::Valid, + exchange, + ) + .unwrap(); + + let can = market + .amm_can_fill_order( + &order, + 10, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } + + #[test] + fn low_risk_path_succeeds_when_auction_elapsed() { + let market = base_market(); + let mut order = base_order(); + order.slot = 0; + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // clock_slot sufficiently beyond min_auction_duration + let can = market + .amm_can_fill_order( + &order, + 15, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(can); + } + + #[test] + fn low_risk_path_succeeds_when_auction_elapsed_with_stale_for_immediate() { + let market = base_market(); + let mut order = base_order(); + order.slot = 0; // order placed at slot 0 + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // clock_slot sufficiently beyond min_auction_duration + let can = market + .amm_can_fill_order( + &order, + 15, + FillMode::Fill, + &state, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: false, + }, + true, + &mm, + ) + .unwrap(); + assert!(can); + } + + #[test] + fn high_risk_immediate_requires_user_and_market_skip() { + let mut market = base_market(); + let mut order = base_order(); + order.slot = 20; + market.amm.amm_jit_intensity = 100; + + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // cnat fill if user cant skip auction duration + let can1 = market + .amm_can_fill_order( + &order, + 21, + FillMode::Fill, + &state, + OracleValidity::Valid, + false, + &mm, + ) + .unwrap(); + assert!(!can1); + + // valid oracle for immediate and user can skip, market can skip due to low inventory => can fill + market.amm.base_asset_amount_with_amm = -2; // taker long improves balance + market.amm.order_step_size = 1; + market.amm.base_asset_reserve = 1_000_000; + market.amm.quote_asset_reserve = 1_000_000; + market.amm.sqrt_k = 1_000_000; + market.amm.max_base_asset_reserve = 2_000_000; + market.amm.min_base_asset_reserve = 0; + + let can2 = market + .amm_can_fill_order( + &order, + 21, + FillMode::Fill, + &state, + OracleValidity::Valid, + true, + &mm, + ) + .unwrap(); + assert!(can2); + } + + #[test] + fn invalid_safe_oracle_validity_blocks_low_risk() { + let market = base_market(); + let order = base_order(); + let state = base_state(); + let (mm, _) = mm_oracle_ok_and_as_recent(); + + // Order is old but invalid oracle validity + let can = market + .amm_can_fill_order( + &order, + 20, + FillMode::Fill, + &state, + OracleValidity::StaleForAMM { + immediate: true, + low_risk: true, + }, + true, + &mm, + ) + .unwrap(); + assert!(!can); + } +} diff --git a/programs/drift/src/state/revenue_share.rs b/programs/drift/src/state/revenue_share.rs new file mode 100644 index 0000000000..6d99427054 --- /dev/null +++ b/programs/drift/src/state/revenue_share.rs @@ -0,0 +1,572 @@ +use std::cell::{Ref, RefMut}; + +use anchor_lang::prelude::Pubkey; +use anchor_lang::*; +use anchor_lang::{account, zero_copy}; +use borsh::{BorshDeserialize, BorshSerialize}; +use prelude::AccountInfo; + +use crate::error::{DriftResult, ErrorCode}; +use crate::math::casting::Cast; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::state::user::{MarketType, OrderStatus, User}; +use crate::validate; +use crate::{msg, ID}; + +pub const REVENUE_SHARE_PDA_SEED: &str = "REV_SHARE"; +pub const REVENUE_SHARE_ESCROW_PDA_SEED: &str = "REV_ESCROW"; + +#[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq, Default)] +pub enum RevenueShareOrderBitFlag { + #[default] + Init = 0b00000000, + Open = 0b00000001, + Completed = 0b00000010, + Referral = 0b00000100, +} + +#[account(zero_copy(unsafe))] +#[derive(Eq, PartialEq, Debug, Default)] +pub struct RevenueShare { + /// the owner of this account, a builder or referrer + pub authority: Pubkey, + pub total_referrer_rewards: u64, + pub total_builder_rewards: u64, + pub padding: [u8; 18], +} + +impl RevenueShare { + pub fn space() -> usize { + 8 + 32 + 8 + 8 + 18 + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +pub struct RevenueShareOrder { + /// fees accrued so far for this order slot. This is not exclusively fees from this order_id + /// and may include fees from other orders in the same market. This may be swept to the + /// builder's SpotPosition during settle_pnl. + pub fees_accrued: u64, + /// the order_id of the current active order in this slot. It's only relevant while bit_flag = Open + pub order_id: u32, + /// the builder fee on this order, in tenths of a bps, e.g. 100 = 0.01% + pub fee_tenth_bps: u16, + pub market_index: u16, + /// the subaccount_id of the user who created this order. It's only relevant while bit_flag = Open + pub sub_account_id: u16, + /// the index of the RevenueShareEscrow.approved_builders list, that this order's fee will settle to. Ignored + /// if bit_flag = Referral. + pub builder_idx: u8, + /// bitflags that describe the state of the order. + /// [`RevenueShareOrderBitFlag::Init`]: this order slot is available for use. + /// [`RevenueShareOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order. + /// [`RevenueShareOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into. + /// the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders. + /// [`RevenueShareOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled for this market. + /// If it is set, no other bitflag should be set. + pub bit_flags: u8, + /// the index into the User's orders list when this RevenueShareOrder was created, make sure to verify that order_id matches. + pub user_order_index: u8, + pub market_type: MarketType, + pub padding: [u8; 10], +} + +impl RevenueShareOrder { + pub fn new( + builder_idx: u8, + sub_account_id: u16, + order_id: u32, + fee_tenth_bps: u16, + market_type: MarketType, + market_index: u16, + bit_flags: u8, + user_order_index: u8, + ) -> Self { + Self { + builder_idx, + order_id, + fee_tenth_bps, + market_type, + market_index, + fees_accrued: 0, + bit_flags, + sub_account_id, + user_order_index, + padding: [0; 10], + } + } + + pub fn space() -> usize { + std::mem::size_of::() + } + + pub fn add_bit_flag(&mut self, flag: RevenueShareOrderBitFlag) { + self.bit_flags |= flag as u8; + } + + pub fn is_bit_flag_set(&self, flag: RevenueShareOrderBitFlag) -> bool { + (self.bit_flags & flag as u8) != 0 + } + + // An order is Open after it is created, the slot is considered occupied + // and it is waiting to become `Completed` (filled or canceled). + pub fn is_open(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Open) + } + + // An order is Completed after it is filled or canceled. It is waiting to be settled + // into the builder's account + pub fn is_completed(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Completed) + } + + /// An order slot is available (can be written to) if it is neither Completed nor Open. + pub fn is_available(&self) -> bool { + !self.is_completed() && !self.is_open() && !self.is_referral_order() + } + + pub fn is_referral_order(&self) -> bool { + self.is_bit_flag_set(RevenueShareOrderBitFlag::Referral) + } + + /// Checks if `self` can be merged with `other`. Merged orders track cumulative fees accrued + /// and are settled together, making more efficient use of the orders list. + pub fn is_mergeable(&self, other: &RevenueShareOrder) -> bool { + (self.is_referral_order() == other.is_referral_order()) + && other.is_completed() + && other.market_index == self.market_index + && other.market_type == self.market_type + && other.builder_idx == self.builder_idx + } + + /// Merges `other` into `self`. The orders must be mergeable. + pub fn merge(mut self, other: &RevenueShareOrder) -> DriftResult { + validate!( + self.is_mergeable(other), + ErrorCode::DefaultError, + "Orders are not mergeable" + )?; + self.fees_accrued = self + .fees_accrued + .checked_add(other.fees_accrued) + .ok_or(ErrorCode::MathError)?; + Ok(self) + } +} + +#[zero_copy] +#[derive(Default, Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct BuilderInfo { + pub authority: Pubkey, // builder authority + pub max_fee_tenth_bps: u16, + pub padding: [u8; 6], +} + +impl BuilderInfo { + pub fn space() -> usize { + std::mem::size_of::() + } + + pub fn is_revoked(&self) -> bool { + self.max_fee_tenth_bps == 0 + } +} + +#[account] +#[derive(Eq, PartialEq, Debug)] +#[repr(C)] +pub struct RevenueShareEscrow { + /// the owner of this account, a user + pub authority: Pubkey, + pub referrer: Pubkey, + pub referrer_boost_expire_ts: u32, + pub referrer_reward_offset: i8, + pub referee_fee_numerator_offset: i8, + pub referrer_boost_numerator: i8, + pub reserved_fixed: [u8; 17], + pub padding0: u32, // align with [`RevenueShareEscrow::orders`] 4 bytes len prefix + pub orders: Vec, + pub padding1: u32, // align with [`RevenueShareEscrow::approved_builders`] 4 bytes len prefix + pub approved_builders: Vec, +} + +impl RevenueShareEscrow { + pub fn space(num_orders: usize, num_builders: usize) -> usize { + 8 + // discriminator + std::mem::size_of::() + // fixed header + 4 + // orders Vec length prefix + 4 + // padding0 + num_orders * std::mem::size_of::() + // orders data + 4 + // approved_builders Vec length prefix + 4 + // padding1 + num_builders * std::mem::size_of::() // builders data + } + + pub fn validate(&self) -> DriftResult<()> { + validate!( + self.orders.len() <= 128 && self.approved_builders.len() <= 128, + ErrorCode::DefaultError, + "RevenueShareEscrow orders and approved_builders len must be between 1 and 128" + )?; + Ok(()) + } +} + +#[zero_copy] +#[derive(Eq, PartialEq, Debug, BorshDeserialize, BorshSerialize)] +#[repr(C)] +pub struct RevenueShareEscrowFixed { + pub authority: Pubkey, + pub referrer: Pubkey, + pub referrer_boost_expire_ts: u32, + pub referrer_reward_offset: i8, + pub referee_fee_numerator_offset: i8, + pub referrer_boost_numerator: i8, + pub reserved_fixed: [u8; 17], +} + +impl Default for RevenueShareEscrowFixed { + fn default() -> Self { + Self { + authority: Pubkey::default(), + referrer: Pubkey::default(), + referrer_boost_expire_ts: 0, + referrer_reward_offset: 0, + referee_fee_numerator_offset: 0, + referrer_boost_numerator: 0, + reserved_fixed: [0; 17], + } + } +} + +impl Default for RevenueShareEscrow { + fn default() -> Self { + Self { + authority: Pubkey::default(), + referrer: Pubkey::default(), + referrer_boost_expire_ts: 0, + referrer_reward_offset: 0, + referee_fee_numerator_offset: 0, + referrer_boost_numerator: 0, + reserved_fixed: [0; 17], + padding0: 0, + orders: Vec::new(), + padding1: 0, + approved_builders: Vec::new(), + } + } +} + +pub struct RevenueShareEscrowZeroCopy<'a> { + pub fixed: Ref<'a, RevenueShareEscrowFixed>, + pub data: Ref<'a, [u8]>, +} + +impl<'a> RevenueShareEscrowZeroCopy<'a> { + pub fn orders_len(&self) -> u32 { + let length_bytes = &self.data[4..8]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + pub fn approved_builders_len(&self) -> u32 { + let orders_data_size = + self.orders_len() as usize * std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + orders_data_size + 4; // RevenueShareEscrow.padding1 + let length_bytes = &self.data[offset..offset + 4]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + + pub fn get_order(&self, index: u32) -> DriftResult<&RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; // orders data + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn get_approved_builder(&self, index: u32) -> DriftResult<&BuilderInfo> { + validate!( + index < self.approved_builders_len(), + ErrorCode::DefaultError, + "Builder index out of bounds" + )?; + let size = std::mem::size_of::(); + let offset = 4 + 4 + // Skip orders Vec length prefix + padding0 + self.orders_len() as usize * std::mem::size_of::() + // orders data + 4; // Skip approved_builders Vec length prefix + padding1 + let start = offset + index as usize * size; + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn iter_orders(&self) -> impl Iterator> + '_ { + (0..self.orders_len()).map(move |i| self.get_order(i)) + } + + pub fn iter_approved_builders(&self) -> impl Iterator> + '_ { + (0..self.approved_builders_len()).map(move |i| self.get_approved_builder(i)) + } +} + +pub struct RevenueShareEscrowZeroCopyMut<'a> { + pub fixed: RefMut<'a, RevenueShareEscrowFixed>, + pub data: RefMut<'a, [u8]>, +} + +impl<'a> RevenueShareEscrowZeroCopyMut<'a> { + pub fn has_referrer(&self) -> bool { + self.fixed.referrer != Pubkey::default() + } + + pub fn get_referrer(&self) -> Option { + if self.has_referrer() { + Some(self.fixed.referrer) + } else { + None + } + } + + pub fn orders_len(&self) -> u32 { + // skip RevenueShareEscrow.padding0 + let length_bytes = &self.data[4..8]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + pub fn approved_builders_len(&self) -> u32 { + // Calculate offset to the approved_builders Vec length + let orders_data_size = + self.orders_len() as usize * std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + orders_data_size + + 4; // RevenueShareEscrow.padding1 + let length_bytes = &self.data[offset..offset + 4]; + u32::from_le_bytes([ + length_bytes[0], + length_bytes[1], + length_bytes[2], + length_bytes[3], + ]) + } + + pub fn get_order_mut(&mut self, index: u32) -> DriftResult<&mut RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; + Ok(bytemuck::from_bytes_mut( + &mut self.data[start..(start + size)], + )) + } + + /// Returns the index of an order for a given sub_account_id and order_id, if present. + pub fn find_order_index(&self, sub_account_id: u16, order_id: u32) -> Option { + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.order_id == order_id + && existing_order.sub_account_id == sub_account_id + { + return Some(i); + } + } + } + None + } + + /// Returns the index for the referral order, creating one if necessary. Returns None if a new order + /// cannot be created. + pub fn find_or_create_referral_index(&mut self, market_index: u16) -> Option { + // look for an existing referral order + for i in 0..self.orders_len() { + if let Ok(existing_order) = self.get_order(i) { + if existing_order.is_referral_order() && existing_order.market_index == market_index + { + return Some(i); + } + } + } + + // try to create a referral order in an available order slot + match self.add_order(RevenueShareOrder::new( + 0, + 0, + 0, + 0, + MarketType::Perp, + market_index, + RevenueShareOrderBitFlag::Referral as u8, + 0, + )) { + Ok(idx) => Some(idx), + Err(_) => { + msg!("Failed to add referral order, RevenueShareEscrow is full"); + None + } + } + } + + pub fn get_order(&self, index: u32) -> DriftResult<&RevenueShareOrder> { + validate!( + index < self.orders_len(), + ErrorCode::DefaultError, + "Order index out of bounds" + )?; + let size = std::mem::size_of::(); + let start = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + index as usize * size; // orders data + Ok(bytemuck::from_bytes(&self.data[start..start + size])) + } + + pub fn get_approved_builder_mut(&mut self, index: u8) -> DriftResult<&mut BuilderInfo> { + validate!( + index < self.approved_builders_len().cast::()?, + ErrorCode::DefaultError, + "Builder index out of bounds, index: {}, orderslen: {}, builderslen: {}", + index, + self.orders_len(), + self.approved_builders_len() + )?; + let size = std::mem::size_of::(); + let offset = 4 + // RevenueShareEscrow.padding0 + 4 + // vec len + self.orders_len() as usize * std::mem::size_of::() + // orders data + 4 + // RevenueShareEscrow.padding1 + 4; // vec len + let start = offset + index as usize * size; + Ok(bytemuck::from_bytes_mut( + &mut self.data[start..start + size], + )) + } + + pub fn add_order(&mut self, order: RevenueShareOrder) -> DriftResult { + for i in 0..self.orders_len() { + let existing_order = self.get_order_mut(i)?; + if existing_order.is_mergeable(&order) { + *existing_order = existing_order.merge(&order)?; + return Ok(i); + } else if existing_order.is_available() { + *existing_order = order; + return Ok(i); + } + } + + Err(ErrorCode::RevenueShareEscrowOrdersAccountFull.into()) + } + + /// Marks any [`RevenueShareOrder`]s as Complete if there is no longer a corresponding + /// open order in the user's account. This is used to lazily reconcile state when + /// in place_order and settle_pnl instead of requiring explicit updates on cancels. + pub fn revoke_completed_orders(&mut self, user: &User) -> DriftResult<()> { + for i in 0..self.orders_len() { + if let Ok(rev_share_order) = self.get_order_mut(i) { + if rev_share_order.is_referral_order() { + continue; + } + if user.sub_account_id != rev_share_order.sub_account_id { + continue; + } + if rev_share_order.is_open() && !rev_share_order.is_completed() { + let user_order = user.orders[rev_share_order.user_order_index as usize]; + let still_open = user_order.status == OrderStatus::Open + && user_order.order_id == rev_share_order.order_id; + if !still_open { + if rev_share_order.fees_accrued > 0 { + rev_share_order.add_bit_flag(RevenueShareOrderBitFlag::Completed); + } else { + // order had no fees accrued, we can just clear out the slot + *rev_share_order = RevenueShareOrder::default(); + } + } + } + } + } + + Ok(()) + } +} + +pub trait RevenueShareEscrowLoader<'a> { + fn load_zc(&self) -> DriftResult; + fn load_zc_mut(&self) -> DriftResult; +} + +impl<'a> RevenueShareEscrowLoader<'a> for AccountInfo<'a> { + fn load_zc(&self) -> DriftResult { + let owner = self.owner; + + validate!( + owner == &ID, + ErrorCode::DefaultError, + "invalid RevenueShareEscrow owner", + )?; + + let data = self.try_borrow_data().safe_unwrap()?; + + let (discriminator, data) = Ref::map_split(data, |d| d.split_at(8)); + validate!( + *discriminator == RevenueShareEscrow::discriminator(), + ErrorCode::DefaultError, + "invalid signed_msg user orders discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (fixed, data) = Ref::map_split(data, |d| d.split_at(hdr_size)); + Ok(RevenueShareEscrowZeroCopy { + fixed: Ref::map(fixed, |b| bytemuck::from_bytes(b)), + data, + }) + } + + fn load_zc_mut(&self) -> DriftResult { + let owner = self.owner; + + validate!( + owner == &ID, + ErrorCode::DefaultError, + "invalid RevenueShareEscrow owner", + )?; + + let data = self.try_borrow_mut_data().safe_unwrap()?; + + let (discriminator, data) = RefMut::map_split(data, |d| d.split_at_mut(8)); + validate!( + *discriminator == RevenueShareEscrow::discriminator(), + ErrorCode::DefaultError, + "invalid signed_msg user orders discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (fixed, data) = RefMut::map_split(data, |d| d.split_at_mut(hdr_size)); + Ok(RevenueShareEscrowZeroCopyMut { + fixed: RefMut::map(fixed, |b| bytemuck::from_bytes_mut(b)), + data, + }) + } +} diff --git a/programs/drift/src/state/revenue_share_map.rs b/programs/drift/src/state/revenue_share_map.rs new file mode 100644 index 0000000000..2e45195040 --- /dev/null +++ b/programs/drift/src/state/revenue_share_map.rs @@ -0,0 +1,209 @@ +use crate::error::{DriftResult, ErrorCode}; +use crate::math::safe_unwrap::SafeUnwrap; +use crate::msg; +use crate::state::revenue_share::RevenueShare; +use crate::state::traits::Size; +use crate::state::user::User; +use crate::validate; +use anchor_lang::prelude::AccountLoader; +use anchor_lang::Discriminator; +use arrayref::array_ref; +use solana_program::account_info::AccountInfo; +use solana_program::pubkey::Pubkey; +use std::cell::RefMut; +use std::collections::BTreeMap; +use std::iter::Peekable; +use std::panic::Location; +use std::slice::Iter; + +pub struct RevenueShareEntry<'a> { + pub user: Option>, + pub revenue_share: Option>, +} + +impl<'a> Default for RevenueShareEntry<'a> { + fn default() -> Self { + Self { + user: None, + revenue_share: None, + } + } +} + +pub struct RevenueShareMap<'a>(pub BTreeMap>); + +impl<'a> RevenueShareMap<'a> { + pub fn empty() -> Self { + RevenueShareMap(BTreeMap::new()) + } + + pub fn insert_user( + &mut self, + authority: Pubkey, + user_loader: AccountLoader<'a, User>, + ) -> DriftResult { + let entry = self.0.entry(authority).or_default(); + validate!( + entry.user.is_none(), + ErrorCode::DefaultError, + "Duplicate User for authority {:?}", + authority + )?; + entry.user = Some(user_loader); + Ok(()) + } + + pub fn insert_revenue_share( + &mut self, + authority: Pubkey, + revenue_share_loader: AccountLoader<'a, RevenueShare>, + ) -> DriftResult { + let entry = self.0.entry(authority).or_default(); + validate!( + entry.revenue_share.is_none(), + ErrorCode::DefaultError, + "Duplicate RevenueShare for authority {:?}", + authority + )?; + entry.revenue_share = Some(revenue_share_loader); + Ok(()) + } + + #[track_caller] + #[inline(always)] + pub fn get_user_ref_mut(&self, authority: &Pubkey) -> DriftResult> { + let loader = match self.0.get(authority).and_then(|e| e.user.as_ref()) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find user for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + return Err(ErrorCode::UserNotFound); + } + }; + + match loader.load_mut() { + Ok(user) => Ok(user), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load user for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadUserAccount) + } + } + } + + #[track_caller] + #[inline(always)] + pub fn get_revenue_share_account_mut( + &self, + authority: &Pubkey, + ) -> DriftResult> { + let loader = match self.0.get(authority).and_then(|e| e.revenue_share.as_ref()) { + Some(loader) => loader, + None => { + let caller = Location::caller(); + msg!( + "Could not find revenue share for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + return Err(ErrorCode::UnableToLoadRevenueShareAccount); + } + }; + + match loader.load_mut() { + Ok(revenue_share) => Ok(revenue_share), + Err(e) => { + let caller = Location::caller(); + msg!("{:?}", e); + msg!( + "Could not load revenue share for authority {} at {}:{}", + authority, + caller.file(), + caller.line() + ); + Err(ErrorCode::UnableToLoadRevenueShareAccount) + } + } + } +} + +pub fn load_revenue_share_map<'a: 'b, 'b>( + account_info_iter: &mut Peekable>>, +) -> DriftResult> { + let mut revenue_share_map = RevenueShareMap::empty(); + + let user_discriminator: [u8; 8] = User::discriminator(); + let rev_share_discriminator: [u8; 8] = RevenueShare::discriminator(); + + while let Some(account_info) = account_info_iter.peek() { + let data = account_info + .try_borrow_data() + .or(Err(ErrorCode::DefaultError))?; + + if data.len() < 8 { + break; + } + + let account_discriminator = array_ref![data, 0, 8]; + + if account_discriminator == &user_discriminator { + let user_account_info = account_info_iter.next().safe_unwrap()?; + let is_writable = user_account_info.is_writable; + if !is_writable { + return Err(ErrorCode::UserWrongMutability); + } + + // Extract authority from User account data (after discriminator) + let data = user_account_info + .try_borrow_data() + .or(Err(ErrorCode::CouldNotLoadUserData))?; + let expected_data_len = User::SIZE; + if data.len() < expected_data_len { + return Err(ErrorCode::CouldNotLoadUserData); + } + let authority_slice = array_ref![data, 8, 32]; + let authority = Pubkey::from(*authority_slice); + + let user_account_loader: AccountLoader = + AccountLoader::try_from(user_account_info) + .or(Err(ErrorCode::InvalidUserAccount))?; + + revenue_share_map.insert_user(authority, user_account_loader)?; + continue; + } + + if account_discriminator == &rev_share_discriminator { + let revenue_share_account_info = account_info_iter.next().safe_unwrap()?; + let is_writable = revenue_share_account_info.is_writable; + if !is_writable { + return Err(ErrorCode::DefaultError); + } + + let authority_slice = array_ref![data, 8, 32]; + let authority = Pubkey::from(*authority_slice); + + let revenue_share_account_loader: AccountLoader = + AccountLoader::try_from(revenue_share_account_info) + .or(Err(ErrorCode::InvalidRevenueShareAccount))?; + + revenue_share_map.insert_revenue_share(authority, revenue_share_account_loader)?; + continue; + } + + break; + } + + Ok(revenue_share_map) +} diff --git a/programs/drift/src/state/spot_market.rs b/programs/drift/src/state/spot_market.rs index 84a0cdd89d..c42000a7b1 100644 --- a/programs/drift/src/state/spot_market.rs +++ b/programs/drift/src/state/spot_market.rs @@ -9,7 +9,8 @@ use borsh::{BorshDeserialize, BorshSerialize}; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; use crate::math::constants::{ - AMM_RESERVE_PRECISION, FIVE_MINUTE, MARGIN_PRECISION, ONE_HOUR, SPOT_WEIGHT_PRECISION_U128, + AMM_RESERVE_PRECISION, FIVE_MINUTE, MARGIN_PRECISION, ONE_HOUR, PERCENTAGE_PRECISION, + SPOT_WEIGHT_PRECISION_U128, }; #[cfg(test)] use crate::math::constants::{PRICE_PRECISION_I64, SPOT_CUMULATIVE_INTEREST_PRECISION}; @@ -25,7 +26,7 @@ use crate::state::oracle::{HistoricalIndexData, HistoricalOracleData, OracleSour use crate::state::paused_operations::{InsuranceFundOperation, SpotOperation}; use crate::state::perp_market::{MarketStatus, PoolBalance}; use crate::state::traits::{MarketIndexOffset, Size}; -use crate::{validate, PERCENTAGE_PRECISION}; +use crate::validate; use super::oracle_map::OracleIdentifier; @@ -426,6 +427,7 @@ impl SpotMarket { self.imf_factor, default_liability_weight, SPOT_WEIGHT_PRECISION_U128, + true, )?; let liability_weight = size_based_liability_weight.max(default_liability_weight); diff --git a/programs/drift/src/state/state.rs b/programs/drift/src/state/state.rs index 9e8b0961df..07e9213396 100644 --- a/programs/drift/src/state/state.rs +++ b/programs/drift/src/state/state.rs @@ -3,12 +3,12 @@ use enumflags2::BitFlags; use crate::error::DriftResult; use crate::math::constants::{ - FEE_DENOMINATOR, FEE_PERCENTAGE_DENOMINATOR, MAX_REFERRER_REWARD_EPOCH_UPPER_BOUND, + FEE_DENOMINATOR, FEE_PERCENTAGE_DENOMINATOR, LAMPORTS_PER_SOL_U64, + MAX_REFERRER_REWARD_EPOCH_UPPER_BOUND, PERCENTAGE_PRECISION_U64, }; use crate::math::safe_math::SafeMath; use crate::math::safe_unwrap::SafeUnwrap; use crate::state::traits::Size; -use crate::{LAMPORTS_PER_SOL_U64, PERCENTAGE_PRECISION_U64}; #[cfg(test)] mod tests; @@ -42,7 +42,8 @@ pub struct State { pub max_number_of_sub_accounts: u16, pub max_initialize_user_fee: u16, pub feature_bit_flags: u8, - pub padding: [u8; 9], + pub lp_pool_feature_bit_flags: u8, + pub padding: [u8; 8], } #[derive(BitFlags, Clone, Copy, PartialEq, Debug, Eq)] @@ -120,12 +121,41 @@ impl State { pub fn use_median_trigger_price(&self) -> bool { (self.feature_bit_flags & (FeatureBitFlags::MedianTriggerPrice as u8)) > 0 } + + pub fn builder_codes_enabled(&self) -> bool { + (self.feature_bit_flags & (FeatureBitFlags::BuilderCodes as u8)) > 0 + } + + pub fn builder_referral_enabled(&self) -> bool { + (self.feature_bit_flags & (FeatureBitFlags::BuilderReferral as u8)) > 0 + } + + pub fn allow_settle_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::SettleLpPool as u8)) > 0 + } + + pub fn allow_swap_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::SwapLpPool as u8)) > 0 + } + + pub fn allow_mint_redeem_lp_pool(&self) -> bool { + (self.lp_pool_feature_bit_flags & (LpPoolFeatureBitFlags::MintRedeemLpPool as u8)) > 0 + } } #[derive(Clone, Copy, PartialEq, Debug, Eq)] pub enum FeatureBitFlags { MmOracleUpdate = 0b00000001, MedianTriggerPrice = 0b00000010, + BuilderCodes = 0b00000100, + BuilderReferral = 0b00001000, +} + +#[derive(Clone, Copy, PartialEq, Debug, Eq)] +pub enum LpPoolFeatureBitFlags { + SettleLpPool = 0b00000001, + SwapLpPool = 0b00000010, + MintRedeemLpPool = 0b00000100, } impl Size for State { diff --git a/programs/drift/src/state/user.rs b/programs/drift/src/state/user.rs index 6071635e16..05295bbd32 100644 --- a/programs/drift/src/state/user.rs +++ b/programs/drift/src/state/user.rs @@ -3,8 +3,9 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::auction::{calculate_auction_price, is_auction_complete}; use crate::math::casting::Cast; use crate::math::constants::{ - EPOCH_DURATION, FUEL_OVERFLOW_THRESHOLD_U32, FUEL_START_TS, OPEN_ORDER_MARGIN_REQUIREMENT, - QUOTE_SPOT_MARKET_INDEX, THIRTY_DAY, + EPOCH_DURATION, FUEL_OVERFLOW_THRESHOLD_U32, FUEL_START_TS, MAX_PREDICTION_MARKET_PRICE, + OPEN_ORDER_MARGIN_REQUIREMENT, QUOTE_PRECISION_U64, QUOTE_SPOT_MARKET_INDEX, + SPOT_WEIGHT_PRECISION, SPOT_WEIGHT_PRECISION_I128, THIRTY_DAY, }; use crate::math::margin::MarginRequirementType; use crate::math::orders::{ @@ -18,17 +19,18 @@ use crate::math::spot_balance::{ get_signed_token_amount, get_strict_token_value, get_token_amount, get_token_value, }; use crate::math::stats::calculate_rolling_sum; +use crate::math_error; use crate::msg; +use crate::safe_increment; use crate::state::oracle::StrictOraclePrice; use crate::state::perp_market::ContractType; use crate::state::spot_market::{SpotBalance, SpotBalanceType, SpotMarket}; use crate::state::traits::Size; -use crate::{get_then_update_id, ID, QUOTE_PRECISION_U64}; -use crate::{math_error, SPOT_WEIGHT_PRECISION_I128}; -use crate::{safe_increment, SPOT_WEIGHT_PRECISION}; -use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; +use crate::validate; +use crate::{get_then_update_id, ID}; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; +use bytemuck::{Pod, Zeroable}; use std::cmp::max; use std::fmt; use std::ops::Neg; @@ -1676,6 +1678,10 @@ impl Order { self.is_bit_flag_set(OrderBitFlag::SignedMessage) } + pub fn is_has_builder(&self) -> bool { + self.is_bit_flag_set(OrderBitFlag::HasBuilder) + } + pub fn add_bit_flag(&mut self, flag: OrderBitFlag) { self.bit_flags |= flag as u8; } @@ -1693,6 +1699,26 @@ impl Order { || (self.triggered() && !(self.reduce_only && self.is_bit_flag_set(OrderBitFlag::NewTriggerReduceOnly))) } + + pub fn is_low_risk_for_amm( + &self, + mm_oracle_delay: i64, + clock_slot: u64, + is_liquidation: bool, + ) -> DriftResult { + if self.market_type == MarketType::Spot { + return Ok(false); + } + + let order_older_than_oracle_delay = { + let clock_minus_delay = clock_slot.cast::()?.safe_sub(mm_oracle_delay)?; + clock_minus_delay >= self.slot.cast::()? + }; + + Ok(order_older_than_oracle_delay + || is_liquidation + || self.is_bit_flag_set(OrderBitFlag::SafeTriggerOrder)) + } } impl Default for Order { @@ -1776,12 +1802,16 @@ impl fmt::Display for MarketType { } } +unsafe impl Zeroable for MarketType {} +unsafe impl Pod for MarketType {} + #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] pub enum OrderBitFlag { SignedMessage = 0b00000001, OracleTriggerMarket = 0b00000010, SafeTriggerOrder = 0b00000100, NewTriggerReduceOnly = 0b00001000, + HasBuilder = 0b00010000, } #[derive(Clone, Copy, BorshSerialize, BorshDeserialize, PartialEq, Debug, Eq)] @@ -1865,6 +1895,7 @@ pub struct UserStats { pub enum ReferrerStatus { IsReferrer = 0b00000001, IsReferred = 0b00000010, + BuilderReferral = 0b00000100, } impl ReferrerStatus { @@ -1875,6 +1906,10 @@ impl ReferrerStatus { pub fn is_referred(status: u8) -> bool { status & ReferrerStatus::IsReferred as u8 != 0 } + + pub fn has_builder_referral(status: u8) -> bool { + status & ReferrerStatus::BuilderReferral as u8 != 0 + } } impl Size for UserStats { @@ -2081,6 +2116,14 @@ impl UserStats { } } + pub fn update_builder_referral_status(&mut self) { + if !self.referrer.eq(&Pubkey::default()) { + self.referrer_status |= ReferrerStatus::BuilderReferral as u8; + } else { + self.referrer_status &= !(ReferrerStatus::BuilderReferral as u8); + } + } + pub fn update_fuel_overflow_status(&mut self, has_overflow: bool) { if has_overflow { self.fuel_overflow_status |= FuelOverflowStatus::Exists as u8; diff --git a/programs/drift/src/state/user/tests.rs b/programs/drift/src/state/user/tests.rs index c4e4351acc..0c819d8aae 100644 --- a/programs/drift/src/state/user/tests.rs +++ b/programs/drift/src/state/user/tests.rs @@ -2649,9 +2649,7 @@ pub mod meets_withdraw_margin_requirement_and_increment_fuel_bonus { } mod update_open_bids_and_asks { - use crate::state::user::{ - Order, OrderBitFlag, OrderTriggerCondition, OrderType, PositionDirection, - }; + use crate::state::user::{Order, OrderBitFlag, OrderTriggerCondition, OrderType}; #[test] fn test_regular_limit_order() { diff --git a/programs/drift/src/state/zero_copy.rs b/programs/drift/src/state/zero_copy.rs new file mode 100644 index 0000000000..b6a11a383f --- /dev/null +++ b/programs/drift/src/state/zero_copy.rs @@ -0,0 +1,181 @@ +use crate::error::ErrorCode; +use crate::math::safe_unwrap::SafeUnwrap; +use anchor_lang::prelude::{AccountInfo, Pubkey}; +use bytemuck::{from_bytes, from_bytes_mut}; +use bytemuck::{Pod, Zeroable}; +use std::cell::{Ref, RefMut}; +use std::marker::PhantomData; + +use crate::error::DriftResult; +use crate::msg; +use crate::validate; + +pub trait HasLen { + fn len(&self) -> u32; +} + +pub struct AccountZeroCopy<'a, T, F> { + pub fixed: Ref<'a, F>, + pub data: Ref<'a, [u8]>, + pub _marker: PhantomData, +} + +impl<'a, T, F> AccountZeroCopy<'a, T, F> +where + T: Pod + Zeroable + Clone + Copy, + F: Pod + HasLen, +{ + pub fn len(&self) -> u32 { + self.fixed.len() + } + + pub fn get(&self, index: u32) -> &T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes(&self.data[start..start + size]) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len()).map(move |i| self.get(i)) + } +} + +pub struct AccountZeroCopyMut<'a, T, F> { + pub fixed: RefMut<'a, F>, + pub data: RefMut<'a, [u8]>, + pub _marker: PhantomData, +} + +impl<'a, T, F> AccountZeroCopyMut<'a, T, F> +where + T: Pod + Zeroable + Clone + Copy, + F: Pod + HasLen, +{ + pub fn len(&self) -> u32 { + self.fixed.len() + } + + pub fn get_mut(&mut self, index: u32) -> &mut T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes_mut(&mut self.data[start..start + size]) + } + + pub fn get(&self, index: u32) -> &T { + let size = std::mem::size_of::(); + let start = index as usize * size; + bytemuck::from_bytes(&self.data[start..start + size]) + } + + pub fn iter(&self) -> impl Iterator + '_ { + (0..self.len()).map(move |i| self.get(i)) + } +} + +pub trait ZeroCopyLoader<'a, T, F> { + fn load_zc(&'a self) -> DriftResult>; + fn load_zc_mut(&'a self) -> DriftResult>; +} + +pub fn load_generic<'a, 'info, F, T>( + acct: &'a AccountInfo<'info>, + expected_disc: [u8; 8], + program_id: Pubkey, +) -> DriftResult> +where + F: Pod + HasLen, + T: Pod, +{ + validate!( + acct.owner == &program_id, + ErrorCode::DefaultError, + "invalid owner {}, program_id: {}", + acct.owner, + program_id, + )?; + + let data = acct.try_borrow_data().safe_unwrap()?; + let (disc, rest) = Ref::map_split(data, |d| d.split_at(8)); + + validate!( + *disc == expected_disc, + ErrorCode::DefaultError, + "invalid discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (hdr_bytes, body) = Ref::map_split(rest, |d| d.split_at(hdr_size)); + let fixed = Ref::map(hdr_bytes, |b| from_bytes::(b)); + Ok(AccountZeroCopy { + fixed, + data: body, + _marker: PhantomData, + }) +} + +pub fn load_generic_mut<'a, 'info, F, T>( + acct: &'a AccountInfo<'info>, + expected_disc: [u8; 8], + program_id: Pubkey, +) -> DriftResult> +where + F: Pod + HasLen, + T: Pod, +{ + validate!( + acct.owner == &program_id, + ErrorCode::DefaultError, + "invalid owner", + )?; + + let data = acct.try_borrow_mut_data().safe_unwrap()?; + let (disc, rest) = RefMut::map_split(data, |d| d.split_at_mut(8)); + + validate!( + *disc == expected_disc, + ErrorCode::DefaultError, + "invalid discriminator", + )?; + + let hdr_size = std::mem::size_of::(); + let (hdr_bytes, body) = RefMut::map_split(rest, |d| d.split_at_mut(hdr_size)); + let fixed = RefMut::map(hdr_bytes, |b| from_bytes_mut::(b)); + Ok(AccountZeroCopyMut { + fixed, + data: body, + _marker: PhantomData, + }) +} + +#[macro_export] +macro_rules! impl_zero_copy_loader { + ($Acc:ty, $ID:path, $Fixed:ty, $Elem:ty) => { + impl<'info> crate::state::zero_copy::ZeroCopyLoader<'_, $Elem, $Fixed> + for AccountInfo<'info> + { + fn load_zc<'a>( + self: &'a Self, + ) -> crate::error::DriftResult< + crate::state::zero_copy::AccountZeroCopy<'a, $Elem, $Fixed>, + > { + crate::state::zero_copy::load_generic::<$Fixed, $Elem>( + self, + <$Acc as anchor_lang::Discriminator>::discriminator(), + $ID(), + ) + } + + fn load_zc_mut<'a>( + self: &'a Self, + ) -> crate::error::DriftResult< + crate::state::zero_copy::AccountZeroCopyMut<'a, $Elem, $Fixed>, + > { + crate::state::zero_copy::load_generic_mut::<$Fixed, $Elem>( + self, + <$Acc as anchor_lang::Discriminator>::discriminator(), + $ID(), + ) + } + } + }; +} diff --git a/programs/drift/src/validation/margin.rs b/programs/drift/src/validation/margin.rs index f790d31121..49c993c89f 100644 --- a/programs/drift/src/validation/margin.rs +++ b/programs/drift/src/validation/margin.rs @@ -1,10 +1,10 @@ use crate::error::{DriftResult, ErrorCode}; use crate::math::constants::{ - LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MAX_MARGIN_RATIO, MIN_MARGIN_RATIO, - SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, + HIGH_LEVERAGE_MIN_MARGIN_RATIO, LIQUIDATION_FEE_TO_MARGIN_PRECISION_RATIO, MAX_MARGIN_RATIO, + MIN_MARGIN_RATIO, SPOT_IMF_PRECISION, SPOT_WEIGHT_PRECISION, }; use crate::msg; -use crate::{validate, HIGH_LEVERAGE_MIN_MARGIN_RATIO}; +use crate::validate; pub fn validate_margin( margin_ratio_initial: u32, diff --git a/programs/drift/src/validation/order.rs b/programs/drift/src/validation/order.rs index 92dd4da465..74452a020f 100644 --- a/programs/drift/src/validation/order.rs +++ b/programs/drift/src/validation/order.rs @@ -4,13 +4,14 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; +use crate::math::constants::MAX_PREDICTION_MARKET_PRICE; use crate::math::orders::{ calculate_base_asset_amount_to_fill_up_to_limit_price, is_multiple_of_step_size, }; use crate::state::paused_operations::PerpOperation; use crate::state::perp_market::PerpMarket; use crate::state::user::{Order, OrderTriggerCondition, OrderType}; -use crate::{validate, MAX_PREDICTION_MARKET_PRICE}; +use crate::validate; #[cfg(test)] mod test; diff --git a/programs/drift/src/validation/perp_market.rs b/programs/drift/src/validation/perp_market.rs index e55257659f..30bce4dedd 100644 --- a/programs/drift/src/validation/perp_market.rs +++ b/programs/drift/src/validation/perp_market.rs @@ -1,12 +1,12 @@ use crate::controller::position::PositionDirection; use crate::error::{DriftResult, ErrorCode}; use crate::math::casting::Cast; -use crate::math::constants::MAX_BASE_ASSET_AMOUNT_WITH_AMM; +use crate::math::constants::{BID_ASK_SPREAD_PRECISION, MAX_BASE_ASSET_AMOUNT_WITH_AMM}; use crate::math::safe_math::SafeMath; use crate::msg; use crate::state::perp_market::{MarketStatus, PerpMarket, AMM}; -use crate::{validate, BID_ASK_SPREAD_PRECISION}; +use crate::validate; #[allow(clippy::comparison_chain)] pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { @@ -81,10 +81,10 @@ pub fn validate_perp_market(market: &PerpMarket) -> DriftResult { market.amm.quote_asset_reserve )?; - let invariant_sqrt_u192 = crate::bn::U192::from(market.amm.sqrt_k); + let invariant_sqrt_u192 = crate::math::bn::U192::from(market.amm.sqrt_k); let invariant = invariant_sqrt_u192.safe_mul(invariant_sqrt_u192)?; let quote_asset_reserve = invariant - .safe_div(crate::bn::U192::from(market.amm.base_asset_reserve))? + .safe_div(crate::math::bn::U192::from(market.amm.base_asset_reserve))? .try_to_u128()?; let rounding_diff = quote_asset_reserve diff --git a/programs/drift/src/validation/sig_verification.rs b/programs/drift/src/validation/sig_verification.rs index 66ba1cab98..c2b6d79be9 100644 --- a/programs/drift/src/validation/sig_verification.rs +++ b/programs/drift/src/validation/sig_verification.rs @@ -58,6 +58,9 @@ pub struct VerifiedMessage { pub take_profit_order_params: Option, pub stop_loss_order_params: Option, pub max_margin_ratio: Option, + pub builder_idx: Option, + pub builder_fee_tenth_bps: Option, + pub isolated_position_deposit: Option, pub signature: [u8; 64], } @@ -96,6 +99,9 @@ pub fn deserialize_into_verified_message( take_profit_order_params: deserialized.take_profit_order_params, stop_loss_order_params: deserialized.stop_loss_order_params, max_margin_ratio: deserialized.max_margin_ratio, + builder_idx: deserialized.builder_idx, + builder_fee_tenth_bps: deserialized.builder_fee_tenth_bps, + isolated_position_deposit: deserialized.isolated_position_deposit, signature: *signature, }); } else { @@ -123,6 +129,9 @@ pub fn deserialize_into_verified_message( take_profit_order_params: deserialized.take_profit_order_params, stop_loss_order_params: deserialized.stop_loss_order_params, max_margin_ratio: deserialized.max_margin_ratio, + builder_idx: deserialized.builder_idx, + builder_fee_tenth_bps: deserialized.builder_fee_tenth_bps, + isolated_position_deposit: deserialized.isolated_position_deposit, signature: *signature, }); } diff --git a/programs/drift/src/validation/sig_verification/tests.rs b/programs/drift/src/validation/sig_verification/tests.rs index fae2456b43..250d5c871d 100644 --- a/programs/drift/src/validation/sig_verification/tests.rs +++ b/programs/drift/src/validation/sig_verification/tests.rs @@ -31,6 +31,9 @@ mod sig_verification { assert!(verified_message.take_profit_order_params.is_none()); assert!(verified_message.stop_loss_order_params.is_none()); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); + // Verify order params let order_params = &verified_message.signed_msg_order_params; assert_eq!(order_params.user_order_id, 1); @@ -68,6 +71,8 @@ mod sig_verification { assert_eq!(verified_message.slot, 2345); assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -117,6 +122,57 @@ mod sig_verification { assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_some()); assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 3456000000u64); + assert_eq!(tp.trigger_price, 240000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 3456000000u64); + assert_eq!(sl.trigger_price, 225000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 3); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 3456000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(230000000i64)); + assert_eq!(order_params.auction_end_price, Some(237000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_non_delegate_with_isolated_position_deposit() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, + 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 1, 64, 58, 105, 13, + 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 0, 0, 1, 1, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, false); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, Some(2)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.isolated_position_deposit.is_some()); + assert_eq!(verified_message.isolated_position_deposit.unwrap(), 1); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -170,6 +226,8 @@ mod sig_verification { assert!(verified_message.take_profit_order_params.is_none()); assert!(verified_message.stop_loss_order_params.is_none()); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); // Verify order params let order_params = &verified_message.signed_msg_order_params; @@ -213,6 +271,8 @@ mod sig_verification { assert_eq!(verified_message.slot, 2345); assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_none()); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); @@ -267,6 +327,112 @@ mod sig_verification { assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); assert!(verified_message.max_margin_ratio.is_some()); assert_eq!(verified_message.max_margin_ratio.unwrap(), 1); + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); + + assert!(verified_message.builder_idx.is_none()); + assert!(verified_message.builder_fee_tenth_bps.is_none()); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 1000000000u64); + assert_eq!(tp.trigger_price, 230000000u64); + + assert!(verified_message.stop_loss_order_params.is_some()); + let sl = verified_message.stop_loss_order_params.unwrap(); + assert_eq!(sl.base_asset_amount, 1000000000u64); + assert_eq!(sl.trigger_price, 250000000u64); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 2); + assert_eq!(order_params.direction, PositionDirection::Short); + assert_eq!(order_params.base_asset_amount, 1000000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(240000000i64)); + assert_eq!(order_params.auction_end_price, Some(238000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_max_margin_ratio_and_builder_params() { + let signature = [1u8; 64]; + let payload = vec![ + 200, 213, 166, 94, 34, 52, 245, 93, 0, 1, 0, 3, 0, 96, 254, 205, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 128, 133, 181, 13, 0, 0, 0, 0, + 1, 64, 85, 32, 14, 0, 0, 0, 0, 2, 0, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, + 71, 49, 1, 0, 28, 78, 14, 0, 0, 0, 0, 0, 96, 254, 205, 0, 0, 0, 0, 0, 1, 255, 255, 1, + 1, 1, 58, 0, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, false); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, Some(2)); + assert_eq!(verified_message.delegate_signed_taker_pubkey, None); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert_eq!(verified_message.max_margin_ratio.unwrap(), 65535); + assert_eq!(verified_message.builder_idx.unwrap(), 1); + assert_eq!(verified_message.builder_fee_tenth_bps.unwrap(), 58); + + assert!(verified_message.take_profit_order_params.is_some()); + let tp = verified_message.take_profit_order_params.unwrap(); + assert_eq!(tp.base_asset_amount, 3456000000u64); + assert_eq!(tp.trigger_price, 240000000u64); + + assert!(verified_message.stop_loss_order_params.is_none()); + + // Verify order params + let order_params = &verified_message.signed_msg_order_params; + assert_eq!(order_params.user_order_id, 3); + assert_eq!(order_params.direction, PositionDirection::Long); + assert_eq!(order_params.base_asset_amount, 3456000000u64); + assert_eq!(order_params.price, 237000000u64); + assert_eq!(order_params.market_index, 0); + assert_eq!(order_params.reduce_only, false); + assert_eq!(order_params.auction_duration, Some(10)); + assert_eq!(order_params.auction_start_price, Some(230000000i64)); + assert_eq!(order_params.auction_end_price, Some(237000000i64)); + } + + #[test] + fn test_deserialize_into_verified_message_delegate_with_isolated_position_deposit() { + let signature = [1u8; 64]; + let payload = vec![ + 66, 101, 102, 56, 199, 37, 158, 35, 0, 1, 1, 2, 0, 202, 154, 59, 0, 0, 0, 0, 64, 85, + 32, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 10, 1, 0, 28, 78, 14, 0, 0, 0, 0, 1, + 128, 151, 47, 14, 0, 0, 0, 0, 241, 148, 164, 10, 232, 65, 33, 157, 18, 12, 251, 132, + 245, 208, 37, 127, 112, 55, 83, 186, 54, 139, 1, 135, 220, 180, 208, 219, 189, 94, 79, + 148, 41, 9, 0, 0, 0, 0, 0, 0, 67, 82, 79, 51, 105, 114, 71, 49, 1, 128, 133, 181, 13, + 0, 0, 0, 0, 0, 202, 154, 59, 0, 0, 0, 0, 1, 128, 178, 230, 14, 0, 0, 0, 0, 0, 202, 154, + 59, 0, 0, 0, 0, 0, 0, 0, 1, 1, + ]; + + // Test deserialization with delegate signer + let result = deserialize_into_verified_message(payload, &signature, true); + assert!(result.is_ok()); + + let verified_message = result.unwrap(); + + // Verify the deserialized message has expected structure + assert_eq!(verified_message.signature, signature); + assert_eq!(verified_message.sub_account_id, None); + assert_eq!( + verified_message.delegate_signed_taker_pubkey, + Some(Pubkey::from_str("HG2iQKnRkkasrLptwMZewV6wT7KPstw9wkA8yyu8Nx3m").unwrap()) + ); + assert_eq!(verified_message.slot, 2345); + assert_eq!(verified_message.uuid, [67, 82, 79, 51, 105, 114, 71, 49]); + assert!(verified_message.isolated_position_deposit.is_some()); + assert_eq!(verified_message.isolated_position_deposit.unwrap(), 1); assert!(verified_message.take_profit_order_params.is_some()); let tp = verified_message.take_profit_order_params.unwrap(); diff --git a/programs/drift/src/validation/user.rs b/programs/drift/src/validation/user.rs index 3f527fed0f..999b342f45 100644 --- a/programs/drift/src/validation/user.rs +++ b/programs/drift/src/validation/user.rs @@ -1,8 +1,9 @@ use crate::error::{DriftResult, ErrorCode}; +use crate::math::constants::THIRTEEN_DAY; use crate::msg; use crate::state::spot_market::SpotBalanceType; use crate::state::user::{User, UserStats}; -use crate::{validate, State, THIRTEEN_DAY}; +use crate::{validate, State}; pub fn validate_user_deletion( user: &User, diff --git a/sdk/VERSION b/sdk/VERSION index b0c8a5df9a..29de64d340 100644 --- a/sdk/VERSION +++ b/sdk/VERSION @@ -1 +1 @@ -2.138.0-beta.9 \ No newline at end of file +2.146.0-beta.12 \ No newline at end of file diff --git a/sdk/bun.lock b/sdk/bun.lock index b97bf95c4b..84ef9f3886 100644 --- a/sdk/bun.lock +++ b/sdk/bun.lock @@ -7,7 +7,8 @@ "@coral-xyz/anchor": "0.29.0", "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@ellipsis-labs/phoenix-sdk": "1.4.5", - "@grpc/grpc-js": "1.12.6", + "@grpc/grpc-js": "1.14.0", + "@msgpack/msgpack": "^3.1.2", "@openbook-dex/openbook-v2": "0.2.10", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.5.3", @@ -17,9 +18,10 @@ "@solana/web3.js": "1.98.0", "@switchboard-xyz/common": "3.0.14", "@switchboard-xyz/on-demand": "2.4.1", - "@triton-one/yellowstone-grpc": "1.3.0", + "@triton-one/yellowstone-grpc": "1.4.1", "anchor-bankrun": "0.3.0", "gill": "^0.10.2", + "helius-laserstream": "0.1.8", "nanoid": "3.3.4", "node-cache": "5.1.2", "rpc-websockets": "7.5.1", @@ -58,15 +60,24 @@ }, }, "overrides": { - "debug": "<4.4.2", - "supports-color": "7.2.0", "ansi-regex": "5.0.1", - "color-convert": "<3.1.1", "ansi-styles": "4.3.0", - "wrap-ansi": "7.0.0", + "backslash": "<0.2.1", "chalk": "4.1.2", - "strip-ansi": "6.0.1", + "chalk-template": "<1.1.1", + "color-convert": "<3.1.1", "color-name": "<2.0.1", + "color-string": "<2.1.1", + "debug": "<4.4.2", + "error-ex": "<1.3.3", + "has-ansi": "<6.0.1", + "is-arrayish": "<0.3.3", + "simple-swizzle": "<0.2.3", + "slice-ansi": "3.0.0", + "strip-ansi": "6.0.1", + "supports-color": "7.2.0", + "supports-hyperlinks": "<4.1.1", + "wrap-ansi": "7.0.0", }, "packages": { "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], @@ -103,9 +114,9 @@ "@eslint/js": ["@eslint/js@8.57.0", "", {}, "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.0", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg=="], - "@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + "@grpc/proto-loader": ["@grpc/proto-loader@0.8.0", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.3", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ=="], "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.11.14", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg=="], @@ -137,6 +148,8 @@ "@metaplex-foundation/solita": ["@metaplex-foundation/solita@0.12.2", "", { "dependencies": { "@metaplex-foundation/beet": "^0.4.0", "@metaplex-foundation/beet-solana": "^0.3.0", "@metaplex-foundation/rustbin": "^0.3.0", "@solana/web3.js": "^1.36.0", "camelcase": "^6.2.1", "debug": "^4.3.3", "js-sha256": "^0.9.0", "prettier": "^2.5.1", "snake-case": "^3.0.4", "spok": "^1.4.3" }, "bin": { "solita": "dist/src/cli/solita.js" } }, "sha512-oczMfE43NNHWweSqhXPTkQBUbap/aAiwjDQw8zLKNnd/J8sXr/0+rKcN5yJIEgcHeKRkp90eTqkmt2WepQc8yw=="], + "@msgpack/msgpack": ["@msgpack/msgpack@3.1.2", "", {}, "sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ=="], + "@noble/curves": ["@noble/curves@1.8.1", "", { "dependencies": { "@noble/hashes": "1.7.1" } }, "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ=="], "@noble/ed25519": ["@noble/ed25519@1.7.3", "", {}, "sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ=="], @@ -293,7 +306,7 @@ "@switchboard-xyz/on-demand": ["@switchboard-xyz/on-demand@2.4.1", "", { "dependencies": { "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@isaacs/ttlcache": "^1.4.1", "@switchboard-xyz/common": ">=3.0.0", "axios": "^1.8.3", "bs58": "^6.0.0", "buffer": "^6.0.3", "js-yaml": "^4.1.0" } }, "sha512-eSlBp+c8lxpcSgh0/2xK8OaLHPziTSZlcs8V96gZGdiCJz1KgWJRNE1qnIJDOwaGdFecZdwcmajfQRtLRLED3w=="], - "@triton-one/yellowstone-grpc": ["@triton-one/yellowstone-grpc@1.3.0", "", { "dependencies": { "@grpc/grpc-js": "^1.8.0" } }, "sha512-tuwHtoYzvqnahsMrecfNNkQceCYwgiY0qKS8RwqtaxvDEgjm0E+0bXwKz2eUD3ZFYifomJmRKDmSBx9yQzAeMQ=="], + "@triton-one/yellowstone-grpc": ["@triton-one/yellowstone-grpc@1.4.1", "", { "dependencies": { "@grpc/grpc-js": "^1.8.0" } }, "sha512-ZN49vooxFbOqWttll8u7AOsIVnX+srqX9ddhZ9ttE+OcehUo8c2p2suK8Gr2puab49cgsV0VGjiTn9Gua/ntIw=="], "@ts-graphviz/adapter": ["@ts-graphviz/adapter@2.0.6", "", { "dependencies": { "@ts-graphviz/common": "^2.1.5" } }, "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q=="], @@ -335,6 +348,8 @@ "@types/node": ["@types/node@22.13.8", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ=="], + "@types/protobufjs": ["@types/protobufjs@6.0.0", "", { "dependencies": { "protobufjs": "*" } }, "sha512-A27RDExpAf3rdDjIrHKiJK6x8kqqJ4CmoChwtipfhVAn1p7+wviQFFP7dppn8FslSbHtQeVPvi8wNKkDjSYjHw=="], + "@types/semver": ["@types/semver@7.7.0", "", {}, "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA=="], "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], @@ -711,6 +726,20 @@ "he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "helius-laserstream": ["helius-laserstream@0.1.8", "", { "dependencies": { "@types/protobufjs": "^6.0.0", "protobufjs": "^7.5.3" }, "optionalDependencies": { "helius-laserstream-darwin-arm64": "0.1.8", "helius-laserstream-darwin-x64": "0.1.8", "helius-laserstream-linux-arm64-gnu": "0.1.8", "helius-laserstream-linux-arm64-musl": "0.1.8", "helius-laserstream-linux-x64-gnu": "0.1.8", "helius-laserstream-linux-x64-musl": "0.1.8" }, "os": [ "linux", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-jXQkwQRWiowbVPGQrGacOkI5shKPhrEixCu93OpoMtL5fs9mnhtD7XKMPi8CX0W8gsqsJjwR4NlaR+EflyANbQ=="], + + "helius-laserstream-darwin-arm64": ["helius-laserstream-darwin-arm64@0.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p/K2Mi3wZnMxEYSLCvu858VyMvtJFonhdF8cLaMcszFv04WWdsK+hINNZpVRfakypvDfDPbMudEhL1Q9USD5+w=="], + + "helius-laserstream-darwin-x64": ["helius-laserstream-darwin-x64@0.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Hd5irFyfOqQZLdoj5a+OV7vML2YfySSBuKlOwtisMHkUuIXZ4NpAexslDmK7iP5VWRI+lOv9X/tA7BhxW7RGSQ=="], + + "helius-laserstream-linux-arm64-gnu": ["helius-laserstream-linux-arm64-gnu@0.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-PlPm1dvOvTGBL1nuzK98Xe40BJq1JWNREXlBHKDVA/B+KCGQnIMJ1s6e1MevSvFE7SOix5i1BxhYIxGioK2GMg=="], + + "helius-laserstream-linux-arm64-musl": ["helius-laserstream-linux-arm64-musl@0.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-LFadfMRuTd1zo6RZqLTgHQapo3gJYioS7wFMWFoBOFulG0BpAqHEDNobkxx0002QArw+zX29MQ/5OaOCf8kKTA=="], + + "helius-laserstream-linux-x64-gnu": ["helius-laserstream-linux-x64-gnu@0.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-IZWK/OQIe0647QqfYikLb1DFK+nYtXLJiMcpj24qnNVWBOtMXmPc1hL6ebazdEiaKt7fxNd5IiM1RqeaqZAZMw=="], + + "helius-laserstream-linux-x64-musl": ["helius-laserstream-linux-x64-musl@0.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-riTS6VgxDae1fHOJ2XC/o/v1OZRbEv/3rcoa3NlAOnooDKp5HDgD0zJTcImjQHpYWwGaejx1oX/Ht53lxNoijw=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -1159,6 +1188,8 @@ "@ellipsis-labs/phoenix-sdk/bs58": ["bs58@5.0.0", "", { "dependencies": { "base-x": "^4.0.0" } }, "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ=="], + "@grpc/proto-loader/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@metaplex-foundation/beet-solana/bs58": ["bs58@5.0.0", "", { "dependencies": { "base-x": "^4.0.0" } }, "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ=="], "@metaplex-foundation/solita/@metaplex-foundation/beet": ["@metaplex-foundation/beet@0.4.0", "", { "dependencies": { "ansicolors": "^0.3.2", "bn.js": "^5.2.0", "debug": "^4.3.3" } }, "sha512-2OAKJnLatCc3mBXNL0QmWVQKAWK2C7XDfepgL0p/9+8oSx4bmRAFHFqptl1A/C0U5O3dxGwKfmKluW161OVGcA=="], @@ -1195,6 +1226,8 @@ "@switchboard-xyz/on-demand/bs58": ["bs58@6.0.0", "", { "dependencies": { "base-x": "^5.0.0" } }, "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw=="], + "@types/protobufjs/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="], @@ -1221,12 +1254,16 @@ "glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "helius-laserstream/protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], "jayson/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "jayson/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "jito-ts/@grpc/grpc-js": ["@grpc/grpc-js@1.12.6", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q=="], + "jito-ts/@solana/web3.js": ["@solana/web3.js@1.77.4", "", { "dependencies": { "@babel/runtime": "^7.12.5", "@noble/curves": "^1.0.0", "@noble/hashes": "^1.3.0", "@solana/buffer-layout": "^4.0.0", "agentkeepalive": "^4.2.1", "bigint-buffer": "^1.1.5", "bn.js": "^5.0.0", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.0", "node-fetch": "^2.6.7", "rpc-websockets": "^7.5.1", "superstruct": "^0.14.2" } }, "sha512-XdN0Lh4jdY7J8FYMyucxCwzn6Ga2Sr1DHDWRbqVzk7ZPmmpSPOVWHzO67X1cVT+jNi1D6gZi2tgjHgDPuj6e9Q=="], "jito-ts/dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], @@ -1311,6 +1348,8 @@ "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + "jito-ts/@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.7.13", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw=="], + "jito-ts/@solana/web3.js/superstruct": ["superstruct@0.14.2", "", {}, "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="], "mocha/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], diff --git a/sdk/package.json b/sdk/package.json index 9f2067b649..6d2bbaa487 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,9 +1,9 @@ { "name": "@drift-labs/sdk", - "version": "2.138.0-beta.9", + "version": "2.146.0-beta.12", "main": "lib/node/index.js", "types": "lib/node/index.d.ts", - "browser": "./lib/browser/index.js", + "module": "./lib/browser/index.js", "author": "crispheaney", "homepage": "https://www.drift.trade/", "repository": { @@ -20,6 +20,7 @@ "test:bignum": "mocha -r ts-node/register tests/bn/**/*.ts", "test:ci": "mocha -r ts-node/register tests/ci/**/*.ts", "test:dlob": "mocha -r ts-node/register tests/dlob/**/*.ts", + "test:events": "mocha -r ts-node/register tests/events/**/*.ts", "patch-and-pub": "npm version patch --force && npm publish", "prettify": "prettier --check './src/***/*.ts'", "prettify:fix": "prettier --write './{src,tests}/***/*.ts'", @@ -42,7 +43,8 @@ "@coral-xyz/anchor": "0.29.0", "@coral-xyz/anchor-30": "npm:@coral-xyz/anchor@0.30.1", "@ellipsis-labs/phoenix-sdk": "1.4.5", - "@grpc/grpc-js": "1.12.6", + "@grpc/grpc-js": "1.14.0", + "@msgpack/msgpack": "^3.1.2", "@openbook-dex/openbook-v2": "0.2.10", "@project-serum/serum": "0.13.65", "@pythnetwork/client": "2.5.3", @@ -52,9 +54,10 @@ "@solana/web3.js": "1.98.0", "@switchboard-xyz/common": "3.0.14", "@switchboard-xyz/on-demand": "2.4.1", - "@triton-one/yellowstone-grpc": "1.3.0", + "@triton-one/yellowstone-grpc": "1.4.1", "anchor-bankrun": "0.3.0", "gill": "^0.10.2", + "helius-laserstream": "0.1.8", "nanoid": "3.3.4", "node-cache": "5.1.2", "rpc-websockets": "7.5.1", @@ -136,5 +139,11 @@ "chalk-template": "<1.1.1", "supports-hyperlinks": "<4.1.1", "has-ansi": "<6.0.1" + }, + "browser": { + "helius-laserstream": false, + "@triton-one/yellowstone-grpc": false, + "@grpc/grpc-js": false, + "zstddec": false } } diff --git a/sdk/scripts/deposit-isolated-positions.ts b/sdk/scripts/deposit-isolated-positions.ts new file mode 100644 index 0000000000..6bff228d26 --- /dev/null +++ b/sdk/scripts/deposit-isolated-positions.ts @@ -0,0 +1,110 @@ +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import dotenv from 'dotenv'; +import { AnchorProvider, Idl, Program, ProgramAccount, BN } from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; +import { + DRIFT_PROGRAM_ID, + PerpMarketAccount, + SpotMarketAccount, + OracleInfo, + Wallet, + numberToSafeBN, +} from '../src'; +import { DriftClient } from '../src/driftClient'; +import { DriftClientConfig } from '../src/driftClientConfig'; + +async function main() { + dotenv.config({ path: '../' }); + + const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + if (!RPC_ENDPOINT) throw new Error('RPC_ENDPOINT env var required'); + + let keypair: Keypair; + const pk = process.env.PRIVATE_KEY; + if (pk) { + const secret = Uint8Array.from(JSON.parse(pk)); + keypair = Keypair.fromSecretKey(secret); + } else { + keypair = new Keypair(); + console.warn('Using ephemeral keypair. Provide PRIVATE_KEY to use a real wallet.'); + } + const wallet = new Wallet(keypair); + + const connection = new Connection(RPC_ENDPOINT); + const provider = new AnchorProvider(connection, wallet as any, { + commitment: 'processed', + }); + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const program = new Program(driftIDL as Idl, programId, provider); + + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketIndexes = allPerpMarketProgramAccounts.map((val) => val.account.marketIndex); + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketIndexes = allSpotMarketProgramAccounts.map((val) => val.account.marketIndex); + + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of allPerpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${Object.keys(acct.account.amm.oracleSource)[0]}`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ publicKey: acct.account.amm.oracle, source: acct.account.amm.oracleSource }); + } + } + for (const acct of allSpotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${Object.keys(acct.account.oracleSource)[0]}`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ publicKey: acct.account.oracle, source: acct.account.oracleSource }); + } + } + + const clientConfig: DriftClientConfig = { + connection, + wallet, + programID: programId, + accountSubscription: { type: 'websocket', commitment: 'processed' }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + env: 'devnet', + }; + const client = new DriftClient(clientConfig); + await client.subscribe(); + + const candidates = perpMarketIndexes.filter((i) => i >= 0 && i <= 5); + const targetMarketIndex = candidates.length + ? candidates[Math.floor(Math.random() * candidates.length)] + : perpMarketIndexes[0]; + + const perpMarketAccount = client.getPerpMarketAccount(targetMarketIndex); + const quoteSpotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = client.getSpotMarketAccount(quoteSpotMarketIndex); + + const precision = new BN(10).pow(new BN(spotMarketAccount.decimals)); + const amount = numberToSafeBN(0.01, precision); + + const userTokenAccount = await client.getAssociatedTokenAccount(quoteSpotMarketIndex); + const ix = await client.getDepositIntoIsolatedPerpPositionIx( + amount, + targetMarketIndex, + userTokenAccount, + 0 + ); + + const tx = await client.buildTransaction([ix]); + const { txSig } = await client.sendTransaction(tx); + console.log(`Deposited into isolated perp market ${targetMarketIndex}: ${txSig}`); + + await client.getUser().unsubscribe(); + await client.unsubscribe(); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); + + diff --git a/sdk/scripts/grpc-client-test-comparison.ts b/sdk/scripts/grpc-client-test-comparison.ts new file mode 100644 index 0000000000..c2d7c7947f --- /dev/null +++ b/sdk/scripts/grpc-client-test-comparison.ts @@ -0,0 +1,373 @@ +import { DriftClient } from '../src/driftClient'; +import { grpcDriftClientAccountSubscriberV2 } from '../src/accounts/grpcDriftClientAccountSubscriberV2'; +import { grpcDriftClientAccountSubscriber } from '../src/accounts/grpcDriftClientAccountSubscriber'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { DriftClientConfig } from '../src/driftClientConfig'; +import { + DRIFT_PROGRAM_ID, + PerpMarketAccount, + SpotMarketAccount, + Wallet, + OracleInfo, +} from '../src'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; +import dotenv from 'dotenv'; +import { + AnchorProvider, + Idl, + Program, + ProgramAccount, +} from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; +import assert from 'assert'; + +const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; +const TOKEN = process.env.TOKEN; +const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + +async function initializeGrpcDriftClientV2VersusV1() { + const connection = new Connection(RPC_ENDPOINT); + const wallet = new Wallet(new Keypair()); + dotenv.config({ path: '../' }); + + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const provider = new AnchorProvider( + connection, + // @ts-ignore + wallet, + { + commitment: 'processed', + } + ); + + const program = new Program(driftIDL as Idl, programId, provider); + + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketProgramAccounts = allPerpMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].includes( + val.account.marketIndex + ) + ); + const perpMarketIndexes = perpMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketProgramAccounts = allSpotMarketProgramAccounts.filter((val) => + [0, 1, 2, 3, 4, 5].includes(val.account.marketIndex) + ); + const spotMarketIndexes = spotMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of perpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${ + Object.keys(acct.account.amm.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.amm.oracle, + source: acct.account.amm.oracleSource, + }); + } + } + for (const acct of spotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${ + Object.keys(acct.account.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.oracle, + source: acct.account.oracleSource, + }); + } + } + + const baseAccountSubscription = { + type: 'grpc' as const, + grpcConfigs: { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: CommitmentLevel.PROCESSED, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }, + }; + + const configV2: DriftClientConfig = { + connection, + wallet, + programID: new PublicKey(DRIFT_PROGRAM_ID), + accountSubscription: { + ...baseAccountSubscription, + driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, + }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + }; + + const configV1: DriftClientConfig = { + connection, + wallet, + programID: new PublicKey(DRIFT_PROGRAM_ID), + accountSubscription: { + ...baseAccountSubscription, + driftClientAccountSubscriber: grpcDriftClientAccountSubscriber, + }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + }; + + const clientV2 = new DriftClient(configV2); + const clientV1 = new DriftClient(configV1); + + await Promise.all([clientV1.subscribe(), clientV2.subscribe()]); + const compare = () => { + try { + // 1. Test getStateAccountAndSlot + const state1 = clientV1.accountSubscriber.getStateAccountAndSlot(); + const state2 = clientV2.accountSubscriber.getStateAccountAndSlot(); + assert.deepStrictEqual( + state1.data, + state2.data, + 'State accounts should match' + ); + if ( + state1.slot !== undefined && + state2.slot !== undefined && + state2.slot < state1.slot + ) { + console.error( + `State account slot regression: v2 slot ${state2.slot} < v1 slot ${state1.slot}` + ); + } + + // 2. Test getMarketAccountsAndSlots (all perp markets) - sorted comparison + const allPerpMarkets1 = clientV1.accountSubscriber + .getMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + const allPerpMarkets2 = clientV2.accountSubscriber + .getMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + assert.strictEqual( + allPerpMarkets1.length, + allPerpMarkets2.length, + 'Number of perp markets should match' + ); + + // Compare each perp market in the sorted arrays + for (let i = 0; i < allPerpMarkets1.length; i++) { + const market1 = allPerpMarkets1[i]; + const market2 = allPerpMarkets2[i]; + assert.strictEqual( + market1.data.marketIndex, + market2.data.marketIndex, + `Perp market at position ${i} should have same marketIndex` + ); + // assert.deepStrictEqual( + // market1.data, + // market2.data, + // `Perp market ${market1.data.marketIndex} (from getMarketAccountsAndSlots) should match` + // ); + } + + // 3. Test getMarketAccountAndSlot for each perp market + for (const idx of perpMarketIndexes) { + const market1 = clientV1.accountSubscriber.getMarketAccountAndSlot(idx); + const market2 = clientV2.accountSubscriber.getMarketAccountAndSlot(idx); + // assert.deepStrictEqual( + // market1?.data, + // market2?.data, + // `Perp market ${idx} data should match` + // ); + // assert.strictEqual( + // market1?.slot, + // market2?.slot, + // `Perp market ${idx} slot should match` + // ); + if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot < market1.slot + ) { + console.error( + `Perp market ${idx} slot regression: v2 slot ${market2.slot} < v1 slot ${market1.slot}` + ); + } else if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot > market1.slot + ) { + console.info( + `Perp market ${idx} slot is FASTER! v2: ${market2.slot}, v1: ${market1.slot}` + ); + } + } + + // 4. Test getSpotMarketAccountsAndSlots (all spot markets) - sorted comparison + const allSpotMarkets1 = clientV1.accountSubscriber + .getSpotMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + const allSpotMarkets2 = clientV2.accountSubscriber + .getSpotMarketAccountsAndSlots() + .sort((a, b) => a.data.marketIndex - b.data.marketIndex); + assert.strictEqual( + allSpotMarkets1.length, + allSpotMarkets2.length, + 'Number of spot markets should match' + ); + + // Compare each spot market in the sorted arrays + for (let i = 0; i < allSpotMarkets1.length; i++) { + const market1 = allSpotMarkets1[i]; + const market2 = allSpotMarkets2[i]; + assert.strictEqual( + market1.data.marketIndex, + market2.data.marketIndex, + `Spot market at position ${i} should have same marketIndex` + ); + // assert.deepStrictEqual( + // market1.data, + // market2.data, + // `Spot market ${market1.data.marketIndex} (from getSpotMarketAccountsAndSlots) should match` + // ); + } + + // 5. Test getSpotMarketAccountAndSlot for each spot market + for (const idx of spotMarketIndexes) { + const market1 = + clientV1.accountSubscriber.getSpotMarketAccountAndSlot(idx); + const market2 = + clientV2.accountSubscriber.getSpotMarketAccountAndSlot(idx); + // assert.deepStrictEqual( + // market1?.data, + // market2?.data, + // `Spot market ${idx} data should match` + // ); + // assert.strictEqual( + // market1?.slot, + // market2?.slot, + // `Spot market ${idx} slot should match` + // ); + if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot < market1.slot + ) { + console.error( + `Spot market ${idx} slot regression: v2 slot ${market2.slot} < v1 slot ${market1.slot}` + ); + } else if ( + market1?.slot !== undefined && + market2?.slot !== undefined && + market2.slot > market1.slot + ) { + console.info( + `Spot market ${idx} slot is FASTER! v2: ${market2.slot}, v1: ${market1.slot}` + ); + } + } + + // 6. Test getOraclePriceDataAndSlotForPerpMarket + for (const idx of perpMarketIndexes) { + const oracle1 = + clientV1.accountSubscriber.getOraclePriceDataAndSlotForPerpMarket( + idx + ); + const oracle2 = + clientV2.accountSubscriber.getOraclePriceDataAndSlotForPerpMarket( + idx + ); + // assert.deepStrictEqual( + // oracle1?.data, + // oracle2?.data, + // `Perp market ${idx} oracle data should match` + // ); + // Note: slots might differ slightly due to timing, so we can optionally skip this check or be lenient + // assert.strictEqual(oracle1?.slot, oracle2?.slot, `Perp market ${idx} oracle slot should match`); + if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot < oracle1.slot + ) { + console.error( + `Perp market ${idx} oracle slot regression: v2 slot ${oracle2.slot} < v1 slot ${oracle1.slot}` + ); + } else if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot > oracle1.slot + ) { + console.info( + `Perp market ${idx} oracle slot is FASTER! v2: ${oracle2.slot}, v1: ${oracle1.slot}` + ); + } + } + + // 7. Test getOraclePriceDataAndSlotForSpotMarket + for (const idx of spotMarketIndexes) { + const oracle1 = + clientV1.accountSubscriber.getOraclePriceDataAndSlotForSpotMarket( + idx + ); + const oracle2 = + clientV2.accountSubscriber.getOraclePriceDataAndSlotForSpotMarket( + idx + ); + // assert.deepStrictEqual( + // oracle1?.data, + // oracle2?.data, + // `Spot market ${idx} oracle data should match` + // ); + // Note: slots might differ slightly due to timing + // assert.strictEqual(oracle1?.slot, oracle2?.slot, `Spot market ${idx} oracle slot should match`); + if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot < oracle1.slot + ) { + console.error( + `Spot market ${idx} oracle slot regression: v2 slot ${oracle2.slot} < v1 slot ${oracle1.slot}` + ); + } else if ( + oracle1?.slot !== undefined && + oracle2?.slot !== undefined && + oracle2.slot > oracle1.slot + ) { + console.info( + `Spot market ${idx} oracle slot is FASTER! v2: ${oracle2.slot}, v1: ${oracle1.slot}` + ); + } + } + + console.log('✓ All comparisons passed'); + } catch (error) { + console.error('✗ Comparison failed:', error); + } + }; + + compare(); + const interval = setInterval(compare, 1000); + + const cleanup = async () => { + clearInterval(interval); + await Promise.all([clientV1.unsubscribe(), clientV2.unsubscribe()]); + process.exit(0); + }; + + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); +} + +initializeGrpcDriftClientV2VersusV1().catch(console.error); diff --git a/sdk/scripts/grpc-multiuser-client-test-comparison.ts b/sdk/scripts/grpc-multiuser-client-test-comparison.ts new file mode 100644 index 0000000000..7c1ec78c0b --- /dev/null +++ b/sdk/scripts/grpc-multiuser-client-test-comparison.ts @@ -0,0 +1,156 @@ +import { grpcUserAccountSubscriber } from '../src/accounts/grpcUserAccountSubscriber'; +import { grpcMultiUserAccountSubscriber } from '../src/accounts/grpcMultiUserAccountSubscriber'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { DRIFT_PROGRAM_ID } from '../src'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; +import { AnchorProvider, Idl, Program } from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; +import assert from 'assert'; +import { Wallet } from '../src'; + +const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; +const TOKEN = process.env.TOKEN; +const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + +const USER_ACCOUNT_PUBKEYS = [ + // Add user account public keys here, e.g.: + // new PublicKey('...') +]; + +async function testGrpcUserAccountSubscriberV1VsV2() { + console.log('🚀 Initializing User Account Subscriber V1 vs V2 Test...'); + + if (USER_ACCOUNT_PUBKEYS.length === 0) { + console.error('❌ No user account public keys provided. Please add some to USER_ACCOUNT_PUBKEYS array.'); + process.exit(1); + } + + const connection = new Connection(RPC_ENDPOINT); + const wallet = new Wallet(new Keypair()); + + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const provider = new AnchorProvider( + connection, + // @ts-ignore + wallet, + { + commitment: 'processed', + } + ); + + const program = new Program(driftIDL as Idl, programId, provider); + + const grpcConfigs = { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: CommitmentLevel.PROCESSED, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }; + + console.log(`📊 Testing ${USER_ACCOUNT_PUBKEYS.length} user accounts...`); + + // V1: Create individual subscribers for each user account + const v1Subscribers = USER_ACCOUNT_PUBKEYS.map( + (pubkey) => + new grpcUserAccountSubscriber( + grpcConfigs, + program, + pubkey, + { logResubMessages: true } + ) + ); + + // V2: Create a single multi-subscriber and get per-user interfaces + const v2MultiSubscriber = new grpcMultiUserAccountSubscriber( + program, + grpcConfigs, + { logResubMessages: true } + ); + const v2Subscribers = USER_ACCOUNT_PUBKEYS.map((pubkey) => + v2MultiSubscriber.forUser(pubkey) + ); + + // Subscribe all V1 subscribers + console.log('🔗 Subscribing V1 subscribers...'); + await Promise.all(v1Subscribers.map((sub) => sub.subscribe())); + console.log('✅ V1 subscribers ready'); + + // Subscribe all V2 subscribers + console.log('🔗 Subscribing V2 subscribers...'); + await v2MultiSubscriber.subscribe(); + console.log('✅ V2 subscribers ready'); + + const compare = () => { + try { + let passedTests = 0; + let totalTests = 0; + + // Test each user account + for (let i = 0; i < USER_ACCOUNT_PUBKEYS.length; i++) { + const pubkey = USER_ACCOUNT_PUBKEYS[i]; + const v1Sub = v1Subscribers[i]; + const v2Sub = v2Subscribers[i]; + + totalTests++; + + // 1. Test isSubscribed + assert.strictEqual( + v1Sub.isSubscribed, + v2Sub.isSubscribed, + `User ${pubkey.toBase58()}: isSubscribed should match` + ); + + // 2. Test getUserAccountAndSlot + const v1Data = v1Sub.getUserAccountAndSlot(); + const v2Data = v2Sub.getUserAccountAndSlot(); + + // Compare the user account data + assert.deepStrictEqual( + v1Data.data, + v2Data.data, + `User ${pubkey.toBase58()}: account data should match` + ); + + // Slots might differ slightly due to timing, but let's check if they're close + const slotDiff = Math.abs(v1Data.slot - v2Data.slot); + if (slotDiff > 10) { + console.warn( + `⚠️ User ${pubkey.toBase58()}: slot difference is ${slotDiff} (v1: ${v1Data.slot}, v2: ${v2Data.slot})` + ); + } + + passedTests++; + } + + console.log(`✅ All comparisons passed (${passedTests}/${totalTests} user accounts)`); + } catch (error) { + console.error('❌ Comparison failed:', error); + } + }; + + // Run initial comparison + compare(); + + // Run comparison every second to verify live updates + const interval = setInterval(compare, 1000); + + const cleanup = async () => { + clearInterval(interval); + console.log('🧹 Cleaning up...'); + await Promise.all([ + ...v1Subscribers.map((sub) => sub.unsubscribe()), + ...v2Subscribers.map((sub) => sub.unsubscribe()), + ]); + console.log('✅ Cleanup complete'); + process.exit(0); + }; + + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + } + + testGrpcUserAccountSubscriberV1VsV2().catch(console.error); \ No newline at end of file diff --git a/sdk/scripts/single-grpc-client-test.ts b/sdk/scripts/single-grpc-client-test.ts new file mode 100644 index 0000000000..60de5bd39b --- /dev/null +++ b/sdk/scripts/single-grpc-client-test.ts @@ -0,0 +1,286 @@ +import { DriftClient } from '../src/driftClient'; +import { grpcDriftClientAccountSubscriberV2 } from '../src/accounts/grpcDriftClientAccountSubscriberV2'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { DriftClientConfig } from '../src/driftClientConfig'; +import { + DRIFT_PROGRAM_ID, + PerpMarketAccount, + SpotMarketAccount, + Wallet, + OracleInfo, + decodeName, +} from '../src'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; +import dotenv from 'dotenv'; +import { + AnchorProvider, + Idl, + Program, + ProgramAccount, +} from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; +import { grpcMultiUserAccountSubscriber } from '../src/accounts/grpcMultiUserAccountSubscriber'; + +const GRPC_ENDPOINT = process.env.GRPC_ENDPOINT; +const TOKEN = process.env.TOKEN; +const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + +async function initializeSingleGrpcClient() { + console.log('🚀 Initializing single gRPC Drift Client...'); + + const connection = new Connection(RPC_ENDPOINT); + const wallet = new Wallet(new Keypair()); + dotenv.config({ path: '../' }); + + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const provider = new AnchorProvider( + connection, + // @ts-ignore + wallet, + { + commitment: 'processed', + } + ); + + const program = new Program(driftIDL as Idl, programId, provider); + + // Get perp market accounts + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketProgramAccounts = allPerpMarketProgramAccounts.filter((val) => + [46].includes(val.account.marketIndex) + ); + const perpMarketIndexes = perpMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + // Get spot market accounts + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketProgramAccounts = allSpotMarketProgramAccounts.filter((val) => + [0].includes(val.account.marketIndex) + ); + const spotMarketIndexes = spotMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + // Get oracle infos + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of perpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${ + Object.keys(acct.account.amm.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.amm.oracle, + source: acct.account.amm.oracleSource, + }); + } + } + for (const acct of spotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${ + Object.keys(acct.account.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.oracle, + source: acct.account.oracleSource, + }); + } + } + + console.log( + `📊 Markets: ${perpMarketIndexes.length} perp, ${spotMarketIndexes.length} spot` + ); + console.log(`🔮 Oracles: ${oracleInfos.length}`); + + + const grpcConfigs = { + endpoint: GRPC_ENDPOINT, + token: TOKEN, + commitmentLevel: CommitmentLevel.PROCESSED, + channelOptions: { + 'grpc.keepalive_time_ms': 10_000, + 'grpc.keepalive_timeout_ms': 1_000, + 'grpc.keepalive_permit_without_calls': 1, + }, + }; + + const multiUserSubsciber = new grpcMultiUserAccountSubscriber( + program, + grpcConfigs + ); + + const baseAccountSubscription = { + type: 'grpc' as const, + grpcConfigs, + grpcMultiUserAccountSubscriber: multiUserSubsciber, + }; + + const config: DriftClientConfig = { + connection, + wallet, + programID: new PublicKey(DRIFT_PROGRAM_ID), + accountSubscription: { + ...baseAccountSubscription, + driftClientAccountSubscriber: grpcDriftClientAccountSubscriberV2, + }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + }; + + const client = new DriftClient(config); + + // Set up event listeners + const eventCounts = { + stateAccountUpdate: 0, + perpMarketAccountUpdate: 0, + spotMarketAccountUpdate: 0, + oraclePriceUpdate: 0, + update: 0, + }; + + console.log('🎧 Setting up event listeners...'); + + client.eventEmitter.on('stateAccountUpdate', (_data) => { + eventCounts.stateAccountUpdate++; + }); + + client.eventEmitter.on('perpMarketAccountUpdate', (_data) => { + eventCounts.perpMarketAccountUpdate++; + }); + + client.eventEmitter.on('spotMarketAccountUpdate', (_data) => { + eventCounts.spotMarketAccountUpdate++; + }); + + client.eventEmitter.on('oraclePriceUpdate', (_publicKey, _source, _data) => { + eventCounts.oraclePriceUpdate++; + }); + + client.accountSubscriber.eventEmitter.on('update', () => { + eventCounts.update++; + }); + + // Subscribe + console.log('🔗 Subscribing to accounts...'); + await client.subscribe(); + + console.log('✅ Client subscribed successfully!'); + console.log( + '🚀 Starting high-load testing (50 reads/sec per perp market)...' + ); + + // High-frequency load testing - 50 reads per second per perp market + const loadTestInterval = setInterval(async () => { + try { + // Test getPerpMarketAccount for each perp market (50 times per second per market) + for (const marketIndex of perpMarketIndexes) { + const perpMarketAccount = client.getPerpMarketAccount(marketIndex); + if (!perpMarketAccount) { + console.log(`Perp market ${marketIndex} not found`); + continue; + } + console.log( + 'perpMarketAccount name: ', + decodeName(perpMarketAccount.name) + ); + console.log( + 'perpMarketAccount data: ', + JSON.stringify({ + marketIndex: perpMarketAccount.marketIndex, + name: decodeName(perpMarketAccount.name), + baseAssetReserve: perpMarketAccount.amm.baseAssetReserve.toString(), + quoteAssetReserve: + perpMarketAccount.amm.quoteAssetReserve.toString(), + }) + ); + } + + // Test getMMOracleDataForPerpMarket for each perp market (50 times per second per market) + for (const marketIndex of perpMarketIndexes) { + try { + const oracleData = client.getMMOracleDataForPerpMarket(marketIndex); + console.log('oracleData price: ', oracleData.price.toString()); + console.log( + 'oracleData: ', + JSON.stringify({ + price: oracleData.price.toString(), + confidence: oracleData.confidence?.toString(), + slot: oracleData.slot?.toString(), + }) + ); + } catch (error) { + // Ignore errors for load testing + } + } + + for (const marketIndex of perpMarketIndexes) { + try { + const { data, slot } = + client.accountSubscriber.getMarketAccountAndSlot(marketIndex); + if (!data) { + console.log( + `Perp market getMarketAccountAndSlot ${marketIndex} not found` + ); + continue; + } + console.log( + 'marketAccountAndSlot: ', + JSON.stringify({ + marketIndex: data.marketIndex, + name: decodeName(data.name), + slot: slot?.toString(), + baseAssetReserve: data.amm.baseAssetReserve.toString(), + quoteAssetReserve: data.amm.quoteAssetReserve.toString(), + }) + ); + } catch (error) { + console.error( + `Error getting market account and slot for market ${marketIndex}:`, + error + ); + } + } + } catch (error) { + console.error('Load test error:', error); + } + }, 20); // 50 times per second = 1000ms / 50 = 20ms interval + + // Log periodic stats + const statsInterval = setInterval(() => { + console.log('\n📈 Event Counts:', eventCounts); + console.log(`⏱️ Client subscribed: ${client.isSubscribed}`); + console.log( + `🔗 Account subscriber subscribed: ${client.accountSubscriber.isSubscribed}` + ); + console.log( + `🔥 Load: ${perpMarketIndexes.length * 50 * 2} reads/sec (${ + perpMarketIndexes.length + } markets × 50 getPerpMarketAccount + 50 getMMOracleDataForPerpMarket)` + ); + }, 5000); + + // Handle shutdown signals - just exit without cleanup since they never unsubscribe + process.on('SIGINT', () => { + console.log('\n🛑 Shutting down...'); + clearInterval(loadTestInterval); + clearInterval(statsInterval); + process.exit(0); + }); + + process.on('SIGTERM', () => { + console.log('\n🛑 Shutting down...'); + clearInterval(loadTestInterval); + clearInterval(statsInterval); + process.exit(0); + }); + + return client; +} + +initializeSingleGrpcClient().catch(console.error); diff --git a/sdk/scripts/withdraw-isolated-positions.ts b/sdk/scripts/withdraw-isolated-positions.ts new file mode 100644 index 0000000000..51f9a74c34 --- /dev/null +++ b/sdk/scripts/withdraw-isolated-positions.ts @@ -0,0 +1,174 @@ +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import dotenv from 'dotenv'; +import { + AnchorProvider, + Idl, + Program, + ProgramAccount, +} from '@coral-xyz/anchor'; +import driftIDL from '../src/idl/drift.json'; +import { + DRIFT_PROGRAM_ID, + PerpMarketAccount, + SpotMarketAccount, + OracleInfo, + Wallet, + ZERO, +} from '../src'; +import { DriftClient } from '../src/driftClient'; +import { DriftClientConfig } from '../src/driftClientConfig'; + +function isStatusOpen(status: any) { + return !!status && 'open' in status; +} + +function isPerpMarketType(marketType: any) { + return !!marketType && 'perp' in marketType; +} + +async function main() { + dotenv.config({ path: '../' }); + + const RPC_ENDPOINT = process.env.RPC_ENDPOINT; + if (!RPC_ENDPOINT) throw new Error('RPC_ENDPOINT env var required'); + + // Load wallet + // For safety this creates a new ephemeral wallet unless PRIVATE_KEY is provided (base58 array) + let keypair: Keypair; + const pk = process.env.PRIVATE_KEY; + if (pk) { + const secret = Uint8Array.from(JSON.parse(pk)); + keypair = Keypair.fromSecretKey(secret); + } else { + keypair = new Keypair(); + console.warn( + 'Using ephemeral keypair. Provide PRIVATE_KEY for real withdrawals.' + ); + } + const wallet = new Wallet(keypair); + + // Connection and program for market discovery + const connection = new Connection(RPC_ENDPOINT); + const provider = new AnchorProvider(connection, wallet as any, { + commitment: 'processed', + }); + const programId = new PublicKey(DRIFT_PROGRAM_ID); + const program = new Program(driftIDL as Idl, programId, provider); + + // Discover markets and oracles (like the example test script) + const allPerpMarketProgramAccounts = + (await program.account.perpMarket.all()) as ProgramAccount[]; + const perpMarketIndexes = allPerpMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + const allSpotMarketProgramAccounts = + (await program.account.spotMarket.all()) as ProgramAccount[]; + const spotMarketIndexes = allSpotMarketProgramAccounts.map( + (val) => val.account.marketIndex + ); + + const seen = new Set(); + const oracleInfos: OracleInfo[] = []; + for (const acct of allPerpMarketProgramAccounts) { + const key = `${acct.account.amm.oracle.toBase58()}-${ + Object.keys(acct.account.amm.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.amm.oracle, + source: acct.account.amm.oracleSource, + }); + } + } + for (const acct of allSpotMarketProgramAccounts) { + const key = `${acct.account.oracle.toBase58()}-${ + Object.keys(acct.account.oracleSource)[0] + }`; + if (!seen.has(key)) { + seen.add(key); + oracleInfos.push({ + publicKey: acct.account.oracle, + source: acct.account.oracleSource, + }); + } + } + + // Build DriftClient with websocket subscription (lightweight) + const clientConfig: DriftClientConfig = { + connection, + wallet, + programID: programId, + accountSubscription: { + type: 'websocket', + commitment: 'processed', + }, + perpMarketIndexes, + spotMarketIndexes, + oracleInfos, + env: 'devnet', + }; + const client = new DriftClient(clientConfig); + await client.subscribe(); + + // Ensure user exists and is subscribed + const user = client.getUser(); + await user.subscribe(); + + const userAccount = user.getUserAccount(); + const openOrders = user.getOpenOrders(); + + const marketsWithOpenOrders = new Set(); + for (const o of openOrders ?? []) { + if (isStatusOpen(o.status) && isPerpMarketType(o.marketType)) { + marketsWithOpenOrders.add(o.marketIndex); + } + } + + const withdrawTargets = userAccount.perpPositions.filter((pos) => { + const isZeroBase = pos.baseAssetAmount.eq(ZERO); + const hasIso = pos.isolatedPositionScaledBalance.gt(ZERO); + const hasOpenOrders = marketsWithOpenOrders.has(pos.marketIndex); + return isZeroBase && hasIso && !hasOpenOrders; + }); + + console.log( + `Found ${withdrawTargets.length} isolated perp positions to withdraw` + ); + + for (const pos of withdrawTargets) { + try { + const amount = client.getIsolatedPerpPositionTokenAmount(pos.marketIndex); + if (amount.lte(ZERO)) continue; + + const perpMarketAccount = client.getPerpMarketAccount(pos.marketIndex); + const quoteAta = await client.getAssociatedTokenAccount( + perpMarketAccount.quoteSpotMarketIndex + ); + + const ixs = await client.getWithdrawFromIsolatedPerpPositionIxsBundle( + amount, + pos.marketIndex, + 0, + quoteAta, + true + ); + + const tx = await client.buildTransaction(ixs); + const { txSig } = await client.sendTransaction(tx); + console.log( + `Withdrew isolated deposit for perp market ${pos.marketIndex}: ${txSig}` + ); + } catch (e) { + console.error(`Failed to withdraw for market ${pos.marketIndex}:`, e); + } + } + + await user.unsubscribe(); + await client.unsubscribe(); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/sdk/src/accounts/basicUserStatsAccountSubscriber.ts b/sdk/src/accounts/basicUserStatsAccountSubscriber.ts new file mode 100644 index 0000000000..dc5b48671d --- /dev/null +++ b/sdk/src/accounts/basicUserStatsAccountSubscriber.ts @@ -0,0 +1,65 @@ +import { + DataAndSlot, + UserStatsAccountEvents, + UserStatsAccountSubscriber, +} from './types'; +import { PublicKey } from '@solana/web3.js'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { UserStatsAccount } from '../types'; + +/** + * Basic implementation of UserStatsAccountSubscriber. It will only take in UserStatsAccount + * data during initialization and will not fetch or subscribe to updates. + */ +export class BasicUserStatsAccountSubscriber + implements UserStatsAccountSubscriber +{ + isSubscribed: boolean; + eventEmitter: StrictEventEmitter; + userStatsAccountPublicKey: PublicKey; + + callbackId?: string; + errorCallbackId?: string; + + userStats: DataAndSlot; + + public constructor( + userStatsAccountPublicKey: PublicKey, + data?: UserStatsAccount, + slot?: number + ) { + this.isSubscribed = true; + this.eventEmitter = new EventEmitter(); + this.userStatsAccountPublicKey = userStatsAccountPublicKey; + this.userStats = { data, slot }; + } + + async subscribe(_userStatsAccount?: UserStatsAccount): Promise { + return true; + } + + async addToAccountLoader(): Promise {} + + async fetch(): Promise {} + + doesAccountExist(): boolean { + return this.userStats !== undefined; + } + + async unsubscribe(): Promise {} + + assertIsSubscribed(): void {} + + public getUserStatsAccountAndSlot(): DataAndSlot { + return this.userStats; + } + + public updateData(userStatsAccount: UserStatsAccount, slot: number): void { + if (!this.userStats || slot >= (this.userStats.slot ?? 0)) { + this.userStats = { data: userStatsAccount, slot }; + this.eventEmitter.emit('userStatsAccountUpdate', userStatsAccount); + this.eventEmitter.emit('update'); + } + } +} diff --git a/sdk/src/accounts/fetch.ts b/sdk/src/accounts/fetch.ts index cc92eb5e1d..63c7bfd7f0 100644 --- a/sdk/src/accounts/fetch.ts +++ b/sdk/src/accounts/fetch.ts @@ -1,6 +1,13 @@ import { Connection, PublicKey } from '@solana/web3.js'; -import { UserAccount, UserStatsAccount } from '../types'; import { + RevenueShareAccount, + RevenueShareEscrowAccount, + UserAccount, + UserStatsAccount, +} from '../types'; +import { + getRevenueShareAccountPublicKey, + getRevenueShareEscrowAccountPublicKey, getUserAccountPublicKey, getUserStatsAccountPublicKey, } from '../addresses/pda'; @@ -64,3 +71,45 @@ export async function fetchUserStatsAccount( ) as UserStatsAccount) : undefined; } + +export async function fetchRevenueShareAccount( + connection: Connection, + program: Program, + authority: PublicKey +): Promise { + const revenueShareAccountPublicKey = getRevenueShareAccountPublicKey( + program.programId, + authority + ); + const accountInfo = await connection.getAccountInfo( + revenueShareAccountPublicKey + ); + if (!accountInfo) return null; + return program.account.revenueShare.coder.accounts.decode( + 'RevenueShare', + accountInfo.data + ) as RevenueShareAccount; +} + +export async function fetchRevenueShareEscrowAccount( + connection: Connection, + program: Program, + authority: PublicKey +): Promise { + const revenueShareEscrowPubKey = getRevenueShareEscrowAccountPublicKey( + program.programId, + authority + ); + + const escrow = await connection.getAccountInfo(revenueShareEscrowPubKey); + + if (!escrow) return null; + + const escrowAccount = + program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + escrow.data + ) as RevenueShareEscrowAccount; + + return escrowAccount; +} diff --git a/sdk/src/accounts/grpcAccountSubscriber.ts b/sdk/src/accounts/grpcAccountSubscriber.ts index be141c3746..50adb2f318 100644 --- a/sdk/src/accounts/grpcAccountSubscriber.ts +++ b/sdk/src/accounts/grpcAccountSubscriber.ts @@ -39,13 +39,16 @@ export class grpcAccountSubscriber extends WebSocketAccountSubscriber { program: Program, accountPublicKey: PublicKey, decodeBuffer?: (buffer: Buffer) => U, - resubOpts?: ResubOpts + resubOpts?: ResubOpts, + clientProp?: Client ): Promise> { - const client = await createClient( - grpcConfigs.endpoint, - grpcConfigs.token, - grpcConfigs.channelOptions ?? {} - ); + const client = clientProp + ? clientProp + : await createClient( + grpcConfigs.endpoint, + grpcConfigs.token, + grpcConfigs.channelOptions ?? {} + ); const commitmentLevel = // @ts-ignore :: isomorphic exported enum fails typescript but will work at runtime grpcConfigs.commitmentLevel ?? CommitmentLevel.CONFIRMED; diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriber.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriber.ts index 8545b5f029..31d34e32ca 100644 --- a/sdk/src/accounts/grpcDriftClientAccountSubscriber.ts +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriber.ts @@ -12,7 +12,7 @@ import { grpcAccountSubscriber } from './grpcAccountSubscriber'; import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types'; import { getOracleId } from '../oracles/oracleId'; -export class gprcDriftClientAccountSubscriber extends WebSocketDriftClientAccountSubscriber { +export class grpcDriftClientAccountSubscriber extends WebSocketDriftClientAccountSubscriber { private grpcConfigs: GrpcConfigs; constructor( diff --git a/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts new file mode 100644 index 0000000000..d2770dc3c7 --- /dev/null +++ b/sdk/src/accounts/grpcDriftClientAccountSubscriberV2.ts @@ -0,0 +1,756 @@ +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { OracleInfo, OraclePriceData } from '../oracles/types'; +import { Program } from '@coral-xyz/anchor'; +import { PublicKey } from '@solana/web3.js'; +import { findAllMarketAndOracles } from '../config'; +import { + getDriftStateAccountPublicKey, + getPerpMarketPublicKey, + getPerpMarketPublicKeySync, + getSpotMarketPublicKey, + getSpotMarketPublicKeySync, +} from '../addresses/pda'; +import { + AccountSubscriber, + DataAndSlot, + DelistedMarketSetting, + DriftClientAccountEvents, + DriftClientAccountSubscriber, + NotSubscribedError, + GrpcConfigs, + ResubOpts, +} from './types'; +import { grpcAccountSubscriber } from './grpcAccountSubscriber'; +import { grpcMultiAccountSubscriber } from './grpcMultiAccountSubscriber'; +import { PerpMarketAccount, SpotMarketAccount, StateAccount } from '../types'; +import { + getOracleId, + getPublicKeyAndSourceFromOracleId, +} from '../oracles/oracleId'; +import { OracleClientCache } from '../oracles/oracleClientCache'; +import { findDelistedPerpMarketsAndOracles } from './utils'; + +export class grpcDriftClientAccountSubscriberV2 + implements DriftClientAccountSubscriber +{ + private grpcConfigs: GrpcConfigs; + private perpMarketsSubscriber?: grpcMultiAccountSubscriber; + private spotMarketsSubscriber?: grpcMultiAccountSubscriber; + private oracleMultiSubscriber?: grpcMultiAccountSubscriber< + OraclePriceData, + OracleInfo + >; + private perpMarketIndexToAccountPubkeyMap = new Map(); + private spotMarketIndexToAccountPubkeyMap = new Map(); + private delistedMarketSetting: DelistedMarketSetting; + + public eventEmitter: StrictEventEmitter< + EventEmitter, + DriftClientAccountEvents + >; + public isSubscribed: boolean; + public isSubscribing: boolean; + public program: Program; + public perpMarketIndexes: number[]; + public spotMarketIndexes: number[]; + public shouldFindAllMarketsAndOracles: boolean; + public oracleInfos: OracleInfo[]; + public initialPerpMarketAccountData: Map; + public initialSpotMarketAccountData: Map; + public initialOraclePriceData: Map; + public perpOracleMap = new Map(); + public perpOracleStringMap = new Map(); + public spotOracleMap = new Map(); + public spotOracleStringMap = new Map(); + private oracleIdToOracleDataMap = new Map< + string, + DataAndSlot + >(); + public stateAccountSubscriber?: AccountSubscriber; + oracleClientCache = new OracleClientCache(); + private resubOpts?: ResubOpts; + + private subscriptionPromise: Promise; + protected subscriptionPromiseResolver: (val: boolean) => void; + + constructor( + grpcConfigs: GrpcConfigs, + program: Program, + perpMarketIndexes: number[], + spotMarketIndexes: number[], + oracleInfos: OracleInfo[], + shouldFindAllMarketsAndOracles: boolean, + delistedMarketSetting: DelistedMarketSetting, + resubOpts?: ResubOpts + ) { + this.eventEmitter = new EventEmitter(); + this.isSubscribed = false; + this.isSubscribing = false; + this.program = program; + this.perpMarketIndexes = perpMarketIndexes; + this.spotMarketIndexes = spotMarketIndexes; + this.shouldFindAllMarketsAndOracles = shouldFindAllMarketsAndOracles; + this.oracleInfos = oracleInfos; + this.initialPerpMarketAccountData = new Map(); + this.initialSpotMarketAccountData = new Map(); + this.initialOraclePriceData = new Map(); + this.perpOracleMap = new Map(); + this.perpOracleStringMap = new Map(); + this.spotOracleMap = new Map(); + this.spotOracleStringMap = new Map(); + this.grpcConfigs = grpcConfigs; + this.resubOpts = resubOpts; + this.delistedMarketSetting = delistedMarketSetting; + } + + chunks = (array: readonly T[], size: number): T[][] => { + return new Array(Math.ceil(array.length / size)) + .fill(null) + .map((_, index) => index * size) + .map((begin) => array.slice(begin, begin + size)); + }; + + async setInitialData(): Promise { + const connection = this.program.provider.connection; + + if ( + !this.initialPerpMarketAccountData || + this.initialPerpMarketAccountData.size === 0 + ) { + const perpMarketPublicKeys = this.perpMarketIndexes.map((marketIndex) => + getPerpMarketPublicKeySync(this.program.programId, marketIndex) + ); + const perpMarketPublicKeysChunks = this.chunks(perpMarketPublicKeys, 75); + const perpMarketAccountInfos = ( + await Promise.all( + perpMarketPublicKeysChunks.map((perpMarketPublicKeysChunk) => + connection.getMultipleAccountsInfo(perpMarketPublicKeysChunk) + ) + ) + ).flat(); + this.initialPerpMarketAccountData = new Map( + perpMarketAccountInfos + .filter((accountInfo) => !!accountInfo) + .map((accountInfo) => { + const perpMarket = this.program.coder.accounts.decode( + 'PerpMarket', + accountInfo.data + ); + return [perpMarket.marketIndex, perpMarket]; + }) + ); + } + + if ( + !this.initialSpotMarketAccountData || + this.initialSpotMarketAccountData.size === 0 + ) { + const spotMarketPublicKeys = this.spotMarketIndexes.map((marketIndex) => + getSpotMarketPublicKeySync(this.program.programId, marketIndex) + ); + const spotMarketPublicKeysChunks = this.chunks(spotMarketPublicKeys, 75); + const spotMarketAccountInfos = ( + await Promise.all( + spotMarketPublicKeysChunks.map((spotMarketPublicKeysChunk) => + connection.getMultipleAccountsInfo(spotMarketPublicKeysChunk) + ) + ) + ).flat(); + this.initialSpotMarketAccountData = new Map( + spotMarketAccountInfos + .filter((accountInfo) => !!accountInfo) + .map((accountInfo) => { + const spotMarket = this.program.coder.accounts.decode( + 'SpotMarket', + accountInfo.data + ); + return [spotMarket.marketIndex, spotMarket]; + }) + ); + } + + const oracleAccountPubkeyChunks = this.chunks( + this.oracleInfos.map((oracleInfo) => oracleInfo.publicKey), + 75 + ); + const oracleAccountInfos = ( + await Promise.all( + oracleAccountPubkeyChunks.map((oracleAccountPublicKeysChunk) => + connection.getMultipleAccountsInfo(oracleAccountPublicKeysChunk) + ) + ) + ).flat(); + this.initialOraclePriceData = new Map( + this.oracleInfos.reduce((result, oracleInfo, i) => { + if (!oracleAccountInfos[i]) { + return result; + } + const oracleClient = this.oracleClientCache.get( + oracleInfo.source, + connection, + this.program + ); + const oraclePriceData = oracleClient.getOraclePriceDataFromBuffer( + oracleAccountInfos[i].data + ); + result.push([ + getOracleId(oracleInfo.publicKey, oracleInfo.source), + oraclePriceData, + ]); + return result; + }, []) + ); + } + + async addPerpMarket(_marketIndex: number): Promise { + if (!this.perpMarketIndexes.includes(_marketIndex)) { + this.perpMarketIndexes = this.perpMarketIndexes.concat(_marketIndex); + } + return true; + } + + async addSpotMarket(_marketIndex: number): Promise { + return true; + } + + async addOracle(oracleInfo: OracleInfo): Promise { + if (this.resubOpts?.logResubMessages) { + console.log('[grpcDriftClientAccountSubscriberV2] addOracle'); + } + if (oracleInfo.publicKey.equals(PublicKey.default)) { + return true; + } + + const exists = this.oracleInfos.some( + (o) => + o.source === oracleInfo.source && + o.publicKey.equals(oracleInfo.publicKey) + ); + if (exists) { + return true; // Already exists, don't add duplicate + } + + this.oracleInfos = this.oracleInfos.concat(oracleInfo); + this.oracleMultiSubscriber?.addAccounts([oracleInfo.publicKey]); + + return true; + } + + public async subscribe(): Promise { + if (this.isSubscribed) { + return true; + } + + if (this.isSubscribing) { + return await this.subscriptionPromise; + } + + this.isSubscribing = true; + + this.subscriptionPromise = new Promise((res) => { + this.subscriptionPromiseResolver = res; + }); + + if (this.shouldFindAllMarketsAndOracles) { + const { + perpMarketIndexes, + perpMarketAccounts, + spotMarketIndexes, + spotMarketAccounts, + oracleInfos, + } = await findAllMarketAndOracles(this.program); + this.perpMarketIndexes = perpMarketIndexes; + this.spotMarketIndexes = spotMarketIndexes; + this.oracleInfos = oracleInfos; + // front run and set the initial data here to save extra gma call in set initial data + this.initialPerpMarketAccountData = new Map( + perpMarketAccounts.map((market) => [market.marketIndex, market]) + ); + this.initialSpotMarketAccountData = new Map( + spotMarketAccounts.map((market) => [market.marketIndex, market]) + ); + } + + const statePublicKey = await getDriftStateAccountPublicKey( + this.program.programId + ); + + // create and activate main state account subscription + this.stateAccountSubscriber = + await grpcAccountSubscriber.create( + this.grpcConfigs, + 'state', + this.program, + statePublicKey, + undefined, + undefined + ); + await this.stateAccountSubscriber.subscribe((data: StateAccount) => { + this.eventEmitter.emit('stateAccountUpdate', data); + this.eventEmitter.emit('update'); + }); + + // set initial data to avoid spamming getAccountInfo calls in webSocketAccountSubscriber + await this.setInitialData(); + + // subscribe to perp + spot markets (separate) and oracles + await Promise.all([ + this.subscribeToPerpMarketAccounts(), + this.subscribeToSpotMarketAccounts(), + this.subscribeToOracles(), + ]); + + this.eventEmitter.emit('update'); + + await this.handleDelistedMarkets(); + + await Promise.all([this.setPerpOracleMap(), this.setSpotOracleMap()]); + + this.subscriptionPromiseResolver(true); + + this.isSubscribing = false; + this.isSubscribed = true; + + // delete initial data + this.removeInitialData(); + + return true; + } + + public async fetch(): Promise { + await this.stateAccountSubscriber?.fetch(); + await this.perpMarketsSubscriber?.fetch(); + await this.spotMarketsSubscriber?.fetch(); + await this.oracleMultiSubscriber?.fetch(); + } + + private assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } + + public getStateAccountAndSlot(): DataAndSlot { + this.assertIsSubscribed(); + return this.stateAccountSubscriber.dataAndSlot; + } + + public getMarketAccountsAndSlots(): DataAndSlot[] { + const map = this.perpMarketsSubscriber?.getAccountDataMap(); + return Array.from(map?.values() ?? []); + } + + public getSpotMarketAccountsAndSlots(): DataAndSlot[] { + const map = this.spotMarketsSubscriber?.getAccountDataMap(); + return Array.from(map?.values() ?? []); + } + + getMarketAccountAndSlot( + marketIndex: number + ): DataAndSlot | undefined { + return this.perpMarketsSubscriber?.getAccountData( + this.perpMarketIndexToAccountPubkeyMap.get(marketIndex) + ); + } + + getSpotMarketAccountAndSlot( + marketIndex: number + ): DataAndSlot | undefined { + return this.spotMarketsSubscriber?.getAccountData( + this.spotMarketIndexToAccountPubkeyMap.get(marketIndex) + ); + } + + public getOraclePriceDataAndSlot( + oracleId: string + ): DataAndSlot | undefined { + this.assertIsSubscribed(); + // we need to rely on a map we store in this class because the grpcMultiAccountSubscriber does not track a mapping or oracle ID. + // DO NOT call getAccountData on the oracleMultiSubscriber, it will not return the correct data in certain cases(BONK spot and perp market subscribed too at once). + return this.oracleIdToOracleDataMap.get(oracleId); + } + + public getOraclePriceDataAndSlotForPerpMarket( + marketIndex: number + ): DataAndSlot | undefined { + const perpMarketAccount = this.getMarketAccountAndSlot(marketIndex); + const oracle = this.perpOracleMap.get(marketIndex); + const oracleId = this.perpOracleStringMap.get(marketIndex); + if (!perpMarketAccount || !oracleId) { + return undefined; + } + + if (!perpMarketAccount.data.amm.oracle.equals(oracle)) { + // If the oracle has changed, we need to update the oracle map in background + this.setPerpOracleMap(); + } + + return this.getOraclePriceDataAndSlot(oracleId); + } + + public getOraclePriceDataAndSlotForSpotMarket( + marketIndex: number + ): DataAndSlot | undefined { + const spotMarketAccount = this.getSpotMarketAccountAndSlot(marketIndex); + const oracle = this.spotOracleMap.get(marketIndex); + const oracleId = this.spotOracleStringMap.get(marketIndex); + if (!spotMarketAccount || !oracleId) { + return undefined; + } + + if (!spotMarketAccount.data.oracle.equals(oracle)) { + // If the oracle has changed, we need to update the oracle map in background + this.setSpotOracleMap(); + } + + return this.getOraclePriceDataAndSlot(oracleId); + } + + async setPerpOracleMap() { + const perpMarketsMap = this.perpMarketsSubscriber?.getAccountDataMap(); + const perpMarkets = Array.from(perpMarketsMap.values()); + const addOraclePromises = []; + for (const perpMarket of perpMarkets) { + if (!perpMarket || !perpMarket.data) { + continue; + } + const perpMarketAccount = perpMarket.data; + const perpMarketIndex = perpMarketAccount.marketIndex; + const oracle = perpMarketAccount.amm.oracle; + const oracleId = getOracleId(oracle, perpMarket.data.amm.oracleSource); + if (!this.oracleMultiSubscriber?.getAccountDataMap().has(oracleId)) { + addOraclePromises.push( + this.addOracle({ + publicKey: oracle, + source: perpMarket.data.amm.oracleSource, + }) + ); + } + this.perpOracleMap.set(perpMarketIndex, oracle); + this.perpOracleStringMap.set(perpMarketIndex, oracleId); + } + await Promise.all(addOraclePromises); + } + + async setSpotOracleMap() { + const spotMarketsMap = this.spotMarketsSubscriber?.getAccountDataMap(); + const spotMarkets = Array.from(spotMarketsMap.values()); + const addOraclePromises = []; + for (const spotMarket of spotMarkets) { + if (!spotMarket || !spotMarket.data) { + continue; + } + const spotMarketAccount = spotMarket.data; + const spotMarketIndex = spotMarketAccount.marketIndex; + const oracle = spotMarketAccount.oracle; + const oracleId = getOracleId(oracle, spotMarketAccount.oracleSource); + if (!this.oracleMultiSubscriber?.getAccountDataMap().has(oracleId)) { + addOraclePromises.push( + this.addOracle({ + publicKey: oracle, + source: spotMarketAccount.oracleSource, + }) + ); + } + this.spotOracleMap.set(spotMarketIndex, oracle); + this.spotOracleStringMap.set(spotMarketIndex, oracleId); + } + await Promise.all(addOraclePromises); + } + + async subscribeToPerpMarketAccounts(): Promise { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] subscribeToPerpMarketAccounts' + ); + } + const perpMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = + await Promise.all( + this.perpMarketIndexes.map(async (marketIndex) => [ + marketIndex, + await getPerpMarketPublicKey(this.program.programId, marketIndex), + ]) + ); + for (const [ + marketIndex, + accountPubkey, + ] of perpMarketIndexToAccountPubkeys) { + this.perpMarketIndexToAccountPubkeyMap.set( + marketIndex, + accountPubkey.toBase58() + ); + } + + const perpMarketPubkeys = perpMarketIndexToAccountPubkeys.map( + ([_, accountPubkey]) => accountPubkey + ); + + this.perpMarketsSubscriber = + await grpcMultiAccountSubscriber.create( + this.grpcConfigs, + 'perpMarket', + this.program, + undefined, + this.resubOpts, + undefined, + async () => { + try { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] perp markets subscriber unsubscribed; resubscribing' + ); + } + await this.subscribeToPerpMarketAccounts(); + } catch (e) { + console.error('Perp markets resubscribe failed:', e); + } + } + ); + + for (const data of this.initialPerpMarketAccountData.values()) { + this.perpMarketsSubscriber.setAccountData(data.pubkey.toBase58(), data); + } + + await this.perpMarketsSubscriber.subscribe( + perpMarketPubkeys, + (_accountId, data) => { + this.eventEmitter.emit( + 'perpMarketAccountUpdate', + data as PerpMarketAccount + ); + this.eventEmitter.emit('update'); + } + ); + + return true; + } + + async subscribeToSpotMarketAccounts(): Promise { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] subscribeToSpotMarketAccounts' + ); + } + const spotMarketIndexToAccountPubkeys: Array<[number, PublicKey]> = + await Promise.all( + this.spotMarketIndexes.map(async (marketIndex) => [ + marketIndex, + await getSpotMarketPublicKey(this.program.programId, marketIndex), + ]) + ); + for (const [ + marketIndex, + accountPubkey, + ] of spotMarketIndexToAccountPubkeys) { + this.spotMarketIndexToAccountPubkeyMap.set( + marketIndex, + accountPubkey.toBase58() + ); + } + + const spotMarketPubkeys = spotMarketIndexToAccountPubkeys.map( + ([_, accountPubkey]) => accountPubkey + ); + + this.spotMarketsSubscriber = + await grpcMultiAccountSubscriber.create( + this.grpcConfigs, + 'spotMarket', + this.program, + undefined, + this.resubOpts, + undefined, + async () => { + try { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] spot markets subscriber unsubscribed; resubscribing' + ); + } + await this.subscribeToSpotMarketAccounts(); + } catch (e) { + console.error('Spot markets resubscribe failed:', e); + } + } + ); + + for (const data of this.initialSpotMarketAccountData.values()) { + this.spotMarketsSubscriber.setAccountData(data.pubkey.toBase58(), data); + } + + await this.spotMarketsSubscriber.subscribe( + spotMarketPubkeys, + (_accountId, data) => { + this.eventEmitter.emit( + 'spotMarketAccountUpdate', + data as SpotMarketAccount + ); + this.eventEmitter.emit('update'); + } + ); + + return true; + } + + async subscribeToOracles(): Promise { + if (this.resubOpts?.logResubMessages) { + console.log('grpcDriftClientAccountSubscriberV2 subscribeToOracles'); + } + const oraclePubkeyToInfosMap = new Map(); + for (const info of this.oracleInfos) { + const pubkey = info.publicKey.toBase58(); + if (!oraclePubkeyToInfosMap.has(pubkey)) { + oraclePubkeyToInfosMap.set(pubkey, []); + } + oraclePubkeyToInfosMap.get(pubkey).push(info); + } + + const oraclePubkeys = Array.from( + new Set(this.oracleInfos.map((info) => info.publicKey)) + ); + + this.oracleMultiSubscriber = await grpcMultiAccountSubscriber.create< + OraclePriceData, + OracleInfo + >( + this.grpcConfigs, + 'oracle', + this.program, + (buffer: Buffer, pubkey?: string, accountProps?: OracleInfo) => { + if (!pubkey) { + throw new Error('Oracle pubkey missing in decode'); + } + + const client = this.oracleClientCache.get( + accountProps.source, + this.program.provider.connection, + this.program + ); + const price = client.getOraclePriceDataFromBuffer(buffer); + return price; + }, + this.resubOpts, + undefined, + async () => { + try { + if (this.resubOpts?.logResubMessages) { + console.log( + '[grpcDriftClientAccountSubscriberV2] oracle subscriber unsubscribed; resubscribing' + ); + } + await this.subscribeToOracles(); + } catch (e) { + console.error('Oracle resubscribe failed:', e); + } + }, + oraclePubkeyToInfosMap + ); + + for (const data of this.initialOraclePriceData.entries()) { + const { publicKey } = getPublicKeyAndSourceFromOracleId(data[0]); + this.oracleMultiSubscriber.setAccountData(publicKey.toBase58(), data[1]); + this.oracleIdToOracleDataMap.set(data[0], { + data: data[1], + slot: 0, + }); + } + + await this.oracleMultiSubscriber.subscribe( + oraclePubkeys, + (accountId, data, context, _b, accountProps) => { + const oracleId = getOracleId(accountId, accountProps.source); + this.oracleIdToOracleDataMap.set(oracleId, { + data, + slot: context.slot, + }); + this.eventEmitter.emit( + 'oraclePriceUpdate', + accountId, + accountProps.source, + data + ); + + this.eventEmitter.emit('update'); + } + ); + + return true; + } + + async handleDelistedMarkets(): Promise { + if (this.delistedMarketSetting === DelistedMarketSetting.Subscribe) { + return; + } + + const { perpMarketIndexes, oracles } = findDelistedPerpMarketsAndOracles( + Array.from( + this.perpMarketsSubscriber?.getAccountDataMap().values() || [] + ), + Array.from(this.spotMarketsSubscriber?.getAccountDataMap().values() || []) + ); + + // Build array of perp market pubkeys to remove + const perpMarketPubkeysToRemove = perpMarketIndexes + .map((marketIndex) => { + const pubkeyString = + this.perpMarketIndexToAccountPubkeyMap.get(marketIndex); + return pubkeyString ? new PublicKey(pubkeyString) : null; + }) + .filter((pubkey) => pubkey !== null) as PublicKey[]; + + // Build array of oracle pubkeys to remove + const oraclePubkeysToRemove = oracles.map((oracle) => oracle.publicKey); + + // Remove accounts in batches - perp markets + if (perpMarketPubkeysToRemove.length > 0) { + await this.perpMarketsSubscriber.removeAccounts( + perpMarketPubkeysToRemove + ); + } + + // Remove accounts in batches - oracles + if (oraclePubkeysToRemove.length > 0) { + await this.oracleMultiSubscriber.removeAccounts(oraclePubkeysToRemove); + } + } + + removeInitialData() { + this.initialPerpMarketAccountData = new Map(); + this.initialSpotMarketAccountData = new Map(); + this.initialOraclePriceData = new Map(); + } + + async unsubscribeFromOracles(): Promise { + if (this.oracleMultiSubscriber) { + await this.oracleMultiSubscriber.unsubscribe(); + this.oracleMultiSubscriber = undefined; + return; + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + this.isSubscribed = false; + this.isSubscribing = false; + + await this.stateAccountSubscriber?.unsubscribe(); + await this.unsubscribeFromOracles(); + await this.perpMarketsSubscriber?.unsubscribe(); + await this.spotMarketsSubscriber?.unsubscribe(); + + // Clean up all maps to prevent memory leaks + this.perpMarketIndexToAccountPubkeyMap.clear(); + this.spotMarketIndexToAccountPubkeyMap.clear(); + this.oracleIdToOracleDataMap.clear(); + this.perpOracleMap.clear(); + this.perpOracleStringMap.clear(); + this.spotOracleMap.clear(); + this.spotOracleStringMap.clear(); + } +} diff --git a/sdk/src/accounts/grpcMultiAccountSubscriber.ts b/sdk/src/accounts/grpcMultiAccountSubscriber.ts new file mode 100644 index 0000000000..7a442ac5cd --- /dev/null +++ b/sdk/src/accounts/grpcMultiAccountSubscriber.ts @@ -0,0 +1,476 @@ +import { Program } from '@coral-xyz/anchor'; +import { Commitment, Context, PublicKey } from '@solana/web3.js'; +import * as Buffer from 'buffer'; +import bs58 from 'bs58'; + +import { + Client, + ClientDuplexStream, + CommitmentLevel, + SubscribeRequest, + SubscribeUpdate, + createClient, +} from '../isomorphic/grpc'; +import { BufferAndSlot, DataAndSlot, GrpcConfigs, ResubOpts } from './types'; + +interface AccountInfoLike { + owner: PublicKey; + lamports: number; + data: Buffer; + executable: boolean; + rentEpoch: number; +} + +function commitmentLevelToCommitment( + commitmentLevel: CommitmentLevel +): Commitment { + switch (commitmentLevel) { + case CommitmentLevel.PROCESSED: + return 'processed'; + case CommitmentLevel.CONFIRMED: + return 'confirmed'; + case CommitmentLevel.FINALIZED: + return 'finalized'; + default: + return 'confirmed'; + } +} + +export class grpcMultiAccountSubscriber { + private client: Client; + private stream: ClientDuplexStream; + private commitmentLevel: CommitmentLevel; + private program: Program; + private accountName: string; + private decodeBufferFn?: ( + buffer: Buffer, + pubkey?: string, + accountProps?: U + ) => T; + private resubOpts?: ResubOpts; + private onUnsubscribe?: () => Promise; + + public listenerId?: number; + public isUnsubscribing = false; + private timeoutId?: ReturnType; + private receivingData = false; + + private subscribedAccounts = new Set(); + private onChangeMap = new Map< + string, + (data: T, context: Context, buffer: Buffer, accountProps: U) => void + >(); + + private dataMap = new Map>(); + private accountPropsMap = new Map>(); + private bufferMap = new Map(); + + private constructor( + client: Client, + commitmentLevel: CommitmentLevel, + accountName: string, + program: Program, + decodeBuffer?: (buffer: Buffer, pubkey?: string) => T, + resubOpts?: ResubOpts, + onUnsubscribe?: () => Promise, + accountPropsMap?: Map> + ) { + this.client = client; + this.commitmentLevel = commitmentLevel; + this.accountName = accountName; + this.program = program; + this.decodeBufferFn = decodeBuffer; + this.resubOpts = resubOpts; + this.onUnsubscribe = onUnsubscribe; + this.accountPropsMap = accountPropsMap; + } + + public static async create( + grpcConfigs: GrpcConfigs, + accountName: string, + program: Program, + decodeBuffer?: (buffer: Buffer, pubkey?: string, accountProps?: U) => T, + resubOpts?: ResubOpts, + clientProp?: Client, + onUnsubscribe?: () => Promise, + accountPropsMap?: Map> + ): Promise> { + const client = clientProp + ? clientProp + : await createClient( + grpcConfigs.endpoint, + grpcConfigs.token, + grpcConfigs.channelOptions ?? {} + ); + const commitmentLevel = + // @ts-ignore :: isomorphic exported enum fails typescript but will work at runtime + grpcConfigs.commitmentLevel ?? CommitmentLevel.CONFIRMED; + + return new grpcMultiAccountSubscriber( + client, + commitmentLevel, + accountName, + program, + decodeBuffer, + resubOpts, + onUnsubscribe, + accountPropsMap + ); + } + + setAccountData(accountPubkey: string, data: T, slot?: number): void { + this.dataMap.set(accountPubkey, { data, slot }); + } + + getAccountData(accountPubkey: string): DataAndSlot | undefined { + return this.dataMap.get(accountPubkey); + } + + getAccountDataMap(): Map> { + return this.dataMap; + } + + async fetch(): Promise { + try { + // Chunk account IDs into groups of 100 (getMultipleAccounts limit) + const chunkSize = 100; + const chunks: string[][] = []; + const accountIds = Array.from(this.subscribedAccounts.values()); + for (let i = 0; i < accountIds.length; i += chunkSize) { + chunks.push(accountIds.slice(i, i + chunkSize)); + } + + // Process all chunks concurrently + await Promise.all( + chunks.map(async (chunk) => { + const accountAddresses = chunk.map( + (accountId) => new PublicKey(accountId) + ); + const rpcResponseAndContext = + await this.program.provider.connection.getMultipleAccountsInfoAndContext( + accountAddresses, + { + commitment: commitmentLevelToCommitment(this.commitmentLevel), + } + ); + + const rpcResponse = rpcResponseAndContext.value; + const currentSlot = rpcResponseAndContext.context.slot; + + for (let i = 0; i < chunk.length; i++) { + const accountId = chunk[i]; + const accountInfo = rpcResponse[i]; + if (accountInfo) { + const prev = this.bufferMap.get(accountId); + const newBuffer = accountInfo.data as Buffer; + if (prev && currentSlot < prev.slot) { + continue; + } + if ( + prev && + prev.buffer && + newBuffer && + newBuffer.equals(prev.buffer) + ) { + continue; + } + this.bufferMap.set(accountId, { + buffer: newBuffer, + slot: currentSlot, + }); + + const accountDecoded = this.program.coder.accounts.decode( + this.capitalize(this.accountName), + newBuffer + ); + this.setAccountData(accountId, accountDecoded, currentSlot); + } + } + }) + ); + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.accountName}] grpcMultiAccountSubscriber error fetching accounts:`, + error + ); + } + } + } + + async subscribe( + accounts: PublicKey[], + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer, + accountProps: U + ) => void + ): Promise { + if (this.resubOpts?.logResubMessages) { + console.log(`[${this.accountName}] grpcMultiAccountSubscriber subscribe`); + } + if (this.listenerId != null || this.isUnsubscribing) { + return; + } + + // Track accounts and single onChange for all + for (const pk of accounts) { + const key = pk.toBase58(); + this.subscribedAccounts.add(key); + this.onChangeMap.set(key, (data, ctx, buffer, accountProps) => { + this.setAccountData(key, data, ctx.slot); + onChange(new PublicKey(key), data, ctx, buffer, accountProps); + }); + } + + this.stream = + (await this.client.subscribe()) as unknown as typeof this.stream; + const request: SubscribeRequest = { + slots: {}, + accounts: { + account: { + account: accounts.map((a) => a.toBase58()), + owner: [], + filters: [], + }, + }, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + commitment: this.commitmentLevel, + entry: {}, + transactionsStatus: {}, + }; + + this.stream.on('data', (chunk: SubscribeUpdate) => { + if (!chunk.account) { + return; + } + const slot = Number(chunk.account.slot); + const accountPubkeyBytes = chunk.account.account.pubkey; + const accountPubkey = bs58.encode( + accountPubkeyBytes as unknown as Uint8Array + ); + if (!accountPubkey || !this.subscribedAccounts.has(accountPubkey)) { + return; + } + + // Touch resub timer on any incoming account update for subscribed keys + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + this.setTimeout(); + } + + // Skip processing if we already have data for this account at a newer slot + const existing = this.dataMap.get(accountPubkey); + if (existing?.slot !== undefined && existing.slot > slot) { + return; + } + const accountInfo: AccountInfoLike = { + owner: new PublicKey(chunk.account.account.owner), + lamports: Number(chunk.account.account.lamports), + data: Buffer.Buffer.from(chunk.account.account.data), + executable: chunk.account.account.executable, + rentEpoch: Number(chunk.account.account.rentEpoch), + }; + + const context = { slot } as Context; + const buffer = accountInfo.data; + + // Check existing buffer for this account and skip if unchanged or slot regressed + const prevBuffer = this.bufferMap.get(accountPubkey); + if (prevBuffer && slot < prevBuffer.slot) { + return; + } + if ( + prevBuffer && + prevBuffer.buffer && + buffer && + buffer.equals(prevBuffer.buffer) + ) { + return; + } + this.bufferMap.set(accountPubkey, { buffer, slot }); + const accountProps = this.accountPropsMap?.get(accountPubkey); + + const handleDataBuffer = ( + context: Context, + buffer: Buffer, + accountProps: U + ) => { + const data = this.decodeBufferFn + ? this.decodeBufferFn(buffer, accountPubkey, accountProps) + : this.program.account[this.accountName].coder.accounts.decode( + this.capitalize(this.accountName), + buffer + ); + const handler = this.onChangeMap.get(accountPubkey); + if (handler) { + handler(data, context, buffer, accountProps); + } + }; + + if (Array.isArray(accountProps)) { + for (const props of accountProps) { + handleDataBuffer(context, buffer, props); + } + } else { + handleDataBuffer(context, buffer, accountProps); + } + }); + + return new Promise((resolve, reject) => { + this.stream.write(request, (err) => { + if (err === null || err === undefined) { + this.listenerId = 1; + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + } + resolve(); + } else { + reject(err); + } + }); + }).catch((reason) => { + console.error(reason); + throw reason; + }); + } + + async addAccounts(accounts: PublicKey[]): Promise { + for (const pk of accounts) { + this.subscribedAccounts.add(pk.toBase58()); + } + const request: SubscribeRequest = { + slots: {}, + accounts: { + account: { + account: Array.from(this.subscribedAccounts.values()), + owner: [], + filters: [], + }, + }, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + commitment: this.commitmentLevel, + entry: {}, + transactionsStatus: {}, + }; + + await new Promise((resolve, reject) => { + this.stream.write(request, (err) => { + if (err === null || err === undefined) { + resolve(); + } else { + reject(err); + } + }); + }); + await this.fetch(); + } + + async removeAccounts(accounts: PublicKey[]): Promise { + for (const pk of accounts) { + const k = pk.toBase58(); + this.subscribedAccounts.delete(k); + this.onChangeMap.delete(k); + } + const request: SubscribeRequest = { + slots: {}, + accounts: { + account: { + account: Array.from(this.subscribedAccounts.values()), + owner: [], + filters: [], + }, + }, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + commitment: this.commitmentLevel, + entry: {}, + transactionsStatus: {}, + }; + + await new Promise((resolve, reject) => { + this.stream.write(request, (err) => { + if (err === null || err === undefined) { + resolve(); + } else { + reject(err); + } + }); + }); + } + + async unsubscribe(): Promise { + this.isUnsubscribing = true; + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + + if (this.listenerId != null) { + const promise = new Promise((resolve, reject) => { + const request: SubscribeRequest = { + slots: {}, + accounts: {}, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + entry: {}, + transactionsStatus: {}, + }; + this.stream.write(request, (err) => { + if (err === null || err === undefined) { + this.listenerId = undefined; + this.isUnsubscribing = false; + resolve(); + } else { + reject(err); + } + }); + }).catch((reason) => { + console.error(reason); + throw reason; + }); + return promise; + } else { + this.isUnsubscribing = false; + } + + if (this.onUnsubscribe) { + try { + await this.onUnsubscribe(); + } catch (e) { + console.error(e); + } + } + } + + private setTimeout(): void { + this.timeoutId = setTimeout( + async () => { + if (this.isUnsubscribing) { + return; + } + if (this.receivingData) { + await this.unsubscribe(); + this.receivingData = false; + } + }, + this.resubOpts?.resubTimeoutMs + ); + } + + private capitalize(value: string): string { + if (!value) return value; + return value.charAt(0).toUpperCase() + value.slice(1); + } +} diff --git a/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts b/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts new file mode 100644 index 0000000000..38ea46c78c --- /dev/null +++ b/sdk/src/accounts/grpcMultiUserAccountSubscriber.ts @@ -0,0 +1,281 @@ +import { + DataAndSlot, + GrpcConfigs, + NotSubscribedError, + ResubOpts, + UserAccountEvents, + UserAccountSubscriber, +} from './types'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Context, PublicKey } from '@solana/web3.js'; +import { Program } from '@coral-xyz/anchor'; +import { UserAccount } from '../types'; +import { grpcMultiAccountSubscriber } from './grpcMultiAccountSubscriber'; + +export class grpcMultiUserAccountSubscriber { + private program: Program; + private multiSubscriber: grpcMultiAccountSubscriber; + + private userData = new Map>(); + private listeners = new Map< + string, + Set> + >(); + private keyToPk = new Map(); + private pendingAddKeys = new Set(); + private debounceTimer?: ReturnType; + private debounceMs = 20; + private isMultiSubscribed = false; + private userAccountSubscribers = new Map(); + private grpcConfigs: GrpcConfigs; + resubOpts?: ResubOpts; + + private handleAccountChange = ( + accountId: PublicKey, + data: UserAccount, + context: Context, + _buffer?: unknown, + _accountProps?: unknown + ): void => { + const k = accountId.toBase58(); + this.userData.set(k, { data, slot: context.slot }); + const setForKey = this.listeners.get(k); + if (setForKey) { + for (const emitter of setForKey) { + emitter.emit('userAccountUpdate', data); + emitter.emit('update'); + } + } + }; + + public constructor( + program: Program, + grpcConfigs: GrpcConfigs, + resubOpts?: ResubOpts, + multiSubscriber?: grpcMultiAccountSubscriber + ) { + this.program = program; + this.multiSubscriber = multiSubscriber; + this.grpcConfigs = grpcConfigs; + this.resubOpts = resubOpts; + } + + public async subscribe(): Promise { + if (!this.multiSubscriber) { + this.multiSubscriber = + await grpcMultiAccountSubscriber.create( + this.grpcConfigs, + 'user', + this.program, + undefined, + this.resubOpts + ); + } + + // Subscribe all per-user subscribers first + await Promise.all( + Array.from(this.userAccountSubscribers.values()).map((subscriber) => + subscriber.subscribe() + ) + ); + // Ensure we immediately register any pending keys and kick off underlying subscription/fetch + await this.flushPending(); + // Proactively fetch once to populate data for all subscribed accounts + await this.multiSubscriber.fetch(); + // Wait until the underlying multi-subscriber has data for every registered user key + const targetKeys = Array.from(this.listeners.keys()); + if (targetKeys.length === 0) return; + // Poll until all keys are present in dataMap + // Use debounceMs as the polling cadence to avoid introducing new magic numbers + // eslint-disable-next-line no-constant-condition + while (true) { + const map = this.multiSubscriber.getAccountDataMap(); + let allPresent = true; + for (const k of targetKeys) { + if (!map.has(k)) { + allPresent = false; + break; + } + } + if (allPresent) break; + await new Promise((resolve) => setTimeout(resolve, this.debounceMs)); + } + } + + public forUser(userAccountPublicKey: PublicKey): UserAccountSubscriber { + if (this.userAccountSubscribers.has(userAccountPublicKey.toBase58())) { + return this.userAccountSubscribers.get(userAccountPublicKey.toBase58())!; + } + const key = userAccountPublicKey.toBase58(); + const perUserEmitter: StrictEventEmitter = + new EventEmitter(); + // eslint-disable-next-line @typescript-eslint/no-this-alias + const parent = this; + let isSubscribed = false; + + const registerHandlerIfNeeded = async () => { + if (!this.listeners.has(key)) { + this.listeners.set(key, new Set()); + this.keyToPk.set(key, userAccountPublicKey); + this.pendingAddKeys.add(key); + if (this.isMultiSubscribed) { + // only schedule flush if already subscribed to the multi-subscriber + this.scheduleFlush(); + } + } + }; + + const perUser: UserAccountSubscriber = { + get eventEmitter() { + return perUserEmitter; + }, + set eventEmitter(_v) {}, + + get isSubscribed() { + return isSubscribed; + }, + set isSubscribed(_v: boolean) { + isSubscribed = _v; + }, + + async subscribe(userAccount?: UserAccount): Promise { + if (isSubscribed) return true; + if (userAccount) { + this.updateData(userAccount, 0); + } + await registerHandlerIfNeeded(); + const setForKey = parent.listeners.get(key)!; + setForKey.add(perUserEmitter); + isSubscribed = true; + return true; + }, + + async fetch(): Promise { + if (!isSubscribed) { + throw new NotSubscribedError( + 'Must subscribe before fetching account updates' + ); + } + const account = (await parent.program.account.user.fetch( + userAccountPublicKey + )) as UserAccount; + this.updateData(account, 0); + }, + + updateData(userAccount: UserAccount, slot: number): void { + const existingData = parent.userData.get(key); + if (existingData && existingData.slot > slot) { + return; + } + parent.userData.set(key, { data: userAccount, slot }); + perUserEmitter.emit('userAccountUpdate', userAccount); + perUserEmitter.emit('update'); + }, + + async unsubscribe(): Promise { + if (!isSubscribed) return; + const setForKey = parent.listeners.get(key); + if (setForKey) { + setForKey.delete(perUserEmitter); + if (setForKey.size === 0) { + parent.listeners.delete(key); + await parent.multiSubscriber.removeAccounts([userAccountPublicKey]); + parent.userData.delete(key); + parent.keyToPk.delete(key); + parent.pendingAddKeys.delete(key); + } + } + isSubscribed = false; + }, + + getUserAccountAndSlot(): DataAndSlot { + const das = parent.userData.get(key); + if (!das) { + throw new NotSubscribedError( + 'Must subscribe before getting user account data' + ); + } + return das; + }, + }; + + this.userAccountSubscribers.set(userAccountPublicKey.toBase58(), perUser); + return perUser; + } + + private scheduleFlush(): void { + if (this.debounceTimer) return; + this.debounceTimer = setTimeout(() => { + void this.flushPending(); + }, this.debounceMs); + } + + private async flushPending(): Promise { + const hasPending = this.pendingAddKeys.size > 0; + if (!hasPending) { + this.debounceTimer = undefined; + return; + } + + const allPks: PublicKey[] = []; + for (const k of this.listeners.keys()) { + const pk = this.keyToPk.get(k); + if (pk) allPks.push(pk); + } + if (allPks.length === 0) { + this.pendingAddKeys.clear(); + this.debounceTimer = undefined; + return; + } + + if (!this.isMultiSubscribed) { + await this.multiSubscriber.subscribe(allPks, this.handleAccountChange); + this.isMultiSubscribed = true; + await this.multiSubscriber.fetch(); + for (const k of this.pendingAddKeys) { + const pk = this.keyToPk.get(k); + if (pk) { + const data = this.multiSubscriber.getAccountData(k); + if (data) { + this.handleAccountChange( + pk, + data.data, + { slot: data.slot }, + undefined, + undefined + ); + } + } + } + } else { + const ms = this.multiSubscriber as unknown as { + onChangeMap: Map< + string, + ( + data: UserAccount, + context: Context, + buffer: unknown, + accountProps: unknown + ) => void + >; + }; + for (const k of this.pendingAddKeys) { + ms.onChangeMap.set(k, (data, ctx, buffer, accountProps) => { + this.multiSubscriber.setAccountData(k, data, ctx.slot); + this.handleAccountChange( + new PublicKey(k), + data, + ctx, + buffer, + accountProps + ); + }); + } + await this.multiSubscriber.addAccounts(allPks); + } + + this.pendingAddKeys.clear(); + this.debounceTimer = undefined; + } +} diff --git a/sdk/src/accounts/laserProgramAccountSubscriber.ts b/sdk/src/accounts/laserProgramAccountSubscriber.ts new file mode 100644 index 0000000000..a2315ad92f --- /dev/null +++ b/sdk/src/accounts/laserProgramAccountSubscriber.ts @@ -0,0 +1,215 @@ +import { GrpcConfigs, ResubOpts } from './types'; +import { Program } from '@coral-xyz/anchor'; +import { Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import * as Buffer from 'buffer'; +import { WebSocketProgramAccountSubscriber } from './webSocketProgramAccountSubscriber'; + +import { + LaserCommitmentLevel, + LaserSubscribe, + LaserstreamConfig, + LaserSubscribeRequest, + LaserSubscribeUpdate, + CompressionAlgorithms, + CommitmentLevel, +} from '../isomorphic/grpc'; + +type LaserCommitment = + (typeof LaserCommitmentLevel)[keyof typeof LaserCommitmentLevel]; + +export class LaserstreamProgramAccountSubscriber< + T, +> extends WebSocketProgramAccountSubscriber { + private stream: + | { + id: string; + cancel: () => void; + write?: (req: LaserSubscribeRequest) => Promise; + } + | undefined; + + private commitmentLevel: CommitmentLevel; + public listenerId?: number; + + private readonly laserConfig: LaserstreamConfig; + + private constructor( + laserConfig: LaserstreamConfig, + commitmentLevel: CommitmentLevel, + subscriptionName: string, + accountDiscriminator: string, + program: Program, + decodeBufferFn: (accountName: string, ix: Buffer) => T, + options: { filters: MemcmpFilter[] } = { filters: [] }, + resubOpts?: ResubOpts + ) { + super( + subscriptionName, + accountDiscriminator, + program, + decodeBufferFn, + options, + resubOpts + ); + this.laserConfig = laserConfig; + this.commitmentLevel = this.toLaserCommitment(commitmentLevel); + } + + public static async create( + grpcConfigs: GrpcConfigs, + subscriptionName: string, + accountDiscriminator: string, + program: Program, + decodeBufferFn: (accountName: string, ix: Buffer) => U, + options: { filters: MemcmpFilter[] } = { + filters: [], + }, + resubOpts?: ResubOpts + ): Promise> { + const laserConfig: LaserstreamConfig = { + apiKey: grpcConfigs.token, + endpoint: grpcConfigs.endpoint, + maxReconnectAttempts: grpcConfigs.enableReconnect ? 10 : 0, + channelOptions: { + 'grpc.default_compression_algorithm': CompressionAlgorithms.zstd, + 'grpc.max_receive_message_length': 1_000_000_000, + }, + }; + + const commitmentLevel = + grpcConfigs.commitmentLevel ?? CommitmentLevel.CONFIRMED; + + return new LaserstreamProgramAccountSubscriber( + laserConfig, + commitmentLevel, + subscriptionName, + accountDiscriminator, + program, + decodeBufferFn, + options, + resubOpts + ); + } + + async subscribe( + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void + ): Promise { + if (this.listenerId != null || this.isUnsubscribing) return; + + this.onChange = onChange; + + const filters = this.options.filters.map((filter) => { + return { + memcmp: { + offset: filter.memcmp.offset, + base58: filter.memcmp.bytes, + }, + }; + }); + + const request: LaserSubscribeRequest = { + slots: {}, + accounts: { + drift: { + account: [], + owner: [this.program.programId.toBase58()], + filters, + }, + }, + transactions: {}, + blocks: {}, + blocksMeta: {}, + accountsDataSlice: [], + commitment: this.commitmentLevel, + entry: {}, + transactionsStatus: {}, + }; + + try { + const stream = await LaserSubscribe( + this.laserConfig, + request, + async (update: LaserSubscribeUpdate) => { + if (update.account) { + const slot = Number(update.account.slot); + const acc = update.account.account; + + const accountInfo = { + owner: new PublicKey(acc.owner), + lamports: Number(acc.lamports), + data: Buffer.Buffer.from(acc.data), + executable: acc.executable, + rentEpoch: Number(acc.rentEpoch), + }; + + const payload = { + accountId: new PublicKey(acc.pubkey), + accountInfo, + }; + + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + this.handleRpcResponse({ slot }, payload); + this.setTimeout(); + } else { + this.handleRpcResponse({ slot }, payload); + } + } + }, + async (error) => { + console.error('LaserStream client error:', error); + throw error; + } + ); + + this.stream = stream; + this.listenerId = 1; + + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + this.setTimeout(); + } + } catch (err) { + console.error('Failed to start LaserStream client:', err); + throw err; + } + } + + public async unsubscribe(onResub = false): Promise { + if (!onResub && this.resubOpts) { + this.resubOpts.resubTimeoutMs = undefined; + } + this.isUnsubscribing = true; + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + + if (this.listenerId != null && this.stream) { + try { + this.stream.cancel(); + } finally { + this.listenerId = undefined; + this.isUnsubscribing = false; + } + } else { + this.isUnsubscribing = false; + } + } + + public toLaserCommitment( + level: string | number | undefined + ): LaserCommitment { + if (typeof level === 'string') { + return ( + (LaserCommitmentLevel as any)[level.toUpperCase()] ?? + LaserCommitmentLevel.CONFIRMED + ); + } + return (level as LaserCommitment) ?? LaserCommitmentLevel.CONFIRMED; + } +} diff --git a/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts b/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts new file mode 100644 index 0000000000..a464063f6c --- /dev/null +++ b/sdk/src/accounts/oneShotUserStatsAccountSubscriber.ts @@ -0,0 +1,69 @@ +import { Commitment, PublicKey } from '@solana/web3.js'; +import { UserStatsAccount } from '../types'; +import { BasicUserStatsAccountSubscriber } from './basicUserStatsAccountSubscriber'; +import { Program } from '@coral-xyz/anchor'; +import { UserStatsAccountSubscriber } from './types'; + +/** + * Simple implementation of UserStatsAccountSubscriber. It will fetch the UserStatsAccount + * data on subscribe (or call to fetch) if no account data is provided on init. + * Expect to use only 1 RPC call unless you call fetch repeatedly. + */ +export class OneShotUserStatsAccountSubscriber + extends BasicUserStatsAccountSubscriber + implements UserStatsAccountSubscriber +{ + program: Program; + commitment: Commitment; + + public constructor( + program: Program, + userStatsAccountPublicKey: PublicKey, + data?: UserStatsAccount, + slot?: number, + commitment?: Commitment + ) { + super(userStatsAccountPublicKey, data, slot); + this.program = program; + this.commitment = commitment ?? 'confirmed'; + } + + async subscribe(userStatsAccount?: UserStatsAccount): Promise { + if (userStatsAccount) { + this.userStats = { data: userStatsAccount, slot: this.userStats.slot }; + return true; + } + + await this.fetchIfUnloaded(); + if (this.doesAccountExist()) { + this.eventEmitter.emit('update'); + } + return true; + } + + async fetchIfUnloaded(): Promise { + if (this.userStats.data === undefined) { + await this.fetch(); + } + } + + async fetch(): Promise { + try { + const dataAndContext = + await this.program.account.userStats.fetchAndContext( + this.userStatsAccountPublicKey, + this.commitment + ); + if (dataAndContext.context.slot > (this.userStats?.slot ?? 0)) { + this.userStats = { + data: dataAndContext.data as UserStatsAccount, + slot: dataAndContext.context.slot, + }; + } + } catch (e) { + console.error( + `OneShotUserStatsAccountSubscriber.fetch() UserStatsAccount does not exist: ${e.message}` + ); + } + } +} diff --git a/sdk/src/accounts/types.ts b/sdk/src/accounts/types.ts index 98ab7133fb..db9cf2112b 100644 --- a/sdk/src/accounts/types.ts +++ b/sdk/src/accounts/types.ts @@ -6,6 +6,7 @@ import { UserAccount, UserStatsAccount, InsuranceFundStake, + ConstituentAccount, HighLeverageModeConfig, } from '../types'; import StrictEventEmitter from 'strict-event-emitter-types'; @@ -234,6 +235,7 @@ export type GrpcConfigs = { * Defaults to false, will throw on connection loss. */ enableReconnect?: boolean; + client?: 'yellowstone' | 'laser'; }; export interface HighLeverageModeConfigAccountSubscriber { @@ -259,3 +261,22 @@ export interface HighLeverageModeConfigAccountEvents { update: void; error: (e: Error) => void; } + +export interface ConstituentAccountSubscriber { + eventEmitter: StrictEventEmitter; + isSubscribed: boolean; + + subscribe(constituentAccount?: ConstituentAccount): Promise; + sync(): Promise; + unsubscribe(): Promise; +} + +export interface ConstituentAccountEvents { + onAccountUpdate: ( + account: ConstituentAccount, + pubkey: PublicKey, + slot: number + ) => void; + update: void; + error: (e: Error) => void; +} diff --git a/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts new file mode 100644 index 0000000000..31a68ce02e --- /dev/null +++ b/sdk/src/accounts/webSocketProgramAccountSubscriberV2.ts @@ -0,0 +1,995 @@ +import { BufferAndSlot, ProgramAccountSubscriber, ResubOpts } from './types'; +import { AnchorProvider, Program } from '@coral-xyz/anchor'; +import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import { + AccountInfoBase, + AccountInfoWithBase58EncodedData, + AccountInfoWithBase64EncodedData, + createSolanaClient, + isAddress, + Lamports, + Slot, + Address, + Commitment as GillCommitment, +} from 'gill'; +import bs58 from 'bs58'; + +type ProgramAccountSubscriptionAsyncIterable = AsyncIterable< + Readonly<{ + context: Readonly<{ + slot: Slot; + }>; + value: Readonly<{ + account: Readonly<{ + executable: boolean; + lamports: Lamports; + owner: Address; + rentEpoch: bigint; + space: bigint; + }> & + Readonly; + pubkey: Address; + }>; + }> +>; +/** + * WebSocketProgramAccountsSubscriberV2 + * + * High-level overview + * - WebSocket-first subscriber for Solana program accounts that also layers in + * targeted polling to detect missed updates reliably. + * - Emits decoded account updates via the provided `onChange` callback. + * - Designed to focus extra work on the specific accounts the consumer cares + * about ("monitored accounts") while keeping baseline WS behavior for the + * full program subscription. + * + * Why polling if this is a WebSocket subscriber? + * - WS infra can stall, drop, or reorder notifications under network stress or + * provider hiccups. When that happens, critical account changes can be missed. + * - To mitigate this, the class accepts a set of accounts (provided via constructor) to monitor + * and uses light polling to verify whether a WS change was missed. + * - If polling detects a newer slot with different data than the last seen + * buffer, a centralized resubscription is triggered to restore a clean stream. + * + * Initial fetch (on subscribe) + * - On `subscribe()`, we first perform a single batched fetch of all monitored + * accounts ("initial monitor fetch"). + * - Purpose: seed the internal `bufferAndSlotMap` and emit the latest state so + * consumers have up-to-date data immediately, even before WS events arrive. + * - This step does not decide resubscription; it only establishes ground truth. + * + * Continuous polling (only for monitored accounts) + * - After seeding, each monitored account is put into a monitoring cycle: + * 1) If no WS notification for an account is observed for `pollingIntervalMs`, + * we enqueue it for a batched fetch (buffered for a short window). + * 2) Once an account enters the "currently polling" set, a shared batch poll + * runs every `pollingIntervalMs` across all such accounts. + * 3) If WS notifications resume for an account, that account is removed from + * the polling set and returns to passive monitoring. + * - Polling compares the newly fetched buffer with the last stored buffer at a + * later slot. A difference indicates a missed update; we schedule a single + * resubscription (coalesced across accounts) to re-sync. + * + * Accounts the consumer cares about + * - Provide accounts up-front via the constructor `accountsToMonitor`, or add + * them dynamically with `addAccountToMonitor()` and remove with + * `removeAccountFromMonitor()`. + * - Only these accounts incur additional polling safeguards; other accounts are + * still processed from the WS stream normally. + * + * Resubscription strategy + * - Missed updates from any monitored account are coalesced and trigger a single + * resubscription after a short delay. This avoids rapid churn. + * - If `resubOpts.resubTimeoutMs` is set, an inactivity timer also performs a + * batch check of monitored accounts. If a missed update is found, the same + * centralized resubscription flow is used. + * + * Tuning knobs + * - `setPollingInterval(ms)`: adjust how often monitoring/polling runs + * (default 30s). Shorter = faster detection, higher RPC load. + * - Debounced immediate poll (~100ms): batches accounts added to polling right after inactivity. + * - Batch size for `getMultipleAccounts` is limited to 100, requests are chunked + * and processed concurrently. + */ + +export class WebSocketProgramAccountsSubscriberV2 + implements ProgramAccountSubscriber +{ + subscriptionName: string; + accountDiscriminator: string; + bufferAndSlotMap: Map = new Map(); + program: Program; + decodeBuffer: (accountName: string, ix: Buffer) => T; + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void; + listenerId?: number; + resubOpts: ResubOpts; + isUnsubscribing = false; + timeoutId?: ReturnType; + options: { filters: MemcmpFilter[]; commitment?: Commitment }; + + receivingData = false; + + // Gill client components + private rpc: ReturnType['rpc']; + private rpcSubscriptions: ReturnType< + typeof createSolanaClient + >['rpcSubscriptions']; + private abortController?: AbortController; + + // Polling logic for specific accounts + private accountsToMonitor: Set = new Set(); + private pollingIntervalMs: number = 30000; // 30 seconds + private pollingTimeouts: Map> = + new Map(); + private lastWsNotificationTime: Map = new Map(); // Track last WS notification time per account + private accountsCurrentlyPolling: Set = new Set(); // Track which accounts are being polled + private batchPollingTimeout?: ReturnType; // Single timeout for batch polling + + // Debounced immediate poll to batch multiple additions within a short window + private debouncedImmediatePollTimeout?: ReturnType; + private debouncedImmediatePollMs: number = 100; // configurable short window + + // Centralized resubscription handling + private missedChangeDetected = false; // Flag to track if any missed change was detected + private resubscriptionTimeout?: ReturnType; // Timeout for delayed resubscription + private accountsWithMissedUpdates: Set = new Set(); // Track which accounts had missed updates + + public constructor( + subscriptionName: string, + accountDiscriminator: string, + program: Program, + decodeBufferFn: (accountName: string, ix: Buffer) => T, + options: { filters: MemcmpFilter[]; commitment?: Commitment } = { + filters: [], + }, + resubOpts?: ResubOpts, + accountsToMonitor?: PublicKey[] // Optional list of accounts to poll + ) { + this.subscriptionName = subscriptionName; + this.accountDiscriminator = accountDiscriminator; + this.program = program; + this.decodeBuffer = decodeBufferFn; + this.resubOpts = resubOpts ?? { + resubTimeoutMs: 30000, + usePollingInsteadOfResub: true, + logResubMessages: false, + }; + if (this.resubOpts?.resubTimeoutMs < 1000) { + console.log( + 'resubTimeoutMs should be at least 1000ms to avoid spamming resub' + ); + } + this.options = options; + this.receivingData = false; + + // Initialize accounts to monitor + if (accountsToMonitor) { + accountsToMonitor.forEach((account) => { + this.accountsToMonitor.add(account.toBase58()); + }); + } + + // Initialize gill client using the same RPC URL as the program provider + const rpcUrl = (this.program.provider as AnchorProvider).connection + .rpcEndpoint; + const { rpc, rpcSubscriptions } = createSolanaClient({ + urlOrMoniker: rpcUrl, + }); + this.rpc = rpc; + this.rpcSubscriptions = rpcSubscriptions; + } + + private async handleNotificationLoop( + notificationPromise: Promise + ) { + try { + const subscriptionIterable = await notificationPromise; + for await (const notification of subscriptionIterable) { + try { + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + clearTimeout(this.timeoutId); + this.handleRpcResponse( + notification.context, + notification.value.pubkey, + notification.value.account.data + ); + this.setTimeout(); + } else { + this.handleRpcResponse( + notification.context, + notification.value.pubkey, + notification.value.account.data + ); + } + } catch (error) { + console.error( + `Error handling RPC response for pubkey ${notification.value.pubkey}:`, + error + ); + } + } + } catch (error) { + console.error( + `[${this.subscriptionName}] Error in notification loop:`, + error + ); + } + } + + async subscribe( + onChange: ( + accountId: PublicKey, + data: T, + context: Context, + buffer: Buffer + ) => void + ): Promise { + /** + * Start the WebSocket subscription and initialize polling safeguards. + * + * Flow + * - Seeds all monitored accounts with a single batched RPC fetch and emits + * their current state. + * - Subscribes to program notifications via WS using gill. + * - If `resubOpts.resubTimeoutMs` is set, starts an inactivity timer that + * batch-checks monitored accounts when WS goes quiet. + * - Begins monitoring for accounts that may need polling when WS + * notifications are not observed within `pollingIntervalMs`. + * + * @param onChange Callback invoked with decoded account data when an update + * is detected (via WS or batch RPC fetch). + */ + const startTime = performance.now(); + if (this.listenerId != null || this.isUnsubscribing) { + return; + } + + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] initializing subscription. This many monitored accounts: ${this.accountsToMonitor.size}` + ); + } + + this.onChange = onChange; + + // initial fetch of monitored data - only fetch and populate, don't check for missed changes + await this.fetchAndPopulateAllMonitoredAccounts(); + + // Create abort controller for proper cleanup + const abortController = new AbortController(); + this.abortController = abortController; + + this.listenerId = Math.random(); // Unique ID for logging purposes + + if (this.resubOpts?.resubTimeoutMs) { + this.receivingData = true; + this.setTimeout(); + } + + // Subscribe to program account changes using gill's rpcSubscriptions + const programId = this.program.programId.toBase58(); + if (isAddress(programId)) { + const subscriptionPromise = this.rpcSubscriptions + .programNotifications(programId, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + filters: this.options.filters.map((filter) => { + // Convert filter bytes from base58 to base64 if needed + let bytes = filter.memcmp.bytes; + if ( + typeof bytes === 'string' && + /^[1-9A-HJ-NP-Za-km-z]+$/.test(bytes) + ) { + // Looks like base58 - convert to base64 + const decoded = bs58.decode(bytes); + bytes = Buffer.from(decoded).toString('base64'); + } + + return { + memcmp: { + offset: BigInt(filter.memcmp.offset), + bytes: bytes as any, + encoding: 'base64' as const, + }, + }; + }), + }) + .subscribe({ + abortSignal: abortController.signal, + }); + + // Start notification loop without awaiting + this.handleNotificationLoop(subscriptionPromise); + // Start monitoring for accounts that may need polling if no WS event is received + this.startMonitoringForAccounts(); + } + const endTime = performance.now(); + console.log( + `[PROFILING] ${this.subscriptionName}.subscribe() completed in ${ + endTime - startTime + }ms` + ); + } + + protected setTimeout(): void { + if (!this.onChange) { + throw new Error('onChange callback function must be set'); + } + this.timeoutId = setTimeout( + async () => { + if (this.isUnsubscribing) { + // If we are in the process of unsubscribing, do not attempt to resubscribe + return; + } + + if (this.receivingData) { + if (this.resubOpts?.logResubMessages) { + console.log( + `No ws data from ${this.subscriptionName} in ${this.resubOpts?.resubTimeoutMs}ms, checking for missed changes` + ); + } + + // Check for missed changes in monitored accounts + const missedChangeDetected = await this.fetchAllMonitoredAccounts(); + + if (missedChangeDetected) { + // Signal missed change with a generic identifier since we don't have specific account IDs from this context + this.signalMissedChange('timeout-check'); + } else { + // No missed changes, continue monitoring + this.receivingData = false; + this.setTimeout(); + } + } + }, + this.resubOpts?.resubTimeoutMs + ); + } + + handleRpcResponse( + context: { slot: bigint }, + accountId: Address, + accountInfo?: AccountInfoBase & + ( + | AccountInfoWithBase58EncodedData + | AccountInfoWithBase64EncodedData + )['data'] + ): void { + const newSlot = Number(context.slot); + let newBuffer: Buffer | undefined = undefined; + + if (accountInfo) { + // Handle different data formats from gill + if (Array.isArray(accountInfo)) { + // If it's a tuple [data, encoding] + const [data, encoding] = accountInfo; + + if (encoding === ('base58' as any)) { + // Convert base58 to buffer using bs58 + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + + const accountIdString = accountId.toString(); + const existingBufferAndSlot = this.bufferAndSlotMap.get(accountIdString); + + // Track WebSocket notification time for this account + this.lastWsNotificationTime.set(accountIdString, Date.now()); + + // If this account was being polled, stop polling it if the buffer has changed + if ( + this.accountsCurrentlyPolling.has(accountIdString) && + !existingBufferAndSlot?.buffer.equals(newBuffer) + ) { + this.accountsCurrentlyPolling.delete(accountIdString); + + // If no more accounts are being polled, stop batch polling + if ( + this.accountsCurrentlyPolling.size === 0 && + this.batchPollingTimeout + ) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + } + + if (!existingBufferAndSlot) { + if (newBuffer) { + this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString); + } + return; + } + + if (newSlot < existingBufferAndSlot.slot) { + return; + } + + const oldBuffer = existingBufferAndSlot.buffer; + if (newBuffer && (!oldBuffer || !newBuffer.equals(oldBuffer))) { + this.updateBufferAndHandleChange(newBuffer, newSlot, accountIdString); + } + } + + private startMonitoringForAccounts(): void { + // Clear any existing polling timeouts + this.clearPollingTimeouts(); + + // Start monitoring for each account in the accountsToMonitor set + this.accountsToMonitor.forEach((accountIdString) => { + this.startMonitoringForAccount(accountIdString); + }); + } + + private startMonitoringForAccount(accountIdString: string): void { + // Clear existing timeout for this account + const existingTimeout = this.pollingTimeouts.get(accountIdString); + if (existingTimeout) { + clearTimeout(existingTimeout); + } + + // Set up monitoring timeout - only start polling if no WS notification in 30s + const timeoutId = setTimeout(async () => { + // Check if we've received a WS notification for this account recently + const lastNotificationTime = + this.lastWsNotificationTime.get(accountIdString) || 0; + const currentTime = Date.now(); + + if ( + !lastNotificationTime || + currentTime - lastNotificationTime >= this.pollingIntervalMs + ) { + if (this.resubOpts?.logResubMessages) { + console.debug( + `[${this.subscriptionName}] No recent WS notification for ${accountIdString}, adding to polling set` + ); + } + // No recent WS notification: add to polling and schedule debounced poll + this.accountsCurrentlyPolling.add(accountIdString); + this.scheduleDebouncedImmediatePoll(); + } else { + // We received a WS notification recently, continue monitoring + this.startMonitoringForAccount(accountIdString); + } + }, this.pollingIntervalMs); + + this.pollingTimeouts.set(accountIdString, timeoutId); + } + + private scheduleDebouncedImmediatePoll(): void { + if (this.debouncedImmediatePollTimeout) { + clearTimeout(this.debouncedImmediatePollTimeout); + } + this.debouncedImmediatePollTimeout = setTimeout(async () => { + try { + await this.pollAllAccounts(); + // After the immediate poll, ensure continuous batch polling is active + if ( + !this.batchPollingTimeout && + this.accountsCurrentlyPolling.size > 0 + ) { + this.startBatchPolling(); + } + } catch (e) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error during debounced immediate poll:`, + e + ); + } + } + }, this.debouncedImmediatePollMs); + } + + private startBatchPolling(): void { + if (this.resubOpts?.logResubMessages) { + console.debug(`[${this.subscriptionName}] Scheduling batch polling`); + } + // Clear existing batch polling timeout + if (this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + } + + // Set up batch polling interval + this.batchPollingTimeout = setTimeout(async () => { + await this.pollAllAccounts(); + // Schedule next batch poll + this.startBatchPolling(); + }, this.pollingIntervalMs); + } + + private async pollAllAccounts(): Promise { + try { + // Get all accounts currently being polled + const accountsToPoll = Array.from(this.accountsCurrentlyPolling); + if (accountsToPoll.length === 0) { + return; + } + + if (this.resubOpts?.logResubMessages) { + console.debug( + `[${this.subscriptionName}] Polling all accounts`, + accountsToPoll.length, + 'accounts' + ); + } + + // Use the shared batch fetch method + await this.fetchAccountsBatch(accountsToPoll); + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error batch polling accounts:`, + error + ); + } + } + } + + /** + * Fetches and populates all monitored accounts data without checking for missed changes + * This is used during initial subscription to populate data + */ + private async fetchAndPopulateAllMonitoredAccounts(): Promise { + try { + // Get all accounts currently being polled + const accountsToMonitor = Array.from(this.accountsToMonitor); + if (accountsToMonitor.length === 0) { + return; + } + + // Fetch all accounts in a single batch request + const accountAddresses = accountsToMonitor.map( + (accountId) => accountId as Address + ); + const rpcResponse = await this.rpc + .getMultipleAccounts(accountAddresses, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + }) + .send(); + + const currentSlot = Number(rpcResponse.context.slot); + + // Process each account response + for (let i = 0; i < accountsToMonitor.length; i++) { + const accountIdString = accountsToMonitor[i]; + const accountInfo = rpcResponse.value[i]; + + if (!accountInfo) { + continue; + } + + const existingBufferAndSlot = + this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it + let newBuffer: Buffer | undefined = undefined; + if (accountInfo) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + newBuffer = Buffer.from(data, encoding); + } + } + + if (newBuffer) { + this.updateBufferAndHandleChange( + newBuffer, + currentSlot, + accountIdString + ); + } + continue; + } + + // For initial population, just update the slot if we have newer data + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + if (encoding === ('base58' as any)) { + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + + // Update with newer data if available + if (newBuffer) { + this.updateBufferAndHandleChange( + newBuffer, + currentSlot, + accountIdString + ); + } + } + } + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error fetching and populating monitored accounts:`, + error + ); + } + } + } + + /** + * Fetches all monitored accounts and checks for missed changes + * Returns true if a missed change was detected and resubscription is needed + */ + private async fetchAllMonitoredAccounts(): Promise { + try { + // Get all accounts currently being polled + const accountsToMonitor = Array.from(this.accountsToMonitor); + if (accountsToMonitor.length === 0) { + return false; + } + + // Fetch all accounts in a single batch request + const accountAddresses = accountsToMonitor.map( + (accountId) => accountId as Address + ); + const rpcResponse = await this.rpc + .getMultipleAccounts(accountAddresses, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + }) + .send(); + + const currentSlot = Number(rpcResponse.context.slot); + + // Process each account response + for (let i = 0; i < accountsToMonitor.length; i++) { + const accountIdString = accountsToMonitor[i]; + const accountInfo = rpcResponse.value[i]; + + if (!accountInfo) { + continue; + } + + const existingBufferAndSlot = + this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + newBuffer = Buffer.from(data, encoding); + } + } + + if (newBuffer) { + this.updateBufferAndHandleChange( + newBuffer, + currentSlot, + accountIdString + ); + } + continue; + } + + // Check if we missed an update + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + if (encoding === ('base58' as any)) { + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + + // Check if buffer has changed + if ( + newBuffer && + (!existingBufferAndSlot.buffer || + !newBuffer.equals(existingBufferAndSlot.buffer)) + ) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, resubscribing` + ); + } + // We missed an update, return true to indicate resubscription is needed + return true; + } + } + } + + // No missed changes detected + return false; + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error batch polling accounts:`, + error + ); + } + return false; + } + } + + private async fetchAccountsBatch(accountIds: string[]): Promise { + try { + // Chunk account IDs into groups of 100 (getMultipleAccounts limit) + const chunkSize = 100; + const chunks: string[][] = []; + for (let i = 0; i < accountIds.length; i += chunkSize) { + chunks.push(accountIds.slice(i, i + chunkSize)); + } + + // Process all chunks concurrently + await Promise.all( + chunks.map(async (chunk) => { + const accountAddresses = chunk.map( + (accountId) => accountId as Address + ); + const rpcResponse = await this.rpc + .getMultipleAccounts(accountAddresses, { + commitment: this.options.commitment as GillCommitment, + encoding: 'base64', + }) + .send(); + + const currentSlot = Number(rpcResponse.context.slot); + + // Process each account response in this chunk + for (let i = 0; i < chunk.length; i++) { + const accountIdString = chunk[i]; + const accountInfo = rpcResponse.value[i]; + + if (!accountInfo) { + continue; + } + + const existingBufferAndSlot = + this.bufferAndSlotMap.get(accountIdString); + + if (!existingBufferAndSlot) { + // Account not in our map yet, add it + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + newBuffer = Buffer.from(data, encoding); + } + } + + if (newBuffer) { + this.updateBufferAndHandleChange( + newBuffer, + currentSlot, + accountIdString + ); + } + continue; + } + + // Check if we missed an update + if (currentSlot > existingBufferAndSlot.slot) { + let newBuffer: Buffer | undefined = undefined; + if (accountInfo.data) { + if (Array.isArray(accountInfo.data)) { + const [data, encoding] = accountInfo.data; + if (encoding === ('base58' as any)) { + newBuffer = Buffer.from(bs58.decode(data)); + } else { + newBuffer = Buffer.from(data, 'base64'); + } + } + } + + // Check if buffer has changed + if ( + newBuffer && + (!existingBufferAndSlot.buffer || + !newBuffer.equals(existingBufferAndSlot.buffer)) + ) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Batch polling detected missed update for account ${accountIdString}, signaling resubscription` + ); + } + // Signal missed change instead of immediately resubscribing + this.signalMissedChange(accountIdString); + return; + } + } + } + }) + ); + } catch (error) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Error fetching accounts batch:`, + error + ); + } + } + } + + private clearPollingTimeouts(): void { + this.pollingTimeouts.forEach((timeoutId) => { + clearTimeout(timeoutId); + }); + this.pollingTimeouts.clear(); + + // Clear batch polling timeout + if (this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + + // Clear initial fetch timeout + // if (this.initialFetchTimeout) { + // clearTimeout(this.initialFetchTimeout); + // this.initialFetchTimeout = undefined; + // } + + // Clear resubscription timeout + if (this.resubscriptionTimeout) { + clearTimeout(this.resubscriptionTimeout); + this.resubscriptionTimeout = undefined; + } + + // Clear accounts currently polling + this.accountsCurrentlyPolling.clear(); + + // Clear accounts pending initial monitor fetch + // this.accountsPendingInitialMonitorFetch.clear(); + + // Reset missed change flag and clear accounts with missed updates + this.missedChangeDetected = false; + this.accountsWithMissedUpdates.clear(); + } + + /** + * Centralized resubscription handler that only resubscribes once after checking all accounts + */ + private async handleResubscription(): Promise { + if (this.missedChangeDetected) { + if (this.resubOpts?.logResubMessages) { + console.log( + `[${this.subscriptionName}] Missed change detected for ${ + this.accountsWithMissedUpdates.size + } accounts: ${Array.from(this.accountsWithMissedUpdates).join( + ', ' + )}, resubscribing` + ); + } + await this.unsubscribe(true); + this.receivingData = false; + await this.subscribe(this.onChange); + this.missedChangeDetected = false; + this.accountsWithMissedUpdates.clear(); + } + } + + /** + * Signal that a missed change was detected and schedule resubscription + */ + private signalMissedChange(accountIdString: string): void { + if (!this.missedChangeDetected) { + this.missedChangeDetected = true; + this.accountsWithMissedUpdates.add(accountIdString); + + // Clear any existing resubscription timeout + if (this.resubscriptionTimeout) { + clearTimeout(this.resubscriptionTimeout); + } + + // Schedule resubscription after a short delay to allow for batch processing + this.resubscriptionTimeout = setTimeout(async () => { + await this.handleResubscription(); + }, 100); // 100ms delay to allow for batch processing + } else { + // If already detected, just add the account to the set + this.accountsWithMissedUpdates.add(accountIdString); + } + } + + unsubscribe(onResub = false): Promise { + if (!onResub) { + this.resubOpts.resubTimeoutMs = undefined; + } + this.isUnsubscribing = true; + clearTimeout(this.timeoutId); + this.timeoutId = undefined; + + // Clear polling timeouts + this.clearPollingTimeouts(); + + // Abort the WebSocket subscription + if (this.abortController) { + this.abortController.abort('unsubscribing'); + this.abortController = undefined; + } + + this.listenerId = undefined; + this.isUnsubscribing = false; + + return Promise.resolve(); + } + + // Method to add accounts to the polling list + /** + * Add an account to the monitored set. + * - Monitored accounts are subject to initial fetch and periodic batch polls + * if WS notifications are not observed within `pollingIntervalMs`. + */ + addAccountToMonitor(accountId: PublicKey): void { + const accountIdString = accountId.toBase58(); + this.accountsToMonitor.add(accountIdString); + + // If already subscribed, start monitoring for this account + if (this.listenerId != null && !this.isUnsubscribing) { + this.startMonitoringForAccount(accountIdString); + } + } + + // Method to remove accounts from the polling list + removeAccountFromMonitor(accountId: PublicKey): void { + const accountIdString = accountId.toBase58(); + this.accountsToMonitor.delete(accountIdString); + + // Clear monitoring timeout for this account + const timeoutId = this.pollingTimeouts.get(accountIdString); + if (timeoutId) { + clearTimeout(timeoutId); + this.pollingTimeouts.delete(accountIdString); + } + + // Remove from currently polling set if it was being polled + this.accountsCurrentlyPolling.delete(accountIdString); + + // If no more accounts are being polled, stop batch polling + if (this.accountsCurrentlyPolling.size === 0 && this.batchPollingTimeout) { + clearTimeout(this.batchPollingTimeout); + this.batchPollingTimeout = undefined; + } + } + + // Method to set polling interval + /** + * Set the monitoring/polling interval for monitored accounts. + * Shorter intervals detect missed updates sooner but increase RPC load. + */ + setPollingInterval(intervalMs: number): void { + this.pollingIntervalMs = intervalMs; + // Restart monitoring with new interval if already subscribed + if (this.listenerId != null && !this.isUnsubscribing) { + this.startMonitoringForAccounts(); + } + } + + private updateBufferAndHandleChange( + newBuffer: Buffer, + newSlot: number, + accountIdString: string + ) { + this.bufferAndSlotMap.set(accountIdString, { + buffer: newBuffer, + slot: newSlot, + }); + const account = this.decodeBuffer(this.accountDiscriminator, newBuffer); + const accountIdPubkey = new PublicKey(accountIdString); + this.onChange(accountIdPubkey, account, { slot: newSlot }, newBuffer); + } +} diff --git a/sdk/src/addresses/pda.ts b/sdk/src/addresses/pda.ts index 2dd95c417a..13d7bde79f 100644 --- a/sdk/src/addresses/pda.ts +++ b/sdk/src/addresses/pda.ts @@ -1,7 +1,11 @@ import { PublicKey } from '@solana/web3.js'; import * as anchor from '@coral-xyz/anchor'; import { BN } from '@coral-xyz/anchor'; -import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + getAssociatedTokenAddress, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; import { SpotMarketAccount, TokenProgramFlag } from '../types'; export async function getDriftStateAccountPublicKeyAndNonce( @@ -394,3 +398,139 @@ export function getIfRebalanceConfigPublicKey( programId )[0]; } + +export function getRevenueShareAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('REV_SHARE')), + authority.toBuffer(), + ], + programId + )[0]; +} + +export function getRevenueShareEscrowAccountPublicKey( + programId: PublicKey, + authority: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('REV_ESCROW')), + authority.toBuffer(), + ], + programId + )[0]; +} + +export function getLpPoolPublicKey( + programId: PublicKey, + lpPoolId: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('lp_pool')), + new anchor.BN(lpPoolId).toArrayLike(Buffer, 'le', 1), + ], + programId + )[0]; +} + +export function getLpPoolTokenVaultPublicKey( + programId: PublicKey, + lpPool: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('LP_POOL_TOKEN_VAULT')), + lpPool.toBuffer(), + ], + programId + )[0]; +} +export function getAmmConstituentMappingPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('AMM_MAP')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export function getConstituentTargetBasePublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from( + anchor.utils.bytes.utf8.encode('constituent_target_base_seed') + ), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export function getConstituentPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey, + spotMarketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('CONSTITUENT')), + lpPoolPublicKey.toBuffer(), + new anchor.BN(spotMarketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0]; +} + +export function getConstituentVaultPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey, + spotMarketIndex: number +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('CONSTITUENT_VAULT')), + lpPoolPublicKey.toBuffer(), + new anchor.BN(spotMarketIndex).toArrayLike(Buffer, 'le', 2), + ], + programId + )[0]; +} + +export function getAmmCachePublicKey(programId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from(anchor.utils.bytes.utf8.encode('amm_cache_seed'))], + programId + )[0]; +} + +export function getConstituentCorrelationsPublicKey( + programId: PublicKey, + lpPoolPublicKey: PublicKey +): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from(anchor.utils.bytes.utf8.encode('constituent_correlations')), + lpPoolPublicKey.toBuffer(), + ], + programId + )[0]; +} + +export async function getLpPoolTokenTokenAccountPublicKey( + lpPoolTokenMint: PublicKey, + authority: PublicKey +): Promise { + return await getAssociatedTokenAddress(lpPoolTokenMint, authority, true); +} diff --git a/sdk/src/adminClient.ts b/sdk/src/adminClient.ts index 352e051bf2..9698e13716 100644 --- a/sdk/src/adminClient.ts +++ b/sdk/src/adminClient.ts @@ -1,4 +1,7 @@ import { + AddressLookupTableAccount, + Keypair, + LAMPORTS_PER_SOL, PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY, @@ -15,6 +18,12 @@ import { AssetTier, SpotFulfillmentConfigStatus, IfRebalanceConfigParams, + TxParams, + AddAmmConstituentMappingDatum, + SwapReduceOnly, + InitializeConstituentParams, + ConstituentStatus, + LPPoolAccount, } from './types'; import { DEFAULT_MARKET_NAME, encodeName } from './userName'; import { BN } from '@coral-xyz/anchor'; @@ -39,9 +48,26 @@ import { getFuelOverflowAccountPublicKey, getTokenProgramForSpotMarket, getIfRebalanceConfigPublicKey, + getInsuranceFundStakeAccountPublicKey, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + getConstituentTargetBasePublicKey, + getConstituentPublicKey, + getConstituentVaultPublicKey, + getAmmCachePublicKey, + getLpPoolTokenVaultPublicKey, + getDriftSignerPublicKey, + getConstituentCorrelationsPublicKey, } from './addresses/pda'; import { squareRootBN } from './math/utils'; -import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { + createInitializeMint2Instruction, + createMintToInstruction, + createTransferCheckedInstruction, + getAssociatedTokenAddressSync, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; import { DriftClient } from './driftClient'; import { PEG_PRECISION, @@ -50,6 +76,7 @@ import { ONE, BASE_PRECISION, PRICE_PRECISION, + GOV_SPOT_MARKET_INDEX, } from './constants/numericConstants'; import { calculateTargetPriceTrade } from './math/trade'; import { calculateAmmReservesAfterSwap, getSwapDirection } from './math/amm'; @@ -57,6 +84,8 @@ import { PROGRAM_ID as PHOENIX_PROGRAM_ID } from '@ellipsis-labs/phoenix-sdk'; import { DRIFT_ORACLE_RECEIVER_ID } from './config'; import { getFeedIdUint8Array } from './util/pythOracleUtils'; import { FUEL_RESET_LOG_ACCOUNT } from './constants/txConstants'; +import { JupiterClient, QuoteResponse } from './jupiter/jupiterClient'; +import { SwapMode } from './swap/UnifiedSwapClient'; const OPENBOOK_PROGRAM_ID = new PublicKey( 'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb' @@ -473,11 +502,18 @@ export class AdminClient extends DriftClient { concentrationCoefScale = ONE, curveUpdateIntensity = 0, ammJitIntensity = 0, - name = DEFAULT_MARKET_NAME + name = DEFAULT_MARKET_NAME, + lpPoolId: number = 0 ): Promise { const currentPerpMarketIndex = this.getStateAccount().numberOfMarkets; - const initializeMarketIx = await this.getInitializePerpMarketIx( + const ammCachePublicKey = getAmmCachePublicKey(this.program.programId); + const ammCacheAccount = await this.connection.getAccountInfo( + ammCachePublicKey + ); + const mustInitializeAmmCache = ammCacheAccount?.data == null; + + const initializeMarketIxs = await this.getInitializePerpMarketIx( marketIndex, priceOracle, baseAssetReserve, @@ -503,9 +539,11 @@ export class AdminClient extends DriftClient { concentrationCoefScale, curveUpdateIntensity, ammJitIntensity, - name + name, + lpPoolId, + mustInitializeAmmCache ); - const tx = await this.buildTransaction(initializeMarketIx); + const tx = await this.buildTransaction(initializeMarketIxs); const { txSig } = await this.sendTransaction(tx, [], this.opts); @@ -549,15 +587,23 @@ export class AdminClient extends DriftClient { concentrationCoefScale = ONE, curveUpdateIntensity = 0, ammJitIntensity = 0, - name = DEFAULT_MARKET_NAME - ): Promise { + name = DEFAULT_MARKET_NAME, + lpPoolId: number = 0, + includeInitAmmCacheIx = false + ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, marketIndex ); + const ixs: TransactionInstruction[] = []; + + if (includeInitAmmCacheIx) { + ixs.push(await this.getInitializeAmmCacheIx()); + } + const nameBuffer = encodeName(name); - return await this.program.instruction.initializePerpMarket( + const initPerpIx = await this.program.instruction.initializePerpMarket( marketIndex, baseAssetReserve, quoteAssetReserve, @@ -583,6 +629,7 @@ export class AdminClient extends DriftClient { curveUpdateIntensity, ammJitIntensity, nameBuffer, + lpPoolId, { accounts: { state: await this.getStatePublicKey(), @@ -591,11 +638,207 @@ export class AdminClient extends DriftClient { : this.wallet.publicKey, oracle: priceOracle, perpMarket: perpMarketPublicKey, + ammCache: getAmmCachePublicKey(this.program.programId), rent: SYSVAR_RENT_PUBKEY, systemProgram: anchor.web3.SystemProgram.programId, }, } ); + ixs.push(initPerpIx); + return ixs; + } + + public async initializeAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getInitializeAmmCacheIx(); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getInitializeAmmCacheIx(): Promise { + return await this.program.instruction.initializeAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + ammCache: getAmmCachePublicKey(this.program.programId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async resizeAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getResizeAmmCacheIx(); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getResizeAmmCacheIx(): Promise { + return await this.program.instruction.resizeAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + ammCache: getAmmCachePublicKey(this.program.programId), + rent: SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async deleteAmmCache( + txParams?: TxParams + ): Promise { + const deleteAmmCacheIx = await this.getDeleteAmmCacheIx(); + + const tx = await this.buildTransaction(deleteAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getDeleteAmmCacheIx(): Promise { + return await this.program.instruction.deleteAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.getStateAccount().admin, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + }); + } + + public async updateInitialAmmCacheInfo( + perpMarketIndexes: number[], + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getUpdateInitialAmmCacheInfoIx( + perpMarketIndexes + ); + + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateInitialAmmCacheInfoIx( + perpMarketIndexes: number[] + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readablePerpMarketIndex: perpMarketIndexes, + readableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], + }); + return await this.program.instruction.updateInitialAmmCacheInfo({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + remainingAccounts, + }); + } + + public async overrideAmmCacheInfo( + perpMarketIndex: number, + params: { + quoteOwedFromLpPool?: BN; + lastSettleTs?: BN; + lastFeePoolTokenAmount?: BN; + lastNetPnlPoolTokenAmount?: BN; + ammPositionScalar?: number; + ammInventoryLimit?: BN; + }, + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getOverrideAmmCacheInfoIx( + perpMarketIndex, + params + ); + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getOverrideAmmCacheInfoIx( + perpMarketIndex: number, + params: { + quoteOwedFromLpPool?: BN; + lastSettleSlot?: BN; + lastFeePoolTokenAmount?: BN; + lastNetPnlPoolTokenAmount?: BN; + ammPositionScalar?: number; + ammInventoryLimit?: BN; + } + ): Promise { + return this.program.instruction.overrideAmmCacheInfo( + perpMarketIndex, + Object.assign( + {}, + { + quoteOwedFromLpPool: null, + lastSettleSlot: null, + lastFeePoolTokenAmount: null, + lastNetPnlPoolTokenAmount: null, + ammPositionScalar: null, + ammInventoryLimit: null, + }, + params + ), + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ); + } + + public async resetAmmCache( + txParams?: TxParams + ): Promise { + const initializeAmmCacheIx = await this.getResetAmmCacheIx(); + const tx = await this.buildTransaction(initializeAmmCacheIx, txParams); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getResetAmmCacheIx(): Promise { + return this.program.instruction.resetAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + ammCache: getAmmCachePublicKey(this.program.programId), + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); } public async initializePredictionMarket( @@ -869,6 +1112,76 @@ export class AdminClient extends DriftClient { ); } + public async updatePerpMarketLpPoolId( + perpMarketIndex: number, + lpPoolId: number + ) { + const updatePerpMarketLpPoolIIx = await this.getUpdatePerpMarketLpPoolIdIx( + perpMarketIndex, + lpPoolId + ); + + const tx = await this.buildTransaction(updatePerpMarketLpPoolIIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketLpPoolIdIx( + perpMarketIndex: number, + lpPoolId: number + ): Promise { + return await this.program.instruction.updatePerpMarketLpPoolId(lpPoolId, { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + }, + }); + } + + public async updatePerpMarketLpPoolStatus( + perpMarketIndex: number, + lpStatus: number + ) { + const updatePerpMarketLpPoolStatusIx = + await this.getUpdatePerpMarketLpPoolStatusIx(perpMarketIndex, lpStatus); + + const tx = await this.buildTransaction(updatePerpMarketLpPoolStatusIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketLpPoolStatusIx( + perpMarketIndex: number, + lpStatus: number + ): Promise { + return await this.program.instruction.updatePerpMarketLpPoolStatus( + lpStatus, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ); + } + public async moveAmmToPrice( perpMarketIndex: number, targetPrice: BN @@ -1059,6 +1372,13 @@ export class AdminClient extends DriftClient { sourceVault: PublicKey ): Promise { const spotMarket = this.getQuoteSpotMarketAccount(); + const remainingAccounts = [ + { + pubkey: spotMarket.mint, + isWritable: false, + isSigner: false, + }, + ]; return await this.program.instruction.depositIntoPerpMarketFeePool(amount, { accounts: { @@ -1076,6 +1396,7 @@ export class AdminClient extends DriftClient { spotMarketVault: spotMarket.vault, tokenProgram: TOKEN_PROGRAM_ID, }, + remainingAccounts, }); } @@ -1229,6 +1550,46 @@ export class AdminClient extends DriftClient { ); } + public async updatePerpMarketReferencePriceOffsetDeadbandPct( + perpMarketIndex: number, + referencePriceOffsetDeadbandPct: number + ): Promise { + const updatePerpMarketReferencePriceOffsetDeadbandPctIx = + await this.getUpdatePerpMarketReferencePriceOffsetDeadbandPctIx( + perpMarketIndex, + referencePriceOffsetDeadbandPct + ); + + const tx = await this.buildTransaction( + updatePerpMarketReferencePriceOffsetDeadbandPctIx + ); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdatePerpMarketReferencePriceOffsetDeadbandPctIx( + perpMarketIndex: number, + referencePriceOffsetDeadbandPct: number + ): Promise { + return await this.program.instruction.updatePerpMarketReferencePriceOffsetDeadbandPct( + referencePriceOffsetDeadbandPct, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + perpMarket: await getPerpMarketPublicKey( + this.program.programId, + perpMarketIndex + ), + }, + } + ); + } + public async updatePerpMarketTargetBaseAssetAmountPerLp( perpMarketIndex: number, targetBaseAssetAmountPerLP: number @@ -2306,6 +2667,7 @@ export class AdminClient extends DriftClient { ), oracle: oracle, oldOracle: this.getPerpMarketAccount(perpMarketIndex).amm.oracle, + ammCache: getAmmCachePublicKey(this.program.programId), }, } ); @@ -3116,6 +3478,7 @@ export class AdminClient extends DriftClient { this.program.programId, perpMarketIndex ), + ammCache: getAmmCachePublicKey(this.program.programId), }, } ); @@ -4005,34 +4368,34 @@ export class AdminClient extends DriftClient { ); } - public async updatePerpMarketTakerSpeedBumpOverride( + public async updatePerpMarketOracleLowRiskSlotDelayOverride( perpMarketIndex: number, - takerSpeedBumpOverride: number + oracleLowRiskSlotDelayOverride: number ): Promise { - const updatePerpMarketTakerSpeedBumpOverrideIx = - await this.getUpdatePerpMarketTakerSpeedBumpOverrideIx( + const updatePerpMarketOracleLowRiskSlotDelayOverrideIx = + await this.getUpdatePerpMarketOracleLowRiskSlotDelayOverrideIx( perpMarketIndex, - takerSpeedBumpOverride + oracleLowRiskSlotDelayOverride ); const tx = await this.buildTransaction( - updatePerpMarketTakerSpeedBumpOverrideIx + updatePerpMarketOracleLowRiskSlotDelayOverrideIx ); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdatePerpMarketTakerSpeedBumpOverrideIx( + public async getUpdatePerpMarketOracleLowRiskSlotDelayOverrideIx( perpMarketIndex: number, - takerSpeedBumpOverride: number + oracleLowRiskSlotDelayOverride: number ): Promise { const perpMarketPublicKey = await getPerpMarketPublicKey( this.program.programId, perpMarketIndex ); - return await this.program.instruction.updatePerpMarketTakerSpeedBumpOverride( - takerSpeedBumpOverride, + return await this.program.instruction.updatePerpMarketOracleLowRiskSlotDelayOverride( + oracleLowRiskSlotDelayOverride, { accounts: { admin: this.useHotWalletAdmin @@ -4610,9 +4973,9 @@ export class AdminClient extends DriftClient { ): Promise { return await this.program.instruction.zeroMmOracleFields({ accounts: { - admin: this.isSubscribed - ? this.getStateAccount().admin - : this.wallet.publicKey, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, state: await this.getStatePublicKey(), perpMarket: await getPerpMarketPublicKey( this.program.programId, @@ -4649,4 +5012,1521 @@ export class AdminClient extends DriftClient { } ); } + + public async updateFeatureBitFlagsBuilderCodes( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderCodesIx = + await this.getUpdateFeatureBitFlagsBuilderCodesIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsBuilderCodesIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderCodesIx( + enable: boolean + ): Promise { + return this.program.instruction.updateFeatureBitFlagsBuilderCodes(enable, { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + }); + } + + public async updateFeatureBitFlagsBuilderReferral( + enable: boolean + ): Promise { + const updateFeatureBitFlagsBuilderReferralIx = + await this.getUpdateFeatureBitFlagsBuilderReferralIx(enable); + + const tx = await this.buildTransaction( + updateFeatureBitFlagsBuilderReferralIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsBuilderReferralIx( + enable: boolean + ): Promise { + return this.program.instruction.updateFeatureBitFlagsBuilderReferral( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateFeatureBitFlagsMedianTriggerPrice( + enable: boolean + ): Promise { + const updateFeatureBitFlagsMedianTriggerPriceIx = + await this.getUpdateFeatureBitFlagsMedianTriggerPriceIx(enable); + const tx = await this.buildTransaction( + updateFeatureBitFlagsMedianTriggerPriceIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsMedianTriggerPriceIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsMedianTriggerPrice( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateDelegateUserGovTokenInsuranceStake( + authority: PublicKey, + delegate: PublicKey + ): Promise { + const updateDelegateUserGovTokenInsuranceStakeIx = + await this.getUpdateDelegateUserGovTokenInsuranceStakeIx( + authority, + delegate + ); + + const tx = await this.buildTransaction( + updateDelegateUserGovTokenInsuranceStakeIx + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateDelegateUserGovTokenInsuranceStakeIx( + authority: PublicKey, + delegate: PublicKey + ): Promise { + const marketIndex = GOV_SPOT_MARKET_INDEX; + const spotMarket = this.getSpotMarketAccount(marketIndex); + const ifStakeAccountPublicKey = getInsuranceFundStakeAccountPublicKey( + this.program.programId, + delegate, + marketIndex + ); + const userStatsPublicKey = getUserStatsAccountPublicKey( + this.program.programId, + authority + ); + + const ix = + this.program.instruction.getUpdateDelegateUserGovTokenInsuranceStakeIx({ + accounts: { + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: ifStakeAccountPublicKey, + userStats: userStatsPublicKey, + signer: this.wallet.publicKey, + insuranceFundVault: spotMarket.insuranceFund.vault, + }, + }); + + return ix; + } + + public async depositIntoInsuranceFundStake( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey, + txParams?: TxParams + ): Promise { + const tx = await this.buildTransaction( + await this.getDepositIntoInsuranceFundStakeIx( + marketIndex, + amount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userTokenAccountPublicKey + ), + txParams + ); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositIntoInsuranceFundStakeIx( + marketIndex: number, + amount: BN, + userStatsPublicKey: PublicKey, + insuranceFundStakePublicKey: PublicKey, + userTokenAccountPublicKey: PublicKey + ): Promise { + const spotMarket = this.getSpotMarketAccount(marketIndex); + return await this.program.instruction.depositIntoInsuranceFundStake( + marketIndex, + amount, + { + accounts: { + signer: this.wallet.publicKey, + state: await this.getStatePublicKey(), + spotMarket: spotMarket.pubkey, + insuranceFundStake: insuranceFundStakePublicKey, + userStats: userStatsPublicKey, + spotMarketVault: spotMarket.vault, + insuranceFundVault: spotMarket.insuranceFund.vault, + userTokenAccount: userTokenAccountPublicKey, + tokenProgram: this.getTokenProgramForSpotMarket(spotMarket), + driftSigner: this.getSignerPublicKey(), + }, + } + ); + } + + public async updateFeatureBitFlagsSettleLpPool( + enable: boolean + ): Promise { + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsSettleLpPoolIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsSettleLpPoolIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsSettleLpPool( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateFeatureBitFlagsSwapLpPool( + enable: boolean + ): Promise { + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsSwapLpPoolIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsSwapLpPoolIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsSwapLpPool( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateFeatureBitFlagsMintRedeemLpPool( + enable: boolean + ): Promise { + const updateFeatureBitFlagsSettleLpPoolIx = + await this.getUpdateFeatureBitFlagsMintRedeemLpPoolIx(enable); + + const tx = await this.buildTransaction(updateFeatureBitFlagsSettleLpPoolIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateFeatureBitFlagsMintRedeemLpPoolIx( + enable: boolean + ): Promise { + return await this.program.instruction.updateFeatureBitFlagsMintRedeemLpPool( + enable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async adminDisableUpdatePerpBidAskTwap( + authority: PublicKey, + disable: boolean + ): Promise { + const disableBidAskTwapUpdateIx = + await this.getAdminDisableUpdatePerpBidAskTwapIx(authority, disable); + + const tx = await this.buildTransaction(disableBidAskTwapUpdateIx); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getAdminDisableUpdatePerpBidAskTwapIx( + authority: PublicKey, + disable: boolean + ): Promise { + return await this.program.instruction.adminDisableUpdatePerpBidAskTwap( + disable, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + }, + } + ); + } + + public async initializeLpPool( + lpPoolId: number, + minMintFee: BN, + maxAum: BN, + maxSettleQuoteAmountPerMarket: BN, + mint: Keypair, + whitelistMint?: PublicKey + ): Promise { + const ixs = await this.getInitializeLpPoolIx( + lpPoolId, + minMintFee, + maxAum, + maxSettleQuoteAmountPerMarket, + mint, + whitelistMint + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, [mint]); + return txSig; + } + + public async getInitializeLpPoolIx( + lpPoolId: number, + minMintFee: BN, + maxAum: BN, + maxSettleQuoteAmountPerMarket: BN, + mint: Keypair, + whitelistMint?: PublicKey + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + const lamports = + await this.program.provider.connection.getMinimumBalanceForRentExemption( + MINT_SIZE + ); + const createMintAccountIx = SystemProgram.createAccount({ + fromPubkey: this.wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: MINT_SIZE, + lamports: Math.min(0.05 * LAMPORTS_PER_SOL, lamports), // should be 0.0014616 ? but bankrun returns 10 SOL + programId: TOKEN_PROGRAM_ID, + }); + const createMintIx = createInitializeMint2Instruction( + mint.publicKey, + 6, + lpPool, + null, + TOKEN_PROGRAM_ID + ); + + return [ + createMintAccountIx, + createMintIx, + this.program.instruction.initializeLpPool( + lpPoolId, + minMintFee, + maxAum, + maxSettleQuoteAmountPerMarket, + whitelistMint ?? PublicKey.default, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool + ), + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + ammConstituentMapping, + constituentTargetBase, + mint: mint.publicKey, + state: await this.getStatePublicKey(), + tokenProgram: TOKEN_PROGRAM_ID, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + }, + signers: [mint], + } + ), + ]; + } + + public async initializeConstituent( + lpPoolId: number, + initializeConstituentParams: InitializeConstituentParams + ): Promise { + const ixs = await this.getInitializeConstituentIx( + lpPoolId, + initializeConstituentParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getInitializeConstituentIx( + lpPoolId: number, + initializeConstituentParams: InitializeConstituentParams + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const spotMarketIndex = initializeConstituentParams.spotMarketIndex; + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + const constituent = getConstituentPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ); + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + + return [ + this.program.instruction.initializeConstituent( + spotMarketIndex, + initializeConstituentParams.decimals, + initializeConstituentParams.maxWeightDeviation, + initializeConstituentParams.swapFeeMin, + initializeConstituentParams.swapFeeMax, + initializeConstituentParams.maxBorrowTokenAmount, + initializeConstituentParams.oracleStalenessThreshold, + initializeConstituentParams.costToTrade, + initializeConstituentParams.constituentDerivativeIndex != null + ? initializeConstituentParams.constituentDerivativeIndex + : null, + initializeConstituentParams.constituentDerivativeDepegThreshold != null + ? initializeConstituentParams.constituentDerivativeDepegThreshold + : ZERO, + initializeConstituentParams.constituentDerivativeIndex != null + ? initializeConstituentParams.derivativeWeight + : ZERO, + initializeConstituentParams.volatility != null + ? initializeConstituentParams.volatility + : 10, + initializeConstituentParams.gammaExecution != null + ? initializeConstituentParams.gammaExecution + : 2, + initializeConstituentParams.gammaInventory != null + ? initializeConstituentParams.gammaInventory + : 2, + initializeConstituentParams.xi != null + ? initializeConstituentParams.xi + : 2, + initializeConstituentParams.constituentCorrelations, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentTargetBase, + constituent, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + spotMarketMint: spotMarketAccount.mint, + constituentVault: getConstituentVaultPublicKey( + this.program.programId, + lpPool, + spotMarketIndex + ), + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + spotMarket: spotMarketAccount.pubkey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + signers: [], + } + ), + ]; + } + + public async updateConstituentStatus( + constituent: PublicKey, + constituentStatus: ConstituentStatus + ): Promise { + const updateConstituentStatusIx = await this.getUpdateConstituentStatusIx( + constituent, + constituentStatus + ); + + const tx = await this.buildTransaction(updateConstituentStatusIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateConstituentStatusIx( + constituent: PublicKey, + constituentStatus: ConstituentStatus + ): Promise { + return await this.program.instruction.updateConstituentStatus( + constituentStatus, + { + accounts: { + constituent, + admin: this.isSubscribed + ? this.getStateAccount().admin + : this.wallet.publicKey, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateConstituentPausedOperations( + constituent: PublicKey, + pausedOperations: number + ): Promise { + const updateConstituentPausedOperationsIx = + await this.getUpdateConstituentPausedOperationsIx( + constituent, + pausedOperations + ); + + const tx = await this.buildTransaction(updateConstituentPausedOperationsIx); + + const { txSig } = await this.sendTransaction(tx, [], this.opts); + + return txSig; + } + + public async getUpdateConstituentPausedOperationsIx( + constituent: PublicKey, + pausedOperations: number + ): Promise { + return await this.program.instruction.updateConstituentPausedOperations( + pausedOperations, + { + accounts: { + constituent, + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + }, + } + ); + } + + public async updateConstituentParams( + lpPoolId: number, + constituentPublicKey: PublicKey, + updateConstituentParams: { + maxWeightDeviation?: BN; + swapFeeMin?: BN; + swapFeeMax?: BN; + maxBorrowTokenAmount?: BN; + oracleStalenessThreshold?: BN; + costToTradeBps?: number; + derivativeWeight?: BN; + constituentDerivativeIndex?: number; + volatility?: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; + } + ): Promise { + const ixs = await this.getUpdateConstituentParamsIx( + lpPoolId, + constituentPublicKey, + updateConstituentParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateConstituentParamsIx( + lpPoolId: number, + constituentPublicKey: PublicKey, + updateConstituentParams: { + maxWeightDeviation?: BN; + swapFeeMin?: BN; + swapFeeMax?: BN; + maxBorrowTokenAmount?: BN; + oracleStalenessThreshold?: BN; + derivativeWeight?: BN; + constituentDerivativeIndex?: number; + volatility?: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; + } + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + return [ + this.program.instruction.updateConstituentParams( + Object.assign( + { + maxWeightDeviation: null, + swapFeeMin: null, + swapFeeMax: null, + maxBorrowTokenAmount: null, + oracleStalenessThreshold: null, + costToTradeBps: null, + stablecoinWeight: null, + derivativeWeight: null, + constituentDerivativeIndex: null, + volatility: null, + gammaExecution: null, + gammaInventory: null, + xi: null, + }, + updateConstituentParams + ), + { + accounts: { + admin: this.wallet.publicKey, + constituent: constituentPublicKey, + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ), + }, + signers: [], + } + ), + ]; + } + + public async updateLpPoolParams( + lpPoolId: number, + updateLpPoolParams: { + maxSettleQuoteAmount?: BN; + volatility?: BN; + gammaExecution?: number; + xi?: number; + whitelistMint?: PublicKey; + maxAum?: BN; + } + ): Promise { + const ixs = await this.getUpdateLpPoolParamsIx( + lpPoolId, + updateLpPoolParams + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateLpPoolParamsIx( + lpPoolId: number, + updateLpPoolParams: { + maxSettleQuoteAmount?: BN; + volatility?: BN; + gammaExecution?: number; + xi?: number; + whitelistMint?: PublicKey; + maxAum?: BN; + } + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + return [ + this.program.instruction.updateLpPoolParams( + Object.assign( + { + maxSettleQuoteAmount: null, + volatility: null, + gammaExecution: null, + xi: null, + whitelistMint: null, + maxAum: null, + }, + updateLpPoolParams + ), + { + accounts: { + admin: this.wallet.publicKey, + state: await this.getStatePublicKey(), + lpPool, + }, + signers: [], + } + ), + ]; + } + + public async addAmmConstituentMappingData( + lpPoolId: number, + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getAddAmmConstituentMappingDataIx( + lpPoolId, + addAmmConstituentMappingData + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getAddAmmConstituentMappingDataIx( + lpPoolId: number, + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.addAmmConstituentMappingData( + addAmmConstituentMappingData, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + constituentTargetBase, + rent: SYSVAR_RENT_PUBKEY, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async updateAmmConstituentMappingData( + lpPoolId: number, + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const ixs = await this.getUpdateAmmConstituentMappingDataIx( + lpPoolId, + addAmmConstituentMappingData + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateAmmConstituentMappingDataIx( + lpPoolId: number, + addAmmConstituentMappingData: AddAmmConstituentMappingDatum[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + return [ + this.program.instruction.updateAmmConstituentMappingData( + addAmmConstituentMappingData, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async removeAmmConstituentMappingData( + lpPoolId: number, + perpMarketIndex: number, + constituentIndex: number + ): Promise { + const ixs = await this.getRemoveAmmConstituentMappingDataIx( + lpPoolId, + perpMarketIndex, + constituentIndex + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getRemoveAmmConstituentMappingDataIx( + lpPoolId: number, + perpMarketIndex: number, + constituentIndex: number + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + + return [ + this.program.instruction.removeAmmConstituentMappingData( + perpMarketIndex, + constituentIndex, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + ammConstituentMapping, + systemProgram: SystemProgram.programId, + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + public async updateConstituentCorrelationData( + lpPoolId: number, + index1: number, + index2: number, + correlation: BN + ): Promise { + const ixs = await this.getUpdateConstituentCorrelationDataIx( + lpPoolId, + index1, + index2, + correlation + ); + const tx = await this.buildTransaction(ixs); + const { txSig } = await this.sendTransaction(tx, []); + return txSig; + } + + public async getUpdateConstituentCorrelationDataIx( + lpPoolId: number, + index1: number, + index2: number, + correlation: BN + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + return [ + this.program.instruction.updateConstituentCorrelationData( + index1, + index2, + correlation, + { + accounts: { + admin: this.wallet.publicKey, + lpPool, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + state: await this.getStatePublicKey(), + }, + } + ), + ]; + } + + /** + * Get the drift begin_swap and end_swap instructions + * + * @param outMarketIndex the market index of the token you're buying + * @param inMarketIndex the market index of the token you're selling + * @param amountIn the amount of the token to sell + * @param inTokenAccount the token account to move the tokens being sold (admin signer ata for lp swap) + * @param outTokenAccount the token account to receive the tokens being bought (admin signer ata for lp swap) + * @param limitPrice the limit price of the swap + * @param reduceOnly + * @param userAccountPublicKey optional, specify a custom userAccountPublicKey to use instead of getting the current user account; can be helpful if the account is being created within the current tx + */ + public async getSwapIx( + { + lpPoolId, + outMarketIndex, + inMarketIndex, + amountIn, + inTokenAccount, + outTokenAccount, + limitPrice, + reduceOnly, + userAccountPublicKey, + }: { + lpPoolId: number; + outMarketIndex: number; + inMarketIndex: number; + amountIn: BN; + inTokenAccount: PublicKey; + outTokenAccount: PublicKey; + limitPrice?: BN; + reduceOnly?: SwapReduceOnly; + userAccountPublicKey?: PublicKey; + }, + lpSwap?: boolean + ): Promise<{ + beginSwapIx: TransactionInstruction; + endSwapIx: TransactionInstruction; + }> { + if (!lpSwap) { + return super.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn, + inTokenAccount, + outTokenAccount, + limitPrice, + reduceOnly, + userAccountPublicKey, + }); + } + const outSpotMarket = this.getSpotMarketAccount(outMarketIndex); + const inSpotMarket = this.getSpotMarketAccount(inMarketIndex); + + const outTokenProgram = this.getTokenProgramForSpotMarket(outSpotMarket); + const inTokenProgram = this.getTokenProgramForSpotMarket(inSpotMarket); + + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + + const outConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + + const beginSwapIx = this.program.instruction.beginLpSwap( + inMarketIndex, + outMarketIndex, + amountIn, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + signerOutTokenAccount: outTokenAccount, + signerInTokenAccount: inTokenAccount, + constituentOutTokenAccount: outConstituentTokenAccount, + constituentInTokenAccount: inConstituentTokenAccount, + outConstituent, + inConstituent, + lpPool, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + tokenProgram: inTokenProgram, + }, + } + ); + + const remainingAccounts = []; + remainingAccounts.push({ + pubkey: outTokenProgram, + isWritable: false, + isSigner: false, + }); + + const endSwapIx = this.program.instruction.endLpSwap( + inMarketIndex, + outMarketIndex, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + signerOutTokenAccount: outTokenAccount, + signerInTokenAccount: inTokenAccount, + constituentOutTokenAccount: outConstituentTokenAccount, + constituentInTokenAccount: inConstituentTokenAccount, + outConstituent, + inConstituent, + lpPool, + tokenProgram: inTokenProgram, + instructions: anchor.web3.SYSVAR_INSTRUCTIONS_PUBKEY, + }, + remainingAccounts, + } + ); + + return { beginSwapIx, endSwapIx }; + } + + public async getLpJupiterSwapIxV6({ + jupiterClient, + outMarketIndex, + inMarketIndex, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + quote, + lpPoolId, + }: { + jupiterClient: JupiterClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + quote?: QuoteResponse; + lpPoolId: number; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + if (!quote) { + const fetchedQuote = await jupiterClient.getQuote({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + }); + + quote = fetchedQuote; + } + + if (!quote) { + throw new Error("Could not fetch Jupiter's quote. Please try again."); + } + + const isExactOut = swapMode === 'ExactOut' || quote.swapMode === 'ExactOut'; + const amountIn = new BN(quote.inAmount); + const exactOutBufferedAmountIn = amountIn.muln(1001).divn(1000); // Add 10bp buffer + + const transaction = await jupiterClient.getSwap({ + quote, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + }); + + const { transactionMessage, lookupTables } = + await jupiterClient.getTransactionMessageAndLookupTables({ + transaction, + }); + + const jupiterInstructions = jupiterClient.getJupiterInstructions({ + transactionMessage, + inputMint: inMarket.mint, + outputMint: outMarket.mint, + }); + + const preInstructions = []; + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + const outAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const outAccountInfo = await this.connection.getAccountInfo( + outAssociatedTokenAccount + ); + if (!outAccountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + outAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } + + const inTokenProgram = this.getTokenProgramForSpotMarket(inMarket); + const inAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + inTokenProgram + ); + + const inAccountInfo = await this.connection.getAccountInfo( + inAssociatedTokenAccount + ); + if (!inAccountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + inAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + lpPoolId, + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amountIn, + inTokenAccount: inAssociatedTokenAccount, + outTokenAccount: outAssociatedTokenAccount, + }); + + const ixs = [ + ...preInstructions, + beginSwapIx, + ...jupiterInstructions, + endSwapIx, + ]; + + return { ixs, lookupTables }; + } + + public async getDevnetLpSwapIxs( + amountIn: BN, + amountOut: BN, + externalUserAuthority: PublicKey, + externalUserInTokenAccount: PublicKey, + externalUserOutTokenAccount: PublicKey, + inSpotMarketIndex: number, + outSpotMarketIndex: number + ): Promise { + const inSpotMarketAccount = this.getSpotMarketAccount(inSpotMarketIndex); + const outSpotMarketAccount = this.getSpotMarketAccount(outSpotMarketIndex); + + const outTokenAccount = await this.getAssociatedTokenAccount( + outSpotMarketAccount.marketIndex, + false, + getTokenProgramForSpotMarket(outSpotMarketAccount) + ); + const inTokenAccount = await this.getAssociatedTokenAccount( + inSpotMarketAccount.marketIndex, + false, + getTokenProgramForSpotMarket(inSpotMarketAccount) + ); + + const externalCreateInTokenAccountIx = + this.createAssociatedTokenAccountIdempotentInstruction( + externalUserInTokenAccount, + this.wallet.publicKey, + externalUserAuthority, + this.getSpotMarketAccount(inSpotMarketIndex)!.mint + ); + + const externalCreateOutTokenAccountIx = + this.createAssociatedTokenAccountIdempotentInstruction( + externalUserOutTokenAccount, + this.wallet.publicKey, + externalUserAuthority, + this.getSpotMarketAccount(outSpotMarketIndex)!.mint + ); + + const outTransferIx = createTransferCheckedInstruction( + externalUserOutTokenAccount, + outSpotMarketAccount.mint, + outTokenAccount, + externalUserAuthority, + amountOut.toNumber(), + outSpotMarketAccount.decimals, + undefined, + getTokenProgramForSpotMarket(outSpotMarketAccount) + ); + + const inTransferIx = createTransferCheckedInstruction( + inTokenAccount, + inSpotMarketAccount.mint, + externalUserInTokenAccount, + this.wallet.publicKey, + amountIn.toNumber(), + inSpotMarketAccount.decimals, + undefined, + getTokenProgramForSpotMarket(inSpotMarketAccount) + ); + + const ixs = [ + externalCreateInTokenAccountIx, + externalCreateOutTokenAccountIx, + outTransferIx, + inTransferIx, + ]; + return ixs; + } + + public async getAllDevnetLpSwapIxs( + lpPoolId: number, + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + externalUserAuthority: PublicKey + ) { + const { beginSwapIx, endSwapIx } = await this.getSwapIx( + { + lpPoolId, + inMarketIndex, + outMarketIndex, + amountIn: inAmount, + inTokenAccount: await this.getAssociatedTokenAccount( + inMarketIndex, + false + ), + outTokenAccount: await this.getAssociatedTokenAccount( + outMarketIndex, + false + ), + }, + true + ); + + const devnetLpSwapIxs = await this.getDevnetLpSwapIxs( + inAmount, + minOutAmount, + externalUserAuthority, + await this.getAssociatedTokenAccount( + inMarketIndex, + false, + getTokenProgramForSpotMarket(this.getSpotMarketAccount(inMarketIndex)), + externalUserAuthority + ), + await this.getAssociatedTokenAccount( + outMarketIndex, + false, + getTokenProgramForSpotMarket(this.getSpotMarketAccount(outMarketIndex)), + externalUserAuthority + ), + inMarketIndex, + outMarketIndex + ); + + return [ + beginSwapIx, + ...devnetLpSwapIxs, + endSwapIx, + ] as TransactionInstruction[]; + } + + public async depositWithdrawToProgramVault( + lpPoolId: number, + depositMarketIndex: number, + borrowMarketIndex: number, + amountToDeposit: BN, + amountToBorrow: BN + ): Promise { + const { depositIx, withdrawIx } = + await this.getDepositWithdrawToProgramVaultIxs( + lpPoolId, + depositMarketIndex, + borrowMarketIndex, + amountToDeposit, + amountToBorrow + ); + + const tx = await this.buildTransaction([depositIx, withdrawIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositWithdrawToProgramVaultIxs( + lpPoolId: number, + depositMarketIndex: number, + borrowMarketIndex: number, + amountToDeposit: BN, + amountToBorrow: BN + ): Promise<{ + depositIx: TransactionInstruction; + withdrawIx: TransactionInstruction; + }> { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const depositSpotMarket = this.getSpotMarketAccount(depositMarketIndex); + const withdrawSpotMarket = this.getSpotMarketAccount(borrowMarketIndex); + + const depositTokenProgram = + this.getTokenProgramForSpotMarket(depositSpotMarket); + const withdrawTokenProgram = + this.getTokenProgramForSpotMarket(withdrawSpotMarket); + + const depositConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + depositMarketIndex + ); + const withdrawConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + borrowMarketIndex + ); + + const depositConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + depositMarketIndex + ); + const withdrawConstituentTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + borrowMarketIndex + ); + + const depositIx = this.program.instruction.depositToProgramVault( + amountToDeposit, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + constituent: depositConstituent, + constituentTokenAccount: depositConstituentTokenAccount, + spotMarket: depositSpotMarket.pubkey, + spotMarketVault: depositSpotMarket.vault, + tokenProgram: depositTokenProgram, + mint: depositSpotMarket.mint, + oracle: depositSpotMarket.oracle, + }, + } + ); + + const withdrawIx = this.program.instruction.withdrawFromProgramVault( + amountToBorrow, + { + accounts: { + state: await this.getStatePublicKey(), + admin: this.wallet.publicKey, + constituent: withdrawConstituent, + constituentTokenAccount: withdrawConstituentTokenAccount, + spotMarket: withdrawSpotMarket.pubkey, + spotMarketVault: withdrawSpotMarket.vault, + tokenProgram: withdrawTokenProgram, + mint: withdrawSpotMarket.mint, + driftSigner: getDriftSignerPublicKey(this.program.programId), + oracle: withdrawSpotMarket.oracle, + }, + } + ); + + return { depositIx, withdrawIx }; + } + + public async depositToProgramVault( + lpPoolId: number, + depositMarketIndex: number, + amountToDeposit: BN + ): Promise { + const depositIx = await this.getDepositToProgramVaultIx( + lpPoolId, + depositMarketIndex, + amountToDeposit + ); + + const tx = await this.buildTransaction([depositIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async withdrawFromProgramVault( + lpPoolId: number, + borrowMarketIndex: number, + amountToWithdraw: BN + ): Promise { + const withdrawIx = await this.getWithdrawFromProgramVaultIx( + lpPoolId, + borrowMarketIndex, + amountToWithdraw + ); + const tx = await this.buildTransaction([withdrawIx]); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getDepositToProgramVaultIx( + lpPoolId: number, + depositMarketIndex: number, + amountToDeposit: BN + ): Promise { + const { depositIx } = await this.getDepositWithdrawToProgramVaultIxs( + lpPoolId, + depositMarketIndex, + depositMarketIndex, + amountToDeposit, + new BN(0) + ); + return depositIx; + } + + public async getWithdrawFromProgramVaultIx( + lpPoolId: number, + borrowMarketIndex: number, + amountToWithdraw: BN + ): Promise { + const { withdrawIx } = await this.getDepositWithdrawToProgramVaultIxs( + lpPoolId, + borrowMarketIndex, + borrowMarketIndex, + new BN(0), + amountToWithdraw + ); + return withdrawIx; + } + + public async updatePerpMarketLpPoolFeeTransferScalar( + marketIndex: number, + lpFeeTransferScalar?: number, + lpExchangeFeeExcluscionScalar?: number + ) { + const ix = await this.getUpdatePerpMarketLpPoolFeeTransferScalarIx( + marketIndex, + lpFeeTransferScalar, + lpExchangeFeeExcluscionScalar + ); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getUpdatePerpMarketLpPoolFeeTransferScalarIx( + marketIndex: number, + lpFeeTransferScalar?: number, + lpExchangeFeeExcluscionScalar?: number + ): Promise { + return this.program.instruction.updatePerpMarketLpPoolFeeTransferScalar( + lpFeeTransferScalar ?? null, + lpExchangeFeeExcluscionScalar ?? null, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, + }, + } + ); + } + + public async updatePerpMarketLpPoolPausedOperations( + marketIndex: number, + pausedOperations: number + ) { + const ix = await this.getUpdatePerpMarketLpPoolPausedOperationsIx( + marketIndex, + pausedOperations + ); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getUpdatePerpMarketLpPoolPausedOperationsIx( + marketIndex: number, + pausedOperations: number + ): Promise { + return this.program.instruction.updatePerpMarketLpPoolPausedOperations( + pausedOperations, + { + accounts: { + admin: this.useHotWalletAdmin + ? this.wallet.publicKey + : this.getStateAccount().admin, + state: await this.getStatePublicKey(), + perpMarket: this.getPerpMarketAccount(marketIndex).pubkey, + }, + } + ); + } + + public async mintLpWhitelistToken( + lpPool: LPPoolAccount, + authority: PublicKey + ): Promise { + const ix = await this.getMintLpWhitelistTokenIx(lpPool, authority); + const tx = await this.buildTransaction(ix); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getMintLpWhitelistTokenIx( + lpPool: LPPoolAccount, + authority: PublicKey + ): Promise { + const mintAmount = 1000; + const associatedTokenAccount = getAssociatedTokenAddressSync( + lpPool.whitelistMint, + authority, + false + ); + + const ixs: TransactionInstruction[] = []; + const createInstruction = + this.createAssociatedTokenAccountIdempotentInstruction( + associatedTokenAccount, + this.wallet.publicKey, + authority, + lpPool.whitelistMint + ); + ixs.push(createInstruction); + const mintToInstruction = createMintToInstruction( + lpPool.whitelistMint, + associatedTokenAccount, + this.wallet.publicKey, + mintAmount, + [], + TOKEN_PROGRAM_ID + ); + ixs.push(mintToInstruction); + return ixs; + } } diff --git a/sdk/src/constants/numericConstants.ts b/sdk/src/constants/numericConstants.ts index e82de7d81e..1128d77674 100644 --- a/sdk/src/constants/numericConstants.ts +++ b/sdk/src/constants/numericConstants.ts @@ -1,5 +1,6 @@ import { LAMPORTS_PER_SOL } from '@solana/web3.js'; import { BN } from '@coral-xyz/anchor'; +import { BigNum } from '../factory/bigNum'; export const ZERO = new BN(0); export const ONE = new BN(1); @@ -116,3 +117,7 @@ export const FUEL_START_TS = new BN(1723147200); // unix timestamp export const MAX_PREDICTION_PRICE = PRICE_PRECISION; export const GET_MULTIPLE_ACCOUNTS_CHUNK_SIZE = 99; + +// integer constants +export const MAX_I64 = BigNum.fromPrint('9223372036854775807').val; +export const MIN_I64 = BigNum.fromPrint('-9223372036854775808').val; diff --git a/sdk/src/constants/perpMarkets.ts b/sdk/src/constants/perpMarkets.ts index 03643e7d06..981ea9521f 100644 --- a/sdk/src/constants/perpMarkets.ts +++ b/sdk/src/constants/perpMarkets.ts @@ -1297,9 +1297,6 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ oracle: new PublicKey('GAzR3C5cn7gGVvuqJB57wSYTPWP3n2Lw4mRJRxvTvqYy'), launchTs: 1747318237000, oracleSource: OracleSource.PYTH_LAZER, - pythFeedId: - '0x6d74813ee17291d5be18a355fe4d43fd300d625caea6554d49f740e7d112141e', - pythLazerId: 1571, }, { fullName: 'PUMP', @@ -1312,6 +1309,93 @@ export const MainnetPerpMarkets: PerpMarketConfig[] = [ oracleSource: OracleSource.PYTH_LAZER, pythLazerId: 1578, }, + { + fullName: 'ASTER', + category: ['DEX'], + symbol: 'ASTER-PERP', + baseAssetSymbol: 'ASTER', + marketIndex: 76, + oracle: new PublicKey('E4tyjB3os4jVczLVQ258uxLdcwjuqmhcsPquVWgrpah4'), + launchTs: 1758632629000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0xa903b5a82cb572397e3d47595d2889cf80513f5b4cf7a36b513ae10cc8b1e338', + pythLazerId: 2310, + }, + { + fullName: 'PLASMA', + category: ['DEX'], + symbol: 'XPL-PERP', + baseAssetSymbol: 'XPL', + marketIndex: 77, + oracle: new PublicKey('6kgE1KJcxTux4tkPLE8LL8GRyW2cAsvyZsDFWqCrhHVe'), + launchTs: 1758898862000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0x9873512f5cb33c77ad7a5af098d74812c62111166be395fd0941c8cedb9b00d4', + pythLazerId: 2312, + }, + { + fullName: 'Double Zero', + category: ['Infra'], + symbol: '2Z-PERP', + baseAssetSymbol: '2Z', + marketIndex: 78, + oracle: new PublicKey('4HTDpcHAwBTHCJLNMwT35w4FGc4nfA4YhT1BkcZQwQ2m'), + launchTs: 1759412919000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0xf2b3ab1c49e35e881003c3c0482d18b181a1560b697b844c24c8f85aba1cab95', + pythLazerId: 2316, + }, + { + fullName: 'ZCash', + category: ['Privacy'], + symbol: 'ZEC-PERP', + baseAssetSymbol: 'ZEC', + marketIndex: 79, + oracle: new PublicKey('BXunfRSyiQWJHv88qMvE42mpMpksWEC8Bf13p2msnRms'), + launchTs: 1760366017000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0xbe9b59d178f0d6a97ab4c343bff2aa69caa1eaae3e9048a65788c529b125bb24', + pythLazerId: 66, + }, + { + fullName: 'Mantle', + category: ['L1'], + symbol: 'MNT-PERP', + baseAssetSymbol: 'MNT', + marketIndex: 80, + oracle: new PublicKey('Gy7cJ4U1nxMA44XXC3hwqkpcxEB1mZTYiwJVkaqZfU7u'), + launchTs: 1760366017000, + oracleSource: OracleSource.PYTH_LAZER, + pythFeedId: + '0x4e3037c822d852d79af3ac80e35eb420ee3b870dca49f9344a38ef4773fb0585', + pythLazerId: 199, + }, + { + fullName: '1KPUMP', + category: ['Launchpad'], + symbol: '1KPUMP-PERP', + baseAssetSymbol: '1KPUMP', + marketIndex: 81, + oracle: new PublicKey('5r8RWTaRiMgr9Lph3FTUE3sGb1vymhpCrm83Bovjfcps'), + launchTs: 1760366017000, + oracleSource: OracleSource.PYTH_LAZER_1K, + pythLazerId: 1578, + }, + { + fullName: 'Meteroa', + category: ['Solana', 'DEX'], + symbol: 'MET-PERP', + baseAssetSymbol: 'MET', + marketIndex: 82, + oracle: new PublicKey('HN7qfUNM5Q7gQTwyEucmYdCF4CjwUrspj3DbNQ4V8P52'), + launchTs: 1761225524000, + oracleSource: OracleSource.PYTH_LAZER, + pythLazerId: 2382, + }, ]; export const PerpMarkets: { [key in DriftEnv]: PerpMarketConfig[] } = { diff --git a/sdk/src/constants/spotMarkets.ts b/sdk/src/constants/spotMarkets.ts index 503607398c..8388699374 100644 --- a/sdk/src/constants/spotMarkets.ts +++ b/sdk/src/constants/spotMarkets.ts @@ -39,8 +39,8 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ symbol: 'USDC', marketIndex: 0, poolId: 0, - oracle: new PublicKey('En8hkHLkRe9d9DraYmBTrus518BvmVH448YcvmrFM6Ce'), - oracleSource: OracleSource.PYTH_STABLE_COIN_PULL, + oracle: new PublicKey('9VCioxmni2gDLv11qufWzT3RDERhQE4iY5Gf7NTfYyAV'), + oracleSource: OracleSource.PYTH_LAZER_STABLE_COIN, mint: new PublicKey('8zGuJQqwhZafTah7Uc7Z4tXRnguqkn5KLFAP8oV6PHe2'), precision: new BN(10).pow(SIX), precisionExp: SIX, @@ -52,8 +52,8 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ symbol: 'SOL', marketIndex: 1, poolId: 0, - oracle: new PublicKey('BAtFj4kQttZRVep3UZS2aZRDixkGYgWsbqTBVDbnSsPF'), - oracleSource: OracleSource.PYTH_PULL, + oracle: new PublicKey('3m6i4RFWEDw2Ft4tFHPJtYgmpPe21k56M3FHeWYrgGBz'), + oracleSource: OracleSource.PYTH_LAZER, mint: new PublicKey(WRAPPED_SOL_MINT), precision: LAMPORTS_PRECISION, precisionExp: LAMPORTS_EXP, @@ -129,6 +129,30 @@ export const DevnetSpotMarkets: SpotMarketConfig[] = [ '0xeaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a', pythLazerId: 7, }, + { + symbol: 'GLXY', + marketIndex: 7, + poolId: 2, + oracle: new PublicKey('4wFrjUQHzRBc6qjVtMDbt28aEVgn6GaNiWR6vEff4KxR'), + oracleSource: OracleSource.Prelaunch, + mint: new PublicKey('2vVfXmcWXEaFzp7iaTVnQ4y1gR41S6tJQQMo1S5asJyC'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + pythFeedId: + '0x67e031d1723e5c89e4a826d80b2f3b41a91b05ef6122d523b8829a02e0f563aa', + }, + { + symbol: 'GLXY', + marketIndex: 8, + poolId: 2, + oracle: new PublicKey('4wFrjUQHzRBc6qjVtMDbt28aEVgn6GaNiWR6vEff4KxR'), + oracleSource: OracleSource.Prelaunch, + mint: new PublicKey('2vVfXmcWXEaFzp7iaTVnQ4y1gR41S6tJQQMo1S5asJyC'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + pythFeedId: + '0x67e031d1723e5c89e4a826d80b2f3b41a91b05ef6122d523b8829a02e0f563aa', + }, ]; export const MainnetSpotMarkets: SpotMarketConfig[] = [ @@ -435,14 +459,15 @@ export const MainnetSpotMarkets: SpotMarketConfig[] = [ symbol: 'JLP', marketIndex: 19, poolId: 0, - oracle: new PublicKey('5Mb11e5rt1Sp6A286B145E4TmgMzsM2UX9nCF2vas5bs'), - oracleSource: OracleSource.PYTH_PULL, + oracle: new PublicKey('4VMtKepA6iFwMTJ7bBbdcGxavNRKiDjxxRr1CaB2NnFJ'), + oracleSource: OracleSource.PYTH_LAZER, mint: new PublicKey('27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4'), precision: new BN(10).pow(SIX), precisionExp: SIX, launchTs: 1719415157000, pythFeedId: '0xc811abc82b4bad1f9bd711a2773ccaa935b03ecef974236942cec5e0eb845a3a', + pythLazerId: 459, }, { symbol: 'POPCAT', @@ -934,6 +959,34 @@ export const MainnetSpotMarkets: SpotMarketConfig[] = [ '0x8f257aab6e7698bb92b15511915e593d6f8eae914452f781874754b03d0c612b', launchTs: 1756392947000, }, + { + symbol: '2Z', + marketIndex: 59, + poolId: 0, + oracle: new PublicKey('4HTDpcHAwBTHCJLNMwT35w4FGc4nfA4YhT1BkcZQwQ2m'), + oracleSource: OracleSource.PYTH_LAZER, + mint: new PublicKey('J6pQQ3FAcJQeWPPGppWRb4nM8jU3wLyYbRrLh7feMfvd'), + precision: new BN(10).pow(EIGHT), + precisionExp: EIGHT, + pythFeedId: + '0xf2b3ab1c49e35e881003c3c0482d18b181a1560b697b844c24c8f85aba1cab95', + pythLazerId: 2316, + launchTs: 1759412919000, + }, + { + symbol: 'MET', + marketIndex: 60, + poolId: 0, + oracle: new PublicKey('HN7qfUNM5Q7gQTwyEucmYdCF4CjwUrspj3DbNQ4V8P52'), + oracleSource: OracleSource.PYTH_LAZER, + mint: new PublicKey('METvsvVRapdj9cFLzq4Tr43xK4tAjQfwX76z3n6mWQL'), + precision: new BN(10).pow(SIX), + precisionExp: SIX, + pythFeedId: + '0x0292e0f405bcd4a496d34e48307f6787349ad2bcd8505c3d3a9f77d81a67a682', + pythLazerId: 2382, + launchTs: 1761225524000, + }, ]; export const SpotMarkets: { [key in DriftEnv]: SpotMarketConfig[] } = { diff --git a/sdk/src/constituentMap/constituentMap.ts b/sdk/src/constituentMap/constituentMap.ts new file mode 100644 index 0000000000..77f3c9ed87 --- /dev/null +++ b/sdk/src/constituentMap/constituentMap.ts @@ -0,0 +1,285 @@ +import { + Commitment, + Connection, + MemcmpFilter, + PublicKey, + RpcResponseAndContext, +} from '@solana/web3.js'; +import { ConstituentAccountSubscriber, DataAndSlot } from '../accounts/types'; +import { ConstituentAccount } from '../types'; +import { PollingConstituentAccountSubscriber } from './pollingConstituentAccountSubscriber'; +import { WebSocketConstituentAccountSubscriber } from './webSocketConstituentAccountSubscriber'; +import { DriftClient } from '../driftClient'; +import { getConstituentFilter, getConstituentLpPoolFilter } from '../memcmp'; +import { ZSTDDecoder } from 'zstddec'; +import { getLpPoolPublicKey } from '../addresses/pda'; + +const MAX_CONSTITUENT_SIZE_BYTES = 480; // TODO: update this when account is finalized + +export type ConstituentMapConfig = { + driftClient: DriftClient; + connection?: Connection; + subscriptionConfig: + | { + type: 'polling'; + frequency: number; + commitment?: Commitment; + } + | { + type: 'websocket'; + resubTimeoutMs?: number; + logResubMessages?: boolean; + commitment?: Commitment; + }; + lpPoolId?: number; + // potentially use these to filter Constituent accounts + additionalFilters?: MemcmpFilter[]; +}; + +export interface ConstituentMapInterface { + subscribe(): Promise; + unsubscribe(): Promise; + has(key: string): boolean; + get(key: string): ConstituentAccount | undefined; + getFromSpotMarketIndex( + spotMarketIndex: number + ): ConstituentAccount | undefined; + getFromConstituentIndex( + constituentIndex: number + ): ConstituentAccount | undefined; + + getWithSlot(key: string): DataAndSlot | undefined; + mustGet(key: string): Promise; + mustGetWithSlot(key: string): Promise>; +} + +export class ConstituentMap implements ConstituentMapInterface { + private driftClient: DriftClient; + private constituentMap = new Map>(); + private constituentAccountSubscriber: ConstituentAccountSubscriber; + private additionalFilters?: MemcmpFilter[]; + private commitment?: Commitment; + private connection?: Connection; + + private constituentIndexToKeyMap = new Map(); + private spotMarketIndexToKeyMap = new Map(); + + private lpPoolId: number; + + constructor(config: ConstituentMapConfig) { + this.driftClient = config.driftClient; + this.additionalFilters = config.additionalFilters; + this.commitment = config.subscriptionConfig.commitment; + this.connection = config.connection || this.driftClient.connection; + this.lpPoolId = config.lpPoolId ?? 0; + + if (config.subscriptionConfig.type === 'polling') { + this.constituentAccountSubscriber = + new PollingConstituentAccountSubscriber( + this, + this.driftClient.program, + config.subscriptionConfig.frequency, + config.subscriptionConfig.commitment, + this.getFilters() + ); + } else if (config.subscriptionConfig.type === 'websocket') { + this.constituentAccountSubscriber = + new WebSocketConstituentAccountSubscriber( + this, + this.driftClient.program, + config.subscriptionConfig.resubTimeoutMs, + config.subscriptionConfig.commitment, + this.getFilters() + ); + } + + // Listen for account updates from the subscriber + this.constituentAccountSubscriber.eventEmitter.on( + 'onAccountUpdate', + (account: ConstituentAccount, pubkey: PublicKey, slot: number) => { + this.updateConstituentAccount(pubkey.toString(), account, slot); + } + ); + } + + private getFilters(): MemcmpFilter[] { + const filters = [ + getConstituentFilter(), + getConstituentLpPoolFilter( + getLpPoolPublicKey(this.driftClient.program.programId, this.lpPoolId) + ), + ]; + if (this.additionalFilters) { + filters.push(...this.additionalFilters); + } + return filters; + } + + private decode(name: string, buffer: Buffer): ConstituentAccount { + return this.driftClient.program.account.constituent.coder.accounts.decodeUnchecked( + name, + buffer + ); + } + + public async sync(): Promise { + try { + const rpcRequestArgs = [ + this.driftClient.program.programId.toBase58(), + { + commitment: this.commitment, + filters: this.getFilters(), + encoding: 'base64+zstd', + withContext: true, + }, + ]; + + // @ts-ignore + const rpcJSONResponse: any = await this.connection._rpcRequest( + 'getProgramAccounts', + rpcRequestArgs + ); + const rpcResponseAndContext: RpcResponseAndContext< + Array<{ pubkey: PublicKey; account: { data: [string, string] } }> + > = rpcJSONResponse.result; + const slot = rpcResponseAndContext.context.slot; + + const promises = rpcResponseAndContext.value.map( + async (programAccount) => { + const compressedUserData = Buffer.from( + programAccount.account.data[0], + 'base64' + ); + const decoder = new ZSTDDecoder(); + await decoder.init(); + const buffer = Buffer.from( + decoder.decode(compressedUserData, MAX_CONSTITUENT_SIZE_BYTES) + ); + const key = programAccount.pubkey.toString(); + const currAccountWithSlot = this.getWithSlot(key); + + if (currAccountWithSlot) { + if (slot >= currAccountWithSlot.slot) { + const constituentAcc = this.decode('Constituent', buffer); + this.updateConstituentAccount(key, constituentAcc, slot); + } + } else { + const constituentAcc = this.decode('Constituent', buffer); + this.updateConstituentAccount(key, constituentAcc, slot); + } + } + ); + await Promise.all(promises); + } catch (error) { + console.log(`ConstituentMap.sync() error: ${error.message}`); + } + } + + public async subscribe(): Promise { + await this.constituentAccountSubscriber.subscribe(); + } + + public async unsubscribe(): Promise { + await this.constituentAccountSubscriber.unsubscribe(); + this.constituentMap.clear(); + } + + public has(key: string): boolean { + return this.constituentMap.has(key); + } + + public get(key: string): ConstituentAccount | undefined { + return this.constituentMap.get(key)?.data; + } + + public getFromConstituentIndex( + constituentIndex: number + ): ConstituentAccount | undefined { + const key = this.constituentIndexToKeyMap.get(constituentIndex); + return key ? this.get(key) : undefined; + } + + public getFromSpotMarketIndex( + spotMarketIndex: number + ): ConstituentAccount | undefined { + const key = this.spotMarketIndexToKeyMap.get(spotMarketIndex); + return key ? this.get(key) : undefined; + } + + public getWithSlot(key: string): DataAndSlot | undefined { + return this.constituentMap.get(key); + } + + public async mustGet(key: string): Promise { + if (!this.has(key)) { + await this.sync(); + } + const result = this.constituentMap.get(key); + if (!result) { + throw new Error(`ConstituentAccount not found for key: ${key}`); + } + return result.data; + } + + public async mustGetWithSlot( + key: string + ): Promise> { + if (!this.has(key)) { + await this.sync(); + } + const result = this.constituentMap.get(key); + if (!result) { + throw new Error(`ConstituentAccount not found for key: ${key}`); + } + return result; + } + + public size(): number { + return this.constituentMap.size; + } + + public *values(): IterableIterator { + for (const dataAndSlot of this.constituentMap.values()) { + yield dataAndSlot.data; + } + } + + public valuesWithSlot(): IterableIterator> { + return this.constituentMap.values(); + } + + public *entries(): IterableIterator<[string, ConstituentAccount]> { + for (const [key, dataAndSlot] of this.constituentMap.entries()) { + yield [key, dataAndSlot.data]; + } + } + + public entriesWithSlot(): IterableIterator< + [string, DataAndSlot] + > { + return this.constituentMap.entries(); + } + + public updateConstituentAccount( + key: string, + constituentAccount: ConstituentAccount, + slot: number + ): void { + const existingData = this.getWithSlot(key); + if (existingData) { + if (slot >= existingData.slot) { + this.constituentMap.set(key, { + data: constituentAccount, + slot, + }); + } + } else { + this.constituentMap.set(key, { + data: constituentAccount, + slot, + }); + } + this.constituentIndexToKeyMap.set(constituentAccount.constituentIndex, key); + this.spotMarketIndexToKeyMap.set(constituentAccount.spotMarketIndex, key); + } +} diff --git a/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts b/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts new file mode 100644 index 0000000000..e50b34df4b --- /dev/null +++ b/sdk/src/constituentMap/pollingConstituentAccountSubscriber.ts @@ -0,0 +1,97 @@ +import { + NotSubscribedError, + ConstituentAccountEvents, + ConstituentAccountSubscriber, +} from '../accounts/types'; +import { Program } from '@coral-xyz/anchor'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Commitment, MemcmpFilter } from '@solana/web3.js'; +import { ConstituentMap } from './constituentMap'; + +export class PollingConstituentAccountSubscriber + implements ConstituentAccountSubscriber +{ + isSubscribed: boolean; + program: Program; + frequency: number; + commitment?: Commitment; + additionalFilters?: MemcmpFilter[]; + eventEmitter: StrictEventEmitter; + + intervalId?: NodeJS.Timeout; + constituentMap: ConstituentMap; + + public constructor( + constituentMap: ConstituentMap, + program: Program, + frequency: number, + commitment?: Commitment, + additionalFilters?: MemcmpFilter[] + ) { + this.constituentMap = constituentMap; + this.isSubscribed = false; + this.program = program; + this.frequency = frequency; + this.commitment = commitment; + this.additionalFilters = additionalFilters; + this.eventEmitter = new EventEmitter(); + } + + async subscribe(): Promise { + if (this.isSubscribed || this.frequency <= 0) { + return true; + } + + const executeSync = async () => { + await this.sync(); + this.intervalId = setTimeout(executeSync, this.frequency); + }; + + // Initial sync + await this.sync(); + + // Start polling + this.intervalId = setTimeout(executeSync, this.frequency); + + this.isSubscribed = true; + return true; + } + + async sync(): Promise { + try { + await this.constituentMap.sync(); + this.eventEmitter.emit('update'); + } catch (error) { + console.log( + `PollingConstituentAccountSubscriber.sync() error: ${error.message}` + ); + this.eventEmitter.emit('error', error); + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + if (this.intervalId) { + clearTimeout(this.intervalId); + this.intervalId = undefined; + } + + this.isSubscribed = false; + } + + assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } + + didSubscriptionSucceed(): boolean { + return this.isSubscribed; + } +} diff --git a/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts b/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts new file mode 100644 index 0000000000..816acd6211 --- /dev/null +++ b/sdk/src/constituentMap/webSocketConstituentAccountSubscriber.ts @@ -0,0 +1,112 @@ +import { + NotSubscribedError, + ConstituentAccountEvents, + ConstituentAccountSubscriber, +} from '../accounts/types'; +import { Program } from '@coral-xyz/anchor'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import { EventEmitter } from 'events'; +import { Commitment, Context, MemcmpFilter, PublicKey } from '@solana/web3.js'; +import { ConstituentAccount } from '../types'; +import { WebSocketProgramAccountSubscriber } from '../accounts/webSocketProgramAccountSubscriber'; +import { getConstituentFilter } from '../memcmp'; +import { ConstituentMap } from './constituentMap'; + +export class WebSocketConstituentAccountSubscriber + implements ConstituentAccountSubscriber +{ + isSubscribed: boolean; + resubTimeoutMs?: number; + commitment?: Commitment; + program: Program; + eventEmitter: StrictEventEmitter; + + constituentDataAccountSubscriber: WebSocketProgramAccountSubscriber; + constituentMap: ConstituentMap; + private additionalFilters?: MemcmpFilter[]; + + public constructor( + constituentMap: ConstituentMap, + program: Program, + resubTimeoutMs?: number, + commitment?: Commitment, + additionalFilters?: MemcmpFilter[] + ) { + this.constituentMap = constituentMap; + this.isSubscribed = false; + this.program = program; + this.eventEmitter = new EventEmitter(); + this.resubTimeoutMs = resubTimeoutMs; + this.commitment = commitment; + this.additionalFilters = additionalFilters; + } + + async subscribe(): Promise { + if (this.isSubscribed) { + return true; + } + this.constituentDataAccountSubscriber = + new WebSocketProgramAccountSubscriber( + 'LpPoolConstituent', + 'Constituent', + this.program, + this.program.account.constituent.coder.accounts.decode.bind( + this.program.account.constituent.coder.accounts + ), + { + filters: [getConstituentFilter(), ...(this.additionalFilters || [])], + commitment: this.commitment, + } + ); + + await this.constituentDataAccountSubscriber.subscribe( + (accountId: PublicKey, account: ConstituentAccount, context: Context) => { + this.constituentMap.updateConstituentAccount( + accountId.toBase58(), + account, + context.slot + ); + this.eventEmitter.emit( + 'onAccountUpdate', + account, + accountId, + context.slot + ); + } + ); + + this.eventEmitter.emit('update'); + this.isSubscribed = true; + return true; + } + + async sync(): Promise { + try { + await this.constituentMap.sync(); + this.eventEmitter.emit('update'); + } catch (error) { + console.log( + `WebSocketConstituentAccountSubscriber.sync() error: ${error.message}` + ); + this.eventEmitter.emit('error', error); + } + } + + async unsubscribe(): Promise { + if (!this.isSubscribed) { + return; + } + + await Promise.all([this.constituentDataAccountSubscriber.unsubscribe()]); + + this.isSubscribed = false; + } + + assertIsSubscribed(): void { + if (!this.isSubscribed) { + throw new NotSubscribedError( + 'You must call `subscribe` before using this function' + ); + } + } +} diff --git a/sdk/src/decode/user.ts b/sdk/src/decode/user.ts index e0d852f6e8..f363b3901a 100644 --- a/sdk/src/decode/user.ts +++ b/sdk/src/decode/user.ts @@ -84,6 +84,11 @@ export function decodeUser(buffer: Buffer): UserAccount { const quoteAssetAmount = readSignedBigInt64LE(buffer, offset + 16); const lpShares = readUnsignedBigInt64LE(buffer, offset + 64); const openOrders = buffer.readUInt8(offset + 94); + const positionFlag = buffer.readUInt8(offset + 95); + const isolatedPositionScaledBalance = readUnsignedBigInt64LE( + buffer, + offset + 96 + ); if ( baseAssetAmount.eq(ZERO) && @@ -117,7 +122,6 @@ export function decodeUser(buffer: Buffer): UserAccount { offset += 3; const perLpBase = buffer.readUInt8(offset); offset += 1; - perpPositions.push({ lastCumulativeFundingRate, baseAssetAmount, @@ -135,6 +139,8 @@ export function decodeUser(buffer: Buffer): UserAccount { openOrders, perLpBase, maxMarginRatio, + positionFlag, + isolatedPositionScaledBalance, }); } diff --git a/sdk/src/dlob/DLOB.ts b/sdk/src/dlob/DLOB.ts index 15c2ec72f2..5b8ec0c896 100644 --- a/sdk/src/dlob/DLOB.ts +++ b/sdk/src/dlob/DLOB.ts @@ -467,10 +467,6 @@ export class DLOB { const isAmmPaused = ammPaused(stateAccount, marketAccount); - const minAuctionDuration = isVariant(marketType, 'perp') - ? stateAccount.minPerpAuctionDuration - : 0; - const { makerRebateNumerator, makerRebateDenominator } = this.getMakerRebate(marketType, stateAccount, marketAccount); @@ -481,7 +477,8 @@ export class DLOB { marketType, oraclePriceData, isAmmPaused, - minAuctionDuration, + stateAccount, + marketAccount, fallbackAsk, fallbackBid ); @@ -493,7 +490,8 @@ export class DLOB { marketType, oraclePriceData, isAmmPaused, - minAuctionDuration, + stateAccount, + marketAccount, makerRebateNumerator, makerRebateDenominator, fallbackAsk, @@ -597,7 +595,10 @@ export class DLOB { ? OraclePriceData : MMOraclePriceData, isAmmPaused: boolean, - minAuctionDuration: number, + stateAccount: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount, makerRebateNumerator: number, makerRebateDenominator: number, fallbackAsk: BN | undefined, @@ -636,7 +637,8 @@ export class DLOB { (askPrice) => { return askPrice.lte(fallbackBidWithBuffer); }, - minAuctionDuration + stateAccount, + marketAccount ); for (const askCrossingFallback of asksCrossingFallback) { @@ -664,7 +666,8 @@ export class DLOB { (bidPrice) => { return bidPrice.gte(fallbackAskWithBuffer); }, - minAuctionDuration + stateAccount, + marketAccount ); for (const bidCrossingFallback of bidsCrossingFallback) { @@ -683,7 +686,10 @@ export class DLOB { ? OraclePriceData : MMOraclePriceData, isAmmPaused: boolean, - minAuctionDuration: number, + state: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount, fallbackAsk: BN | undefined, fallbackBid?: BN | undefined ): NodeToFill[] { @@ -736,7 +742,8 @@ export class DLOB { (takerPrice) => { return takerPrice === undefined || takerPrice.lte(fallbackBid); }, - minAuctionDuration + state, + marketAccount ); for (const takingAskCrossingFallback of takingAsksCrossingFallback) { @@ -793,7 +800,8 @@ export class DLOB { (takerPrice) => { return takerPrice === undefined || takerPrice.gte(fallbackAsk); }, - minAuctionDuration + state, + marketAccount ); for (const marketBidCrossingFallback of takingBidsCrossingFallback) { nodesToFill.push(marketBidCrossingFallback); @@ -911,7 +919,10 @@ export class DLOB { : MMOraclePriceData, nodeGenerator: Generator, doesCross: (nodePrice: BN | undefined) => boolean, - minAuctionDuration: number + state: StateAccount, + marketAccount: T extends { spot: unknown } + ? SpotMarketAccount + : PerpMarketAccount ): NodeToFill[] { const nodesToFill = new Array(); @@ -934,8 +945,10 @@ export class DLOB { isVariant(marketType, 'spot') || isFallbackAvailableLiquiditySource( node.order, - minAuctionDuration, - slot + oraclePriceData as MMOraclePriceData, + slot, + state, + marketAccount as PerpMarketAccount ); if (crosses && fallbackAvailable) { diff --git a/sdk/src/driftClient.ts b/sdk/src/driftClient.ts index abff8433df..a6be8b31b4 100644 --- a/sdk/src/driftClient.ts +++ b/sdk/src/driftClient.ts @@ -57,7 +57,6 @@ import { StateAccount, SwapReduceOnly, SignedMsgOrderParamsMessage, - TakerInfo, TxParams, UserAccount, UserStatsAccount, @@ -65,6 +64,10 @@ import { SignedMsgOrderParamsDelegateMessage, TokenProgramFlag, PostOnlyParams, + LPPoolAccount, + ConstituentAccount, + ConstituentTargetBaseAccount, + AmmCache, } from './types'; import driftIDL from './idl/drift.json'; @@ -113,6 +116,17 @@ import { getUserStatsAccountPublicKey, getSignedMsgWsDelegatesAccountPublicKey, getIfRebalanceConfigPublicKey, + getRevenueShareAccountPublicKey, + getRevenueShareEscrowAccountPublicKey, + getConstituentTargetBasePublicKey, + getAmmConstituentMappingPublicKey, + getLpPoolPublicKey, + getConstituentPublicKey, + getAmmCachePublicKey, + getLpPoolTokenVaultPublicKey, + getConstituentVaultPublicKey, + getConstituentCorrelationsPublicKey, + getLpPoolTokenTokenAccountPublicKey, } from './addresses/pda'; import { DataAndSlot, @@ -124,6 +138,7 @@ import { TxSender, TxSigAndSlot } from './tx/types'; import { BASE_PRECISION, GOV_SPOT_MARKET_INDEX, + MARGIN_PRECISION, ONE, PERCENTAGE_PRECISION, PRICE_PRECISION, @@ -131,12 +146,17 @@ import { QUOTE_SPOT_MARKET_INDEX, ZERO, } from './constants/numericConstants'; -import { findDirectionToClose, positionIsAvailable } from './math/position'; +import { + calculateClaimablePnl, + findDirectionToClose, + positionIsAvailable, +} from './math/position'; import { getSignedTokenAmount, getTokenAmount } from './math/spotBalance'; import { decodeName, DEFAULT_USER_NAME, encodeName } from './userName'; import { MMOraclePriceData, OraclePriceData } from './oracles/types'; import { DriftClientConfig } from './driftClientConfig'; import { PollingDriftClientAccountSubscriber } from './accounts/pollingDriftClientAccountSubscriber'; +import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; import { RetryTxSender } from './tx/retryTxSender'; import { User } from './user'; import { UserSubscriptionConfig } from './userConfig'; @@ -154,18 +174,19 @@ import { isSpotPositionAvailable } from './math/spotPosition'; import { calculateMarketMaxAvailableInsurance } from './math/market'; import { fetchUserStatsAccount } from './accounts/fetch'; import { castNumberToSpotPrecision } from './math/spotMarket'; -import { - JupiterClient, - QuoteResponse, - SwapMode, -} from './jupiter/jupiterClient'; +import { JupiterClient, QuoteResponse } from './jupiter/jupiterClient'; +import { SwapMode } from './swap/UnifiedSwapClient'; import { getNonIdleUserFilter } from './memcmp'; import { UserStatsSubscriptionConfig } from './userStatsConfig'; import { getMarinadeDepositIx, getMarinadeFinanceProgram } from './marinade'; import { getOrderParams, isUpdateHighLeverageMode } from './orderParams'; import { numberToSafeBN } from './math/utils'; import { TransactionParamProcessor } from './tx/txParamProcessor'; -import { isOracleValid, trimVaaSignatures } from './math/oracles'; +import { + isOracleTooDivergent, + isOracleValid, + trimVaaSignatures, +} from './math/oracles'; import { TxHandler } from './tx/txHandler'; import { DEFAULT_RECEIVER_PROGRAM_ID, @@ -183,18 +204,35 @@ import { createMinimalEd25519VerifyIx } from './util/ed25519Utils'; import { createNativeInstructionDiscriminatorBuffer, isVersionedTransaction, + MAX_TX_BYTE_SIZE, } from './tx/utils'; import pythSolanaReceiverIdl from './idl/pyth_solana_receiver.json'; import { asV0Tx, PullFeed, AnchorUtils } from '@switchboard-xyz/on-demand'; -import { gprcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; +import { grpcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; import nacl from 'tweetnacl'; import { Slothash } from './slot/SlothashSubscriber'; import { getOracleId } from './oracles/oracleId'; import { SignedMsgOrderParams } from './types'; +import { TakerInfo } from './types'; +// BN is already imported globally in this file via other imports import { sha256 } from '@noble/hashes/sha256'; import { getOracleConfidenceFromMMOracleData } from './oracles/utils'; -import { Commitment } from 'gill'; -import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; +import { ConstituentMap } from './constituentMap/constituentMap'; +import { hasBuilder } from './math/orders'; +import { RevenueShareEscrowMap } from './userMap/revenueShareEscrowMap'; +import { + isBuilderOrderReferral, + isBuilderOrderCompleted, +} from './math/builder'; +import { BigNum } from './factory/bigNum'; +import { TitanClient, SwapMode as TitanSwapMode } from './titan/titanClient'; +import { UnifiedSwapClient } from './swap/UnifiedSwapClient'; + +/** + * Union type for swap clients (Titan and Jupiter) - Legacy type + * @deprecated Use UnifiedSwapClient class instead + */ +export type SwapClient = TitanClient | JupiterClient; type RemainingAccountParams = { userAccounts: UserAccount[]; @@ -263,6 +301,46 @@ export class DriftClient { return this._isSubscribed && this.accountSubscriber.isSubscribed; } + private async getPrePlaceOrderIxs( + orderParams: OptionalOrderParams, + userAccount: UserAccount, + options?: { positionMaxLev?: number; isolatedPositionDepositAmount?: BN } + ): Promise { + const preIxs: TransactionInstruction[] = []; + + if (isVariant(orderParams.marketType, 'perp')) { + const { positionMaxLev, isolatedPositionDepositAmount } = options ?? {}; + + if ( + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(orderParams, userAccount.subAccountId) + ) { + preIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + userAccount.subAccountId + ) + ); + } + + if (positionMaxLev) { + const marginRatio = Math.floor( + (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() + ); + preIxs.push( + await this.getUpdateUserPerpPositionCustomMarginRatioIx( + orderParams.marketIndex, + marginRatio, + userAccount.subAccountId + ) + ); + } + } + + return preIxs; + } + public set isSubscribed(val: boolean) { this._isSubscribed = val; } @@ -359,6 +437,8 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, grpcConfigs: config.accountSubscription?.grpcConfigs, + grpcMultiUserAccountSubscriber: + config.accountSubscription?.grpcMultiUserAccountSubscriber, }; this.userStatsAccountSubscriptionConfig = { type: 'grpc', @@ -425,7 +505,10 @@ export class DriftClient { delistedMarketSetting ); } else if (config.accountSubscription?.type === 'grpc') { - this.accountSubscriber = new gprcDriftClientAccountSubscriber( + const accountSubscriberClass = + config.accountSubscription?.driftClientAccountSubscriber ?? + grpcDriftClientAccountSubscriber; + this.accountSubscriber = new accountSubscriberClass( config.accountSubscription.grpcConfigs, this.program, config.perpMarketIndexes ?? [], @@ -453,7 +536,7 @@ export class DriftClient { resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, logResubMessages: config.accountSubscription?.logResubMessages, }, - config.accountSubscription?.commitment as Commitment + config.accountSubscription?.commitment ); } this.eventEmitter = this.accountSubscriber.eventEmitter; @@ -614,8 +697,7 @@ export class DriftClient { public getSpotMarketAccount( marketIndex: number ): SpotMarketAccount | undefined { - return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex) - ?.data; + return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex).data; } /** @@ -626,8 +708,7 @@ export class DriftClient { marketIndex: number ): Promise { await this.accountSubscriber.fetch(); - return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex) - ?.data; + return this.accountSubscriber.getSpotMarketAccountAndSlot(marketIndex).data; } public getSpotMarketAccounts(): SpotMarketAccount[] { @@ -733,7 +814,6 @@ export class DriftClient { return lookupTableAccount; } - public async fetchAllLookupTableAccounts(): Promise< AddressLookupTableAccount[] > { @@ -924,7 +1004,7 @@ export class DriftClient { this.userStats = new UserStats({ driftClient: this, userStatsAccountPublicKey: this.userStatsAccountPublicKey, - accountSubscription: this.userAccountSubscriptionConfig, + accountSubscription: this.userStatsAccountSubscriptionConfig, }); this.userStats.subscribe(); @@ -1232,6 +1312,206 @@ export class DriftClient { return ix; } + public async initializeRevenueShare( + authority: PublicKey, + txParams?: TxParams + ): Promise { + const ix = await this.getInitializeRevenueShareIx(authority); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getInitializeRevenueShareIx( + authority: PublicKey, + overrides?: { + payer?: PublicKey; + } + ): Promise { + const revenueShare = getRevenueShareAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.initializeRevenueShare({ + accounts: { + revenueShare, + authority, + payer: overrides?.payer ?? this.wallet.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async initializeRevenueShareEscrow( + authority: PublicKey, + numOrders: number, + txParams?: TxParams + ): Promise { + const ix = await this.getInitializeRevenueShareEscrowIx( + authority, + numOrders + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getInitializeRevenueShareEscrowIx( + authority: PublicKey, + numOrders: number, + overrides?: { + payer?: PublicKey; + } + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.initializeRevenueShareEscrow(numOrders, { + accounts: { + escrow, + authority, + payer: overrides?.payer ?? this.wallet.publicKey, + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + state: await this.getStatePublicKey(), + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + public async migrateReferrer( + authority: PublicKey, + txParams?: TxParams + ): Promise { + const ix = await this.getMigrateReferrerIx(authority); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getMigrateReferrerIx( + authority: PublicKey + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.migrateReferrer({ + accounts: { + escrow, + authority, + userStats: getUserStatsAccountPublicKey( + this.program.programId, + authority + ), + state: await this.getStatePublicKey(), + payer: this.wallet.publicKey, + }, + }); + } + + public async resizeRevenueShareEscrowOrders( + authority: PublicKey, + numOrders: number, + txParams?: TxParams + ): Promise { + const ix = await this.getResizeRevenueShareEscrowOrdersIx( + authority, + numOrders + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + public async getResizeRevenueShareEscrowOrdersIx( + authority: PublicKey, + numOrders: number + ): Promise { + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.resizeRevenueShareEscrowOrders(numOrders, { + accounts: { + escrow, + authority, + payer: this.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }, + }); + } + + /** + * Creates the transaction to add or update an approved builder. + * This allows the builder to receive revenue share from referrals. + * + * @param builder - The public key of the builder to add or update. + * @param maxFeeTenthBps - The maximum fee tenth bps to set for the builder. + * @param add - Whether to add or update the builder. If the builder already exists, `add = true` will update the `maxFeeTenthBps`, otherwise it will add the builder. If `add = false`, the builder's `maxFeeTenthBps` will be set to 0. + * @param txParams - The transaction parameters to use for the transaction. + * @returns The transaction to add or update an approved builder. + */ + public async changeApprovedBuilder( + builder: PublicKey, + maxFeeTenthBps: number, + add: boolean, + txParams?: TxParams + ): Promise { + const ix = await this.getChangeApprovedBuilderIx( + builder, + maxFeeTenthBps, + add + ); + const tx = await this.buildTransaction([ix], txParams); + const { txSig } = await this.sendTransaction(tx, [], this.opts); + return txSig; + } + + /** + * Creates the transaction instruction to add or update an approved builder. + * This allows the builder to receive revenue share from referrals. + * + * @param builder - The public key of the builder to add or update. + * @param maxFeeTenthBps - The maximum fee tenth bps to set for the builder. + * @param add - Whether to add or update the builder. If the builder already exists, `add = true` will update the `maxFeeTenthBps`, otherwise it will add the builder. If `add = false`, the builder's `maxFeeTenthBps` will be set to 0. + * @returns The transaction instruction to add or update an approved builder. + */ + public async getChangeApprovedBuilderIx( + builder: PublicKey, + maxFeeTenthBps: number, + add: boolean, + overrides?: { + authority?: PublicKey; + payer?: PublicKey; + } + ): Promise { + const authority = overrides?.authority ?? this.wallet.publicKey; + const payer = overrides?.payer ?? this.wallet.publicKey; + const escrow = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + authority + ); + return this.program.instruction.changeApprovedBuilder( + builder, + maxFeeTenthBps, + add, + { + accounts: { + escrow, + authority, + payer, + systemProgram: anchor.web3.SystemProgram.programId, + }, + } + ); + } + public async addSignedMsgWsDelegate( authority: PublicKey, delegate: PublicKey, @@ -1511,7 +1791,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getUpdateUserCustomMarginRatioIx( marginRatio: number, subAccountId = 0 @@ -1545,11 +1824,11 @@ export class DriftClient { ): Promise { const userAccountPublicKey = getUserAccountPublicKeySync( this.program.programId, - this.wallet.publicKey, + this.authority, subAccountId ); - await this.addUser(subAccountId, this.wallet.publicKey); + await this.addUser(subAccountId, this.authority); const ix = this.program.instruction.updateUserPerpPositionCustomMarginRatio( subAccountId, @@ -1570,14 +1849,21 @@ export class DriftClient { perpMarketIndex: number, marginRatio: number, subAccountId = 0, - txParams?: TxParams + txParams?: TxParams, + enterHighLeverageMode?: boolean ): Promise { - const ix = await this.getUpdateUserPerpPositionCustomMarginRatioIx( + const ixs = []; + if (enterHighLeverageMode) { + const enableIx = await this.getEnableHighLeverageModeIx(subAccountId); + ixs.push(enableIx); + } + const updateIx = await this.getUpdateUserPerpPositionCustomMarginRatioIx( perpMarketIndex, marginRatio, subAccountId ); - const tx = await this.buildTransaction(ix, txParams ?? this.txParams); + ixs.push(updateIx); + const tx = await this.buildTransaction(ixs, txParams ?? this.txParams); const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } @@ -1959,6 +2245,20 @@ export class DriftClient { writableSpotMarketIndexes, }); + for (const order of userAccount.orders) { + if (hasBuilder(order)) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + break; + } + } + const tokenPrograms = new Set(); for (const spotPosition of userAccount.spotPositions) { if (isSpotPositionAvailable(spotPosition)) { @@ -2290,7 +2590,6 @@ export class DriftClient { this.mustIncludeSpotMarketIndexes.add(spotMarketIndex); }); } - getRemainingAccounts(params: RemainingAccountParams): AccountMeta[] { const { oracleAccountMap, spotMarketAccountMap, perpMarketAccountMap } = this.getRemainingAccountMapsForUsers(params.userAccounts); @@ -2469,6 +2768,35 @@ export class DriftClient { } } + addBuilderToRemainingAccounts( + builders: PublicKey[], + remainingAccounts: AccountMeta[] + ): void { + for (const builder of builders) { + // Add User account for the builder + const builderUserAccount = getUserAccountPublicKeySync( + this.program.programId, + builder, + 0 // subAccountId 0 for builder user account + ); + remainingAccounts.push({ + pubkey: builderUserAccount, + isSigner: false, + isWritable: true, + }); + + const builderAccount = getRevenueShareAccountPublicKey( + this.program.programId, + builder + ); + remainingAccounts.push({ + pubkey: builderAccount, + isSigner: false, + isWritable: true, + }); + } + } + getRemainingAccountMapsForUsers(userAccounts: UserAccount[]): { oracleAccountMap: Map; spotMarketAccountMap: Map; @@ -2545,17 +2873,19 @@ export class DriftClient { public async getAssociatedTokenAccount( marketIndex: number, useNative = true, - tokenProgram = TOKEN_PROGRAM_ID + tokenProgram = TOKEN_PROGRAM_ID, + authority = this.wallet.publicKey, + allowOwnerOffCurve = false ): Promise { const spotMarket = this.getSpotMarketAccount(marketIndex); if (useNative && spotMarket.mint.equals(WRAPPED_SOL_MINT)) { - return this.wallet.publicKey; + return authority; } const mint = spotMarket.mint; return await getAssociatedTokenAddress( mint, - this.wallet.publicKey, - undefined, + authority, + allowOwnerOffCurve, tokenProgram ); } @@ -3124,7 +3454,6 @@ export class DriftClient { userAccountPublicKey, }; } - public async createInitializeUserAccountAndDepositCollateral( amount: BN, userTokenAccount: PublicKey, @@ -3852,25 +4181,26 @@ export class DriftClient { subAccountId?: number, txParams?: TxParams ): Promise { - const { txSig } = await this.sendTransaction( - await this.buildTransaction( - await this.getTransferIsolatedPerpPositionDepositIx( - amount, - perpMarketIndex, - subAccountId - ), - txParams + const tx = await this.buildTransaction( + await this.getTransferIsolatedPerpPositionDepositIx( + amount, + perpMarketIndex, + subAccountId ), - [], - this.opts + txParams ); + const { txSig } = await this.sendTransaction(tx, [], { + ...this.opts, + skipPreflight: true, + }); return txSig; } public async getTransferIsolatedPerpPositionDepositIx( amount: BN, perpMarketIndex: number, - subAccountId?: number + subAccountId?: number, + noAmountBuffer?: boolean ): Promise { const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, @@ -3888,10 +4218,15 @@ export class DriftClient { readablePerpMarketIndex: [perpMarketIndex], }); + const amountWithBuffer = + noAmountBuffer || amount.eq(BigNum.fromPrint('-9223372036854775808').val) + ? amount + : amount.add(amount.div(new BN(250))); // .4% buffer + return await this.program.instruction.transferIsolatedPerpPositionDeposit( spotMarketIndex, perpMarketIndex, - amount, + amountWithBuffer, { accounts: { state: await this.getStatePublicKey(), @@ -3912,37 +4247,103 @@ export class DriftClient { subAccountId?: number, txParams?: TxParams ): Promise { + const instructions = + await this.getWithdrawFromIsolatedPerpPositionIxsBundle( + amount, + perpMarketIndex, + subAccountId, + userTokenAccount + ); const { txSig } = await this.sendTransaction( - await this.buildTransaction( - await this.getWithdrawFromIsolatedPerpPositionIx( - amount, - perpMarketIndex, - userTokenAccount, - subAccountId - ), - txParams - ) + await this.buildTransaction(instructions, txParams) ); return txSig; } - public async getWithdrawFromIsolatedPerpPositionIx( + public async getWithdrawFromIsolatedPerpPositionIxsBundle( amount: BN, perpMarketIndex: number, - userTokenAccount: PublicKey, - subAccountId?: number - ): Promise { + subAccountId?: number, + userTokenAccount?: PublicKey + ): Promise { const userAccountPublicKey = await getUserAccountPublicKey( this.program.programId, this.authority, subAccountId ?? this.activeSubAccountId ); - const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); - const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; - const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount(subAccountId)], - writableSpotMarketIndexes: [spotMarketIndex], + const userAccount = this.getUserAccount(subAccountId); + + const tokenAmountDeposited = + this.getIsolatedPerpPositionTokenAmount(perpMarketIndex); + const isolatedPositionUnrealizedPnl = calculateClaimablePnl( + this.getPerpMarketAccount(perpMarketIndex), + this.getSpotMarketAccount( + this.getPerpMarketAccount(perpMarketIndex).quoteSpotMarketIndex + ), + userAccount.perpPositions.find((p) => p.marketIndex === perpMarketIndex), + this.getOracleDataForSpotMarket( + this.getPerpMarketAccount(perpMarketIndex).quoteSpotMarketIndex + ) + ); + + const depositAmountPlusUnrealizedPnl = tokenAmountDeposited.add( + isolatedPositionUnrealizedPnl + ); + + const amountToWithdraw = amount.gt(depositAmountPlusUnrealizedPnl) + ? BigNum.fromPrint('-9223372036854775808').val // min i64 + : amount; + console.log('amountToWithdraw', amountToWithdraw.toString()); + console.log('amount', amount.toString()); + + let associatedTokenAccount = userTokenAccount; + if (!associatedTokenAccount) { + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const quoteSpotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + associatedTokenAccount = await this.getAssociatedTokenAccount( + quoteSpotMarketIndex + ); + } + + const withdrawIx = await this.getWithdrawFromIsolatedPerpPositionIx( + amountToWithdraw, + perpMarketIndex, + associatedTokenAccount, + subAccountId + ); + const ixs = [withdrawIx]; + + const needsToSettle = + amount.gt(tokenAmountDeposited) && isolatedPositionUnrealizedPnl.gt(ZERO); + if (needsToSettle) { + const settleIx = await this.settleMultiplePNLsIx( + userAccountPublicKey, + userAccount, + [perpMarketIndex], + SettlePnlMode.TRY_SETTLE + ); + ixs.push(settleIx); + } + return ixs; + } + + public async getWithdrawFromIsolatedPerpPositionIx( + amount: BN, + perpMarketIndex: number, + userTokenAccount: PublicKey, + subAccountId?: number + ): Promise { + const userAccountPublicKey = await getUserAccountPublicKey( + this.program.programId, + this.authority, + subAccountId ?? this.activeSubAccountId + ); + const perpMarketAccount = this.getPerpMarketAccount(perpMarketIndex); + const spotMarketIndex = perpMarketAccount.quoteSpotMarketIndex; + const spotMarketAccount = this.getSpotMarketAccount(spotMarketIndex); + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(subAccountId)], + writableSpotMarketIndexes: [spotMarketIndex], readablePerpMarketIndex: [perpMarketIndex], }); @@ -4109,7 +4510,6 @@ export class DriftClient { } ); } - public async getRemovePerpLpSharesIx( marketIndex: number, sharesToBurn?: BN, @@ -4265,7 +4665,9 @@ export class DriftClient { bracketOrdersParams = new Array(), referrerInfo?: ReferrerInfo, cancelExistingOrders?: boolean, - settlePnl?: boolean + settlePnl?: boolean, + positionMaxLev?: number, + isolatedPositionDepositAmount?: BN ): Promise<{ cancelExistingOrdersTx?: Transaction | VersionedTransaction; settlePnlTx?: Transaction | VersionedTransaction; @@ -4281,7 +4683,10 @@ export class DriftClient { const marketIndex = orderParams.marketIndex; const orderId = userAccount.nextOrderId; - const ixPromisesForTxs: Record> = { + const ixPromisesForTxs: Record< + TxKeys, + Promise + > = { cancelExistingOrdersTx: undefined, settlePnlTx: undefined, fillTx: undefined, @@ -4290,11 +4695,26 @@ export class DriftClient { const txKeys = Object.keys(ixPromisesForTxs); - ixPromisesForTxs.marketOrderTx = this.getPlaceOrdersIx( - [orderParams, ...bracketOrdersParams], - userAccount.subAccountId + const preIxs: TransactionInstruction[] = await this.getPrePlaceOrderIxs( + orderParams, + userAccount, + { + positionMaxLev, + isolatedPositionDepositAmount, + } ); + ixPromisesForTxs.marketOrderTx = (async () => { + const placeOrdersIx = await this.getPlaceOrdersIx( + [orderParams, ...bracketOrdersParams], + userAccount.subAccountId + ); + if (preIxs.length) { + return [...preIxs, placeOrdersIx] as unknown as TransactionInstruction; + } + return placeOrdersIx; + })(); + /* Cancel open orders in market if requested */ if (cancelExistingOrders && isVariant(orderParams.marketType, 'perp')) { ixPromisesForTxs.cancelExistingOrdersTx = this.getCancelOrdersIx( @@ -4334,7 +4754,10 @@ export class DriftClient { const ixsMap = ixs.reduce((acc, ix, i) => { acc[txKeys[i]] = ix; return acc; - }, {}) as MappedRecord; + }, {}) as MappedRecord< + typeof ixPromisesForTxs, + TransactionInstruction | TransactionInstruction[] + >; const txsMap = (await this.buildTransactionsMap( ixsMap, @@ -4408,12 +4831,32 @@ export class DriftClient { public async placePerpOrder( orderParams: OptionalOrderParams, txParams?: TxParams, - subAccountId?: number + subAccountId?: number, + isolatedPositionDepositAmount?: BN ): Promise { + const preIxs: TransactionInstruction[] = []; + if ( + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(orderParams, subAccountId) + ) { + preIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + subAccountId + ) + ); + } + const { txSig, slot } = await this.sendTransaction( await this.buildTransaction( await this.getPlacePerpOrderIx(orderParams, subAccountId), - txParams + txParams, + undefined, + undefined, + undefined, + undefined, + preIxs ), [], this.opts @@ -4601,13 +5044,31 @@ export class DriftClient { public async cancelOrder( orderId?: number, txParams?: TxParams, - subAccountId?: number + subAccountId?: number, + overrides?: { withdrawIsolatedDepositAmount?: BN } ): Promise { + const cancelIx = await this.getCancelOrderIx(orderId, subAccountId); + + const instructions: TransactionInstruction[] = [cancelIx]; + + if (overrides?.withdrawIsolatedDepositAmount !== undefined) { + const order = this.getOrder(orderId, subAccountId); + const perpMarketIndex = order?.marketIndex; + const withdrawAmount = overrides.withdrawIsolatedDepositAmount; + + if (withdrawAmount.gt(ZERO)) { + const withdrawIxs = + await this.getWithdrawFromIsolatedPerpPositionIxsBundle( + withdrawAmount, + perpMarketIndex, + subAccountId + ); + instructions.push(...withdrawIxs); + } + } + const { txSig } = await this.sendTransaction( - await this.buildTransaction( - await this.getCancelOrderIx(orderId, subAccountId), - txParams - ), + await this.buildTransaction(instructions, txParams), [], this.opts ); @@ -4689,11 +5150,19 @@ export class DriftClient { orderIds?: number[], txParams?: TxParams, subAccountId?: number, - user?: User + user?: User, + overrides?: { + authority?: PublicKey; + } ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( - await this.getCancelOrdersByIdsIx(orderIds, subAccountId, user), + await this.getCancelOrdersByIdsIx( + orderIds, + subAccountId, + user, + overrides + ), txParams ), [], @@ -4713,7 +5182,10 @@ export class DriftClient { public async getCancelOrdersByIdsIx( orderIds?: number[], subAccountId?: number, - user?: User + user?: User, + overrides?: { + authority?: PublicKey; + } ): Promise { const userAccountPubKey = user?.userAccountPublicKey ?? @@ -4726,11 +5198,13 @@ export class DriftClient { useMarketLastSlotCache: true, }); + const authority = overrides?.authority ?? this.wallet.publicKey; + return await this.program.instruction.cancelOrdersByIds(orderIds, { accounts: { state: await this.getStatePublicKey(), user: userAccountPubKey, - authority: this.wallet.publicKey, + authority, }, remainingAccounts, }); @@ -4828,7 +5302,8 @@ export class DriftClient { params: OrderParams[], txParams?: TxParams, subAccountId?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ): Promise { const { txSig } = await this.sendTransaction( ( @@ -4836,7 +5311,8 @@ export class DriftClient { params, txParams, subAccountId, - optionalIxs + optionalIxs, + isolatedPositionDepositAmount ) ).placeOrdersTx, [], @@ -4850,10 +5326,29 @@ export class DriftClient { params: OrderParams[], txParams?: TxParams, subAccountId?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ) { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); + const preIxs: TransactionInstruction[] = []; + if (params?.length === 1) { + const p = params[0]; + if ( + isVariant(p.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(p, subAccountId) + ) { + preIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + p.marketIndex, + subAccountId + ) + ); + } + } + const tx = await this.buildTransaction( await this.getPlaceOrdersIx(params, subAccountId), txParams, @@ -4861,17 +5356,19 @@ export class DriftClient { lookupTableAccounts, undefined, undefined, - optionalIxs + [...preIxs, ...(optionalIxs ?? [])] ); return { placeOrdersTx: tx, }; } - public async getPlaceOrdersIx( params: OptionalOrderParams[], - subAccountId?: number + subAccountId?: number, + overrides?: { + authority?: PublicKey; + } ): Promise { const user = await this.getUserAccountPublicKey(subAccountId); @@ -4906,18 +5403,85 @@ export class DriftClient { } const formattedParams = params.map((item) => getOrderParams(item)); + const authority = overrides?.authority ?? this.wallet.publicKey; return await this.program.instruction.placeOrders(formattedParams, { accounts: { state: await this.getStatePublicKey(), user, userStats: this.getUserStatsAccountPublicKey(), - authority: this.wallet.publicKey, + authority, }, remainingAccounts, }); } + public async getPlaceOrdersAndSetPositionMaxLevIx( + params: OptionalOrderParams[], + positionMaxLev: number, + subAccountId?: number + ): Promise { + const user = await this.getUserAccountPublicKey(subAccountId); + + const readablePerpMarketIndex: number[] = []; + const readableSpotMarketIndexes: number[] = []; + for (const param of params) { + if (!param.marketType) { + throw new Error('must set param.marketType'); + } + if (isVariant(param.marketType, 'perp')) { + readablePerpMarketIndex.push(param.marketIndex); + } else { + readableSpotMarketIndexes.push(param.marketIndex); + } + } + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [this.getUserAccount(subAccountId)], + readablePerpMarketIndex, + readableSpotMarketIndexes, + useMarketLastSlotCache: true, + }); + + for (const param of params) { + if (isUpdateHighLeverageMode(param.bitFlags)) { + remainingAccounts.push({ + pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), + isWritable: true, + isSigner: false, + }); + } + } + + const formattedParams = params.map((item) => getOrderParams(item)); + + const placeOrdersIxs = await this.program.instruction.placeOrders( + formattedParams, + { + accounts: { + state: await this.getStatePublicKey(), + user, + userStats: this.getUserStatsAccountPublicKey(), + authority: this.wallet.publicKey, + }, + remainingAccounts, + } + ); + + const marginRatio = Math.floor( + (1 / positionMaxLev) * MARGIN_PRECISION.toNumber() + ); + // Keep existing behavior but note: prefer using getPostPlaceOrderIxs path + const setPositionMaxLevIxs = + await this.getUpdateUserPerpPositionCustomMarginRatioIx( + readablePerpMarketIndex[0], + marginRatio, + subAccountId + ); + + return [placeOrdersIxs, setPositionMaxLevIxs]; + } + public async fillPerpOrder( userAccountPublicKey: PublicKey, user: UserAccount, @@ -4926,7 +5490,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, txParams?: TxParams, fillerSubAccountId?: number, - fillerAuthority?: PublicKey + fillerAuthority?: PublicKey, + hasBuilderFee?: boolean ): Promise { const { txSig } = await this.sendTransaction( await this.buildTransaction( @@ -4938,7 +5503,8 @@ export class DriftClient { referrerInfo, fillerSubAccountId, undefined, - fillerAuthority + fillerAuthority, + hasBuilderFee ), txParams ), @@ -4956,7 +5522,8 @@ export class DriftClient { referrerInfo?: ReferrerInfo, fillerSubAccountId?: number, isSignedMsg?: boolean, - fillerAuthority?: PublicKey + fillerAuthority?: PublicKey, + hasBuilderFee?: boolean ): Promise { const userStatsPublicKey = getUserStatsAccountPublicKey( this.program.programId, @@ -5038,6 +5605,36 @@ export class DriftClient { } } + let withBuilder = false; + if (hasBuilderFee) { + withBuilder = true; + } else { + // figure out if we need builder account or not + if (order && !isSignedMsg) { + const userOrder = userAccount.orders.find( + (o) => o.orderId === order.orderId + ); + if (userOrder) { + withBuilder = hasBuilder(userOrder); + } + } else if (isSignedMsg) { + // Order hasn't been placed yet, we cant tell if it has a builder or not. + // Include it optimistically + withBuilder = true; + } + } + + if (withBuilder) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + userAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + const orderId = isSignedMsg ? null : order.orderId; return await this.program.instruction.fillPerpOrder(orderId, null, { accounts: { @@ -5529,22 +6126,24 @@ export class DriftClient { } /** - * Swap tokens in drift account using jupiter - * @param jupiterClient jupiter client to find routes and jupiter instructions + * Swap tokens in drift account using titan or jupiter + * @param swapClient swap client to find routes and instructions (Titan or Jupiter) + * @param jupiterClient @deprecated Use swapClient instead. Legacy parameter for backward compatibility * @param outMarketIndex the market index of the token you're buying * @param inMarketIndex the market index of the token you're selling - * @param outAssociatedTokenAccount the token account to receive the token being sold on jupiter + * @param outAssociatedTokenAccount the token account to receive the token being sold on titan or jupiter * @param inAssociatedTokenAccount the token account to * @param amount the amount of TokenIn, regardless of swapMode - * @param slippageBps the max slippage passed to jupiter api - * @param swapMode jupiter swapMode (ExactIn or ExactOut), default is ExactIn - * @param route the jupiter route to use for the swap + * @param slippageBps the max slippage passed to titan or jupiter api + * @param swapMode titan or jupiter swapMode (ExactIn or ExactOut), default is ExactIn + * @param route the titan or jupiter route to use for the swap * @param reduceOnly specify if In or Out token on the drift account must reduceOnly, checked at end of swap * @param v6 pass in the quote response from Jupiter quote's API (deprecated, use quote instead) * @param quote pass in the quote response from Jupiter quote's API * @param txParams */ public async swap({ + swapClient, jupiterClient, outMarketIndex, inMarketIndex, @@ -5559,7 +6158,9 @@ export class DriftClient { quote, onlyDirectRoutes = false, }: { - jupiterClient: JupiterClient; + swapClient?: UnifiedSwapClient | SwapClient; + /** @deprecated Use swapClient instead. Legacy parameter for backward compatibility */ + jupiterClient?: JupiterClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; @@ -5575,21 +6176,68 @@ export class DriftClient { }; quote?: QuoteResponse; }): Promise { - const quoteToUse = quote ?? v6?.quote; + // Handle backward compatibility: use jupiterClient if swapClient is not provided + const clientToUse = swapClient || jupiterClient; + + if (!clientToUse) { + throw new Error('Either swapClient or jupiterClient must be provided'); + } + + let res: { + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }; + + // Use unified SwapClient if available + if (clientToUse instanceof UnifiedSwapClient) { + res = await this.getSwapIxV2({ + swapClient: clientToUse, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + quote, + v6, + }); + } else if (clientToUse instanceof TitanClient) { + res = await this.getTitanSwapIx({ + titanClient: clientToUse, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + }); + } else if (clientToUse instanceof JupiterClient) { + const quoteToUse = quote ?? v6?.quote; + res = await this.getJupiterSwapIxV6({ + jupiterClient: clientToUse, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + quote: quoteToUse, + reduceOnly, + onlyDirectRoutes, + }); + } else { + throw new Error( + 'Invalid swap client type. Must be SwapClient, TitanClient, or JupiterClient.' + ); + } - const res = await this.getJupiterSwapIxV6({ - jupiterClient, - outMarketIndex, - inMarketIndex, - outAssociatedTokenAccount, - inAssociatedTokenAccount, - amount, - slippageBps, - swapMode, - quote: quoteToUse, - reduceOnly, - onlyDirectRoutes, - }); const ixs = res.ixs; const lookupTables = res.lookupTables; @@ -5607,8 +6255,8 @@ export class DriftClient { return txSig; } - public async getJupiterSwapIxV6({ - jupiterClient, + public async getTitanSwapIx({ + titanClient, outMarketIndex, inMarketIndex, outAssociatedTokenAccount, @@ -5617,20 +6265,18 @@ export class DriftClient { slippageBps, swapMode, onlyDirectRoutes, - quote, reduceOnly, userAccountPublicKey, }: { - jupiterClient: JupiterClient; + titanClient: TitanClient; outMarketIndex: number; inMarketIndex: number; outAssociatedTokenAccount?: PublicKey; inAssociatedTokenAccount?: PublicKey; amount: BN; slippageBps?: number; - swapMode?: SwapMode; + swapMode?: string; onlyDirectRoutes?: boolean; - quote?: QuoteResponse; reduceOnly?: SwapReduceOnly; userAccountPublicKey?: PublicKey; }): Promise<{ @@ -5640,20 +6286,142 @@ export class DriftClient { const outMarket = this.getSpotMarketAccount(outMarketIndex); const inMarket = this.getSpotMarketAccount(inMarketIndex); - if (!quote) { - const fetchedQuote = await jupiterClient.getQuote({ - inputMint: inMarket.mint, - outputMint: outMarket.mint, - amount, - slippageBps, - swapMode, - onlyDirectRoutes, - }); + const isExactOut = swapMode === 'ExactOut'; + const exactOutBufferedAmountIn = amount.muln(1001).divn(1000); // Add 10bp buffer - quote = fetchedQuote; + const preInstructions = []; + if (!outAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + outAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + outAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + outAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } } - if (!quote) { + if (!inAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(inMarket); + inAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + inAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + inAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + } + + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amount, + inTokenAccount: inAssociatedTokenAccount, + outTokenAccount: outAssociatedTokenAccount, + reduceOnly, + userAccountPublicKey, + }); + + const { transactionMessage, lookupTables } = await titanClient.getSwap({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + swapMode: isExactOut ? TitanSwapMode.ExactOut : TitanSwapMode.ExactIn, + onlyDirectRoutes, + sizeConstraint: MAX_TX_BYTE_SIZE - 375, // buffer for drift instructions + }); + + const titanInstructions = titanClient.getTitanInstructions({ + transactionMessage, + inputMint: inMarket.mint, + outputMint: outMarket.mint, + }); + + const ixs = [ + ...preInstructions, + beginSwapIx, + ...titanInstructions, + endSwapIx, + ]; + + return { ixs, lookupTables }; + } + + public async getJupiterSwapIxV6({ + jupiterClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + quote, + reduceOnly, + userAccountPublicKey, + }: { + jupiterClient: JupiterClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + quote?: QuoteResponse; + reduceOnly?: SwapReduceOnly; + userAccountPublicKey?: PublicKey; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + if (!quote) { + const fetchedQuote = await jupiterClient.getQuote({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + }); + + quote = fetchedQuote; + } + + if (!quote) { throw new Error("Could not fetch Jupiter's quote. Please try again."); } @@ -5886,6 +6654,134 @@ export class DriftClient { return { beginSwapIx, endSwapIx }; } + public async getSwapIxV2({ + swapClient, + outMarketIndex, + inMarketIndex, + outAssociatedTokenAccount, + inAssociatedTokenAccount, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + reduceOnly, + quote, + v6, + }: { + swapClient: UnifiedSwapClient; + outMarketIndex: number; + inMarketIndex: number; + outAssociatedTokenAccount?: PublicKey; + inAssociatedTokenAccount?: PublicKey; + amount: BN; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + reduceOnly?: SwapReduceOnly; + quote?: QuoteResponse; + v6?: { + quote?: QuoteResponse; + }; + }): Promise<{ + ixs: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + // Get market accounts to determine mints + const outMarket = this.getSpotMarketAccount(outMarketIndex); + const inMarket = this.getSpotMarketAccount(inMarketIndex); + + const isExactOut = swapMode === 'ExactOut'; + const exactOutBufferedAmountIn = amount.muln(1001).divn(1000); // Add 10bp buffer + + const preInstructions: TransactionInstruction[] = []; + + // Handle token accounts if not provided + let finalOutAssociatedTokenAccount = outAssociatedTokenAccount; + let finalInAssociatedTokenAccount = inAssociatedTokenAccount; + + if (!finalOutAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(outMarket); + finalOutAssociatedTokenAccount = await this.getAssociatedTokenAccount( + outMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + finalOutAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + finalOutAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + outMarket.mint, + tokenProgram + ) + ); + } + } + + if (!finalInAssociatedTokenAccount) { + const tokenProgram = this.getTokenProgramForSpotMarket(inMarket); + finalInAssociatedTokenAccount = await this.getAssociatedTokenAccount( + inMarket.marketIndex, + false, + tokenProgram + ); + + const accountInfo = await this.connection.getAccountInfo( + finalInAssociatedTokenAccount + ); + if (!accountInfo) { + preInstructions.push( + this.createAssociatedTokenAccountIdempotentInstruction( + finalInAssociatedTokenAccount, + this.provider.wallet.publicKey, + this.provider.wallet.publicKey, + inMarket.mint, + tokenProgram + ) + ); + } + } + + // Get drift swap instructions for begin and end + const { beginSwapIx, endSwapIx } = await this.getSwapIx({ + outMarketIndex, + inMarketIndex, + amountIn: isExactOut ? exactOutBufferedAmountIn : amount, + inTokenAccount: finalInAssociatedTokenAccount, + outTokenAccount: finalOutAssociatedTokenAccount, + reduceOnly, + }); + + // Get core swap instructions from SwapClient + const swapResult = await swapClient.getSwapInstructions({ + inputMint: inMarket.mint, + outputMint: outMarket.mint, + amount, + userPublicKey: this.provider.wallet.publicKey, + slippageBps, + swapMode, + onlyDirectRoutes, + quote: quote ?? v6?.quote, + }); + + const allInstructions = [ + ...preInstructions, + beginSwapIx, + ...swapResult.instructions, + endSwapIx, + ]; + + return { + ixs: allInstructions, + lookupTables: swapResult.lookupTables, + }; + } + public async stakeForMSOL({ amount }: { amount: BN }): Promise { const ixs = await this.getStakeForMSOLIx({ amount }); const tx = await this.buildTransaction(ixs); @@ -6276,7 +7172,6 @@ export class DriftClient { this.perpMarketLastSlotCache.set(orderParams.marketIndex, slot); return txSig; } - public async preparePlaceAndTakePerpOrderWithAdditionalOrders( orderParams: OptionalOrderParams, makerInfo?: MakerInfo | MakerInfo[], @@ -6288,7 +7183,8 @@ export class DriftClient { settlePnl?: boolean, exitEarlyIfSimFails?: boolean, auctionDurationPercentage?: number, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + isolatedPositionDepositAmount?: BN ): Promise<{ placeAndTakeTx: Transaction | VersionedTransaction; cancelExistingOrdersTx: Transaction | VersionedTransaction; @@ -6322,6 +7218,20 @@ export class DriftClient { subAccountId ); + if ( + isVariant(orderParams.marketType, 'perp') && + isolatedPositionDepositAmount?.gt?.(ZERO) && + this.isOrderIncreasingPosition(orderParams, subAccountId) + ) { + placeAndTakeIxs.push( + await this.getTransferIsolatedPerpPositionDepositIx( + isolatedPositionDepositAmount as BN, + orderParams.marketIndex, + subAccountId + ) + ); + } + placeAndTakeIxs.push(placeAndTakeIx); if (bracketOrdersParams.length > 0) { @@ -6332,6 +7242,11 @@ export class DriftClient { placeAndTakeIxs.push(bracketOrdersIx); } + // Optional extra ixs can be appended at the front + if (optionalIxs?.length) { + placeAndTakeIxs.unshift(...optionalIxs); + } + const shouldUseSimulationComputeUnits = txParams?.useSimulatedComputeUnits; const shouldExitIfSimulationFails = exitEarlyIfSimFails; @@ -6516,7 +7431,10 @@ export class DriftClient { referrerInfo?: ReferrerInfo, successCondition?: PlaceAndTakeOrderSuccessCondition, auctionDurationPercentage?: number, - subAccountId?: number + subAccountId?: number, + overrides?: { + authority?: PublicKey; + } ): Promise { orderParams = getOrderParams(orderParams, { marketType: MarketType.PERP }); const userStatsPublicKey = await this.getUserStatsAccountPublicKey(); @@ -6584,6 +7502,8 @@ export class DriftClient { ((auctionDurationPercentage ?? 100) << 8) | (successCondition ?? 0); } + const authority = overrides?.authority ?? this.wallet.publicKey; + return await this.program.instruction.placeAndTakePerpOrder( orderParams, optionalParams, @@ -6592,7 +7512,7 @@ export class DriftClient { state: await this.getStatePublicKey(), user, userStats: userStatsPublicKey, - authority: this.wallet.publicKey, + authority, }, remainingAccounts, } @@ -6658,6 +7578,16 @@ export class DriftClient { } const takerOrderId = takerInfo.order.orderId; + if (hasBuilder(takerInfo.order)) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } return await this.program.instruction.placeAndMakePerpOrder( orderParams, takerOrderId, @@ -6738,21 +7668,37 @@ export class DriftClient { if (orderParamsMessage.maxMarginRatio === undefined) { orderParamsMessage.maxMarginRatio = null; } + if (orderParamsMessage.isolatedPositionDeposit === undefined) { + orderParamsMessage.isolatedPositionDeposit = null; + } const anchorIxName = delegateSigner ? 'global' + ':' + 'SignedMsgOrderParamsDelegateMessage' : 'global' + ':' + 'SignedMsgOrderParamsMessage'; const prefix = Buffer.from(sha256(anchorIxName).slice(0, 8)); + + // Backwards-compat: normalize optional builder fields to null for encoding + const withBuilderDefaults = { + ...orderParamsMessage, + builderIdx: + orderParamsMessage.builderIdx !== undefined + ? orderParamsMessage.builderIdx + : null, + builderFeeTenthBps: + orderParamsMessage.builderFeeTenthBps !== undefined + ? orderParamsMessage.builderFeeTenthBps + : null, + }; const buf = Buffer.concat([ prefix, delegateSigner ? this.program.coder.types.encode( 'SignedMsgOrderParamsDelegateMessage', - orderParamsMessage as SignedMsgOrderParamsDelegateMessage + withBuilderDefaults as SignedMsgOrderParamsDelegateMessage ) : this.program.coder.types.encode( 'SignedMsgOrderParamsMessage', - orderParamsMessage as SignedMsgOrderParamsMessage + withBuilderDefaults as SignedMsgOrderParamsMessage ), ]); return buf; @@ -6827,12 +7773,6 @@ export class DriftClient { precedingIxs: TransactionInstruction[] = [], overrideCustomIxIndex?: number ): Promise { - const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [takerInfo.takerUserAccount], - useMarketLastSlotCache: false, - readablePerpMarketIndex: marketIndex, - }); - const isDelegateSigner = takerInfo.signingAuthority.equals( takerInfo.takerUserAccount.delegate ); @@ -6841,34 +7781,58 @@ export class DriftClient { signedSignedMsgOrderParams.orderParams.toString(), 'hex' ); - try { - const { signedMsgOrderParams } = this.decodeSignedMsgOrderParamsMessage( - borshBuf, - isDelegateSigner - ); - if (isUpdateHighLeverageMode(signedMsgOrderParams.bitFlags)) { - remainingAccounts.push({ - pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), - isWritable: true, - isSigner: false, - }); - } - } catch (err) { - console.error('invalid signed order encoding'); - } - const messageLengthBuffer = Buffer.alloc(2); - messageLengthBuffer.writeUInt16LE( - signedSignedMsgOrderParams.orderParams.length + const signedMessage = this.decodeSignedMsgOrderParamsMessage( + borshBuf, + isDelegateSigner ); - const signedMsgIxData = Buffer.concat([ - signedSignedMsgOrderParams.signature, - takerInfo.signingAuthority.toBytes(), - messageLengthBuffer, - signedSignedMsgOrderParams.orderParams, - ]); - + const writableSpotMarketIndexes = signedMessage.isolatedPositionDeposit?.gt( + ZERO + ) + ? [QUOTE_SPOT_MARKET_INDEX] + : undefined; + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [takerInfo.takerUserAccount], + useMarketLastSlotCache: false, + readablePerpMarketIndex: marketIndex, + writableSpotMarketIndexes, + }); + + if (isUpdateHighLeverageMode(signedMessage.signedMsgOrderParams.bitFlags)) { + remainingAccounts.push({ + pubkey: getHighLeverageModeConfigPublicKey(this.program.programId), + isWritable: true, + isSigner: false, + }); + } + if ( + signedMessage.builderFeeTenthBps !== null && + signedMessage.builderIdx !== null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + + const messageLengthBuffer = Buffer.alloc(2); + messageLengthBuffer.writeUInt16LE( + signedSignedMsgOrderParams.orderParams.length + ); + + const signedMsgIxData = Buffer.concat([ + signedSignedMsgOrderParams.signature, + takerInfo.signingAuthority.toBytes(), + messageLengthBuffer, + signedSignedMsgOrderParams.orderParams, + ]); + const signedMsgOrderParamsSignatureIx = createMinimalEd25519VerifyIx( overrideCustomIxIndex || precedingIxs.length + 1, 12, @@ -6985,6 +7949,32 @@ export class DriftClient { }); } + const isDelegateSigner = takerInfo.signingAuthority.equals( + takerInfo.takerUserAccount.delegate + ); + const borshBuf = Buffer.from( + signedSignedMsgOrderParams.orderParams.toString(), + 'hex' + ); + + const signedMessage = this.decodeSignedMsgOrderParamsMessage( + borshBuf, + isDelegateSigner + ); + if ( + signedMessage.builderFeeTenthBps !== null && + signedMessage.builderIdx !== null + ) { + remainingAccounts.push({ + pubkey: getRevenueShareEscrowAccountPublicKey( + this.program.programId, + takerInfo.takerUserAccount.authority + ), + isWritable: true, + isSigner: false, + }); + } + const placeAndMakeIx = await this.program.instruction.placeAndMakeSignedMsgPerpOrder( orderParams, @@ -7064,7 +8054,6 @@ export class DriftClient { this.spotMarketLastSlotCache.set(QUOTE_SPOT_MARKET_INDEX, slot); return txSig; } - public async getPlaceAndTakeSpotOrderIx( orderParams: OptionalOrderParams, fulfillmentConfig?: SerumV3FulfillmentConfigAccount, @@ -7396,13 +8385,19 @@ export class DriftClient { policy?: number; }, subAccountId?: number, - userPublicKey?: PublicKey + overrides?: { + user?: User; + authority?: PublicKey; + } ): Promise { - const user = - userPublicKey ?? (await this.getUserAccountPublicKey(subAccountId)); + const userPubKey = + overrides?.user?.getUserAccountPublicKey() ?? + (await this.getUserAccountPublicKey(subAccountId)); + const userAccount = + overrides?.user?.getUserAccount() ?? this.getUserAccount(subAccountId); const remainingAccounts = this.getRemainingAccounts({ - userAccounts: [this.getUserAccount(subAccountId)], + userAccounts: [userAccount], useMarketLastSlotCache: true, }); @@ -7423,12 +8418,16 @@ export class DriftClient { maxTs: maxTs || null, }; + const authority = + overrides?.authority ?? + overrides?.user?.getUserAccount().authority ?? + this.wallet.publicKey; return await this.program.instruction.modifyOrder(orderId, orderParams, { accounts: { state: await this.getStatePublicKey(), - user, + user: userPubKey, userStats: this.getUserStatsAccountPublicKey(), - authority: this.wallet.publicKey, + authority, }, remainingAccounts, }); @@ -7517,7 +8516,6 @@ export class DriftClient { bitFlags?: number; policy?: ModifyOrderPolicy; maxTs?: BN; - txParams?: TxParams; }, subAccountId?: number ): Promise { @@ -7618,7 +8616,8 @@ export class DriftClient { settleeUserAccountPublicKey: PublicKey; settleeUserAccount: UserAccount; }[], - marketIndexes: number[] + marketIndexes: number[], + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise> { const ixs = []; for (const { settleeUserAccountPublicKey, settleeUserAccount } of users) { @@ -7627,7 +8626,8 @@ export class DriftClient { await this.settlePNLIx( settleeUserAccountPublicKey, settleeUserAccount, - marketIndex + marketIndex, + revenueShareEscrowMap ) ); } @@ -7641,7 +8641,8 @@ export class DriftClient { settleeUserAccount: UserAccount, marketIndex: number, txParams?: TxParams, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { const lookupTableAccounts = await this.fetchAllLookupTableAccounts(); @@ -7650,7 +8651,8 @@ export class DriftClient { await this.settlePNLIx( settleeUserAccountPublicKey, settleeUserAccount, - marketIndex + marketIndex, + revenueShareEscrowMap ), txParams, undefined, @@ -7668,7 +8670,8 @@ export class DriftClient { public async settlePNLIx( settleeUserAccountPublicKey: PublicKey, settleeUserAccount: UserAccount, - marketIndex: number + marketIndex: number, + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], @@ -7676,6 +8679,89 @@ export class DriftClient { writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (revenueShareEscrowMap) { + const escrow = revenueShareEscrowMap.get( + settleeUserAccount.authority.toBase58() + ); + if (escrow) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + + const builders = new Map(); + for (const order of escrow.orders) { + const eligibleBuilder = + isBuilderOrderCompleted(order) && + !isBuilderOrderReferral(order) && + order.feesAccrued.gt(ZERO) && + order.marketIndex === marketIndex; + if (eligibleBuilder && !builders.has(order.builderIdx)) { + builders.set( + order.builderIdx, + escrow.approvedBuilders[order.builderIdx].authority + ); + } + } + if (builders.size > 0) { + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + this.addBuilderToRemainingAccounts( + Array.from(builders.values()), + remainingAccounts + ); + } + + // Include escrow and referrer accounts if referral rewards exist for this market + const hasReferralForMarket = escrow.orders.some( + (o) => + isBuilderOrderReferral(o) && + o.feesAccrued.gt(ZERO) && + o.marketIndex === marketIndex + ); + + if (hasReferralForMarket) { + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + if (!escrow.referrer.equals(PublicKey.default)) { + this.addBuilderToRemainingAccounts( + [escrow.referrer], + remainingAccounts + ); + } + } + } else { + // Stale-cache fallback: if the user has any builder orders, include escrow PDA. This allows + // the program to lazily clean up any completed builder orders. + for (const order of settleeUserAccount.orders) { + if (hasBuilder(order)) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + break; + } + } + } + } + return await this.program.instruction.settlePnl(marketIndex, { accounts: { state: await this.getStatePublicKey(), @@ -7692,6 +8778,7 @@ export class DriftClient { settleeUserAccount: UserAccount, marketIndexes: number[], mode: SettlePnlMode, + revenueShareEscrowMap?: RevenueShareEscrowMap, txParams?: TxParams ): Promise { const { txSig } = await this.sendTransaction( @@ -7700,7 +8787,9 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndexes, - mode + mode, + undefined, + revenueShareEscrowMap ), txParams ), @@ -7716,7 +8805,8 @@ export class DriftClient { marketIndexes: number[], mode: SettlePnlMode, txParams?: TxParams, - optionalIxs?: TransactionInstruction[] + optionalIxs?: TransactionInstruction[], + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { // need multiple TXs because settling more than 4 markets won't fit in a single TX const txsToSign: (Transaction | VersionedTransaction)[] = []; @@ -7730,7 +8820,9 @@ export class DriftClient { settleeUserAccountPublicKey, settleeUserAccount, marketIndexes, - mode + mode, + undefined, + revenueShareEscrowMap ); const computeUnits = Math.min(300_000 * marketIndexes.length, 1_400_000); const tx = await this.buildTransaction( @@ -7775,7 +8867,8 @@ export class DriftClient { mode: SettlePnlMode, overrides?: { authority?: PublicKey; - } + }, + revenueShareEscrowMap?: RevenueShareEscrowMap ): Promise { const remainingAccounts = this.getRemainingAccounts({ userAccounts: [settleeUserAccount], @@ -7783,6 +8876,95 @@ export class DriftClient { writableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); + if (revenueShareEscrowMap) { + const escrow = revenueShareEscrowMap.get( + settleeUserAccount.authority.toBase58() + ); + const builders = new Map(); + if (escrow) { + for (const order of escrow.orders) { + const eligibleBuilder = + isBuilderOrderCompleted(order) && + !isBuilderOrderReferral(order) && + order.feesAccrued.gt(ZERO) && + marketIndexes.includes(order.marketIndex); + if (eligibleBuilder && !builders.has(order.builderIdx)) { + builders.set( + order.builderIdx, + escrow.approvedBuilders[order.builderIdx].authority + ); + } + } + if (builders.size > 0) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + this.addBuilderToRemainingAccounts( + Array.from(builders.values()), + remainingAccounts + ); + } + + // Include escrow and referrer accounts when there are referral rewards + // for any of the markets we are settling, so on-chain sweep can find them. + const hasReferralForRequestedMarkets = escrow.orders.some( + (o) => + isBuilderOrderReferral(o) && + o.feesAccrued.gt(ZERO) && + marketIndexes.includes(o.marketIndex) + ); + + if (hasReferralForRequestedMarkets) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + + // Add referrer's User and RevenueShare accounts + if (!escrow.referrer.equals(PublicKey.default)) { + this.addBuilderToRemainingAccounts( + [escrow.referrer], + remainingAccounts + ); + } + } + } else { + // Stale-cache fallback: if the user has any builder orders, include escrow PDA. This allows + // the program to lazily clean up any completed builder orders. + for (const order of settleeUserAccount.orders) { + if (hasBuilder(order)) { + const escrowPk = getRevenueShareEscrowAccountPublicKey( + this.program.programId, + settleeUserAccount.authority + ); + if (!remainingAccounts.find((a) => a.pubkey.equals(escrowPk))) { + remainingAccounts.push({ + pubkey: escrowPk, + isSigner: false, + isWritable: true, + }); + } + break; + } + } + } + } + return await this.program.instruction.settleMultiplePnls( marketIndexes, mode, @@ -7859,7 +9041,6 @@ export class DriftClient { this.perpMarketLastSlotCache.set(marketIndex, slot); return txSig; } - public async getLiquidatePerpIx( userAccountPublicKey: PublicKey, userAccount: UserAccount, @@ -8650,7 +9831,6 @@ export class DriftClient { } ); } - public async resolveSpotBankruptcy( userAccountPublicKey: PublicKey, userAccount: UserAccount, @@ -8926,12 +10106,21 @@ export class DriftClient { isExchangeOracleMoreRecent = false; } + const conf = getOracleConfidenceFromMMOracleData( + perpMarket.amm.mmOraclePrice, + oracleData + ); + if ( - !isOracleValid( - perpMarket, - oracleData, - stateAccountAndSlot.data.oracleGuardRails, - stateAccountAndSlot.slot + isOracleTooDivergent( + perpMarket.amm, + { + price: perpMarket.amm.mmOraclePrice, + slot: perpMarket.amm.mmOracleSlot, + confidence: conf, + hasSufficientNumberOfDataPoints: true, + }, + stateAccountAndSlot.data.oracleGuardRails ) || perpMarket.amm.mmOraclePrice.eq(ZERO) || isExchangeOracleMoreRecent || @@ -8939,10 +10128,6 @@ export class DriftClient { ) { return { ...oracleData, isMMOracleActive }; } else { - const conf = getOracleConfidenceFromMMOracleData( - perpMarket.amm.mmOraclePrice, - oracleData - ); return { price: perpMarket.amm.mmOraclePrice, slot: perpMarket.amm.mmOracleSlot, @@ -9482,7 +10667,6 @@ export class DriftClient { const { txSig } = await this.sendTransaction(tx, [], this.opts); return txSig; } - public async getSettleRevenueToInsuranceFundIx( spotMarketIndex: number ): Promise { @@ -10277,7 +11461,6 @@ export class DriftClient { ); return config as ProtectedMakerModeConfig; } - public async updateUserProtectedMakerOrders( subAccountId: number, protectedOrders: boolean, @@ -10461,106 +11644,1176 @@ export class DriftClient { }); } - private handleSignedTransaction(signedTxs: SignedTxData[]) { - if (this.enableMetricsEvents && this.metricsEventEmitter) { - this.metricsEventEmitter.emit('txSigned', signedTxs); - } + public async getLpPoolAccount(lpPoolId: number): Promise { + return (await this.program.account.lpPool.fetch( + getLpPoolPublicKey(this.program.programId, lpPoolId) + )) as LPPoolAccount; } - private handlePreSignedTransaction() { - if (this.enableMetricsEvents && this.metricsEventEmitter) { - this.metricsEventEmitter.emit('preTxSigned'); - } + public async getConstituentTargetBaseAccount( + lpPoolId: number + ): Promise { + return (await this.program.account.constituentTargetBase.fetch( + getConstituentTargetBasePublicKey( + this.program.programId, + getLpPoolPublicKey(this.program.programId, lpPoolId) + ) + )) as ConstituentTargetBaseAccount; } - private isVersionedTransaction( - tx: Transaction | VersionedTransaction - ): boolean { - return isVersionedTransaction(tx); + public async getAmmCache(): Promise { + return (await this.program.account.ammCache.fetch( + getAmmCachePublicKey(this.program.programId) + )) as AmmCache; } - /** - * Send a transaction. - * - * @param tx - * @param additionalSigners - * @param opts :: Will fallback to DriftClient's opts if not provided - * @param preSigned - * @returns - */ - sendTransaction( - tx: Transaction | VersionedTransaction, - additionalSigners?: Array, - opts?: ConfirmOptions, - preSigned?: boolean - ): Promise { - const isVersionedTx = this.isVersionedTransaction(tx); - if (isVersionedTx) { - return this.txSender.sendVersionedTransaction( - tx as VersionedTransaction, - additionalSigners, - opts ?? this.opts, - preSigned - ); - } else { - return this.txSender.send( - tx as Transaction, - additionalSigners, - opts ?? this.opts, - preSigned - ); - } + public async updateLpConstituentTargetBase( + lpPoolId: number, + constituents: PublicKey[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateLpConstituentTargetBaseIx(lpPoolId, constituents), + txParams + ), + [], + this.opts + ); + return txSig; } - async buildTransaction( - instructions: TransactionInstruction | TransactionInstruction[], - txParams?: TxParams, - txVersion?: TransactionVersion, - lookupTables?: AddressLookupTableAccount[], - forceVersionedTransaction?: boolean, - recentBlockhash?: BlockhashWithExpiryBlockHeight, - optionalIxs?: TransactionInstruction[] - ): Promise { - return this.txHandler.buildTransaction({ - instructions, - txVersion: txVersion ?? this.txVersion, - txParams: txParams ?? this.txParams, - connection: this.connection, - preFlightCommitment: this.opts.preflightCommitment, - fetchAllMarketLookupTableAccounts: - this.fetchAllLookupTableAccounts.bind(this), - lookupTables, - forceVersionedTransaction, - recentBlockhash, - optionalIxs, + public async getUpdateLpConstituentTargetBaseIx( + lpPoolId: number, + constituents: PublicKey[] + ): Promise { + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + const ammConstituentMappingPublicKey = getAmmConstituentMappingPublicKey( + this.program.programId, + lpPool + ); + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + const ammCache = getAmmCachePublicKey(this.program.programId); + + const remainingAccounts = constituents.map((constituent) => { + return { + isWritable: false, + isSigner: false, + pubkey: constituent, + }; }); - } - async buildBulkTransactions( - instructions: (TransactionInstruction | TransactionInstruction[])[], - txParams?: TxParams, - txVersion?: TransactionVersion, - lookupTables?: AddressLookupTableAccount[], - forceVersionedTransaction?: boolean - ): Promise<(Transaction | VersionedTransaction)[]> { - return this.txHandler.buildBulkTransactions({ - instructions, - txVersion: txVersion ?? this.txVersion, - txParams: txParams ?? this.txParams, - connection: this.connection, - preFlightCommitment: this.opts.preflightCommitment, - fetchAllMarketLookupTableAccounts: - this.fetchAllLookupTableAccounts.bind(this), - lookupTables, - forceVersionedTransaction, + return this.program.instruction.updateLpConstituentTargetBase({ + accounts: { + keeper: this.wallet.publicKey, + lpPool, + ammConstituentMapping: ammConstituentMappingPublicKey, + constituentTargetBase, + state: await this.getStatePublicKey(), + ammCache, + }, + remainingAccounts, }); } - async buildTransactionsMap( - instructionsMap: Record< - string, - TransactionInstruction | TransactionInstruction[] - >, + public async updateLpPoolAum( + lpPool: LPPoolAccount, + spotMarketIndexOfConstituents: number[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateLpPoolAumIxs(lpPool, spotMarketIndexOfConstituents), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateLpPoolAumIxs( + lpPool: LPPoolAccount, + spotMarketIndexOfConstituents: number[] + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: spotMarketIndexOfConstituents, + }); + remainingAccounts.push( + ...spotMarketIndexOfConstituents.map((index) => { + return { + pubkey: getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + index + ), + isSigner: false, + isWritable: true, + }; + }) + ); + return this.program.instruction.updateLpPoolAum({ + accounts: { + keeper: this.wallet.publicKey, + lpPool: lpPool.pubkey, + state: await this.getStatePublicKey(), + constituentTargetBase: getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ), + ammCache: getAmmCachePublicKey(this.program.programId), + }, + remainingAccounts, + }); + } + + public async updateAmmCache( + perpMarketIndexes: number[], + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateAmmCacheIx(perpMarketIndexes), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateAmmCacheIx( + perpMarketIndexes: number[] + ): Promise { + if (perpMarketIndexes.length > 50) { + throw new Error('Cant update more than 50 markets at once'); + } + + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readablePerpMarketIndex: perpMarketIndexes, + }); + + return this.program.instruction.updateAmmCache({ + accounts: { + state: await this.getStatePublicKey(), + keeper: this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + quoteMarket: this.getSpotMarketAccount(0).pubkey, + }, + remainingAccounts, + }); + } + + public async updateConstituentOracleInfo( + constituent: ConstituentAccount + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getUpdateConstituentOracleInfoIx(constituent), + undefined + ), + [], + this.opts + ); + return txSig; + } + + public async getUpdateConstituentOracleInfoIx( + constituent: ConstituentAccount + ): Promise { + const spotMarket = this.getSpotMarketAccount(constituent.spotMarketIndex); + return this.program.instruction.updateConstituentOracleInfo({ + accounts: { + keeper: this.wallet.publicKey, + constituent: constituent.pubkey, + state: await this.getStatePublicKey(), + oracle: spotMarket.oracle, + spotMarket: spotMarket.pubkey, + }, + }); + } + + public async lpPoolSwap( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + lpPool: PublicKey, + userAuthority: PublicKey, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolSwapIx( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + lpPool, + userAuthority + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolSwapIx( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + lpPool: PublicKey, + userAuthority: PublicKey + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex, outMarketIndex], + }); + + const constituentInTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + const constituentOutTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const userInTokenAccount = await getAssociatedTokenAddress( + this.getSpotMarketAccount(inMarketIndex).mint, + userAuthority + ); + const userOutTokenAccount = await getAssociatedTokenAddress( + this.getSpotMarketAccount(outMarketIndex).mint, + userAuthority + ); + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + inMarketIndex + ); + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool, + outMarketIndex + ); + const inMarketMint = this.getSpotMarketAccount(inMarketIndex).mint; + const outMarketMint = this.getSpotMarketAccount(outMarketIndex).mint; + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool + ); + + return this.program.instruction.lpPoolSwap( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + userInTokenAccount, + userOutTokenAccount, + inConstituent, + outConstituent, + inMarketMint, + outMarketMint, + authority: this.wallet.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + public async viewLpPoolSwapFees( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + inTargetWeight: BN, + outTargetWeight: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolSwapFeesIx( + inMarketIndex, + outMarketIndex, + inAmount, + inTargetWeight, + outTargetWeight, + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + inConstituent, + outConstituent + ), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolSwapFeesIx( + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + inTargetWeight: BN, + outTargetWeight: BN, + lpPool: PublicKey, + constituentTargetBase: PublicKey, + constituentInTokenAccount: PublicKey, + constituentOutTokenAccount: PublicKey, + inConstituent: PublicKey, + outConstituent: PublicKey + ): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex, outMarketIndex], + }); + + return this.program.instruction.viewLpPoolSwapFees( + inMarketIndex, + outMarketIndex, + inAmount, + inTargetWeight, + outTargetWeight, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool, + constituentTargetBase, + constituentInTokenAccount, + constituentOutTokenAccount, + constituentCorrelations: getConstituentCorrelationsPublicKey( + this.program.programId, + lpPool + ), + inConstituent, + outConstituent, + authority: this.wallet.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + } + + public async getCreateLpPoolTokenAccountIx( + lpPool: LPPoolAccount + ): Promise { + const lpMint = lpPool.mint; + const userLpTokenAccount = await getLpPoolTokenTokenAccountPublicKey( + lpMint, + this.wallet.publicKey + ); + + return this.createAssociatedTokenAccountIdempotentInstruction( + userLpTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey, + lpMint + ); + } + + public async createLpPoolTokenAccount( + lpPool: LPPoolAccount, + txParams?: TxParams + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getCreateLpPoolTokenAccountIx(lpPool), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async lpPoolAddLiquidity({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + txParams, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + }): Promise { + const ixs: TransactionInstruction[] = []; + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [inMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(inMarketIndex); + const inMarketMint = spotMarket.mint; + const isSolMarket = inMarketMint.equals(WRAPPED_SOL_MINT); + + let wSolTokenAccount: PublicKey | undefined; + if (isSolMarket) { + const { ixs: wSolIxs, pubkey } = + await this.getWrappedSolAccountCreationIxs(inAmount, true); + wSolTokenAccount = pubkey; + ixs.push(...wSolIxs); + } + + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const userInTokenAccount = + wSolTokenAccount ?? + (await this.getAssociatedTokenAccount(inMarketIndex, false)); + const constituentInTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const lpMint = lpPool.mint; + const userLpTokenAccount = await getLpPoolTokenTokenAccountPublicKey( + lpMint, + this.wallet.publicKey + ); + if (!(await this.checkIfAccountExists(userLpTokenAccount))) { + ixs.push( + this.createAssociatedTokenAccountIdempotentInstruction( + userLpTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey, + lpMint + ) + ); + } + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + if (!lpPool.whitelistMint.equals(PublicKey.default)) { + const associatedTokenPublicKey = await getAssociatedTokenAddress( + lpPool.whitelistMint, + this.wallet.publicKey + ); + remainingAccounts.push({ + pubkey: associatedTokenPublicKey, + isWritable: false, + isSigner: false, + }); + } + + const lpPoolAddLiquidityIx = this.program.instruction.lpPoolAddLiquidity( + inMarketIndex, + inAmount, + minMintAmount, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + inMarketMint, + inConstituent, + userInTokenAccount, + constituentInTokenAccount, + userLpTokenAccount, + lpMint, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool.pubkey + ), + constituentTargetBase, + tokenProgram: TOKEN_PROGRAM_ID, + }, + } + ); + ixs.push(lpPoolAddLiquidityIx); + + if (isSolMarket && wSolTokenAccount) { + ixs.push( + createCloseAccountInstruction( + wSolTokenAccount, + this.wallet.publicKey, + this.wallet.publicKey + ) + ); + } + return [...ixs]; + } + + public async viewLpPoolAddLiquidityFees({ + inMarketIndex, + inAmount, + lpPool, + txParams, + }: { + inMarketIndex: number; + inAmount: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + lpPool: LPPoolAccount; + }): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + readableSpotMarketIndexes: [inMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(inMarketIndex); + const inMarketMint = spotMarket.mint; + const inConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + inMarketIndex + ); + const lpMint = lpPool.mint; + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + return this.program.instruction.viewLpPoolAddLiquidityFees( + inMarketIndex, + inAmount, + { + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + inMarketMint, + inConstituent, + lpMint, + constituentTargetBase, + }, + remainingAccounts, + } + ); + } + + public async lpPoolRemoveLiquidity({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + txParams, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + }): Promise { + const ixs: TransactionInstruction[] = []; + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [outMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(outMarketIndex); + const outMarketMint = spotMarket.mint; + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + if (outMarketMint.equals(WRAPPED_SOL_MINT)) { + ixs.push( + createAssociatedTokenAccountIdempotentInstruction( + this.wallet.publicKey, + await this.getAssociatedTokenAccount(outMarketIndex, false), + this.wallet.publicKey, + WRAPPED_SOL_MINT + ) + ); + } + const userOutTokenAccount = await this.getAssociatedTokenAccount( + outMarketIndex, + false + ); + const constituentOutTokenAccount = getConstituentVaultPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + const lpMint = lpPool.mint; + const userLpTokenAccount = await getAssociatedTokenAddress( + lpMint, + this.wallet.publicKey, + true + ); + + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + ixs.push( + this.program.instruction.lpPoolRemoveLiquidity( + outMarketIndex, + lpToBurn, + minAmountOut, + { + remainingAccounts, + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + outMarketMint, + outConstituent, + userOutTokenAccount, + constituentOutTokenAccount, + userLpTokenAccount, + spotMarketTokenAccount: spotMarket.vault, + lpMint, + lpPoolTokenVault: getLpPoolTokenVaultPublicKey( + this.program.programId, + lpPool.pubkey + ), + constituentTargetBase, + tokenProgram: TOKEN_PROGRAM_ID, + ammCache: getAmmCachePublicKey(this.program.programId), + }, + } + ) + ); + return ixs; + } + + public async viewLpPoolRemoveLiquidityFees({ + outMarketIndex, + lpToBurn, + lpPool, + txParams, + }: { + outMarketIndex: number; + lpToBurn: BN; + lpPool: LPPoolAccount; + txParams?: TxParams; + }): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }), + txParams + ), + [], + this.opts + ); + return txSig; + } + + public async getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + lpPool: LPPoolAccount; + }): Promise { + const remainingAccounts = this.getRemainingAccounts({ + userAccounts: [], + writableSpotMarketIndexes: [outMarketIndex], + }); + + const spotMarket = this.getSpotMarketAccount(outMarketIndex); + const outMarketMint = spotMarket.mint; + const outConstituent = getConstituentPublicKey( + this.program.programId, + lpPool.pubkey, + outMarketIndex + ); + const lpMint = lpPool.mint; + const constituentTargetBase = getConstituentTargetBasePublicKey( + this.program.programId, + lpPool.pubkey + ); + + return this.program.instruction.viewLpPoolRemoveLiquidityFees( + outMarketIndex, + lpToBurn, + { + remainingAccounts, + accounts: { + state: await this.getStatePublicKey(), + lpPool: lpPool.pubkey, + authority: this.wallet.publicKey, + outMarketMint, + outConstituent, + lpMint, + constituentTargetBase, + }, + } + ); + } + + public async getAllLpPoolAddLiquidityIxs( + { + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + }: { + inMarketIndex: number; + inAmount: BN; + minMintAmount: BN; + lpPool: LPPoolAccount; + }, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true, + view = false + ): Promise { + const ixs: TransactionInstruction[] = []; + + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs( + lpPool, + constituentMap, + includeUpdateConstituentOracleInfo + )) + ); + + if (view) { + ixs.push( + await this.getViewLpPoolAddLiquidityFeesIx({ + inMarketIndex, + inAmount, + lpPool, + }) + ); + } else { + ixs.push( + ...(await this.getLpPoolAddLiquidityIx({ + inMarketIndex, + inAmount, + minMintAmount, + lpPool, + })) + ); + } + + return ixs; + } + + public async getAllLpPoolRemoveLiquidityIxs( + { + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + }: { + outMarketIndex: number; + lpToBurn: BN; + minAmountOut: BN; + lpPool: LPPoolAccount; + }, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true, + view = false + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push( + ...(await this.getAllSettlePerpToLpPoolIxs( + lpPool.lpPoolId, + this.getPerpMarketAccounts() + .filter((marketAccount) => marketAccount.lpStatus > 0) + .map((marketAccount) => marketAccount.marketIndex) + )) + ); + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs( + lpPool, + constituentMap, + includeUpdateConstituentOracleInfo + )) + ); + if (view) { + ixs.push( + await this.getViewLpPoolRemoveLiquidityFeesIx({ + outMarketIndex, + lpToBurn, + lpPool, + }) + ); + } else { + ixs.push( + ...(await this.getLpPoolRemoveLiquidityIx({ + outMarketIndex, + lpToBurn, + minAmountOut, + lpPool, + })) + ); + } + + return ixs; + } + + public async getAllUpdateLpPoolAumIxs( + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true + ): Promise { + const ixs: TransactionInstruction[] = []; + const constituents: ConstituentAccount[] = Array.from( + constituentMap.values() + ); + + if (includeUpdateConstituentOracleInfo) { + for (const constituent of constituents) { + ixs.push(await this.getUpdateConstituentOracleInfoIx(constituent)); + } + } + + const spotMarketIndexes = constituents.map( + (constituent) => constituent.spotMarketIndex + ); + ixs.push(await this.getUpdateLpPoolAumIxs(lpPool, spotMarketIndexes)); + return ixs; + } + + public async getAllUpdateConstituentTargetBaseIxs( + perpMarketIndexes: number[], + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + includeUpdateConstituentOracleInfo = true + ): Promise { + const ixs: TransactionInstruction[] = []; + + ixs.push(await this.getUpdateAmmCacheIx(perpMarketIndexes)); + + const constituents: ConstituentAccount[] = Array.from( + constituentMap.values() + ); + + if (includeUpdateConstituentOracleInfo) { + for (const constituent of constituents) { + ixs.push(await this.getUpdateConstituentOracleInfoIx(constituent)); + } + } + + ixs.push( + await this.getUpdateLpConstituentTargetBaseIx( + lpPool.lpPoolId, + Array.from(constituentMap.values()).map( + (constituent) => constituent.pubkey + ) + ) + ); + + ixs.push( + ...(await this.getAllUpdateLpPoolAumIxs(lpPool, constituentMap, false)) + ); + + return ixs; + } + + async getAllLpPoolSwapIxs( + lpPool: LPPoolAccount, + constituentMap: ConstituentMap, + inMarketIndex: number, + outMarketIndex: number, + inAmount: BN, + minOutAmount: BN, + userAuthority: PublicKey + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push(...(await this.getAllUpdateLpPoolAumIxs(lpPool, constituentMap))); + ixs.push( + await this.getLpPoolSwapIx( + inMarketIndex, + outMarketIndex, + inAmount, + minOutAmount, + lpPool.pubkey, + userAuthority + ) + ); + return ixs; + } + + async settlePerpToLpPool( + lpPoolId: number, + perpMarketIndexes: number[] + ): Promise { + const { txSig } = await this.sendTransaction( + await this.buildTransaction( + await this.getSettlePerpToLpPoolIx(lpPoolId, perpMarketIndexes), + undefined + ), + [], + this.opts + ); + return txSig; + } + + public async getSettlePerpToLpPoolIx( + lpPoolId: number, + perpMarketIndexes: number[] + ): Promise { + const remainingAccounts = []; + remainingAccounts.push( + ...perpMarketIndexes.map((index) => { + return { + pubkey: this.getPerpMarketAccount(index).pubkey, + isSigner: false, + isWritable: true, + }; + }) + ); + const quoteSpotMarketAccount = this.getQuoteSpotMarketAccount(); + const lpPool = getLpPoolPublicKey(this.program.programId, lpPoolId); + return this.program.instruction.settlePerpToLpPool({ + accounts: { + driftSigner: this.getSignerPublicKey(), + state: await this.getStatePublicKey(), + keeper: this.wallet.publicKey, + ammCache: getAmmCachePublicKey(this.program.programId), + quoteMarket: quoteSpotMarketAccount.pubkey, + constituent: getConstituentPublicKey(this.program.programId, lpPool, 0), + constituentQuoteTokenAccount: getConstituentVaultPublicKey( + this.program.programId, + lpPool, + 0 + ), + lpPool, + quoteTokenVault: quoteSpotMarketAccount.vault, + tokenProgram: this.getTokenProgramForSpotMarket(quoteSpotMarketAccount), + }, + remainingAccounts, + }); + } + + public async getAllSettlePerpToLpPoolIxs( + lpPoolId: number, + marketIndexes: number[] + ): Promise { + const ixs: TransactionInstruction[] = []; + ixs.push(await this.getUpdateAmmCacheIx(marketIndexes)); + ixs.push(await this.getSettlePerpToLpPoolIx(lpPoolId, marketIndexes)); + return ixs; + } + + /** + * Below here are the transaction sending functions + */ + + private handleSignedTransaction(signedTxs: SignedTxData[]) { + if (this.enableMetricsEvents && this.metricsEventEmitter) { + this.metricsEventEmitter.emit('txSigned', signedTxs); + } + } + + private handlePreSignedTransaction() { + if (this.enableMetricsEvents && this.metricsEventEmitter) { + this.metricsEventEmitter.emit('preTxSigned'); + } + } + + private isVersionedTransaction( + tx: Transaction | VersionedTransaction + ): boolean { + return isVersionedTransaction(tx); + } + + /** + * Send a transaction. + * + * @param tx + * @param additionalSigners + * @param opts :: Will fallback to DriftClient's opts if not provided + * @param preSigned + * @returns + */ + sendTransaction( + tx: Transaction | VersionedTransaction, + additionalSigners?: Array, + opts?: ConfirmOptions, + preSigned?: boolean + ): Promise { + const isVersionedTx = this.isVersionedTransaction(tx); + if (isVersionedTx) { + return this.txSender.sendVersionedTransaction( + tx as VersionedTransaction, + additionalSigners, + opts ?? this.opts, + preSigned + ); + } else { + return this.txSender.send( + tx as Transaction, + additionalSigners, + opts ?? this.opts, + preSigned + ); + } + } + + async buildTransaction( + instructions: TransactionInstruction | TransactionInstruction[], + txParams?: TxParams, + txVersion?: TransactionVersion, + lookupTables?: AddressLookupTableAccount[], + forceVersionedTransaction?: boolean, + recentBlockhash?: BlockhashWithExpiryBlockHeight, + optionalIxs?: TransactionInstruction[] + ): Promise { + return this.txHandler.buildTransaction({ + instructions, + txVersion: txVersion ?? this.txVersion, + txParams: txParams ?? this.txParams, + connection: this.connection, + preFlightCommitment: this.opts.preflightCommitment, + fetchAllMarketLookupTableAccounts: + this.fetchAllLookupTableAccounts.bind(this), + lookupTables, + forceVersionedTransaction, + recentBlockhash, + optionalIxs, + }); + } + + async buildBulkTransactions( + instructions: (TransactionInstruction | TransactionInstruction[])[], + txParams?: TxParams, + txVersion?: TransactionVersion, + lookupTables?: AddressLookupTableAccount[], + forceVersionedTransaction?: boolean + ): Promise<(Transaction | VersionedTransaction)[]> { + return this.txHandler.buildBulkTransactions({ + instructions, + txVersion: txVersion ?? this.txVersion, + txParams: txParams ?? this.txParams, + connection: this.connection, + preFlightCommitment: this.opts.preflightCommitment, + fetchAllMarketLookupTableAccounts: + this.fetchAllLookupTableAccounts.bind(this), + lookupTables, + forceVersionedTransaction, + }); + } + + async buildTransactionsMap( + instructionsMap: Record< + string, + TransactionInstruction | TransactionInstruction[] + >, txParams?: TxParams, txVersion?: TransactionVersion, lookupTables?: AddressLookupTableAccount[], @@ -10601,4 +12854,24 @@ export class DriftClient { forceVersionedTransaction, }); } + + isOrderIncreasingPosition( + orderParams: OptionalOrderParams, + subAccountId: number + ): boolean { + const userAccount = this.getUserAccount(subAccountId); + const perpPosition = userAccount.perpPositions.find( + (p) => p.marketIndex === orderParams.marketIndex + ); + if (!perpPosition) return true; + + const currentBase = perpPosition.baseAssetAmount; + if (currentBase.eq(ZERO)) return true; + + const orderBaseAmount = isVariant(orderParams.direction, 'long') + ? orderParams.baseAssetAmount + : orderParams.baseAssetAmount.neg(); + + return currentBase.add(orderBaseAmount).abs().gt(currentBase.abs()); + } } diff --git a/sdk/src/driftClientConfig.ts b/sdk/src/driftClientConfig.ts index b3723a2ae8..4ca3a8a9eb 100644 --- a/sdk/src/driftClientConfig.ts +++ b/sdk/src/driftClientConfig.ts @@ -19,9 +19,12 @@ import { import { Coder, Program } from '@coral-xyz/anchor'; import { WebSocketAccountSubscriber } from './accounts/webSocketAccountSubscriber'; import { WebSocketAccountSubscriberV2 } from './accounts/webSocketAccountSubscriberV2'; +import { grpcDriftClientAccountSubscriberV2 } from './accounts/grpcDriftClientAccountSubscriberV2'; +import { grpcDriftClientAccountSubscriber } from './accounts/grpcDriftClientAccountSubscriber'; +import { grpcMultiUserAccountSubscriber } from './accounts/grpcMultiUserAccountSubscriber'; import { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; -import { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; import { WebSocketDriftClientAccountSubscriber } from './accounts/webSocketDriftClientAccountSubscriber'; +import { WebSocketDriftClientAccountSubscriberV2 } from './accounts/webSocketDriftClientAccountSubscriberV2'; export type DriftClientConfig = { connection: Connection; @@ -60,6 +63,18 @@ export type DriftClientSubscriptionConfig = grpcConfigs: GrpcConfigs; resubTimeoutMs?: number; logResubMessages?: boolean; + driftClientAccountSubscriber?: new ( + grpcConfigs: GrpcConfigs, + program: Program, + perpMarketIndexes: number[], + spotMarketIndexes: number[], + oracleInfos: OracleInfo[], + shouldFindAllMarketsAndOracles: boolean, + delistedMarketSetting: DelistedMarketSetting + ) => + | grpcDriftClientAccountSubscriberV2 + | grpcDriftClientAccountSubscriber; + grpcMultiUserAccountSubscriber?: grpcMultiUserAccountSubscriber; } | { type: 'websocket'; @@ -75,7 +90,7 @@ export type DriftClientSubscriptionConfig = resubOpts?: ResubOpts, commitment?: Commitment ) => WebSocketAccountSubscriberV2 | WebSocketAccountSubscriber; - /** If you use V2 here, whatever you pass for perpMarketAccountSubscriber and oracleAccountSubscriber will be ignored and it will use v2 under the hood regardless */ + /** If you use V2 here, whatever you pass for perpMarketAccountSubscriber will be ignored and it will use v2 under the hood regardless */ driftClientAccountSubscriber?: new ( program: Program, perpMarketIndexes: number[], diff --git a/sdk/src/events/parse.ts b/sdk/src/events/parse.ts index 80287550da..96223134e3 100644 --- a/sdk/src/events/parse.ts +++ b/sdk/src/events/parse.ts @@ -1,10 +1,13 @@ import { Program, Event } from '@coral-xyz/anchor'; +import { CuUsageEvent } from './types'; const driftProgramId = 'dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH'; const PROGRAM_LOG = 'Program log: '; +const PROGRAM_INSTRUCTION = 'Program log: Instruction: '; const PROGRAM_DATA = 'Program data: '; const PROGRAM_LOG_START_INDEX = PROGRAM_LOG.length; const PROGRAM_DATA_START_INDEX = PROGRAM_DATA.length; +const PROGRAM_INSTRUCTION_START_INDEX = PROGRAM_INSTRUCTION.length; export function parseLogs( program: Program, @@ -112,6 +115,7 @@ function handleSystemLog( // executing for a given log. class ExecutionContext { stack: string[] = []; + ixStack: string[] = []; program(): string { if (!this.stack.length) { @@ -130,4 +134,115 @@ class ExecutionContext { } this.stack.pop(); } + + ix(): string { + if (!this.ixStack.length) { + throw new Error('Expected the ix stack to have elements'); + } + return this.ixStack[this.ixStack.length - 1]; + } + + pushIx(newIx: string) { + this.ixStack.push(newIx); + } + + popIx() { + this.ixStack.pop(); + } +} + +export function parseLogsForCuUsage( + logs: string[], + programId = driftProgramId +): Event[] { + const cuUsageEvents: Event[] = []; + + const execution = new ExecutionContext(); + for (const log of logs) { + if (log.startsWith('Log truncated')) { + break; + } + + const [newProgram, newIx, didPopProgram, didPopIx] = handleLogForCuUsage( + execution, + log, + programId + ); + if (newProgram) { + execution.push(newProgram); + } + if (newIx) { + execution.pushIx(newIx); + } + if (didPopProgram) { + execution.pop(); + } + if (didPopIx !== null) { + cuUsageEvents.push({ + name: 'CuUsage', + data: { + instruction: execution.ix(), + cuUsage: didPopIx!, + }, + } as any); + execution.popIx(); + } + } + return cuUsageEvents; +} + +function handleLogForCuUsage( + execution: ExecutionContext, + log: string, + programId = driftProgramId +): [string | null, string | null, boolean, number | null] { + if (execution.stack.length > 0 && execution.program() === programId) { + return handleProgramLogForCuUsage(log, programId); + } else { + return handleSystemLogForCuUsage(log, programId); + } +} + +function handleProgramLogForCuUsage( + log: string, + programId = driftProgramId +): [string | null, string | null, boolean, number | null] { + if (log.startsWith(PROGRAM_INSTRUCTION)) { + const ixStr = log.slice(PROGRAM_INSTRUCTION_START_INDEX); + return [null, ixStr, false, null]; + } else { + return handleSystemLogForCuUsage(log, programId); + } +} + +function handleSystemLogForCuUsage( + log: string, + programId = driftProgramId +): [string | null, string | null, boolean, number | null] { + // System component. + const logStart = log.split(':')[0]; + const programStart = `Program ${programId} invoke`; + + // Did the program finish executing? + if (logStart.match(/^Program (.*) success/g) !== null) { + return [null, null, true, null]; + // Recursive call. + } else if (logStart.startsWith(programStart)) { + return [programId, null, false, null]; + // Consumed CU log. + } else if (log.startsWith(`Program ${programId} consumed `)) { + // Extract CU usage, e.g. 'Program ... consumed 29242 of 199700 compute units' + // We need to extract the consumed value (29242) + const matches = log.match(/consumed (\d+) of \d+ compute units/); + if (matches) { + return [null, null, false, Number(matches[1])]; + } + return [null, null, false, null]; + } + // CPI call. + else if (logStart.includes('invoke')) { + return ['cpi', null, false, null]; // Any string will do. + } else { + return [null, null, false, null]; + } } diff --git a/sdk/src/events/types.ts b/sdk/src/events/types.ts index 7909992b4e..69ea7e078d 100644 --- a/sdk/src/events/types.ts +++ b/sdk/src/events/types.ts @@ -24,6 +24,7 @@ import { LPMintRedeemRecord, LPSettleRecord, LPSwapRecord, + LPBorrowLendDepositRecord, } from '../types'; import { EventEmitter } from 'events'; @@ -116,8 +117,8 @@ export type EventMap = { FuelSeasonRecord: Event; InsuranceFundSwapRecord: Event; TransferProtocolIfSharesToRevenuePoolRecord: Event; - LPMintRedeemRecord: Event; LPSettleRecord: Event; + LPMintRedeemRecord: Event; LPSwapRecord: Event; }; @@ -146,8 +147,10 @@ export type DriftEvent = | Event | Event | Event + | Event | Event - | Event; + | Event + | Event; export interface EventSubscriberEvents { newEvent: (event: WrappedEvent) => void; @@ -211,3 +214,24 @@ export type LogProviderConfig = | WebSocketLogProviderConfig | PollingLogProviderConfig | EventsServerLogProviderConfig; + +export type CuUsageEvent = { + name: 'CuUsage'; + fields: [ + { + name: 'instruction'; + type: 'string'; + index: false; + }, + { + name: 'cuUsage'; + type: 'u32'; + index: false; + }, + ]; +}; + +export type CuUsage = { + instruction: string; + cuUsage: number; +}; diff --git a/sdk/src/idl/drift.json b/sdk/src/idl/drift.json index e621bcb8df..f85d4dbe98 100644 --- a/sdk/src/idl/drift.json +++ b/sdk/src/idl/drift.json @@ -1,5 +1,5 @@ { - "version": "2.137.0", + "version": "2.145.1", "name": "drift", "instructions": [ { @@ -1730,31 +1730,6 @@ } ] }, - { - "name": "updateUserAdvancedLp", - "accounts": [ - { - "name": "user", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "subAccountId", - "type": "u16" - }, - { - "name": "advancedLp", - "type": "bool" - } - ] - }, { "name": "updateUserProtectedMakerOrders", "accounts": [ @@ -2249,32 +2224,6 @@ ], "args": [] }, - { - "name": "updateUserOpenOrdersCount", - "accounts": [ - { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "filler", - "isMut": true, - "isSigner": false - }, - { - "name": "user", - "isMut": true, - "isSigner": false - } - ], - "args": [] - }, { "name": "adminDisableUpdatePerpBidAskTwap", "accounts": [ @@ -3298,6 +3247,42 @@ ], "args": [] }, + { + "name": "updateDelegateUserGovTokenInsuranceStake", + "accounts": [ + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "insuranceFundStake", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, { "name": "initializeInsuranceFundStake", "accounts": [ @@ -3547,62 +3532,79 @@ ] }, { - "name": "transferProtocolIfShares", + "name": "beginInsuranceFundSwap", "accounts": [ { - "name": "signer", + "name": "state", "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, "isSigner": true }, { - "name": "transferConfig", + "name": "outInsuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "state", - "isMut": false, + "name": "inInsuranceFundVault", + "isMut": true, "isSigner": false }, { - "name": "spotMarket", + "name": "outTokenAccount", "isMut": true, "isSigner": false }, { - "name": "insuranceFundStake", + "name": "inTokenAccount", "isMut": true, "isSigner": false }, { - "name": "userStats", + "name": "ifRebalanceConfig", "isMut": true, "isSigner": false }, { - "name": "authority", + "name": "tokenProgram", "isMut": false, - "isSigner": true + "isSigner": false }, { - "name": "insuranceFundVault", + "name": "driftSigner", "isMut": false, "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] } ], "args": [ { - "name": "marketIndex", + "name": "inMarketIndex", "type": "u16" }, { - "name": "shares", - "type": "u128" + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" } ] }, { - "name": "beginInsuranceFundSwap", + "name": "endInsuranceFundSwap", "accounts": [ { "name": "state", @@ -3666,15 +3668,11 @@ { "name": "outMarketIndex", "type": "u16" - }, - { - "name": "amountIn", - "type": "u64" } ] }, { - "name": "endInsuranceFundSwap", + "name": "transferProtocolIfSharesToRevenuePool", "accounts": [ { "name": "state", @@ -3687,22 +3685,12 @@ "isSigner": true }, { - "name": "outInsuranceFundVault", - "isMut": true, - "isSigner": false - }, - { - "name": "inInsuranceFundVault", - "isMut": true, - "isSigner": false - }, - { - "name": "outTokenAccount", + "name": "insuranceFundVault", "isMut": true, "isSigner": false }, { - "name": "inTokenAccount", + "name": "spotMarketVault", "isMut": true, "isSigner": false }, @@ -3720,42 +3708,44 @@ "name": "driftSigner", "isMut": false, "isSigner": false - }, - { - "name": "instructions", - "isMut": false, - "isSigner": false, - "docs": [ - "Instructions Sysvar for instruction introspection" - ] } ], "args": [ { - "name": "inMarketIndex", + "name": "marketIndex", "type": "u16" }, { - "name": "outMarketIndex", - "type": "u16" + "name": "amount", + "type": "u64" } ] }, { - "name": "transferProtocolIfSharesToRevenuePool", + "name": "depositIntoInsuranceFundStake", "accounts": [ { - "name": "state", + "name": "signer", "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, "isSigner": false }, { - "name": "authority", + "name": "spotMarket", "isMut": true, - "isSigner": true + "isSigner": false }, { - "name": "insuranceFundVault", + "name": "insuranceFundStake", + "isMut": true, + "isSigner": false + }, + { + "name": "userStats", "isMut": true, "isSigner": false }, @@ -3765,7 +3755,12 @@ "isSigner": false }, { - "name": "ifRebalanceConfig", + "name": "insuranceFundVault", + "isMut": true, + "isSigner": false + }, + { + "name": "userTokenAccount", "isMut": true, "isSigner": false }, @@ -4429,27 +4424,6 @@ } ] }, - { - "name": "updateSerumVault", - "accounts": [ - { - "name": "state", - "isMut": true, - "isSigner": false - }, - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "srmVault", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, { "name": "initializePerpMarket", "accounts": [ @@ -4468,6 +4442,11 @@ "isMut": true, "isSigner": false }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, { "name": "oracle", "isMut": false, @@ -4593,15 +4572,19 @@ 32 ] } + }, + { + "name": "lpPoolId", + "type": "u8" } ] }, { - "name": "initializePredictionMarket", + "name": "initializeAmmCache", "accounts": [ { "name": "admin", - "isMut": false, + "isMut": true, "isSigner": true }, { @@ -4610,15 +4593,25 @@ "isSigner": false }, { - "name": "perpMarket", + "name": "ammCache", "isMut": true, "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } ], "args": [] }, { - "name": "deleteInitializedPerpMarket", + "name": "resizeAmmCache", "accounts": [ { "name": "admin", @@ -4627,32 +4620,105 @@ }, { "name": "state", - "isMut": true, + "isMut": false, "isSigner": false }, { - "name": "perpMarket", + "name": "ammCache", "isMut": true, "isSigner": false - } - ], - "args": [ + }, { - "name": "marketIndex", - "type": "u16" + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false } - ] + ], + "args": [] }, { - "name": "moveAmmPrice", + "name": "updateInitialAmmCacheInfo", "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, { "name": "admin", "isMut": false, "isSigner": true }, { - "name": "state", + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "initializePredictionMarket", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "deleteInitializedPerpMarket", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + } + ] + }, + { + "name": "moveAmmPrice", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", "isMut": false, "isSigner": false }, @@ -4809,6 +4875,97 @@ } ] }, + { + "name": "updatePerpMarketLpPoolPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketLpPoolStatus", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpStatus", + "type": "u8" + } + ] + }, + { + "name": "updatePerpMarketLpPoolFeeTransferScalar", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "optionalLpFeeTransferScalar", + "type": { + "option": "u8" + } + }, + { + "name": "optionalLpNetPnlTransferScalar", + "type": { + "option": "u8" + } + } + ] + }, { "name": "settleExpiredMarketPoolsToRevenuePool", "accounts": [ @@ -5268,6 +5425,32 @@ } ] }, + { + "name": "updatePerpMarketLpPoolId", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolId", + "type": "u8" + } + ] + }, { "name": "updateInsuranceFundUnstakingPeriod", "accounts": [ @@ -6057,6 +6240,32 @@ } ] }, + { + "name": "updatePerpMarketReferencePriceOffsetDeadbandPct", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "perpMarket", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "referencePriceOffsetDeadbandPct", + "type": "u8" + } + ] + }, { "name": "updateLpCooldownTime", "accounts": [ @@ -6300,6 +6509,11 @@ "name": "oldOracle", "isMut": false, "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false } ], "args": [ @@ -6723,7 +6937,7 @@ ] }, { - "name": "updatePerpMarketTakerSpeedBumpOverride", + "name": "updatePerpMarketOracleLowRiskSlotDelayOverride", "accounts": [ { "name": "admin", @@ -6743,7 +6957,7 @@ ], "args": [ { - "name": "takerSpeedBumpOverride", + "name": "oracleLowRiskSlotDelayOverride", "type": "i8" } ] @@ -7044,7 +7258,7 @@ ] }, { - "name": "initializeProtocolIfSharesTransferConfig", + "name": "initializePrelaunchOracle", "accounts": [ { "name": "admin", @@ -7052,7 +7266,7 @@ "isSigner": true }, { - "name": "protocolIfSharesTransferConfig", + "name": "prelaunchOracle", "isMut": true, "isSigner": false }, @@ -7072,10 +7286,17 @@ "isSigner": false } ], - "args": [] + "args": [ + { + "name": "params", + "type": { + "defined": "PrelaunchOracleParams" + } + } + ] }, { - "name": "updateProtocolIfSharesTransferConfig", + "name": "updatePrelaunchOracleParams", "accounts": [ { "name": "admin", @@ -7083,7 +7304,12 @@ "isSigner": true }, { - "name": "protocolIfSharesTransferConfig", + "name": "prelaunchOracle", + "isMut": true, + "isSigner": false + }, + { + "name": "perpMarket", "isMut": true, "isSigner": false }, @@ -7095,26 +7321,15 @@ ], "args": [ { - "name": "whitelistedSigners", - "type": { - "option": { - "array": [ - "publicKey", - 4 - ] - } - } - }, - { - "name": "maxTransferPerEpoch", + "name": "params", "type": { - "option": "u128" + "defined": "PrelaunchOracleParams" } } ] }, { - "name": "initializePrelaunchOracle", + "name": "deletePrelaunchOracle", "accounts": [ { "name": "admin", @@ -7127,32 +7342,25 @@ "isSigner": false }, { - "name": "state", - "isMut": false, - "isSigner": false - }, - { - "name": "rent", + "name": "perpMarket", "isMut": false, "isSigner": false }, { - "name": "systemProgram", + "name": "state", "isMut": false, "isSigner": false } ], "args": [ { - "name": "params", - "type": { - "defined": "PrelaunchOracleParams" - } + "name": "perpMarketIndex", + "type": "u16" } ] }, { - "name": "updatePrelaunchOracleParams", + "name": "initializePythPullOracle", "accounts": [ { "name": "admin", @@ -7160,81 +7368,17 @@ "isSigner": true }, { - "name": "prelaunchOracle", - "isMut": true, + "name": "pythSolanaReceiver", + "isMut": false, "isSigner": false }, { - "name": "perpMarket", + "name": "priceFeed", "isMut": true, "isSigner": false }, { - "name": "state", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "params", - "type": { - "defined": "PrelaunchOracleParams" - } - } - ] - }, - { - "name": "deletePrelaunchOracle", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "prelaunchOracle", - "isMut": true, - "isSigner": false - }, - { - "name": "perpMarket", - "isMut": false, - "isSigner": false - }, - { - "name": "state", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "perpMarketIndex", - "type": "u16" - } - ] - }, - { - "name": "initializePythPullOracle", - "accounts": [ - { - "name": "admin", - "isMut": true, - "isSigner": true - }, - { - "name": "pythSolanaReceiver", - "isMut": false, - "isSigner": false - }, - { - "name": "priceFeed", - "isMut": true, - "isSigner": false - }, - { - "name": "systemProgram", + "name": "systemProgram", "isMut": false, "isSigner": false }, @@ -7635,206 +7779,2179 @@ "type": "bool" } ] - } - ], - "accounts": [ + }, { - "name": "OpenbookV2FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "openbookV2ProgramId", - "type": "publicKey" - }, - { - "name": "openbookV2Market", - "type": "publicKey" - }, - { - "name": "openbookV2MarketAuthority", - "type": "publicKey" - }, - { - "name": "openbookV2EventHeap", - "type": "publicKey" - }, - { - "name": "openbookV2Bids", - "type": "publicKey" - }, - { - "name": "openbookV2Asks", - "type": "publicKey" - }, - { - "name": "openbookV2BaseVault", - "type": "publicKey" - }, - { - "name": "openbookV2QuoteVault", - "type": "publicKey" - }, - { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } - }, - { - "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 4 - ] - } - } - ] - } + "name": "updateFeatureBitFlagsBuilderCodes", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] }, { - "name": "PhoenixV1FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "phoenixProgramId", - "type": "publicKey" - }, - { - "name": "phoenixLogAuthority", - "type": "publicKey" - }, - { - "name": "phoenixMarket", - "type": "publicKey" - }, - { - "name": "phoenixBaseVault", - "type": "publicKey" - }, - { - "name": "phoenixQuoteVault", - "type": "publicKey" - }, - { - "name": "marketIndex", - "type": "u16" - }, - { - "name": "fulfillmentType", - "type": { - "defined": "SpotFulfillmentType" - } - }, - { - "name": "status", - "type": { - "defined": "SpotFulfillmentConfigStatus" - } - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 4 - ] - } - } - ] - } + "name": "initializeRevenueShare", + "accounts": [ + { + "name": "revenueShare", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] }, { - "name": "SerumV3FulfillmentConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "serumProgramId", - "type": "publicKey" - }, - { - "name": "serumMarket", - "type": "publicKey" - }, - { - "name": "serumRequestQueue", - "type": "publicKey" - }, - { + "name": "initializeRevenueShareEscrow", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "userStats", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "numOrders", + "type": "u16" + } + ] + }, + { + "name": "resizeRevenueShareEscrowOrders", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": false + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "numOrders", + "type": "u16" + } + ] + }, + { + "name": "changeApprovedBuilder", + "accounts": [ + { + "name": "escrow", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "builder", + "type": "publicKey" + }, + { + "name": "maxFeeBps", + "type": "u16" + }, + { + "name": "add", + "type": "bool" + } + ] + }, + { + "name": "initializeLpPool", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolId", + "type": "u8" + }, + { + "name": "minMintFee", + "type": "i64" + }, + { + "name": "maxAum", + "type": "u128" + }, + { + "name": "maxSettleQuoteAmountPerMarket", + "type": "u64" + }, + { + "name": "whitelistMint", + "type": "publicKey" + } + ] + }, + { + "name": "updateFeatureBitFlagsSettleLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "updateFeatureBitFlagsSwapLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "updateFeatureBitFlagsMintRedeemLpPool", + "accounts": [ + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "state", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "enable", + "type": "bool" + } + ] + }, + { + "name": "initializeConstituent", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "spotMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentVault", + "isMut": true, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "spotMarketIndex", + "type": "u16" + }, + { + "name": "decimals", + "type": "u8" + }, + { + "name": "maxWeightDeviation", + "type": "i64" + }, + { + "name": "swapFeeMin", + "type": "i64" + }, + { + "name": "swapFeeMax", + "type": "i64" + }, + { + "name": "maxBorrowTokenAmount", + "type": "u64" + }, + { + "name": "oracleStalenessThreshold", + "type": "u64" + }, + { + "name": "costToTrade", + "type": "i32" + }, + { + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "derivativeWeight", + "type": "u64" + }, + { + "name": "volatility", + "type": "u64" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "xi", + "type": "u8" + }, + { + "name": "newConstituentCorrelations", + "type": { + "vec": "i64" + } + } + ] + }, + { + "name": "updateConstituentStatus", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "newStatus", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentPausedOperations", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "pausedOperations", + "type": "u8" + } + ] + }, + { + "name": "updateConstituentParams", + "accounts": [ + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "constituentParams", + "type": { + "defined": "ConstituentParams" + } + } + ] + }, + { + "name": "updateLpPoolParams", + "accounts": [ + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "lpPoolParams", + "type": { + "defined": "LpPoolParams" + } + } + ] + }, + { + "name": "addAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "updateAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "ammConstituentMappingData", + "type": { + "vec": { + "defined": "AddAmmConstituentMappingDatum" + } + } + } + ] + }, + { + "name": "removeAmmConstituentMappingData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "ammConstituentMapping", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "constituentIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentCorrelationData", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "index1", + "type": "u16" + }, + { + "name": "index2", + "type": "u16" + }, + { + "name": "correlation", + "type": "i64" + } + ] + }, + { + "name": "updateLpConstituentTargetBase", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammConstituentMapping", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateLpPoolAum", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": true, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "updateAmmCache", + "accounts": [ + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "overrideAmmCacheInfo", + "accounts": [ + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "overrideParams", + "type": { + "defined": "OverrideAmmCacheParams" + } + } + ] + }, + { + "name": "resetAmmCache", + "accounts": [ + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "lpPoolSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u64" + } + ] + }, + { + "name": "viewLpPoolSwapFees", + "accounts": [ + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentCorrelations", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "inTargetWeight", + "type": "i64" + }, + { + "name": "outTargetWeight", + "type": "i64" + } + ] + }, + { + "name": "lpPoolAddLiquidity", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + }, + { + "name": "minMintAmount", + "type": "u64" + } + ] + }, + { + "name": "lpPoolRemoveLiquidity", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "userOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "userLpTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPoolTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "ammCache", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + }, + { + "name": "minOutAmount", + "type": "u128" + } + ] + }, + { + "name": "viewLpPoolAddLiquidityFees", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "inMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "inConstituent", + "isMut": false, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u128" + } + ] + }, + { + "name": "viewLpPoolRemoveLiquidityFees", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "outMarketMint", + "isMut": false, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": false, + "isSigner": false + }, + { + "name": "lpMint", + "isMut": false, + "isSigner": false + }, + { + "name": "constituentTargetBase", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "inAmount", + "type": "u64" + } + ] + }, + { + "name": "beginLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "amountIn", + "type": "u64" + } + ] + }, + { + "name": "endLpSwap", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "signerOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Signer token accounts" + ] + }, + { + "name": "signerInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentOutTokenAccount", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituent token accounts" + ] + }, + { + "name": "constituentInTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "outConstituent", + "isMut": true, + "isSigner": false, + "docs": [ + "Constituents" + ] + }, + { + "name": "inConstituent", + "isMut": true, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": false, + "isSigner": false + }, + { + "name": "instructions", + "isMut": false, + "isSigner": false, + "docs": [ + "Instructions Sysvar for instruction introspection" + ] + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "outMarketIndex", + "type": "u16" + } + ] + }, + { + "name": "updateConstituentOracleInfo", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "depositToProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "withdrawFromProgramVault", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": true, + "isSigner": true + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "spotMarketVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "oracle", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "amount", + "type": "u64" + } + ] + }, + { + "name": "settlePerpToLpPool", + "accounts": [ + { + "name": "state", + "isMut": false, + "isSigner": false + }, + { + "name": "lpPool", + "isMut": true, + "isSigner": false + }, + { + "name": "keeper", + "isMut": true, + "isSigner": true + }, + { + "name": "ammCache", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteMarket", + "isMut": true, + "isSigner": false + }, + { + "name": "constituent", + "isMut": true, + "isSigner": false + }, + { + "name": "constituentQuoteTokenAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "quoteTokenVault", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "driftSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "AmmCache", + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "cache", + "type": { + "vec": { + "defined": "CacheInfo" + } + } + } + ] + } + }, + { + "name": "OpenbookV2FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "openbookV2ProgramId", + "type": "publicKey" + }, + { + "name": "openbookV2Market", + "type": "publicKey" + }, + { + "name": "openbookV2MarketAuthority", + "type": "publicKey" + }, + { + "name": "openbookV2EventHeap", + "type": "publicKey" + }, + { + "name": "openbookV2Bids", + "type": "publicKey" + }, + { + "name": "openbookV2Asks", + "type": "publicKey" + }, + { + "name": "openbookV2BaseVault", + "type": "publicKey" + }, + { + "name": "openbookV2QuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "PhoenixV1FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "phoenixProgramId", + "type": "publicKey" + }, + { + "name": "phoenixLogAuthority", + "type": "publicKey" + }, + { + "name": "phoenixMarket", + "type": "publicKey" + }, + { + "name": "phoenixBaseVault", + "type": "publicKey" + }, + { + "name": "phoenixQuoteVault", + "type": "publicKey" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "SerumV3FulfillmentConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "serumProgramId", + "type": "publicKey" + }, + { + "name": "serumMarket", + "type": "publicKey" + }, + { + "name": "serumRequestQueue", + "type": "publicKey" + }, + { "name": "serumEventQueue", "type": "publicKey" }, { - "name": "serumBids", - "type": "publicKey" + "name": "serumBids", + "type": "publicKey" + }, + { + "name": "serumAsks", + "type": "publicKey" + }, + { + "name": "serumBaseVault", + "type": "publicKey" + }, + { + "name": "serumQuoteVault", + "type": "publicKey" + }, + { + "name": "serumOpenOrders", + "type": "publicKey" + }, + { + "name": "serumSignerNonce", + "type": "u64" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "fulfillmentType", + "type": { + "defined": "SpotFulfillmentType" + } + }, + { + "name": "status", + "type": { + "defined": "SpotFulfillmentConfigStatus" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "HighLeverageModeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxUsers", + "type": "u32" + }, + { + "name": "currentUsers", + "type": "u32" + }, + { + "name": "reduceOnly", + "type": "u8" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "currentMaintenanceUsers", + "type": "u32" + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 24 + ] + } + } + ] + } + }, + { + "name": "IfRebalanceConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "totalInAmount", + "docs": [ + "total amount to be sold" + ], + "type": "u64" + }, + { + "name": "currentInAmount", + "docs": [ + "amount already sold" + ], + "type": "u64" + }, + { + "name": "currentOutAmount", + "docs": [ + "amount already bought" + ], + "type": "u64" + }, + { + "name": "currentOutAmountTransferred", + "docs": [ + "amount already transferred to revenue pool" + ], + "type": "u64" + }, + { + "name": "currentInAmountSinceLastTransfer", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" + }, + { + "name": "epochStartTs", + "docs": [ + "start time of epoch" + ], + "type": "i64" + }, + { + "name": "epochInAmount", + "docs": [ + "amount already bought in epoch" + ], + "type": "u64" }, { - "name": "serumAsks", - "type": "publicKey" + "name": "epochMaxInAmount", + "docs": [ + "max amount to swap in epoch" + ], + "type": "u64" }, { - "name": "serumBaseVault", - "type": "publicKey" + "name": "epochDuration", + "docs": [ + "duration of epoch" + ], + "type": "i64" }, { - "name": "serumQuoteVault", - "type": "publicKey" + "name": "outMarketIndex", + "docs": [ + "market index to sell" + ], + "type": "u16" }, { - "name": "serumOpenOrders", + "name": "inMarketIndex", + "docs": [ + "market index to buy" + ], + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" + }, + { + "name": "status", + "type": "u8" + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "InsuranceFundStake", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", "type": "publicKey" }, { - "name": "serumSignerNonce", + "name": "ifShares", + "type": "u128" + }, + { + "name": "lastWithdrawRequestShares", + "type": "u128" + }, + { + "name": "ifBase", + "type": "u128" + }, + { + "name": "lastValidTs", + "type": "i64" + }, + { + "name": "lastWithdrawRequestValue", "type": "u64" }, + { + "name": "lastWithdrawRequestTs", + "type": "i64" + }, + { + "name": "costBasis", + "type": "i64" + }, { "name": "marketIndex", "type": "u16" }, { - "name": "fulfillmentType", + "name": "padding", "type": { - "defined": "SpotFulfillmentType" + "array": [ + "u8", + 14 + ] } - }, + } + ] + } + }, + { + "name": "ProtocolIfSharesTransferConfig", + "type": { + "kind": "struct", + "fields": [ { - "name": "status", + "name": "whitelistedSigners", "type": { - "defined": "SpotFulfillmentConfigStatus" + "array": [ + "publicKey", + 4 + ] } }, + { + "name": "maxTransferPerEpoch", + "type": "u128" + }, + { + "name": "currentEpochTransfer", + "type": "u128" + }, + { + "name": "nextEpochTs", + "type": "i64" + }, { "name": "padding", "type": { "array": [ - "u8", - 4 + "u128", + 8 ] } } @@ -7842,41 +9959,150 @@ } }, { - "name": "HighLeverageModeConfig", + "name": "LPPool", "type": { "kind": "struct", "fields": [ { - "name": "maxUsers", - "type": "u32" + "name": "pubkey", + "docs": [ + "address of the vault." + ], + "type": "publicKey" }, { - "name": "currentUsers", - "type": "u32" + "name": "mint", + "type": "publicKey" }, { - "name": "reduceOnly", + "name": "whitelistMint", + "type": "publicKey" + }, + { + "name": "constituentTargetBase", + "type": "publicKey" + }, + { + "name": "constituentCorrelations", + "type": "publicKey" + }, + { + "name": "maxAum", + "docs": [ + "The current number of VaultConstituents in the vault, each constituent is pda(LPPool.address, constituent_index)", + "which constituent is the quote, receives revenue pool distributions. (maybe this should just be implied idx 0)", + "pub quote_constituent_index: u16,", + "QUOTE_PRECISION: Max AUM, Prohibit minting new DLP beyond this" + ], + "type": "u128" + }, + { + "name": "lastAum", + "docs": [ + "QUOTE_PRECISION: AUM of the vault in USD, updated lazily" + ], + "type": "u128" + }, + { + "name": "cumulativeQuoteSentToPerpMarkets", + "docs": [ + "QUOTE PRECISION: Cumulative quotes from settles" + ], + "type": "u128" + }, + { + "name": "cumulativeQuoteReceivedFromPerpMarkets", + "type": "u128" + }, + { + "name": "totalMintRedeemFeesPaid", + "docs": [ + "QUOTE_PRECISION: Total fees paid for minting and redeeming LP tokens" + ], + "type": "i128" + }, + { + "name": "lastAumSlot", + "docs": [ + "timestamp of last AUM slot" + ], + "type": "u64" + }, + { + "name": "maxSettleQuoteAmount", + "type": "u64" + }, + { + "name": "padding", + "docs": [ + "timestamp of last vAMM revenue rebalance" + ], + "type": "u64" + }, + { + "name": "mintRedeemId", + "docs": [ + "Every mint/redeem has a monotonically increasing id. This is the next id to use" + ], + "type": "u64" + }, + { + "name": "settleId", + "type": "u64" + }, + { + "name": "minMintFee", + "docs": [ + "PERCENTAGE_PRECISION" + ], + "type": "i64" + }, + { + "name": "tokenSupply", + "type": "u64" + }, + { + "name": "volatility", + "type": "u64" + }, + { + "name": "constituents", + "type": "u16" + }, + { + "name": "quoteConsituentIndex", + "type": "u16" + }, + { + "name": "bump", "type": "u8" }, { - "name": "padding1", - "type": { - "array": [ - "u8", - 3 - ] - } + "name": "gammaExecution", + "type": "u8" }, { - "name": "currentMaintenanceUsers", - "type": "u32" + "name": "xi", + "type": "u8" }, { - "name": "padding2", + "name": "targetOracleDelayFeeBpsPer10Slots", + "type": "u8" + }, + { + "name": "targetPositionDelayFeeBpsPer10Slots", + "type": "u8" + }, + { + "name": "lpPoolId", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ "u8", - 24 + 174 ] } } @@ -7884,97 +10110,173 @@ } }, { - "name": "IfRebalanceConfig", + "name": "Constituent", "type": { "kind": "struct", "fields": [ { "name": "pubkey", + "docs": [ + "address of the constituent" + ], + "type": "publicKey" + }, + { + "name": "mint", "type": "publicKey" }, { - "name": "totalInAmount", + "name": "lpPool", + "type": "publicKey" + }, + { + "name": "vault", + "type": "publicKey" + }, + { + "name": "totalSwapFees", + "docs": [ + "total fees received by the constituent. Positive = fees received, Negative = fees paid" + ], + "type": "i128" + }, + { + "name": "spotBalance", "docs": [ - "total amount to be sold" + "spot borrow-lend balance for constituent" ], - "type": "u64" + "type": { + "defined": "ConstituentSpotBalance" + } }, { - "name": "currentInAmount", + "name": "lastSpotBalanceTokenAmount", + "type": "i64" + }, + { + "name": "cumulativeSpotInterestAccruedTokenAmount", + "type": "i64" + }, + { + "name": "maxWeightDeviation", "docs": [ - "amount already sold" + "max deviation from target_weight allowed for the constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "currentOutAmount", + "name": "swapFeeMin", "docs": [ - "amount already bought" + "min fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "currentOutAmountTransferred", + "name": "swapFeeMax", "docs": [ - "amount already transferred to revenue pool" + "max fee charged on swaps to/from this constituent", + "precision: PERCENTAGE_PRECISION" ], - "type": "u64" + "type": "i64" }, { - "name": "currentInAmountSinceLastTransfer", + "name": "maxBorrowTokenAmount", "docs": [ - "amount already bought in epoch" + "Max Borrow amount:", + "precision: token precision" ], "type": "u64" }, { - "name": "epochStartTs", + "name": "vaultTokenBalance", "docs": [ - "start time of epoch" + "ata token balance in token precision" ], + "type": "u64" + }, + { + "name": "lastOraclePrice", "type": "i64" }, { - "name": "epochInAmount", - "docs": [ - "amount already bought in epoch" - ], + "name": "lastOracleSlot", "type": "u64" }, { - "name": "epochMaxInAmount", + "name": "oracleStalenessThreshold", "docs": [ - "max amount to swap in epoch" + "Delay allowed for valid AUM calculation" ], "type": "u64" }, { - "name": "epochDuration", + "name": "flashLoanInitialTokenAmount", + "type": "u64" + }, + { + "name": "nextSwapId", "docs": [ - "duration of epoch" + "Every swap to/from this constituent has a monotonically increasing id. This is the next id to use" ], - "type": "i64" + "type": "u64" }, { - "name": "outMarketIndex", + "name": "derivativeWeight", "docs": [ - "market index to sell" + "percentable of derivatve weight to go to this specific derivative PERCENTAGE_PRECISION. Zero if no derivative weight" ], - "type": "u16" + "type": "u64" }, { - "name": "inMarketIndex", + "name": "volatility", + "type": "u64" + }, + { + "name": "constituentDerivativeDepegThreshold", + "type": "u64" + }, + { + "name": "constituentDerivativeIndex", "docs": [ - "market index to buy" + "The `constituent_index` of the parent constituent. -1 if it is a parent index", + "Example: if in a pool with SOL (parent) and dSOL (derivative),", + "SOL.constituent_index = 1, SOL.constituent_derivative_index = -1,", + "dSOL.constituent_index = 2, dSOL.constituent_derivative_index = 1" ], + "type": "i16" + }, + { + "name": "spotMarketIndex", "type": "u16" }, { - "name": "maxSlippageBps", + "name": "constituentIndex", "type": "u16" }, { - "name": "swapMode", + "name": "decimals", + "type": "u8" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "vaultBump", + "type": "u8" + }, + { + "name": "gammaInventory", + "type": "u8" + }, + { + "name": "gammaExecution", + "type": "u8" + }, + { + "name": "xi", "type": "u8" }, { @@ -7982,11 +10284,15 @@ "type": "u8" }, { - "name": "padding2", + "name": "pausedOperations", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ "u8", - 32 + 162 ] } } @@ -7994,92 +10300,98 @@ } }, { - "name": "InsuranceFundStake", + "name": "AmmConstituentMapping", "type": { "kind": "struct", "fields": [ { - "name": "authority", + "name": "lpPool", "type": "publicKey" }, { - "name": "ifShares", - "type": "u128" - }, - { - "name": "lastWithdrawRequestShares", - "type": "u128" - }, - { - "name": "ifBase", - "type": "u128" - }, - { - "name": "lastValidTs", - "type": "i64" - }, - { - "name": "lastWithdrawRequestValue", - "type": "u64" - }, - { - "name": "lastWithdrawRequestTs", - "type": "i64" - }, - { - "name": "costBasis", - "type": "i64" - }, - { - "name": "marketIndex", - "type": "u16" + "name": "bump", + "type": "u8" }, { "name": "padding", "type": { "array": [ "u8", - 14 + 3 ] } + }, + { + "name": "weights", + "type": { + "vec": { + "defined": "AmmConstituentDatum" + } + } } ] } }, { - "name": "ProtocolIfSharesTransferConfig", + "name": "ConstituentTargetBase", "type": { "kind": "struct", "fields": [ { - "name": "whitelistedSigners", + "name": "lpPool", + "type": "publicKey" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "padding", "type": { "array": [ - "publicKey", - 4 + "u8", + 3 ] } }, { - "name": "maxTransferPerEpoch", - "type": "u128" - }, + "name": "targets", + "type": { + "vec": { + "defined": "TargetsDatum" + } + } + } + ] + } + }, + { + "name": "ConstituentCorrelations", + "type": { + "kind": "struct", + "fields": [ { - "name": "currentEpochTransfer", - "type": "u128" + "name": "lpPool", + "type": "publicKey" }, { - "name": "nextEpochTs", - "type": "i64" + "name": "bump", + "type": "u8" }, { "name": "padding", "type": { "array": [ - "u128", - 8 + "u8", + 3 ] } + }, + { + "name": "correlations", + "type": { + "vec": "i64" + } } ] } @@ -8402,48 +10714,133 @@ "type": "u8" }, { - "name": "padding1", - "type": "u32" + "name": "lpFeeTransferScalar", + "type": "u8" + }, + { + "name": "lpStatus", + "type": "u8" + }, + { + "name": "lpPausedOperations", + "type": "u8" + }, + { + "name": "lpExchangeFeeExcluscionScalar", + "type": "u8" }, { "name": "lastFillPrice", "type": "u64" }, + { + "name": "lpPoolId", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 24 + 23 + ] + } + } + ] + } + }, + { + "name": "ProtectedMakerModeConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxUsers", + "type": "u32" + }, + { + "name": "currentUsers", + "type": "u32" + }, + { + "name": "reduceOnly", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 31 + ] + } + } + ] + } + }, + { + "name": "PythLazerOracle", + "type": { + "kind": "struct", + "fields": [ + { + "name": "price", + "type": "i64" + }, + { + "name": "publishTime", + "type": "u64" + }, + { + "name": "postedSlot", + "type": "u64" + }, + { + "name": "exponent", + "type": "i32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 ] } + }, + { + "name": "conf", + "type": "u64" } ] } }, { - "name": "ProtectedMakerModeConfig", + "name": "RevenueShare", "type": { "kind": "struct", "fields": [ { - "name": "maxUsers", - "type": "u32" + "name": "authority", + "docs": [ + "the owner of this account, a builder or referrer" + ], + "type": "publicKey" }, { - "name": "currentUsers", - "type": "u32" + "name": "totalReferrerRewards", + "type": "u64" }, { - "name": "reduceOnly", - "type": "u8" + "name": "totalBuilderRewards", + "type": "u64" }, { "name": "padding", "type": { "array": [ "u8", - 31 + 18 ] } } @@ -8451,38 +10848,69 @@ } }, { - "name": "PythLazerOracle", + "name": "RevenueShareEscrow", "type": { "kind": "struct", "fields": [ { - "name": "price", - "type": "i64" + "name": "authority", + "docs": [ + "the owner of this account, a user" + ], + "type": "publicKey" }, { - "name": "publishTime", - "type": "u64" + "name": "referrer", + "type": "publicKey" }, { - "name": "postedSlot", - "type": "u64" + "name": "referrerBoostExpireTs", + "type": "u32" }, { - "name": "exponent", - "type": "i32" + "name": "referrerRewardOffset", + "type": "i8" }, { - "name": "padding", + "name": "refereeFeeNumeratorOffset", + "type": "i8" + }, + { + "name": "referrerBoostNumerator", + "type": "i8" + }, + { + "name": "reservedFixed", "type": { "array": [ "u8", - 4 + 17 ] } }, { - "name": "conf", - "type": "u64" + "name": "padding0", + "type": "u32" + }, + { + "name": "orders", + "type": { + "vec": { + "defined": "RevenueShareOrder" + } + } + }, + { + "name": "padding1", + "type": "u32" + }, + { + "name": "approvedBuilders", + "type": { + "vec": { + "defined": "BuilderInfo" + } + } } ] } @@ -9069,91 +11497,352 @@ } }, { - "name": "spotFeeStructure", - "type": { - "defined": "FeeStructure" - } + "name": "spotFeeStructure", + "type": { + "defined": "FeeStructure" + } + }, + { + "name": "oracleGuardRails", + "type": { + "defined": "OracleGuardRails" + } + }, + { + "name": "numberOfAuthorities", + "type": "u64" + }, + { + "name": "numberOfSubAccounts", + "type": "u64" + }, + { + "name": "lpCooldownTime", + "type": "u64" + }, + { + "name": "liquidationMarginBufferRatio", + "type": "u32" + }, + { + "name": "settlementDuration", + "type": "u16" + }, + { + "name": "numberOfMarkets", + "type": "u16" + }, + { + "name": "numberOfSpotMarkets", + "type": "u16" + }, + { + "name": "signerNonce", + "type": "u8" + }, + { + "name": "minPerpAuctionDuration", + "type": "u8" + }, + { + "name": "defaultMarketOrderTimeInForce", + "type": "u8" + }, + { + "name": "defaultSpotAuctionDuration", + "type": "u8" + }, + { + "name": "exchangeStatus", + "type": "u8" + }, + { + "name": "liquidationDuration", + "type": "u8" + }, + { + "name": "initialPctToLiquidate", + "type": "u16" + }, + { + "name": "maxNumberOfSubAccounts", + "type": "u16" + }, + { + "name": "maxInitializeUserFee", + "type": "u16" + }, + { + "name": "featureBitFlags", + "type": "u8" + }, + { + "name": "lpPoolFeatureBitFlags", + "type": "u8" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 8 + ] + } + } + ] + } + }, + { + "name": "User", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "docs": [ + "The owner/authority of the account" + ], + "type": "publicKey" + }, + { + "name": "delegate", + "docs": [ + "An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw" + ], + "type": "publicKey" + }, + { + "name": "name", + "docs": [ + "Encoded display name e.g. \"toly\"" + ], + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "spotPositions", + "docs": [ + "The user's spot positions" + ], + "type": { + "array": [ + { + "defined": "SpotPosition" + }, + 8 + ] + } + }, + { + "name": "perpPositions", + "docs": [ + "The user's perp positions" + ], + "type": { + "array": [ + { + "defined": "PerpPosition" + }, + 8 + ] + } + }, + { + "name": "orders", + "docs": [ + "The user's orders" + ], + "type": { + "array": [ + { + "defined": "Order" + }, + 32 + ] + } + }, + { + "name": "lastAddPerpLpSharesTs", + "docs": [ + "The last time the user added perp lp positions" + ], + "type": "i64" + }, + { + "name": "totalDeposits", + "docs": [ + "The total values of deposits the user has made", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalWithdraws", + "docs": [ + "The total values of withdrawals the user has made", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "totalSocialLoss", + "docs": [ + "The total socialized loss the users has incurred upon the protocol", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "settledPerpPnl", + "docs": [ + "Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps", + "precision: QUOTE_PRECISION" + ], + "type": "i64" }, { - "name": "oracleGuardRails", - "type": { - "defined": "OracleGuardRails" - } + "name": "cumulativeSpotFees", + "docs": [ + "Fees (taker fees, maker rebate, filler reward) for spot", + "precision: QUOTE_PRECISION" + ], + "type": "i64" }, { - "name": "numberOfAuthorities", - "type": "u64" + "name": "cumulativePerpFunding", + "docs": [ + "Cumulative funding paid/received for perps", + "precision: QUOTE_PRECISION" + ], + "type": "i64" }, { - "name": "numberOfSubAccounts", + "name": "liquidationMarginFreed", + "docs": [ + "The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time", + "Defaults to zero when not being liquidated", + "precision: QUOTE_PRECISION" + ], "type": "u64" }, { - "name": "lpCooldownTime", + "name": "lastActiveSlot", + "docs": [ + "The last slot a user was active. Used to determine if a user is idle" + ], "type": "u64" }, { - "name": "liquidationMarginBufferRatio", + "name": "nextOrderId", + "docs": [ + "Every user order has an order id. This is the next order id to be used" + ], "type": "u32" }, { - "name": "settlementDuration", - "type": "u16" + "name": "maxMarginRatio", + "docs": [ + "Custom max initial margin ratio for the user" + ], + "type": "u32" }, { - "name": "numberOfMarkets", + "name": "nextLiquidationId", + "docs": [ + "The next liquidation id to be used for user" + ], "type": "u16" }, { - "name": "numberOfSpotMarkets", + "name": "subAccountId", + "docs": [ + "The sub account id for this user" + ], "type": "u16" }, { - "name": "signerNonce", + "name": "status", + "docs": [ + "Whether the user is active, being liquidated or bankrupt" + ], "type": "u8" }, { - "name": "minPerpAuctionDuration", - "type": "u8" + "name": "isMarginTradingEnabled", + "docs": [ + "Whether the user has enabled margin trading" + ], + "type": "bool" }, { - "name": "defaultMarketOrderTimeInForce", - "type": "u8" + "name": "idle", + "docs": [ + "User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows", + "Off-chain keeper bots can ignore users that are idle" + ], + "type": "bool" }, { - "name": "defaultSpotAuctionDuration", + "name": "openOrders", + "docs": [ + "number of open orders" + ], "type": "u8" }, { - "name": "exchangeStatus", - "type": "u8" + "name": "hasOpenOrder", + "docs": [ + "Whether or not user has open order" + ], + "type": "bool" }, { - "name": "liquidationDuration", + "name": "openAuctions", + "docs": [ + "number of open orders with auction" + ], "type": "u8" }, { - "name": "initialPctToLiquidate", - "type": "u16" + "name": "hasOpenAuction", + "docs": [ + "Whether or not user has open order with auction" + ], + "type": "bool" }, { - "name": "maxNumberOfSubAccounts", - "type": "u16" + "name": "marginMode", + "type": { + "defined": "MarginMode" + } }, { - "name": "maxInitializeUserFee", - "type": "u16" + "name": "poolId", + "type": "u8" }, { - "name": "featureBitFlags", - "type": "u8" + "name": "padding1", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "lastFuelBonusUpdateTs", + "type": "u32" }, { "name": "padding", "type": { "array": [ "u8", - 9 + 12 ] } } @@ -9161,577 +11850,629 @@ } }, { - "name": "User", + "name": "UserStats", "type": { "kind": "struct", "fields": [ { "name": "authority", "docs": [ - "The owner/authority of the account" + "The authority for all of a users sub accounts" ], "type": "publicKey" }, { - "name": "delegate", + "name": "referrer", "docs": [ - "An addresses that can control the account on the authority's behalf. Has limited power, cant withdraw" + "The address that referred this user" ], "type": "publicKey" }, { - "name": "name", + "name": "fees", "docs": [ - "Encoded display name e.g. \"toly\"" + "Stats on the fees paid by the user" ], "type": { - "array": [ - "u8", - 32 - ] + "defined": "UserFees" } }, { - "name": "spotPositions", + "name": "nextEpochTs", "docs": [ - "The user's spot positions" + "The timestamp of the next epoch", + "Epoch is used to limit referrer rewards earned in single epoch" ], - "type": { - "array": [ - { - "defined": "SpotPosition" - }, - 8 - ] - } + "type": "i64" }, { - "name": "perpPositions", + "name": "makerVolume30d", "docs": [ - "The user's perp positions" + "Rolling 30day maker volume for user", + "precision: QUOTE_PRECISION" ], - "type": { - "array": [ - { - "defined": "PerpPosition" - }, - 8 - ] - } + "type": "u64" }, { - "name": "orders", + "name": "takerVolume30d", "docs": [ - "The user's orders" + "Rolling 30day taker volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "fillerVolume30d", + "docs": [ + "Rolling 30day filler volume for user", + "precision: QUOTE_PRECISION" + ], + "type": "u64" + }, + { + "name": "lastMakerVolume30dTs", + "docs": [ + "last time the maker volume was updated" + ], + "type": "i64" + }, + { + "name": "lastTakerVolume30dTs", + "docs": [ + "last time the taker volume was updated" + ], + "type": "i64" + }, + { + "name": "lastFillerVolume30dTs", + "docs": [ + "last time the filler volume was updated" + ], + "type": "i64" + }, + { + "name": "ifStakedQuoteAssetAmount", + "docs": [ + "The amount of tokens staked in the quote spot markets if" + ], + "type": "u64" + }, + { + "name": "numberOfSubAccounts", + "docs": [ + "The current number of sub accounts" + ], + "type": "u16" + }, + { + "name": "numberOfSubAccountsCreated", + "docs": [ + "The number of sub accounts created. Can be greater than the number of sub accounts if user", + "has deleted sub accounts" + ], + "type": "u16" + }, + { + "name": "referrerStatus", + "docs": [ + "Flags for referrer status:", + "First bit (LSB): 1 if user is a referrer, 0 otherwise", + "Second bit: 1 if user was referred, 0 otherwise" ], + "type": "u8" + }, + { + "name": "disableUpdatePerpBidAskTwap", + "type": "bool" + }, + { + "name": "padding1", "type": { "array": [ - { - "defined": "Order" - }, - 32 + "u8", + 1 ] } }, { - "name": "lastAddPerpLpSharesTs", + "name": "fuelOverflowStatus", "docs": [ - "The last time the user added perp lp positions" + "whether the user has a FuelOverflow account" ], - "type": "i64" + "type": "u8" }, { - "name": "totalDeposits", + "name": "fuelInsurance", "docs": [ - "The total values of deposits the user has made", - "precision: QUOTE_PRECISION" + "accumulated fuel for token amounts of insurance" ], - "type": "u64" + "type": "u32" }, { - "name": "totalWithdraws", + "name": "fuelDeposits", "docs": [ - "The total values of withdrawals the user has made", - "precision: QUOTE_PRECISION" + "accumulated fuel for notional of deposits" ], - "type": "u64" + "type": "u32" }, { - "name": "totalSocialLoss", + "name": "fuelBorrows", "docs": [ - "The total socialized loss the users has incurred upon the protocol", - "precision: QUOTE_PRECISION" + "accumulate fuel bonus for notional of borrows" ], - "type": "u64" + "type": "u32" }, { - "name": "settledPerpPnl", + "name": "fuelPositions", "docs": [ - "Fees (taker fees, maker rebate, referrer reward, filler reward) and pnl for perps", - "precision: QUOTE_PRECISION" + "accumulated fuel for perp open interest" ], - "type": "i64" + "type": "u32" }, { - "name": "cumulativeSpotFees", + "name": "fuelTaker", "docs": [ - "Fees (taker fees, maker rebate, filler reward) for spot", - "precision: QUOTE_PRECISION" + "accumulate fuel bonus for taker volume" ], - "type": "i64" + "type": "u32" }, { - "name": "cumulativePerpFunding", + "name": "fuelMaker", "docs": [ - "Cumulative funding paid/received for perps", - "precision: QUOTE_PRECISION" + "accumulate fuel bonus for maker volume" ], - "type": "i64" + "type": "u32" }, { - "name": "liquidationMarginFreed", + "name": "ifStakedGovTokenAmount", "docs": [ - "The amount of margin freed during liquidation. Used to force the liquidation to occur over a period of time", - "Defaults to zero when not being liquidated", - "precision: QUOTE_PRECISION" + "The amount of tokens staked in the governance spot markets if" ], "type": "u64" }, { - "name": "lastActiveSlot", + "name": "lastFuelIfBonusUpdateTs", "docs": [ - "The last slot a user was active. Used to determine if a user is idle" + "last unix ts user stats data was used to update if fuel (u32 to save space)" ], - "type": "u64" + "type": "u32" }, { - "name": "nextOrderId", + "name": "padding", + "type": { + "array": [ + "u8", + 12 + ] + } + } + ] + } + }, + { + "name": "ReferrerName", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "user", + "type": "publicKey" + }, + { + "name": "userStats", + "type": "publicKey" + }, + { + "name": "name", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "FuelOverflow", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", "docs": [ - "Every user order has an order id. This is the next order id to be used" + "The authority of this overflow account" ], + "type": "publicKey" + }, + { + "name": "fuelInsurance", + "type": "u128" + }, + { + "name": "fuelDeposits", + "type": "u128" + }, + { + "name": "fuelBorrows", + "type": "u128" + }, + { + "name": "fuelPositions", + "type": "u128" + }, + { + "name": "fuelTaker", + "type": "u128" + }, + { + "name": "fuelMaker", + "type": "u128" + }, + { + "name": "lastFuelSweepTs", "type": "u32" }, { - "name": "maxMarginRatio", - "docs": [ - "Custom max initial margin ratio for the user" - ], + "name": "lastResetTs", "type": "u32" }, { - "name": "nextLiquidationId", - "docs": [ - "The next liquidation id to be used for user" - ], - "type": "u16" + "name": "padding", + "type": { + "array": [ + "u128", + 6 + ] + } + } + ] + } + } + ], + "types": [ + { + "name": "UpdatePerpMarketSummaryStatsParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "quoteAssetAmountWithUnsettledLp", + "type": { + "option": "i64" + } }, { - "name": "subAccountId", - "docs": [ - "The sub account id for this user" - ], - "type": "u16" + "name": "netUnsettledFundingPnl", + "type": { + "option": "i64" + } }, { - "name": "status", - "docs": [ - "Whether the user is active, being liquidated or bankrupt" - ], - "type": "u8" + "name": "updateAmmSummaryStats", + "type": { + "option": "bool" + } }, { - "name": "isMarginTradingEnabled", - "docs": [ - "Whether the user has enabled margin trading" - ], - "type": "bool" + "name": "excludeTotalLiqFee", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "ConstituentParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "maxWeightDeviation", + "type": { + "option": "i64" + } }, { - "name": "idle", - "docs": [ - "User is idle if they haven't interacted with the protocol in 1 week and they have no orders, perp positions or borrows", - "Off-chain keeper bots can ignore users that are idle" - ], - "type": "bool" + "name": "swapFeeMin", + "type": { + "option": "i64" + } }, { - "name": "openOrders", - "docs": [ - "number of open orders" - ], - "type": "u8" + "name": "swapFeeMax", + "type": { + "option": "i64" + } }, { - "name": "hasOpenOrder", - "docs": [ - "Whether or not user has open order" - ], - "type": "bool" + "name": "maxBorrowTokenAmount", + "type": { + "option": "u64" + } }, { - "name": "openAuctions", - "docs": [ - "number of open orders with auction" - ], - "type": "u8" + "name": "oracleStalenessThreshold", + "type": { + "option": "u64" + } }, { - "name": "hasOpenAuction", - "docs": [ - "Whether or not user has open order with auction" - ], - "type": "bool" + "name": "costToTradeBps", + "type": { + "option": "i32" + } + }, + { + "name": "constituentDerivativeIndex", + "type": { + "option": "i16" + } }, { - "name": "marginMode", + "name": "derivativeWeight", "type": { - "defined": "MarginMode" + "option": "u64" } }, { - "name": "poolId", - "type": "u8" + "name": "volatility", + "type": { + "option": "u64" + } }, { - "name": "padding1", + "name": "gammaExecution", "type": { - "array": [ - "u8", - 3 - ] + "option": "u8" } }, { - "name": "lastFuelBonusUpdateTs", - "type": "u32" + "name": "gammaInventory", + "type": { + "option": "u8" + } }, { - "name": "padding", + "name": "xi", "type": { - "array": [ - "u8", - 12 - ] + "option": "u8" } } ] } }, { - "name": "UserStats", + "name": "LpPoolParams", "type": { "kind": "struct", "fields": [ { - "name": "authority", - "docs": [ - "The authority for all of a users sub accounts" - ], - "type": "publicKey" - }, - { - "name": "referrer", - "docs": [ - "The address that referred this user" - ], - "type": "publicKey" - }, - { - "name": "fees", - "docs": [ - "Stats on the fees paid by the user" - ], + "name": "maxSettleQuoteAmount", "type": { - "defined": "UserFees" + "option": "u64" } }, { - "name": "nextEpochTs", - "docs": [ - "The timestamp of the next epoch", - "Epoch is used to limit referrer rewards earned in single epoch" - ], - "type": "i64" - }, - { - "name": "makerVolume30d", - "docs": [ - "Rolling 30day maker volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" - }, - { - "name": "takerVolume30d", - "docs": [ - "Rolling 30day taker volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "volatility", + "type": { + "option": "u64" + } }, { - "name": "fillerVolume30d", - "docs": [ - "Rolling 30day filler volume for user", - "precision: QUOTE_PRECISION" - ], - "type": "u64" + "name": "gammaExecution", + "type": { + "option": "u8" + } }, { - "name": "lastMakerVolume30dTs", - "docs": [ - "last time the maker volume was updated" - ], - "type": "i64" + "name": "xi", + "type": { + "option": "u8" + } }, { - "name": "lastTakerVolume30dTs", - "docs": [ - "last time the taker volume was updated" - ], - "type": "i64" + "name": "maxAum", + "type": { + "option": "u128" + } }, { - "name": "lastFillerVolume30dTs", - "docs": [ - "last time the filler volume was updated" - ], - "type": "i64" - }, + "name": "whitelistMint", + "type": { + "option": "publicKey" + } + } + ] + } + }, + { + "name": "OverrideAmmCacheParams", + "type": { + "kind": "struct", + "fields": [ { - "name": "ifStakedQuoteAssetAmount", - "docs": [ - "The amount of tokens staked in the quote spot markets if" - ], - "type": "u64" + "name": "quoteOwedFromLpPool", + "type": { + "option": "i64" + } }, { - "name": "numberOfSubAccounts", - "docs": [ - "The current number of sub accounts" - ], - "type": "u16" + "name": "lastSettleSlot", + "type": { + "option": "u64" + } }, { - "name": "numberOfSubAccountsCreated", - "docs": [ - "The number of sub accounts created. Can be greater than the number of sub accounts if user", - "has deleted sub accounts" - ], - "type": "u16" + "name": "lastFeePoolTokenAmount", + "type": { + "option": "u128" + } }, { - "name": "referrerStatus", - "docs": [ - "Flags for referrer status:", - "First bit (LSB): 1 if user is a referrer, 0 otherwise", - "Second bit: 1 if user was referred, 0 otherwise" - ], - "type": "u8" + "name": "lastNetPnlPoolTokenAmount", + "type": { + "option": "i128" + } }, { - "name": "disableUpdatePerpBidAskTwap", - "type": "bool" + "name": "ammPositionScalar", + "type": { + "option": "u8" + } }, { - "name": "padding1", + "name": "ammInventoryLimit", "type": { - "array": [ - "u8", - 1 - ] + "option": "i64" } - }, + } + ] + } + }, + { + "name": "AddAmmConstituentMappingDatum", + "type": { + "kind": "struct", + "fields": [ { - "name": "fuelOverflowStatus", - "docs": [ - "whether the user has a FuelOverflow account" - ], - "type": "u8" + "name": "constituentIndex", + "type": "u16" }, { - "name": "fuelInsurance", - "docs": [ - "accumulated fuel for token amounts of insurance" - ], - "type": "u32" + "name": "perpMarketIndex", + "type": "u16" }, { - "name": "fuelDeposits", - "docs": [ - "accumulated fuel for notional of deposits" - ], - "type": "u32" + "name": "weight", + "type": "i64" + } + ] + } + }, + { + "name": "CacheInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "oracle", + "type": "publicKey" }, { - "name": "fuelBorrows", - "docs": [ - "accumulate fuel bonus for notional of borrows" - ], - "type": "u32" + "name": "lastFeePoolTokenAmount", + "type": "u128" }, { - "name": "fuelPositions", - "docs": [ - "accumulated fuel for perp open interest" - ], - "type": "u32" + "name": "lastNetPnlPoolTokenAmount", + "type": "i128" }, { - "name": "fuelTaker", - "docs": [ - "accumulate fuel bonus for taker volume" - ], - "type": "u32" + "name": "lastExchangeFees", + "type": "u128" }, { - "name": "fuelMaker", - "docs": [ - "accumulate fuel bonus for maker volume" - ], - "type": "u32" + "name": "lastSettleAmmExFees", + "type": "u128" }, { - "name": "ifStakedGovTokenAmount", - "docs": [ - "The amount of tokens staked in the governance spot markets if" - ], - "type": "u64" + "name": "lastSettleAmmPnl", + "type": "i128" }, { - "name": "lastFuelIfBonusUpdateTs", + "name": "position", "docs": [ - "last unix ts user stats data was used to update if fuel (u32 to save space)" + "BASE PRECISION" ], - "type": "u32" - }, - { - "name": "padding", - "type": { - "array": [ - "u8", - 12 - ] - } - } - ] - } - }, - { - "name": "ReferrerName", - "type": { - "kind": "struct", - "fields": [ - { - "name": "authority", - "type": "publicKey" + "type": "i64" }, { - "name": "user", - "type": "publicKey" + "name": "slot", + "type": "u64" }, { - "name": "userStats", - "type": "publicKey" + "name": "lastSettleAmount", + "type": "u64" }, { - "name": "name", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "FuelOverflow", - "type": { - "kind": "struct", - "fields": [ + "name": "lastSettleSlot", + "type": "u64" + }, { - "name": "authority", - "docs": [ - "The authority of this overflow account" - ], - "type": "publicKey" + "name": "lastSettleTs", + "type": "i64" }, { - "name": "fuelInsurance", - "type": "u128" + "name": "quoteOwedFromLpPool", + "type": "i64" }, { - "name": "fuelDeposits", - "type": "u128" + "name": "ammInventoryLimit", + "type": "i64" }, { - "name": "fuelBorrows", - "type": "u128" + "name": "oraclePrice", + "type": "i64" }, { - "name": "fuelPositions", - "type": "u128" + "name": "oracleSlot", + "type": "u64" }, { - "name": "fuelTaker", - "type": "u128" + "name": "oracleSource", + "type": "u8" }, { - "name": "fuelMaker", - "type": "u128" + "name": "oracleValidity", + "type": "u8" }, { - "name": "lastFuelSweepTs", - "type": "u32" + "name": "lpStatusForPerpMarket", + "type": "u8" }, { - "name": "lastResetTs", - "type": "u32" + "name": "ammPositionScalar", + "type": "u8" }, { "name": "padding", "type": { "array": [ - "u128", - 6 + "u8", + 36 ] } } ] } - } - ], - "types": [ + }, { - "name": "UpdatePerpMarketSummaryStatsParams", + "name": "AmmCacheFixed", "type": { "kind": "struct", "fields": [ { - "name": "quoteAssetAmountWithUnsettledLp", - "type": { - "option": "i64" - } - }, - { - "name": "netUnsettledFundingPnl", - "type": { - "option": "i64" - } + "name": "bump", + "type": "u8" }, { - "name": "updateAmmSummaryStats", + "name": "pad", "type": { - "option": "bool" + "array": [ + "u8", + 3 + ] } }, { - "name": "excludeTotalLiqFee", - "type": { - "option": "bool" - } + "name": "len", + "type": "u32" } ] } @@ -9960,41 +12701,253 @@ } }, { - "name": "IfRebalanceConfigParams", + "name": "IfRebalanceConfigParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "totalInAmount", + "type": "u64" + }, + { + "name": "epochMaxInAmount", + "type": "u64" + }, + { + "name": "epochDuration", + "type": "i64" + }, + { + "name": "outMarketIndex", + "type": "u16" + }, + { + "name": "inMarketIndex", + "type": "u16" + }, + { + "name": "maxSlippageBps", + "type": "u16" + }, + { + "name": "swapMode", + "type": "u8" + }, + { + "name": "status", + "type": "u8" + } + ] + } + }, + { + "name": "ConstituentSpotBalance", + "type": { + "kind": "struct", + "fields": [ + { + "name": "scaledBalance", + "docs": [ + "The scaled balance of the position. To get the token amount, multiply by the cumulative deposit/borrow", + "interest of corresponding market.", + "precision: token precision" + ], + "type": "u128" + }, + { + "name": "cumulativeDeposits", + "docs": [ + "The cumulative deposits/borrows a user has made into a market", + "precision: token mint precision" + ], + "type": "i64" + }, + { + "name": "marketIndex", + "docs": [ + "The market index of the corresponding spot market" + ], + "type": "u16" + }, + { + "name": "balanceType", + "docs": [ + "Whether the position is deposit or borrow" + ], + "type": { + "defined": "SpotBalanceType" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 5 + ] + } + } + ] + } + }, + { + "name": "AmmConstituentDatum", + "type": { + "kind": "struct", + "fields": [ + { + "name": "perpMarketIndex", + "type": "u16" + }, + { + "name": "constituentIndex", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "lastSlot", + "type": "u64" + }, + { + "name": "weight", + "docs": [ + "PERCENTAGE_PRECISION. The weight this constituent has on the perp market" + ], + "type": "i64" + } + ] + } + }, + { + "name": "AmmConstituentMappingFixed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "lpPool", + "type": "publicKey" + }, + { + "name": "bump", + "type": "u8" + }, + { + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } + }, + { + "name": "len", + "type": "u32" + } + ] + } + }, + { + "name": "TargetsDatum", + "type": { + "kind": "struct", + "fields": [ + { + "name": "costToTradeBps", + "type": "i32" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 4 + ] + } + }, + { + "name": "targetBase", + "type": "i64" + }, + { + "name": "lastOracleSlot", + "type": "u64" + }, + { + "name": "lastPositionSlot", + "type": "u64" + } + ] + } + }, + { + "name": "ConstituentTargetBaseFixed", "type": { "kind": "struct", "fields": [ { - "name": "totalInAmount", - "type": "u64" + "name": "lpPool", + "type": "publicKey" }, { - "name": "epochMaxInAmount", - "type": "u64" + "name": "bump", + "type": "u8" }, { - "name": "epochDuration", - "type": "i64" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "outMarketIndex", - "type": "u16" - }, + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" + } + ] + } + }, + { + "name": "ConstituentCorrelationsFixed", + "type": { + "kind": "struct", + "fields": [ { - "name": "inMarketIndex", - "type": "u16" + "name": "lpPool", + "type": "publicKey" }, { - "name": "maxSlippageBps", - "type": "u16" + "name": "bump", + "type": "u8" }, { - "name": "swapMode", - "type": "u8" + "name": "pad", + "type": { + "array": [ + "u8", + 3 + ] + } }, { - "name": "status", - "type": "u8" + "name": "len", + "docs": [ + "total elements in the flattened `data` vec" + ], + "type": "u32" } ] } @@ -10281,6 +13234,24 @@ "type": { "option": "u16" } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "type": { + "option": "u16" + } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -10334,6 +13305,24 @@ "type": { "option": "u16" } + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + } + }, + { + "name": "builderFeeTenthBps", + "type": { + "option": "u16" + } + }, + { + "name": "isolatedPositionDeposit", + "type": { + "option": "u64" + } } ] } @@ -11129,7 +14118,7 @@ "type": "i8" }, { - "name": "takerSpeedBumpOverride", + "name": "oracleLowRiskSlotDelayOverride", "docs": [ "the override for the state.min_perp_auction_duration", "0 is no override, -1 is disable speed bump, 1-100 is literal speed bump" @@ -11170,12 +14159,16 @@ ], "type": "i8" }, + { + "name": "referencePriceOffsetDeadbandPct", + "type": "u8" + }, { "name": "padding", "type": { "array": [ "u8", - 3 + 2 ] } }, @@ -11186,6 +14179,157 @@ ] } }, + { + "name": "RevenueShareOrder", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feesAccrued", + "docs": [ + "fees accrued so far for this order slot. This is not exclusively fees from this order_id", + "and may include fees from other orders in the same market. This may be swept to the", + "builder's SpotPosition during settle_pnl." + ], + "type": "u64" + }, + { + "name": "orderId", + "docs": [ + "the order_id of the current active order in this slot. It's only relevant while bit_flag = Open" + ], + "type": "u32" + }, + { + "name": "feeTenthBps", + "docs": [ + "the builder fee on this order, in tenths of a bps, e.g. 100 = 0.01%" + ], + "type": "u16" + }, + { + "name": "marketIndex", + "type": "u16" + }, + { + "name": "subAccountId", + "docs": [ + "the subaccount_id of the user who created this order. It's only relevant while bit_flag = Open" + ], + "type": "u16" + }, + { + "name": "builderIdx", + "docs": [ + "the index of the RevenueShareEscrow.approved_builders list, that this order's fee will settle to. Ignored", + "if bit_flag = Referral." + ], + "type": "u8" + }, + { + "name": "bitFlags", + "docs": [ + "bitflags that describe the state of the order.", + "[`RevenueShareOrderBitFlag::Init`]: this order slot is available for use.", + "[`RevenueShareOrderBitFlag::Open`]: this order slot is occupied, `order_id` is the `sub_account_id`'s active order.", + "[`RevenueShareOrderBitFlag::Completed`]: this order has been filled or canceled, and is waiting to be settled into.", + "the builder's account order_id and sub_account_id are no longer relevant, it may be merged with other orders.", + "[`RevenueShareOrderBitFlag::Referral`]: this order stores referral rewards waiting to be settled for this market.", + "If it is set, no other bitflag should be set." + ], + "type": "u8" + }, + { + "name": "userOrderIndex", + "docs": [ + "the index into the User's orders list when this RevenueShareOrder was created, make sure to verify that order_id matches." + ], + "type": "u8" + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + } + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 10 + ] + } + } + ] + } + }, + { + "name": "BuilderInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "maxFeeTenthBps", + "type": "u16" + }, + { + "name": "padding", + "type": { + "array": [ + "u8", + 6 + ] + } + } + ] + } + }, + { + "name": "RevenueShareEscrowFixed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "publicKey" + }, + { + "name": "referrer", + "type": "publicKey" + }, + { + "name": "referrerBoostExpireTs", + "type": "u32" + }, + { + "name": "referrerRewardOffset", + "type": "i8" + }, + { + "name": "refereeFeeNumeratorOffset", + "type": "i8" + }, + { + "name": "referrerBoostNumerator", + "type": "i8" + }, + { + "name": "reservedFixed", + "type": { + "array": [ + "u8", + 17 + ] + } + } + ] + } + }, { "name": "SignedMsgOrderId", "type": { @@ -12020,6 +15164,23 @@ ] } }, + { + "name": "SettlementDirection", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ToLpPool" + }, + { + "name": "FromLpPool" + }, + { + "name": "None" + } + ] + } + }, { "name": "MarginRequirementType", "type": { @@ -12058,7 +15219,17 @@ "name": "InsufficientDataPoints" }, { - "name": "StaleForAMM" + "name": "StaleForAMM", + "fields": [ + { + "name": "immediate", + "type": "bool" + }, + { + "name": "lowRisk", + "type": "bool" + } + ] }, { "name": "Valid" @@ -12084,7 +15255,10 @@ "name": "FillOrderMatch" }, { - "name": "FillOrderAmm" + "name": "FillOrderAmmLowRisk" + }, + { + "name": "FillOrderAmmImmediate" }, { "name": "Liquidate" @@ -12103,6 +15277,15 @@ }, { "name": "UseMMOraclePrice" + }, + { + "name": "UpdateAmmCache" + }, + { + "name": "UpdateLpPoolAum" + }, + { + "name": "LpPoolSwap" } ] } @@ -12123,6 +15306,9 @@ }, { "name": "SafeMMOracle" + }, + { + "name": "Margin" } ] } @@ -12374,6 +15560,9 @@ }, { "name": "StakeTransfer" + }, + { + "name": "AdminDeposit" } ] } @@ -12419,27 +15608,41 @@ "name": "Match", "fields": [ "publicKey", - "u16", - "u64" + "u16", + "u64" + ] + } + ] + } + }, + { + "name": "SpotFulfillmentMethod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "ExternalMarket" + }, + { + "name": "Match", + "fields": [ + "publicKey", + "u16" ] } ] } }, { - "name": "SpotFulfillmentMethod", + "name": "ConstituentStatus", "type": { "kind": "enum", "variants": [ { - "name": "ExternalMarket" + "name": "ReduceOnly" }, { - "name": "Match", - "fields": [ - "publicKey", - "u16" - ] + "name": "Decommissioned" } ] } @@ -12658,6 +15861,37 @@ ] } }, + { + "name": "PerpLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "TrackAmmRevenue" + }, + { + "name": "SettleQuoteOwed" + } + ] + } + }, + { + "name": "ConstituentLpOperation", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Swap" + }, + { + "name": "Deposit" + }, + { + "name": "Withdraw" + } + ] + } + }, { "name": "MarketStatus", "type": { @@ -12693,6 +15927,23 @@ ] } }, + { + "name": "LpStatus", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Uncollateralized" + }, + { + "name": "Active" + }, + { + "name": "Decommissioning" + } + ] + } + }, { "name": "ContractType", "type": { @@ -12737,18 +15988,21 @@ } }, { - "name": "AMMAvailability", + "name": "RevenueShareOrderBitFlag", "type": { "kind": "enum", "variants": [ { - "name": "Immediate" + "name": "Init" + }, + { + "name": "Open" }, { - "name": "AfterMinDuration" + "name": "Completed" }, { - "name": "Unavailable" + "name": "Referral" } ] } @@ -12874,6 +16128,29 @@ }, { "name": "MedianTriggerPrice" + }, + { + "name": "BuilderCodes" + }, + { + "name": "BuilderReferral" + } + ] + } + }, + { + "name": "LpPoolFeatureBitFlags", + "type": { + "kind": "enum", + "variants": [ + { + "name": "SettleLpPool" + }, + { + "name": "SwapLpPool" + }, + { + "name": "MintRedeemLpPool" } ] } @@ -13008,6 +16285,9 @@ }, { "name": "NewTriggerReduceOnly" + }, + { + "name": "HasBuilder" } ] } @@ -13039,6 +16319,9 @@ }, { "name": "IsReferred" + }, + { + "name": "BuilderReferral" } ] } @@ -13828,6 +17111,20 @@ "option": "u64" }, "index": false + }, + { + "name": "builderIdx", + "type": { + "option": "u8" + }, + "index": false + }, + { + "name": "builderFee", + "type": { + "option": "u64" + }, + "index": false } ] }, @@ -14550,6 +17847,62 @@ } ] }, + { + "name": "RevenueShareSettleRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "builder", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "referrer", + "type": { + "option": "publicKey" + }, + "index": false + }, + { + "name": "feeSettled", + "type": "u64", + "index": false + }, + { + "name": "marketIndex", + "type": "u16", + "index": false + }, + { + "name": "marketType", + "type": { + "defined": "MarketType" + }, + "index": false + }, + { + "name": "builderSubAccountId", + "type": "u16", + "index": false + }, + { + "name": "builderTotalReferrerRewards", + "type": "u64", + "index": false + }, + { + "name": "builderTotalBuilderRewards", + "type": "u64", + "index": false + } + ] + }, { "name": "LPSettleRecord", "fields": [ @@ -14607,6 +17960,11 @@ "name": "lpPrice", "type": "u128", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] }, @@ -14717,6 +18075,11 @@ "name": "outSwapId", "type": "u64", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] }, @@ -14812,6 +18175,68 @@ "name": "inMarketTargetWeight", "type": "i64", "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "LPBorrowLendDepositRecord", + "fields": [ + { + "name": "ts", + "type": "i64", + "index": false + }, + { + "name": "slot", + "type": "u64", + "index": false + }, + { + "name": "spotMarketIndex", + "type": "u16", + "index": false + }, + { + "name": "constituentIndex", + "type": "u16", + "index": false + }, + { + "name": "direction", + "type": { + "defined": "DepositDirection" + }, + "index": false + }, + { + "name": "tokenBalance", + "type": "i64", + "index": false + }, + { + "name": "lastTokenBalance", + "type": "i64", + "index": false + }, + { + "name": "interestAccruedTokenAmount", + "type": "i64", + "index": false + }, + { + "name": "amountDepositWithdraw", + "type": "u64", + "index": false + }, + { + "name": "lpPool", + "type": "publicKey", + "index": false } ] } @@ -16406,9 +19831,141 @@ "code": 6317, "name": "InvalidIsolatedPerpMarket", "msg": "Invalid Isolated Perp Market" + }, + { + "code": 6318, + "name": "InvalidRevenueShareResize", + "msg": "Invalid RevenueShare resize" + }, + { + "code": 6319, + "name": "BuilderRevoked", + "msg": "Builder has been revoked" + }, + { + "code": 6320, + "name": "InvalidBuilderFee", + "msg": "Builder fee is greater than max fee bps" + }, + { + "code": 6321, + "name": "RevenueShareEscrowAuthorityMismatch", + "msg": "RevenueShareEscrow authority mismatch" + }, + { + "code": 6322, + "name": "RevenueShareEscrowOrdersAccountFull", + "msg": "RevenueShareEscrow has too many active orders" + }, + { + "code": 6323, + "name": "InvalidRevenueShareAccount", + "msg": "Invalid RevenueShareAccount" + }, + { + "code": 6324, + "name": "CannotRevokeBuilderWithOpenOrders", + "msg": "Cannot revoke builder with open orders" + }, + { + "code": 6325, + "name": "UnableToLoadRevenueShareAccount", + "msg": "Unable to load builder account" + }, + { + "code": 6325, + "name": "InvalidConstituent", + "msg": "Invalid Constituent" + }, + { + "code": 6326, + "name": "InvalidAmmConstituentMappingArgument", + "msg": "Invalid Amm Constituent Mapping argument" + }, + { + "code": 6327, + "name": "ConstituentNotFound", + "msg": "Constituent not found" + }, + { + "code": 6328, + "name": "ConstituentCouldNotLoad", + "msg": "Constituent could not load" + }, + { + "code": 6329, + "name": "ConstituentWrongMutability", + "msg": "Constituent wrong mutability" + }, + { + "code": 6330, + "name": "WrongNumberOfConstituents", + "msg": "Wrong number of constituents passed to instruction" + }, + { + "code": 6331, + "name": "InsufficientConstituentTokenBalance", + "msg": "Insufficient constituent token balance" + }, + { + "code": 6332, + "name": "AMMCacheStale", + "msg": "Amm Cache data too stale" + }, + { + "code": 6333, + "name": "LpPoolAumDelayed", + "msg": "LP Pool AUM not updated recently" + }, + { + "code": 6334, + "name": "ConstituentOracleStale", + "msg": "Constituent oracle is stale" + }, + { + "code": 6335, + "name": "LpInvariantFailed", + "msg": "LP Invariant failed" + }, + { + "code": 6336, + "name": "InvalidConstituentDerivativeWeights", + "msg": "Invalid constituent derivative weights" + }, + { + "code": 6337, + "name": "MaxDlpAumBreached", + "msg": "Max DLP AUM Breached" + }, + { + "code": 6338, + "name": "SettleLpPoolDisabled", + "msg": "Settle Lp Pool Disabled" + }, + { + "code": 6339, + "name": "MintRedeemLpPoolDisabled", + "msg": "Mint/Redeem Lp Pool Disabled" + }, + { + "code": 6340, + "name": "LpPoolSettleInvariantBreached", + "msg": "Settlement amount exceeded" + }, + { + "code": 6341, + "name": "InvalidConstituentOperation", + "msg": "Invalid constituent operation" + }, + { + "code": 6342, + "name": "Unauthorized", + "msg": "Unauthorized for operation" + }, + { + "code": 6343, + "name": "InvalidLpPoolId", + "msg": "Invalid Lp Pool Id for Operation" } - ], - "metadata": { - "address": "dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH" - } + ] } \ No newline at end of file diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 7f30e2afa0..6c73a0de63 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -29,6 +29,7 @@ export * from './accounts/pollingInsuranceFundStakeAccountSubscriber'; export * from './accounts/pollingHighLeverageModeConfigAccountSubscriber'; export * from './accounts/basicUserAccountSubscriber'; export * from './accounts/oneShotUserAccountSubscriber'; +export * from './accounts/oneShotUserStatsAccountSubscriber'; export * from './accounts/types'; export * from './addresses/pda'; export * from './adminClient'; @@ -52,7 +53,10 @@ export * from './events/webSocketLogProvider'; export * from './events/parse'; export * from './events/pollingLogProvider'; export * from './jupiter/jupiterClient'; +// Primary swap client interface - use this for all swap operations +export * from './swap/UnifiedSwapClient'; export * from './math/auction'; +export * from './math/builder'; export * from './math/spotMarket'; export * from './math/conversion'; export * from './math/exchangeStatus'; @@ -121,6 +125,7 @@ export * from './dlob/orderBookLevels'; export * from './userMap/userMap'; export * from './userMap/referrerMap'; export * from './userMap/userStatsMap'; +export * from './userMap/revenueShareEscrowMap'; export * from './userMap/userMapConfig'; export * from './math/bankruptcy'; export * from './orderSubscriber'; @@ -136,5 +141,6 @@ export * from './clock/clockSubscriber'; export * from './math/userStatus'; export * from './indicative-quotes/indicativeQuotesSender'; export * from './constants'; +export * from './constituentMap/constituentMap'; export { BN, PublicKey, pyth }; diff --git a/sdk/src/isomorphic/grpc.node.ts b/sdk/src/isomorphic/grpc.node.ts index 4d58f734d1..267a81c8a6 100644 --- a/sdk/src/isomorphic/grpc.node.ts +++ b/sdk/src/isomorphic/grpc.node.ts @@ -2,17 +2,36 @@ import type Client from '@triton-one/yellowstone-grpc'; import type { SubscribeRequest, SubscribeUpdate, - CommitmentLevel, } from '@triton-one/yellowstone-grpc'; -import { ClientDuplexStream, ChannelOptions } from '@grpc/grpc-js'; +import { CommitmentLevel } from '@triton-one/yellowstone-grpc'; +import type { ClientDuplexStream, ChannelOptions } from '@grpc/grpc-js'; + +import { + CommitmentLevel as LaserCommitmentLevel, + subscribe as LaserSubscribe, + CompressionAlgorithms, +} from 'helius-laserstream'; +import type { + LaserstreamConfig, + SubscribeRequest as LaserSubscribeRequest, + SubscribeUpdate as LaserSubscribeUpdate, +} from 'helius-laserstream'; export { + CommitmentLevel, + Client, + LaserSubscribe, + LaserCommitmentLevel, + CompressionAlgorithms, +}; +export type { ClientDuplexStream, ChannelOptions, SubscribeRequest, SubscribeUpdate, - CommitmentLevel, - Client, + LaserstreamConfig, + LaserSubscribeRequest, + LaserSubscribeUpdate, }; // Export a function to create a new Client instance diff --git a/sdk/src/jupiter/jupiterClient.ts b/sdk/src/jupiter/jupiterClient.ts index e943db5851..a2a2ba483b 100644 --- a/sdk/src/jupiter/jupiterClient.ts +++ b/sdk/src/jupiter/jupiterClient.ts @@ -8,8 +8,7 @@ import { } from '@solana/web3.js'; import fetch from 'node-fetch'; import { BN } from '@coral-xyz/anchor'; - -export type SwapMode = 'ExactIn' | 'ExactOut'; +import { SwapMode } from '../swap/UnifiedSwapClient'; export interface MarketInfo { id: string; diff --git a/sdk/src/margin/README.md b/sdk/src/margin/README.md new file mode 100644 index 0000000000..b074e96ff2 --- /dev/null +++ b/sdk/src/margin/README.md @@ -0,0 +1,143 @@ +## Margin Calculation Snapshot (SDK) + +This document describes the single-source-of-truth margin engine in the SDK that mirrors the on-chain `MarginCalculation` and related semantics. The goal is to compute an immutable snapshot in one pass and have existing `User` getters delegate to it, eliminating duplicative work across getters and UI hooks while maintaining parity with the program. + +### Alignment with on-chain + +- The SDK snapshot shape mirrors `programs/drift/src/state/margin_calculation.rs` field-for-field. +- The inputs and ordering mirror `calculate_margin_requirement_and_total_collateral_and_liability_info` in `programs/drift/src/math/margin.rs`. +- Isolated positions are represented as `isolated_margin_calculations` keyed by perp `market_index`, matching program logic. + +### Core SDK types (shape parity) + +```ts +// Types reflect on-chain names and numeric signs +export type MarginRequirementType = 'Initial' | 'Fill' | 'Maintenance'; +export type MarketType = 'Spot' | 'Perp'; + +export type MarketIdentifier = { + marketType: MarketType; + marketIndex: number; // u16 +}; + +export type MarginCalculationMode = + | { kind: 'Standard' } + | { kind: 'Liquidation'; marketToTrackMarginRequirement?: MarketIdentifier }; + +export type MarginContext = { + marginType: MarginRequirementType; + mode: MarginCalculationMode; + strict: boolean; + ignoreInvalidDepositOracles: boolean; + marginBuffer: BN; // u128 + fuelBonusNumerator: number; // i64 + fuelBonus: number; // u64 + fuelPerpDelta?: { marketIndex: number; delta: BN }; // (u16, i64) + fuelSpotDeltas: Array<{ marketIndex: number; delta: BN }>; // up to 2 entries + marginRatioOverride?: number; // u32 +}; + +export type IsolatedMarginCalculation = { + marginRequirement: BN; // u128 + totalCollateral: BN; // i128 + totalCollateralBuffer: BN; // i128 + marginRequirementPlusBuffer: BN; // u128 +}; + +export type MarginCalculation = { + context: MarginContext; + + totalCollateral: BN; // i128 + totalCollateralBuffer: BN; // i128 + marginRequirement: BN; // u128 + marginRequirementPlusBuffer: BN; // u128 + + isolatedMarginCalculations: Map; // BTreeMap + + numSpotLiabilities: number; // u8 + numPerpLiabilities: number; // u8 + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; + + totalSpotAssetValue: BN; // i128 + totalSpotLiabilityValue: BN; // u128 + totalPerpLiabilityValue: BN; // u128 + totalPerpPnl: BN; // i128 + + trackedMarketMarginRequirement: BN; // u128 + fuelDeposits: number; // u32 + fuelBorrows: number; // u32 + fuelPositions: number; // u32 +}; +``` + +### Engine API + +```ts +// Pure computation, no I/O; uses data already cached in the client/subscribers +export function computeMarginCalculation(user: User, context: MarginContext): MarginCalculation; + +// Helpers that mirror on-chain semantics +export function meets_margin_requirement(calc: MarginCalculation): boolean; +export function meets_margin_requirement_with_buffer(calc: MarginCalculation): boolean; +export function get_cross_free_collateral(calc: MarginCalculation): BN; +export function get_isolated_free_collateral(calc: MarginCalculation, marketIndex: number): BN; +export function cross_margin_shortage(calc: MarginCalculation): BN; // requires buffer mode +export function isolated_margin_shortage(calc: MarginCalculation, marketIndex: number): BN; // requires buffer mode +``` + +### Computation model (on-demand) + +- The SDK computes the snapshot on-demand when `getMarginCalculation(...)` is called. +- No event-driven recomputation by default (oracle prices can change every slot; recomputing every update would be wasteful). +- Callers (UI/bots) decide polling frequency (e.g., UI can refresh every ~1s on active trade forms). + +### User integration + +- Add `user.getMarginCalculation(margin_type = 'Initial', overrides?: Partial)`. +- Existing getters delegate to the snapshot to avoid duplicate work: + - `getTotalCollateral()` → `snapshot.total_collateral` + - `getMarginRequirement(mode)` → `snapshot.margin_requirement` + - `getFreeCollateral()` → `get_cross_free_collateral(snapshot)` + - Per-market isolated FC → `get_isolated_free_collateral(snapshot, marketIndex)` + +Suggested `User` API surface (non-breaking): + +```ts +// Primary entrypoint +getMarginCalculation( + marginType: 'Initial' | 'Maintenance' | 'Fill' = 'Initial', + contextOverrides?: Partial +): MarginCalculation; + +// Optional conveniences for consumers +getIsolatedMarginCalculation( + marketIndex: number, + marginType: 'Initial' | 'Maintenance' | 'Fill' = 'Initial', + contextOverrides?: Partial +): IsolatedMarginCalculation | undefined; + +// Cross views can continue to use helpers on the snapshot: +// get_cross_free_collateral(snapshot), meets_margin_requirement(snapshot), etc. +``` + +### UI compatibility + +- All existing `User` getters remain and delegate to the snapshot, so current UI keeps working without call-site changes. +- New consumers can call `user.getMarginCalculation()` to access isolated breakdowns. + +### Testing and parity + +- Golden tests comparing SDK snapshot against program outputs (cross and isolated, edge cases). +- Keep math/rounding identical to program (ordering, buffers, funding, open-order IM, oracle strictness). + +### Migration plan (brief) + +1. Implement `types` and `engine` with strict parity; land behind a feature flag. +2. Add `user.getMarginCalculation()` and delegate legacy getters. +3. Optionally update UI hooks to read richer fields; not required for compatibility. +4. Expand parity tests; enable by default after validation. + + diff --git a/sdk/src/marginCalculation.ts b/sdk/src/marginCalculation.ts new file mode 100644 index 0000000000..c26333db00 --- /dev/null +++ b/sdk/src/marginCalculation.ts @@ -0,0 +1,306 @@ +import { BN } from '@coral-xyz/anchor'; +import { MARGIN_PRECISION } from './constants/numericConstants'; +import { MarketType } from './types'; + +export type MarginCategory = 'Initial' | 'Maintenance' | 'Fill'; + +export type MarginCalculationMode = + | { type: 'Standard' } + | { type: 'Liquidation' }; + +export class MarketIdentifier { + marketType: MarketType; + marketIndex: number; + + private constructor(marketType: MarketType, marketIndex: number) { + this.marketType = marketType; + this.marketIndex = marketIndex; + } + + static spot(marketIndex: number): MarketIdentifier { + return new MarketIdentifier(MarketType.SPOT, marketIndex); + } + + static perp(marketIndex: number): MarketIdentifier { + return new MarketIdentifier(MarketType.PERP, marketIndex); + } + + equals(other: MarketIdentifier | undefined): boolean { + return ( + !!other && + this.marketType === other.marketType && + this.marketIndex === other.marketIndex + ); + } +} + +export class MarginContext { + marginType: MarginCategory; + mode: MarginCalculationMode; + strict: boolean; + ignoreInvalidDepositOracles: boolean; + marginBuffer: BN; // scaled by MARGIN_PRECISION + marginRatioOverride?: number; + + private constructor(marginType: MarginCategory) { + this.marginType = marginType; + this.mode = { type: 'Standard' }; + this.strict = false; + this.ignoreInvalidDepositOracles = false; + this.marginBuffer = new BN(0); + } + + static standard(marginType: MarginCategory): MarginContext { + return new MarginContext(marginType); + } + + static liquidation(marginBuffer: BN): MarginContext { + const ctx = new MarginContext('Maintenance'); + ctx.mode = { type: 'Liquidation' }; + ctx.marginBuffer = marginBuffer ?? new BN(0); + return ctx; + } + + strictMode(strict: boolean): this { + this.strict = strict; + return this; + } + + ignoreInvalidDeposits(ignore: boolean): this { + this.ignoreInvalidDepositOracles = ignore; + return this; + } + + setMarginBuffer(buffer?: BN): this { + this.marginBuffer = buffer ?? new BN(0); + return this; + } + + setMarginRatioOverride(ratio: number): this { + this.marginRatioOverride = ratio; + return this; + } +} + +export class IsolatedMarginCalculation { + marginRequirement: BN; + totalCollateral: BN; // deposit + pnl + totalCollateralBuffer: BN; + marginRequirementPlusBuffer: BN; + + constructor() { + this.marginRequirement = new BN(0); + this.totalCollateral = new BN(0); + this.totalCollateralBuffer = new BN(0); + this.marginRequirementPlusBuffer = new BN(0); + } + + getTotalCollateralPlusBuffer(): BN { + return this.totalCollateral.add(this.totalCollateralBuffer); + } + + meetsMarginRequirement(): boolean { + return this.totalCollateral.gte(this.marginRequirement); + } + + meetsMarginRequirementWithBuffer(): boolean { + return this.getTotalCollateralPlusBuffer().gte( + this.marginRequirementPlusBuffer + ); + } + + marginShortage(): BN { + const shortage = this.marginRequirementPlusBuffer.sub( + this.getTotalCollateralPlusBuffer() + ); + return shortage.isNeg() ? new BN(0) : shortage; + } +} + +export class MarginCalculation { + context: MarginContext; + totalCollateral: BN; + totalCollateralBuffer: BN; + marginRequirement: BN; + marginRequirementPlusBuffer: BN; + isolatedMarginCalculations: Map; + numSpotLiabilities: number; + numPerpLiabilities: number; + allDepositOraclesValid: boolean; + allLiabilityOraclesValid: boolean; + withPerpIsolatedLiability: boolean; + withSpotIsolatedLiability: boolean; + totalSpotLiabilityValue: BN; + totalPerpLiabilityValue: BN; + trackedMarketMarginRequirement: BN; + fuelDeposits: number; + fuelBorrows: number; + fuelPositions: number; + + constructor(context: MarginContext) { + this.context = context; + this.totalCollateral = new BN(0); + this.totalCollateralBuffer = new BN(0); + this.marginRequirement = new BN(0); + this.marginRequirementPlusBuffer = new BN(0); + this.isolatedMarginCalculations = new Map(); + this.numSpotLiabilities = 0; + this.numPerpLiabilities = 0; + this.allDepositOraclesValid = true; + this.allLiabilityOraclesValid = true; + this.withPerpIsolatedLiability = false; + this.withSpotIsolatedLiability = false; + this.totalSpotLiabilityValue = new BN(0); + this.totalPerpLiabilityValue = new BN(0); + this.trackedMarketMarginRequirement = new BN(0); + this.fuelDeposits = 0; + this.fuelBorrows = 0; + this.fuelPositions = 0; + } + + addCrossMarginTotalCollateral(delta: BN): void { + this.totalCollateral = this.totalCollateral.add(delta); + if (this.context.marginBuffer.gt(new BN(0)) && delta.isNeg()) { + this.totalCollateralBuffer = this.totalCollateralBuffer.add( + delta.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ); + } + } + + addCrossMarginRequirement(marginRequirement: BN, liabilityValue: BN): void { + this.marginRequirement = this.marginRequirement.add(marginRequirement); + if (this.context.marginBuffer.gt(new BN(0))) { + this.marginRequirementPlusBuffer = this.marginRequirementPlusBuffer.add( + marginRequirement.add( + liabilityValue.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ) + ); + } + } + + addIsolatedMarginCalculation( + marketIndex: number, + depositValue: BN, + pnl: BN, + liabilityValue: BN, + marginRequirement: BN + ): void { + const totalCollateral = depositValue.add(pnl); + const totalCollateralBuffer = + this.context.marginBuffer.gt(new BN(0)) && pnl.isNeg() + ? pnl.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + : new BN(0); + + const marginRequirementPlusBuffer = this.context.marginBuffer.gt(new BN(0)) + ? marginRequirement.add( + liabilityValue.mul(this.context.marginBuffer).div(MARGIN_PRECISION) + ) + : new BN(0); + + const iso = new IsolatedMarginCalculation(); + iso.marginRequirement = marginRequirement; + iso.totalCollateral = totalCollateral; + iso.totalCollateralBuffer = totalCollateralBuffer; + iso.marginRequirementPlusBuffer = marginRequirementPlusBuffer; + this.isolatedMarginCalculations.set(marketIndex, iso); + } + + addSpotLiability(): void { + this.numSpotLiabilities += 1; + } + + addPerpLiability(): void { + this.numPerpLiabilities += 1; + } + + addSpotLiabilityValue(spotLiabilityValue: BN): void { + this.totalSpotLiabilityValue = + this.totalSpotLiabilityValue.add(spotLiabilityValue); + } + + addPerpLiabilityValue(perpLiabilityValue: BN): void { + this.totalPerpLiabilityValue = + this.totalPerpLiabilityValue.add(perpLiabilityValue); + } + + updateAllDepositOraclesValid(valid: boolean): void { + this.allDepositOraclesValid = this.allDepositOraclesValid && valid; + } + + updateAllLiabilityOraclesValid(valid: boolean): void { + this.allLiabilityOraclesValid = this.allLiabilityOraclesValid && valid; + } + + updateWithSpotIsolatedLiability(isolated: boolean): void { + this.withSpotIsolatedLiability = this.withSpotIsolatedLiability || isolated; + } + + updateWithPerpIsolatedLiability(isolated: boolean): void { + this.withPerpIsolatedLiability = this.withPerpIsolatedLiability || isolated; + } + + validateNumSpotLiabilities(): void { + if (this.numSpotLiabilities > 0 && this.marginRequirement.eq(new BN(0))) { + throw new Error( + 'InvalidMarginRatio: num_spot_liabilities>0 but margin_requirement=0' + ); + } + } + + getNumOfLiabilities(): number { + return this.numSpotLiabilities + this.numPerpLiabilities; + } + + getCrossTotalCollateralPlusBuffer(): BN { + return this.totalCollateral.add(this.totalCollateralBuffer); + } + + meetsCrossMarginRequirement(): boolean { + return this.totalCollateral.gte(this.marginRequirement); + } + + meetsCrossMarginRequirementWithBuffer(): boolean { + return this.getCrossTotalCollateralPlusBuffer().gte( + this.marginRequirementPlusBuffer + ); + } + + meetsMarginRequirement(): boolean { + if (!this.meetsCrossMarginRequirement()) return false; + for (const [, iso] of this.isolatedMarginCalculations) { + if (!iso.meetsMarginRequirement()) return false; + } + return true; + } + + meetsMarginRequirementWithBuffer(): boolean { + if (!this.meetsCrossMarginRequirementWithBuffer()) return false; + for (const [, iso] of this.isolatedMarginCalculations) { + if (!iso.meetsMarginRequirementWithBuffer()) return false; + } + return true; + } + + getCrossFreeCollateral(): BN { + const free = this.totalCollateral.sub(this.marginRequirement); + return free.isNeg() ? new BN(0) : free; + } + + getIsolatedFreeCollateral(marketIndex: number): BN { + const iso = this.isolatedMarginCalculations.get(marketIndex); + if (!iso) + throw new Error('InvalidMarginCalculation: missing isolated calc'); + const free = iso.totalCollateral.sub(iso.marginRequirement); + return free.isNeg() ? new BN(0) : free; + } + + getIsolatedMarginCalculation( + marketIndex: number + ): IsolatedMarginCalculation | undefined { + return this.isolatedMarginCalculations.get(marketIndex); + } + + hasIsolatedMarginCalculation(marketIndex: number): boolean { + return this.isolatedMarginCalculations.has(marketIndex); + } +} diff --git a/sdk/src/math/amm.ts b/sdk/src/math/amm.ts index 7b41b4a783..88a9ec9f5b 100644 --- a/sdk/src/math/amm.ts +++ b/sdk/src/math/amm.ts @@ -389,6 +389,31 @@ export function calculateInventoryLiquidityRatio( return inventoryScaleBN; } +export function calculateInventoryLiquidityRatioForReferencePriceOffset( + baseAssetAmountWithAmm: BN, + baseAssetReserve: BN, + minBaseAssetReserve: BN, + maxBaseAssetReserve: BN +): BN { + // inventory skew + const [openBids, openAsks] = calculateMarketOpenBidAsk( + baseAssetReserve, + minBaseAssetReserve, + maxBaseAssetReserve + ); + + const avgSideLiquidity = openBids.abs().add(openAsks.abs()).div(TWO); + + const inventoryScaleBN = BN.min( + baseAssetAmountWithAmm + .mul(PERCENTAGE_PRECISION) + .div(BN.max(avgSideLiquidity, ONE)) + .abs(), + PERCENTAGE_PRECISION + ); + return inventoryScaleBN; +} + export function calculateInventoryScale( baseAssetAmountWithAmm: BN, baseAssetReserve: BN, @@ -440,7 +465,7 @@ export function calculateReferencePriceOffset( markTwapSlow: BN, maxOffsetPct: number ): BN { - if (last24hAvgFundingRate.eq(ZERO)) { + if (last24hAvgFundingRate.eq(ZERO) || liquidityFraction.eq(ZERO)) { return ZERO; } @@ -1013,19 +1038,38 @@ export function calculateSpreadReserves( (amm.curveUpdateIntensity - 100) ); - const liquidityFraction = calculateInventoryLiquidityRatio( - amm.baseAssetAmountWithAmm, - amm.baseAssetReserve, - amm.minBaseAssetReserve, - amm.maxBaseAssetReserve - ); + const liquidityFraction = + calculateInventoryLiquidityRatioForReferencePriceOffset( + amm.baseAssetAmountWithAmm, + amm.baseAssetReserve, + amm.minBaseAssetReserve, + amm.maxBaseAssetReserve + ); const liquidityFractionSigned = liquidityFraction.mul( sigNum(amm.baseAssetAmountWithAmm.add(amm.baseAssetAmountWithUnsettledLp)) ); + + let liquidityFractionAfterDeadband = liquidityFractionSigned; + const deadbandPct = amm.referencePriceOffsetDeadbandPct + ? PERCENTAGE_PRECISION.mul( + new BN(amm.referencePriceOffsetDeadbandPct as number) + ).divn(100) + : ZERO; + if (!liquidityFractionAfterDeadband.eq(ZERO) && deadbandPct.gt(ZERO)) { + const abs = liquidityFractionAfterDeadband.abs(); + if (abs.lte(deadbandPct)) { + liquidityFractionAfterDeadband = ZERO; + } else { + liquidityFractionAfterDeadband = liquidityFractionAfterDeadband.sub( + deadbandPct.mul(sigNum(liquidityFractionAfterDeadband)) + ); + } + } + referencePriceOffset = calculateReferencePriceOffset( reservePrice, amm.last24HAvgFundingRate, - liquidityFractionSigned, + liquidityFractionAfterDeadband, amm.historicalOracleData.lastOraclePriceTwap5Min, amm.lastMarkPriceTwap5Min, amm.historicalOracleData.lastOraclePriceTwap, diff --git a/sdk/src/math/auction.ts b/sdk/src/math/auction.ts index 5a58f60054..013f1388b7 100644 --- a/sdk/src/math/auction.ts +++ b/sdk/src/math/auction.ts @@ -1,4 +1,12 @@ -import { isOneOfVariant, isVariant, Order, PositionDirection } from '../types'; +import { + isOneOfVariant, + isVariant, + OracleValidity, + Order, + PerpOperation, + PositionDirection, + StateAccount, +} from '../types'; import { BN } from '@coral-xyz/anchor'; import { ONE, @@ -8,6 +16,10 @@ import { } from '../constants/numericConstants'; import { getVariant, OrderBitFlag, PerpMarketAccount } from '../types'; import { getPerpMarketTierNumber } from './tiers'; +import { MMOraclePriceData } from '../oracles/types'; +import { isLowRiskForAmm } from './orders'; +import { getOracleValidity } from './oracles'; +import { isOperationPaused } from './exchangeStatus'; export function isAuctionComplete(order: Order, slot: number): boolean { if (order.auctionDuration === 0) { @@ -19,14 +31,49 @@ export function isAuctionComplete(order: Order, slot: number): boolean { export function isFallbackAvailableLiquiditySource( order: Order, - minAuctionDuration: number, - slot: number + mmOraclePriceData: MMOraclePriceData, + slot: number, + state: StateAccount, + market: PerpMarketAccount, + isLiquidation?: boolean ): boolean { - if (minAuctionDuration === 0) { + if (isOperationPaused(market.pausedOperations, PerpOperation.AMM_FILL)) { + return false; + } + + // TODO: include too much drawdown check & mm oracle volatility + + const oracleValidity = getOracleValidity( + market!, + { + price: mmOraclePriceData.price, + slot: mmOraclePriceData.slot, + confidence: mmOraclePriceData.confidence, + hasSufficientNumberOfDataPoints: + mmOraclePriceData.hasSufficientNumberOfDataPoints, + }, + state.oracleGuardRails, + new BN(slot) + ); + if (oracleValidity <= OracleValidity.StaleForAMMLowRisk) { + return false; + } + + if (oracleValidity == OracleValidity.Valid) { return true; } - return new BN(slot).sub(order.slot).gt(new BN(minAuctionDuration)); + const isOrderLowRiskForAmm = isLowRiskForAmm( + order, + mmOraclePriceData, + isLiquidation + ); + + if (!isOrderLowRiskForAmm) { + return false; + } else { + return true; + } } /** diff --git a/sdk/src/math/builder.ts b/sdk/src/math/builder.ts new file mode 100644 index 0000000000..75681a1823 --- /dev/null +++ b/sdk/src/math/builder.ts @@ -0,0 +1,20 @@ +import { RevenueShareOrder } from '../types'; + +const FLAG_IS_OPEN = 0x01; +export function isBuilderOrderOpen(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_OPEN) !== 0; +} + +const FLAG_IS_COMPLETED = 0x02; +export function isBuilderOrderCompleted(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_COMPLETED) !== 0; +} + +const FLAG_IS_REFERRAL = 0x04; +export function isBuilderOrderReferral(order: RevenueShareOrder): boolean { + return (order.bitFlags & FLAG_IS_REFERRAL) !== 0; +} + +export function isBuilderOrderAvailable(order: RevenueShareOrder): boolean { + return !isBuilderOrderOpen(order) && !isBuilderOrderCompleted(order); +} diff --git a/sdk/src/math/margin.ts b/sdk/src/math/margin.ts index 63fada436b..452b047bd0 100644 --- a/sdk/src/math/margin.ts +++ b/sdk/src/math/margin.ts @@ -10,6 +10,7 @@ import { MARGIN_PRECISION, PRICE_PRECISION, QUOTE_PRECISION, + PERCENTAGE_PRECISION, } from '../constants/numericConstants'; import { BN } from '@coral-xyz/anchor'; import { OraclePriceData } from '../oracles/types'; @@ -32,7 +33,8 @@ export function calculateSizePremiumLiabilityWeight( size: BN, // AMM_RESERVE_PRECISION imfFactor: BN, liabilityWeight: BN, - precision: BN + precision: BN, + isBounded = true ): BN { if (imfFactor.eq(ZERO)) { return liabilityWeight; @@ -53,10 +55,13 @@ export function calculateSizePremiumLiabilityWeight( .div(denom) // 1e5 ); - const maxLiabilityWeight = BN.max( - liabilityWeight, - sizePremiumLiabilityWeight - ); + let maxLiabilityWeight; + if (isBounded) { + maxLiabilityWeight = BN.max(liabilityWeight, sizePremiumLiabilityWeight); + } else { + maxLiabilityWeight = sizePremiumLiabilityWeight; + } + return maxLiabilityWeight; } @@ -160,8 +165,20 @@ export function calculateWorstCaseBaseAssetAmount( export function calculateWorstCasePerpLiabilityValue( perpPosition: PerpPosition, perpMarket: PerpMarketAccount, - oraclePrice: BN + oraclePrice: BN, + includeOpenOrders: boolean = true ): { worstCaseBaseAssetAmount: BN; worstCaseLiabilityValue: BN } { + // return early if no open orders required + if (!includeOpenOrders) { + return { + worstCaseBaseAssetAmount: perpPosition.baseAssetAmount, + worstCaseLiabilityValue: calculatePerpLiabilityValue( + perpPosition.baseAssetAmount, + oraclePrice, + isVariant(perpMarket.contractType, 'prediction') + ), + }; + } const allBids = perpPosition.baseAssetAmount.add(perpPosition.openBids); const allAsks = perpPosition.baseAssetAmount.add(perpPosition.openAsks); @@ -370,3 +387,37 @@ export function calculateUserMaxPerpOrderSize( return user.getMaxTradeSizeUSDCForPerp(targetMarketIndex, tradeSide); } + +export function calcHighLeverageModeInitialMarginRatioFromSize( + preSizeAdjMarginRatio: BN, + sizeAdjMarginRatio: BN, + defaultMarginRatio: BN +): BN { + let result: BN; + + if (sizeAdjMarginRatio.lt(preSizeAdjMarginRatio)) { + const sizePctDiscountFactor = PERCENTAGE_PRECISION.sub( + preSizeAdjMarginRatio + .sub(sizeAdjMarginRatio) + .mul(PERCENTAGE_PRECISION) + .div(preSizeAdjMarginRatio.div(new BN(5))) + ); + + const hlmMarginDelta = BN.max( + preSizeAdjMarginRatio.sub(defaultMarginRatio), + new BN(1) + ); + + const hlmMarginDeltaProportion = hlmMarginDelta + .mul(sizePctDiscountFactor) + .div(PERCENTAGE_PRECISION); + + result = hlmMarginDeltaProportion.add(defaultMarginRatio); + } else if (sizeAdjMarginRatio.eq(preSizeAdjMarginRatio)) { + result = defaultMarginRatio; + } else { + result = sizeAdjMarginRatio; + } + + return result; +} diff --git a/sdk/src/math/market.ts b/sdk/src/math/market.ts index 6dd3697de6..cae257898a 100644 --- a/sdk/src/math/market.ts +++ b/sdk/src/math/market.ts @@ -19,6 +19,7 @@ import { import { calculateSizeDiscountAssetWeight, calculateSizePremiumLiabilityWeight, + calcHighLeverageModeInitialMarginRatioFromSize, } from './margin'; import { MMOraclePriceData, OraclePriceData } from '../oracles/types'; import { @@ -143,45 +144,74 @@ export function calculateMarketMarginRatio( customMarginRatio = 0, userHighLeverageMode = false ): number { - let marginRationInitial; - let marginRatioMaintenance; + if (market.status === 'Settlement') return 0; - if ( + const isHighLeverageUser = userHighLeverageMode && market.highLeverageMarginRatioInitial > 0 && - market.highLeverageMarginRatioMaintenance - ) { - marginRationInitial = market.highLeverageMarginRatioInitial; - marginRatioMaintenance = market.highLeverageMarginRatioMaintenance; - } else { - marginRationInitial = market.marginRatioInitial; - marginRatioMaintenance = market.marginRatioMaintenance; - } + market.highLeverageMarginRatioMaintenance > 0; + + const marginRatioInitial = isHighLeverageUser + ? market.highLeverageMarginRatioInitial + : market.marginRatioInitial; + + const marginRatioMaintenance = isHighLeverageUser + ? market.highLeverageMarginRatioMaintenance + : market.marginRatioMaintenance; - let marginRatio; + let defaultMarginRatio: number; switch (marginCategory) { - case 'Initial': { - // use lowest leverage between max allowed and optional user custom max - marginRatio = Math.max( - calculateSizePremiumLiabilityWeight( - size, - new BN(market.imfFactor), - new BN(marginRationInitial), - MARGIN_PRECISION - ).toNumber(), - customMarginRatio - ); + case 'Initial': + defaultMarginRatio = marginRatioInitial; break; - } - case 'Maintenance': { - marginRatio = calculateSizePremiumLiabilityWeight( - size, - new BN(market.imfFactor), - new BN(marginRatioMaintenance), - MARGIN_PRECISION - ).toNumber(); + case 'Maintenance': + defaultMarginRatio = marginRatioMaintenance; break; + default: + throw new Error('Invalid margin category'); + } + + let marginRatio: number; + + if (isHighLeverageUser && marginCategory !== 'Maintenance') { + // Use ordinary-mode initial/fill ratios for size-adjusted calc + let preSizeAdjMarginRatio: number; + switch (marginCategory) { + case 'Initial': + preSizeAdjMarginRatio = market.marginRatioInitial; + break; + default: + preSizeAdjMarginRatio = marginRatioMaintenance; + break; } + + const sizeAdjMarginRatio = calculateSizePremiumLiabilityWeight( + size, + new BN(market.imfFactor), + new BN(preSizeAdjMarginRatio), + MARGIN_PRECISION, + false + ).toNumber(); + + marginRatio = calcHighLeverageModeInitialMarginRatioFromSize( + new BN(preSizeAdjMarginRatio), + new BN(sizeAdjMarginRatio), + new BN(defaultMarginRatio) + ).toNumber(); + } else { + const sizeAdjMarginRatio = calculateSizePremiumLiabilityWeight( + size, + new BN(market.imfFactor), + new BN(defaultMarginRatio), + MARGIN_PRECISION, + true + ).toNumber(); + + marginRatio = Math.max(defaultMarginRatio, sizeAdjMarginRatio); + } + + if (marginCategory === 'Initial') { + marginRatio = Math.max(marginRatio, customMarginRatio); } return marginRatio; diff --git a/sdk/src/math/oracles.ts b/sdk/src/math/oracles.ts index 8873c66150..27248326f0 100644 --- a/sdk/src/math/oracles.ts +++ b/sdk/src/math/oracles.ts @@ -3,18 +3,20 @@ import { HistoricalOracleData, OracleGuardRails, OracleSource, + OracleValidity, PerpMarketAccount, + isOneOfVariant, isVariant, } from '../types'; import { OraclePriceData } from '../oracles/types'; import { BID_ASK_SPREAD_PRECISION, MARGIN_PRECISION, - PRICE_PRECISION, ONE, ZERO, FIVE_MINUTE, PERCENTAGE_PRECISION, + FIVE, } from '../constants/numericConstants'; import { assert } from '../assert/assert'; import { BN } from '@coral-xyz/anchor'; @@ -52,6 +54,91 @@ export function getMaxConfidenceIntervalMultiplier( return maxConfidenceIntervalMultiplier; } +export function getOracleValidity( + market: PerpMarketAccount, + oraclePriceData: OraclePriceData, + oracleGuardRails: OracleGuardRails, + slot: BN, + oracleStalenessBuffer = FIVE +): OracleValidity { + const isNonPositive = oraclePriceData.price.lte(ZERO); + const isTooVolatile = BN.max( + oraclePriceData.price, + market.amm.historicalOracleData.lastOraclePriceTwap + ) + .div( + BN.max( + ONE, + BN.min( + oraclePriceData.price, + market.amm.historicalOracleData.lastOraclePriceTwap + ) + ) + ) + .gt(oracleGuardRails.validity.tooVolatileRatio); + + const confPctOfPrice = oraclePriceData.confidence + .mul(BID_ASK_SPREAD_PRECISION) + .div(oraclePriceData.price); + const isConfTooLarge = confPctOfPrice.gt( + oracleGuardRails.validity.confidenceIntervalMaxSize.mul( + getMaxConfidenceIntervalMultiplier(market) + ) + ); + + const oracleDelay = slot.sub(oraclePriceData.slot).sub(oracleStalenessBuffer); + + let isStaleForAmmImmediate = true; + if (market.amm.oracleSlotDelayOverride != 0) { + isStaleForAmmImmediate = oracleDelay.gt( + BN.max(new BN(market.amm.oracleSlotDelayOverride), ZERO) + ); + } + + let isStaleForAmmLowRisk = false; + if (market.amm.oracleLowRiskSlotDelayOverride != 0) { + isStaleForAmmLowRisk = oracleDelay.gt( + BN.max(new BN(market.amm.oracleLowRiskSlotDelayOverride), ZERO) + ); + } else { + isStaleForAmmLowRisk = oracleDelay.gt( + oracleGuardRails.validity.slotsBeforeStaleForAmm + ); + } + + let isStaleForMargin = oracleDelay.gt( + new BN(oracleGuardRails.validity.slotsBeforeStaleForMargin) + ); + if ( + isOneOfVariant(market.amm.oracleSource, [ + 'pythStableCoinPull', + 'pythLazerStableCoin', + ]) + ) { + isStaleForMargin = oracleDelay.gt( + new BN(oracleGuardRails.validity.slotsBeforeStaleForMargin).muln(3) + ); + } + + if (isNonPositive) { + return OracleValidity.NonPositive; + } else if (isTooVolatile) { + return OracleValidity.TooVolatile; + } else if (isConfTooLarge) { + return OracleValidity.TooUncertain; + } else if (isStaleForMargin) { + return OracleValidity.StaleForMargin; + } else if (!oraclePriceData.hasSufficientNumberOfDataPoints) { + return OracleValidity.InsufficientDataPoints; + } else if (isStaleForAmmLowRisk) { + return OracleValidity.StaleForAMMLowRisk; + } else if (isStaleForAmmImmediate) { + return OracleValidity.isStaleForAmmImmediate; + } else { + return OracleValidity.Valid; + } +} + export function isOracleValid( market: PerpMarketAccount, oraclePriceData: OraclePriceData, @@ -97,29 +184,17 @@ export function isOracleValid( export function isOracleTooDivergent( amm: AMM, oraclePriceData: OraclePriceData, - oracleGuardRails: OracleGuardRails, - now: BN + oracleGuardRails: OracleGuardRails ): boolean { - const sinceLastUpdate = now.sub( - amm.historicalOracleData.lastOraclePriceTwapTs - ); - const sinceStart = BN.max(ZERO, FIVE_MINUTE.sub(sinceLastUpdate)); - const oracleTwap5min = amm.historicalOracleData.lastOraclePriceTwap5Min - .mul(sinceStart) - .add(oraclePriceData.price) - .mul(sinceLastUpdate) - .div(sinceStart.add(sinceLastUpdate)); - - const oracleSpread = oracleTwap5min.sub(oraclePriceData.price); - const oracleSpreadPct = oracleSpread.mul(PRICE_PRECISION).div(oracleTwap5min); - + const oracleSpreadPct = oraclePriceData.price + .sub(amm.historicalOracleData.lastOraclePriceTwap5Min) + .mul(PERCENTAGE_PRECISION) + .div(amm.historicalOracleData.lastOraclePriceTwap5Min); const maxDivergence = BN.max( - oracleGuardRails.priceDivergence.markOraclePercentDivergence, - PERCENTAGE_PRECISION.div(new BN(10)) + oracleGuardRails.priceDivergence.oracleTwap5MinPercentDivergence, + PERCENTAGE_PRECISION.div(new BN(2)) ); - const tooDivergent = oracleSpreadPct.abs().gte(maxDivergence); - return tooDivergent; } diff --git a/sdk/src/math/orders.ts b/sdk/src/math/orders.ts index 0ef192717e..653d66ac0b 100644 --- a/sdk/src/math/orders.ts +++ b/sdk/src/math/orders.ts @@ -8,8 +8,16 @@ import { PositionDirection, ProtectedMakerParams, MarketTypeStr, + OrderBitFlag, + StateAccount, } from '../types'; -import { ZERO, TWO, ONE } from '../constants/numericConstants'; +import { + ZERO, + TWO, + ONE, + SPOT_MARKET_IMF_PRECISION, + MARGIN_PRECISION, +} from '../constants/numericConstants'; import { BN } from '@coral-xyz/anchor'; import { MMOraclePriceData, OraclePriceData } from '../oracles/types'; import { @@ -22,6 +30,7 @@ import { calculateMaxBaseAssetAmountToTrade, calculateUpdatedAMM, } from './amm'; +import { calculateSizePremiumLiabilityWeight } from './margin'; export function isOrderRiskIncreasing(user: User, order: Order): boolean { if (!isVariant(order.status, 'open')) { @@ -236,20 +245,46 @@ export function isFillableByVAMM( mmOraclePriceData: MMOraclePriceData, slot: number, ts: number, - minAuctionDuration: number + state: StateAccount ): boolean { return ( - (isFallbackAvailableLiquiditySource(order, minAuctionDuration, slot) && + (isFallbackAvailableLiquiditySource( + order, + mmOraclePriceData, + slot, + state, + market + ) && calculateBaseAssetAmountForAmmToFulfill( order, market, mmOraclePriceData, slot - ).gte(market.amm.minOrderSize)) || + ).gt(ZERO)) || isOrderExpired(order, ts) ); } +export function isLowRiskForAmm( + order: Order, + mmOraclePriceData: MMOraclePriceData, + isLiquidation?: boolean +): boolean { + if (isVariant(order.marketType, 'spot')) { + return false; + } + + const orderOlderThanOracleDelay = new BN(order.slot).lte( + mmOraclePriceData.slot + ); + + return ( + orderOlderThanOracleDelay || + isLiquidation || + (order.bitFlags & OrderBitFlag.SafeTriggerOrder) !== 0 + ); +} + export function calculateBaseAssetAmountForAmmToFulfill( order: Order, market: PerpMarketAccount, @@ -389,6 +424,11 @@ export function isSignedMsgOrder(order: Order): boolean { return (order.bitFlags & FLAG_IS_SIGNED_MSG) !== 0; } +const FLAG_HAS_BUILDER = 0x10; +export function hasBuilder(order: Order): boolean { + return (order.bitFlags & FLAG_HAS_BUILDER) !== 0; +} + export function calculateOrderBaseAssetAmount( order: Order, existingBaseAssetAmount: BN @@ -406,3 +446,72 @@ export function calculateOrderBaseAssetAmount( return BN.min(BN.max(existingBaseAssetAmount, ZERO), order.baseAssetAmount); } } + +// ---------- inverse ---------- +/** + * Invert the size-premium liability weight: given a target margin ratio (liability weight), + * return the max `size` (AMM_RESERVE_PRECISION units) that still yields <= target. + * + * Returns: + * - BN size (>=0) if bounded + * - null if impossible (target < liabilityWeight) OR imfFactor == 0 (unbounded) + */ +export function maxSizeForTargetLiabilityWeightBN( + target: BN, + imfFactor: BN, + liabilityWeight: BN, + market: PerpMarketAccount +): BN | null { + if (target.lt(liabilityWeight)) return null; + if (imfFactor.isZero()) return null; + + const base = liabilityWeight.muln(4).divn(5); + + const denom = new BN(100_000) + .mul(SPOT_MARKET_IMF_PRECISION) + .div(MARGIN_PRECISION); + if (denom.isZero()) + throw new Error('denom=0: bad precision/spotImfPrecision'); + + const allowedInc = target.gt(base) ? target.sub(base) : ZERO; + + const maxSqrt = allowedInc.mul(denom).div(imfFactor); + + if (maxSqrt.lte(ZERO)) { + const fitsZero = calculateSizePremiumLiabilityWeight( + ZERO, + imfFactor, + liabilityWeight, + MARGIN_PRECISION + ).lte(target); + return fitsZero ? ZERO : null; + } + + let hi = maxSqrt.mul(maxSqrt).sub(ONE).divn(10); + if (hi.isNeg()) hi = ZERO; + + let lo = ZERO; + while (lo.lt(hi)) { + const mid = lo.add(hi).add(ONE).divn(2); // upper mid to prevent infinite loop + if ( + calculateSizePremiumLiabilityWeight( + mid, + imfFactor, + liabilityWeight, + MARGIN_PRECISION + ).lte(target) + ) { + lo = mid; + } else { + hi = mid.sub(ONE); + } + } + + // cap at max OI + const maxOpenInterest = market.amm.maxOpenInterest; + if (lo.gt(maxOpenInterest)) { + return maxOpenInterest; + } + + return lo; +} diff --git a/sdk/src/math/position.ts b/sdk/src/math/position.ts index 3db5007a20..d0c3a16a0d 100644 --- a/sdk/src/math/position.ts +++ b/sdk/src/math/position.ts @@ -14,6 +14,7 @@ import { PositionDirection, PerpPosition, SpotMarketAccount, + PositionFlag, } from '../types'; import { calculateUpdatedAMM, @@ -127,7 +128,6 @@ export function calculatePositionPNL( if (withFunding) { const fundingRatePnL = calculateUnsettledFundingPnl(market, perpPosition); - pnl = pnl.add(fundingRatePnL); } @@ -244,7 +244,17 @@ export function positionIsAvailable(position: PerpPosition): boolean { position.baseAssetAmount.eq(ZERO) && position.openOrders === 0 && position.quoteAssetAmount.eq(ZERO) && - position.lpShares.eq(ZERO) + position.lpShares.eq(ZERO) && + position.isolatedPositionScaledBalance.eq(ZERO) && + !positionIsBeingLiquidated(position) + ); +} + +export function positionIsBeingLiquidated(position: PerpPosition): boolean { + return ( + (position.positionFlag & + (PositionFlag.BeingLiquidated | PositionFlag.Bankruptcy)) > + 0 ); } diff --git a/sdk/src/math/spotPosition.ts b/sdk/src/math/spotPosition.ts index 61eae83ec1..d05a7382d4 100644 --- a/sdk/src/math/spotPosition.ts +++ b/sdk/src/math/spotPosition.ts @@ -33,7 +33,8 @@ export function getWorstCaseTokenAmounts( spotMarketAccount: SpotMarketAccount, strictOraclePrice: StrictOraclePrice, marginCategory: MarginCategory, - customMarginRatio?: number + customMarginRatio?: number, + includeOpenOrders: boolean = true ): OrderFillSimulation { const tokenAmount = getSignedTokenAmount( getTokenAmount( @@ -50,7 +51,10 @@ export function getWorstCaseTokenAmounts( strictOraclePrice ); - if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) { + if ( + (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) || + !includeOpenOrders + ) { const { weight, weightedTokenValue } = calculateWeightedTokenValue( tokenAmount, tokenValue, diff --git a/sdk/src/math/state.ts b/sdk/src/math/state.ts index f4414214a9..0565f959ba 100644 --- a/sdk/src/math/state.ts +++ b/sdk/src/math/state.ts @@ -38,3 +38,11 @@ export function useMedianTriggerPrice(stateAccount: StateAccount): boolean { (stateAccount.featureBitFlags & FeatureBitFlags.MEDIAN_TRIGGER_PRICE) > 0 ); } + +export function builderCodesEnabled(stateAccount: StateAccount): boolean { + return (stateAccount.featureBitFlags & FeatureBitFlags.BUILDER_CODES) > 0; +} + +export function builderReferralEnabled(stateAccount: StateAccount): boolean { + return (stateAccount.featureBitFlags & FeatureBitFlags.BUILDER_REFERRAL) > 0; +} diff --git a/sdk/src/memcmp.ts b/sdk/src/memcmp.ts index 896971ebbf..c73d21efdb 100644 --- a/sdk/src/memcmp.ts +++ b/sdk/src/memcmp.ts @@ -1,4 +1,4 @@ -import { MemcmpFilter } from '@solana/web3.js'; +import { MemcmpFilter, PublicKey } from '@solana/web3.js'; import bs58 from 'bs58'; import { BorshAccountsCoder } from '@coral-xyz/anchor'; import { encodeName } from './userName'; @@ -129,3 +129,36 @@ export function getSpotMarketAccountsFilter(): MemcmpFilter { }, }; } + +export function getRevenueShareEscrowFilter(): MemcmpFilter { + return { + memcmp: { + offset: 0, + bytes: bs58.encode( + BorshAccountsCoder.accountDiscriminator('RevenueShareEscrow') + ), + }, + }; +} + +export function getConstituentFilter(): MemcmpFilter { + return { + memcmp: { + offset: 0, + bytes: bs58.encode( + BorshAccountsCoder.accountDiscriminator('Constituent') + ), + }, + }; +} + +export function getConstituentLpPoolFilter( + lpPoolPublicKey: PublicKey +): MemcmpFilter { + return { + memcmp: { + offset: 72, + bytes: lpPoolPublicKey.toBase58(), + }, + }; +} diff --git a/sdk/src/orderSubscriber/grpcSubscription.ts b/sdk/src/orderSubscriber/grpcSubscription.ts index 41101435ab..04160c0bff 100644 --- a/sdk/src/orderSubscriber/grpcSubscription.ts +++ b/sdk/src/orderSubscriber/grpcSubscription.ts @@ -5,6 +5,7 @@ import { OrderSubscriber } from './OrderSubscriber'; import { GrpcConfigs, ResubOpts } from '../accounts/types'; import { UserAccount } from '../types'; import { getUserFilter, getNonIdleUserFilter } from '../memcmp'; +import { LaserstreamProgramAccountSubscriber } from '../accounts/laserProgramAccountSubscriber'; export class grpcSubscription { private orderSubscriber: OrderSubscriber; @@ -12,7 +13,9 @@ export class grpcSubscription { private resubOpts?: ResubOpts; private resyncIntervalMs?: number; - private subscriber?: grpcProgramAccountSubscriber; + private subscriber?: + | grpcProgramAccountSubscriber + | LaserstreamProgramAccountSubscriber; private resyncTimeoutId?: ReturnType; private decoded?: boolean; @@ -47,17 +50,32 @@ export class grpcSubscription { return; } - this.subscriber = await grpcProgramAccountSubscriber.create( - this.grpcConfigs, - 'OrderSubscriber', - 'User', - this.orderSubscriber.driftClient.program, - this.orderSubscriber.decodeFn, - { - filters: [getUserFilter(), getNonIdleUserFilter()], - }, - this.resubOpts - ); + if (this.grpcConfigs.client === 'laser') { + this.subscriber = + await LaserstreamProgramAccountSubscriber.create( + this.grpcConfigs, + 'OrderSubscriber', + 'User', + this.orderSubscriber.driftClient.program, + this.orderSubscriber.decodeFn, + { + filters: [getUserFilter(), getNonIdleUserFilter()], + }, + this.resubOpts + ); + } else { + this.subscriber = await grpcProgramAccountSubscriber.create( + this.grpcConfigs, + 'OrderSubscriber', + 'User', + this.orderSubscriber.driftClient.program, + this.orderSubscriber.decodeFn, + { + filters: [getUserFilter(), getNonIdleUserFilter()], + }, + this.resubOpts + ); + } await this.subscriber.subscribe( ( diff --git a/sdk/src/swap/UnifiedSwapClient.ts b/sdk/src/swap/UnifiedSwapClient.ts new file mode 100644 index 0000000000..966f7e71bd --- /dev/null +++ b/sdk/src/swap/UnifiedSwapClient.ts @@ -0,0 +1,293 @@ +import { + Connection, + PublicKey, + TransactionMessage, + AddressLookupTableAccount, + VersionedTransaction, + TransactionInstruction, +} from '@solana/web3.js'; +import { BN } from '@coral-xyz/anchor'; +import { + JupiterClient, + QuoteResponse as JupiterQuoteResponse, +} from '../jupiter/jupiterClient'; +import { + TitanClient, + QuoteResponse as TitanQuoteResponse, + SwapMode as TitanSwapMode, +} from '../titan/titanClient'; + +export type SwapMode = 'ExactIn' | 'ExactOut'; +export type SwapClientType = 'jupiter' | 'titan'; + +export type UnifiedQuoteResponse = JupiterQuoteResponse | TitanQuoteResponse; + +export interface SwapQuoteParams { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey?: PublicKey; // Required for Titan, optional for Jupiter + maxAccounts?: number; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + excludeDexes?: string[]; + sizeConstraint?: number; // Titan-specific + accountsLimitWritable?: number; // Titan-specific + autoSlippage?: boolean; // Jupiter-specific + maxAutoSlippageBps?: number; // Jupiter-specific + usdEstimate?: number; // Jupiter-specific +} + +export interface SwapTransactionParams { + quote: UnifiedQuoteResponse; + userPublicKey: PublicKey; + slippageBps?: number; +} + +export interface SwapTransactionResult { + transaction?: VersionedTransaction; // Jupiter returns this + transactionMessage?: TransactionMessage; // Titan returns this + lookupTables?: AddressLookupTableAccount[]; // Titan returns this +} + +export class UnifiedSwapClient { + private client: JupiterClient | TitanClient; + private clientType: SwapClientType; + + constructor({ + clientType, + connection, + authToken, + url, + }: { + clientType: SwapClientType; + connection: Connection; + authToken?: string; // Required for Titan, optional for Jupiter + url?: string; // Optional custom URL + }) { + this.clientType = clientType; + + if (clientType === 'jupiter') { + this.client = new JupiterClient({ + connection, + url, + }); + } else if (clientType === 'titan') { + if (!authToken) { + throw new Error('authToken is required for Titan client'); + } + this.client = new TitanClient({ + connection, + authToken, + url, + }); + } else { + throw new Error(`Unsupported client type: ${clientType}`); + } + } + + /** + * Get a swap quote from the underlying client + */ + public async getQuote( + params: SwapQuoteParams + ): Promise { + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + const { + userPublicKey: _userPublicKey, // Not needed for Jupiter + sizeConstraint: _sizeConstraint, // Jupiter-specific params to exclude + accountsLimitWritable: _accountsLimitWritable, + ...jupiterParams + } = params; + + return await jupiterClient.getQuote(jupiterParams); + } else { + const titanClient = this.client as TitanClient; + const { + autoSlippage: _autoSlippage, // Titan-specific params to exclude + maxAutoSlippageBps: _maxAutoSlippageBps, + usdEstimate: _usdEstimate, + ...titanParams + } = params; + + if (!titanParams.userPublicKey) { + throw new Error('userPublicKey is required for Titan quotes'); + } + + // Cast to ensure TypeScript knows userPublicKey is defined + const titanParamsWithUser = { + ...titanParams, + userPublicKey: titanParams.userPublicKey, + swapMode: titanParams.swapMode as string, // Titan expects string + }; + + return await titanClient.getQuote(titanParamsWithUser); + } + } + + /** + * Get a swap transaction from the underlying client + */ + public async getSwap( + params: SwapTransactionParams + ): Promise { + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + // Cast the quote to Jupiter's QuoteResponse type + const jupiterParams = { + ...params, + quote: params.quote as JupiterQuoteResponse, + }; + const transaction = await jupiterClient.getSwap(jupiterParams); + return { transaction }; + } else { + const titanClient = this.client as TitanClient; + const { quote, userPublicKey, slippageBps } = params; + + // For Titan, we need to reconstruct the parameters from the quote + const titanQuote = quote as TitanQuoteResponse; + const result = await titanClient.getSwap({ + inputMint: new PublicKey(titanQuote.inputMint), + outputMint: new PublicKey(titanQuote.outputMint), + amount: new BN(titanQuote.inAmount), + userPublicKey, + slippageBps: slippageBps || titanQuote.slippageBps, + swapMode: titanQuote.swapMode, + }); + + return { + transactionMessage: result.transactionMessage, + lookupTables: result.lookupTables, + }; + } + } + + /** + * Get swap instructions from the underlying client (Jupiter or Titan) + * This is the core swap logic without any context preparation + */ + public async getSwapInstructions({ + inputMint, + outputMint, + amount, + userPublicKey, + slippageBps, + swapMode = 'ExactIn', + onlyDirectRoutes = false, + quote, + sizeConstraint, + }: { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey: PublicKey; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + quote?: UnifiedQuoteResponse; + sizeConstraint?: number; + }): Promise<{ + instructions: TransactionInstruction[]; + lookupTables: AddressLookupTableAccount[]; + }> { + const isExactOut = swapMode === 'ExactOut'; + let swapInstructions: TransactionInstruction[]; + let lookupTables: AddressLookupTableAccount[]; + + if (this.clientType === 'jupiter') { + const jupiterClient = this.client as JupiterClient; + + // Get quote if not provided + let finalQuote = quote as JupiterQuoteResponse; + if (!finalQuote) { + finalQuote = await jupiterClient.getQuote({ + inputMint, + outputMint, + amount, + slippageBps, + swapMode, + onlyDirectRoutes, + }); + } + + if (!finalQuote) { + throw new Error("Could not fetch Jupiter's quote. Please try again."); + } + + // Get swap transaction and extract instructions + const transaction = await jupiterClient.getSwap({ + quote: finalQuote, + userPublicKey, + slippageBps, + }); + + const { transactionMessage, lookupTables: jupiterLookupTables } = + await jupiterClient.getTransactionMessageAndLookupTables({ + transaction, + }); + + swapInstructions = jupiterClient.getJupiterInstructions({ + transactionMessage, + inputMint, + outputMint, + }); + + lookupTables = jupiterLookupTables; + } else { + const titanClient = this.client as TitanClient; + + // For Titan, get swap directly (it handles quote internally) + const { transactionMessage, lookupTables: titanLookupTables } = + await titanClient.getSwap({ + inputMint, + outputMint, + amount, + userPublicKey, + slippageBps, + swapMode: isExactOut ? TitanSwapMode.ExactOut : TitanSwapMode.ExactIn, + onlyDirectRoutes, + sizeConstraint: sizeConstraint || 1280 - 375, // MAX_TX_BYTE_SIZE - buffer for drift instructions + }); + + swapInstructions = titanClient.getTitanInstructions({ + transactionMessage, + inputMint, + outputMint, + }); + + lookupTables = titanLookupTables; + } + + return { instructions: swapInstructions, lookupTables }; + } + + /** + * Get the underlying client instance + */ + public getClient(): JupiterClient | TitanClient { + return this.client; + } + + /** + * Get the client type + */ + public getClientType(): SwapClientType { + return this.clientType; + } + + /** + * Check if this is a Jupiter client + */ + public isJupiter(): boolean { + return this.clientType === 'jupiter'; + } + + /** + * Check if this is a Titan client + */ + public isTitan(): boolean { + return this.clientType === 'titan'; + } +} diff --git a/sdk/src/swift/swiftOrderSubscriber.ts b/sdk/src/swift/swiftOrderSubscriber.ts index bab3032795..eac7e3893c 100644 --- a/sdk/src/swift/swiftOrderSubscriber.ts +++ b/sdk/src/swift/swiftOrderSubscriber.ts @@ -41,6 +41,31 @@ export type SwiftOrderSubscriberConfig = { keypair: Keypair; }; +/** + * Swift order message received from WebSocket + */ +export interface SwiftOrderMessage { + /** Hex string of the order message */ + order_message: string; + /** Base58 string of taker authority */ + taker_authority: string; + /** Base58 string of signing authority */ + signing_authority: string; + /** Base64 string containing the order signature */ + order_signature: string; + /** Swift order UUID */ + uuid: string; + /** Whether the order auction params are likely to be sanitized on submission to program */ + will_sanitize?: boolean; + /** Base64 string of a prerequisite deposit tx. The swift order_message should be bundled + * after the deposit when present */ + depositTx?: string; + /** order market index */ + market_index: number; + /** order timestamp in unix ms */ + ts: number; +} + export class SwiftOrderSubscriber { private heartbeatTimeout: ReturnType | null = null; private readonly heartbeatIntervalMs = 60000; @@ -48,7 +73,7 @@ export class SwiftOrderSubscriber { private driftClient: DriftClient; public userAccountGetter?: AccountGetter; // In practice, this for now is just an OrderSubscriber or a UserMap public onOrder: ( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, @@ -120,13 +145,14 @@ export class SwiftOrderSubscriber { async subscribe( onOrder: ( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, isDelegateSigner?: boolean ) => Promise, - acceptSanitized = false + acceptSanitized = false, + acceptDepositTrade = false ): Promise { this.onOrder = onOrder; @@ -150,13 +176,20 @@ export class SwiftOrderSubscriber { } if (message['order']) { - const order = message['order']; + const order = message['order'] as SwiftOrderMessage; // ignore likely sanitized orders by default - if (order['will_sanitize'] === true && !acceptSanitized) { + if (order.will_sanitize === true && !acceptSanitized) { return; } + // order has a prerequisite deposit tx attached + if (message['deposit']) { + order.depositTx = message['deposit']; + if (!acceptDepositTrade) { + return; + } + } const signedMsgOrderParamsBuf = Buffer.from( - order['order_message'], + order.order_message, 'hex' ); const isDelegateSigner = signedMsgOrderParamsBuf @@ -168,9 +201,7 @@ export class SwiftOrderSubscriber { ).slice(0, 8) ) ); - const signedMessage: - | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage = + const signedMessage = this.driftClient.decodeSignedMsgOrderParamsMessage( signedMsgOrderParamsBuf, isDelegateSigner @@ -224,7 +255,7 @@ export class SwiftOrderSubscriber { } async getPlaceAndMakeSignedMsgOrderIxs( - orderMessageRaw: any, + orderMessageRaw: SwiftOrderMessage, signedMsgOrderParamsMessage: | SignedMsgOrderParamsMessage | SignedMsgOrderParamsDelegateMessage, @@ -235,7 +266,7 @@ export class SwiftOrderSubscriber { } const signedMsgOrderParamsBuf = Buffer.from( - orderMessageRaw['order_message'], + orderMessageRaw.order_message, 'hex' ); @@ -248,18 +279,13 @@ export class SwiftOrderSubscriber { ).slice(0, 8) ) ); - const signedMessage: - | SignedMsgOrderParamsMessage - | SignedMsgOrderParamsDelegateMessage = - this.driftClient.decodeSignedMsgOrderParamsMessage( - signedMsgOrderParamsBuf, - isDelegateSigner - ); - - const takerAuthority = new PublicKey(orderMessageRaw['taker_authority']); - const signingAuthority = new PublicKey( - orderMessageRaw['signing_authority'] + const signedMessage = this.driftClient.decodeSignedMsgOrderParamsMessage( + signedMsgOrderParamsBuf, + isDelegateSigner ); + + const takerAuthority = new PublicKey(orderMessageRaw.taker_authority); + const signingAuthority = new PublicKey(orderMessageRaw.signing_authority); const takerUserPubkey = isDelegateSigner ? (signedMessage as SignedMsgOrderParamsDelegateMessage).takerPubkey : await getUserAccountPublicKey( @@ -273,9 +299,9 @@ export class SwiftOrderSubscriber { const ixs = await this.driftClient.getPlaceAndMakeSignedMsgPerpOrderIxs( { orderParams: signedMsgOrderParamsBuf, - signature: Buffer.from(orderMessageRaw['order_signature'], 'base64'), + signature: Buffer.from(orderMessageRaw.order_signature, 'base64'), }, - decodeUTF8(orderMessageRaw['uuid']), + decodeUTF8(orderMessageRaw.uuid), { taker: takerUserPubkey, takerUserAccount, diff --git a/sdk/src/titan/titanClient.ts b/sdk/src/titan/titanClient.ts new file mode 100644 index 0000000000..e85251957c --- /dev/null +++ b/sdk/src/titan/titanClient.ts @@ -0,0 +1,414 @@ +import { + Connection, + PublicKey, + TransactionMessage, + AddressLookupTableAccount, + TransactionInstruction, +} from '@solana/web3.js'; +import { BN } from '@coral-xyz/anchor'; +import { decode } from '@msgpack/msgpack'; + +export enum SwapMode { + ExactIn = 'ExactIn', + ExactOut = 'ExactOut', +} + +interface RoutePlanStep { + ammKey: Uint8Array; + label: string; + inputMint: Uint8Array; + outputMint: Uint8Array; + inAmount: number; + outAmount: number; + allocPpb: number; + feeMint?: Uint8Array; + feeAmount?: number; + contextSlot?: number; +} + +interface PlatformFee { + amount: number; + fee_bps: number; +} + +type Pubkey = Uint8Array; + +interface AccountMeta { + p: Pubkey; + s: boolean; + w: boolean; +} + +interface Instruction { + p: Pubkey; + a: AccountMeta[]; + d: Uint8Array; +} + +interface SwapRoute { + inAmount: number; + outAmount: number; + slippageBps: number; + platformFee?: PlatformFee; + steps: RoutePlanStep[]; + instructions: Instruction[]; + addressLookupTables: Pubkey[]; + contextSlot?: number; + timeTaken?: number; + expiresAtMs?: number; + expiresAfterSlot?: number; + computeUnits?: number; + computeUnitsSafe?: number; + transaction?: Uint8Array; + referenceId?: string; +} + +interface SwapQuotes { + id: string; + inputMint: Uint8Array; + outputMint: Uint8Array; + swapMode: SwapMode; + amount: number; + quotes: { [key: string]: SwapRoute }; +} + +export interface QuoteResponse { + inputMint: string; + inAmount: string; + outputMint: string; + outAmount: string; + swapMode: SwapMode; + slippageBps: number; + platformFee?: { amount?: string; feeBps?: number }; + routePlan: Array<{ swapInfo: any; percent: number }>; + contextSlot?: number; + timeTaken?: number; + error?: string; + errorCode?: string; +} + +const TITAN_API_URL = 'https://api.titan.exchange'; + +export class TitanClient { + authToken: string; + url: string; + connection: Connection; + + constructor({ + connection, + authToken, + url, + }: { + connection: Connection; + authToken: string; + url?: string; + }) { + this.connection = connection; + this.authToken = authToken; + this.url = url ?? TITAN_API_URL; + } + + /** + * Get routes for a swap + */ + public async getQuote({ + inputMint, + outputMint, + amount, + userPublicKey, + maxAccounts = 50, // 50 is an estimated amount with buffer + slippageBps, + swapMode, + onlyDirectRoutes, + excludeDexes, + sizeConstraint, + accountsLimitWritable, + }: { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey: PublicKey; + maxAccounts?: number; + slippageBps?: number; + swapMode?: string; + onlyDirectRoutes?: boolean; + excludeDexes?: string[]; + sizeConstraint?: number; + accountsLimitWritable?: number; + }): Promise { + const params = new URLSearchParams({ + inputMint: inputMint.toString(), + outputMint: outputMint.toString(), + amount: amount.toString(), + userPublicKey: userPublicKey.toString(), + ...(slippageBps && { slippageBps: slippageBps.toString() }), + ...(swapMode && { + swapMode: + swapMode === 'ExactOut' ? SwapMode.ExactOut : SwapMode.ExactIn, + }), + ...(onlyDirectRoutes && { + onlyDirectRoutes: onlyDirectRoutes.toString(), + }), + ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }), + ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }), + ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }), + ...(accountsLimitWritable && { + accountsLimitWritable: accountsLimitWritable.toString(), + }), + }); + + const response = await fetch( + `${this.url}/api/v1/quote/swap?${params.toString()}`, + { + headers: { + Accept: 'application/vnd.msgpack', + 'Accept-Encoding': 'gzip, deflate, br', + Authorization: `Bearer ${this.authToken}`, + }, + } + ); + + if (!response.ok) { + throw new Error( + `Titan API error: ${response.status} ${response.statusText}` + ); + } + + const buffer = await response.arrayBuffer(); + const data = decode(buffer) as SwapQuotes; + + const route = + data.quotes[ + Object.keys(data.quotes).find((key) => key.toLowerCase() === 'titan') || + '' + ]; + + if (!route) { + throw new Error('No routes available'); + } + + return { + inputMint: inputMint.toString(), + inAmount: amount.toString(), + outputMint: outputMint.toString(), + outAmount: route.outAmount.toString(), + swapMode: data.swapMode, + slippageBps: route.slippageBps, + platformFee: route.platformFee + ? { + amount: route.platformFee.amount.toString(), + feeBps: route.platformFee.fee_bps, + } + : undefined, + routePlan: + route.steps?.map((step: any) => ({ + swapInfo: { + ammKey: new PublicKey(step.ammKey).toString(), + label: step.label, + inputMint: new PublicKey(step.inputMint).toString(), + outputMint: new PublicKey(step.outputMint).toString(), + inAmount: step.inAmount.toString(), + outAmount: step.outAmount.toString(), + feeAmount: step.feeAmount?.toString() || '0', + feeMint: step.feeMint ? new PublicKey(step.feeMint).toString() : '', + }, + percent: 100, + })) || [], + contextSlot: route.contextSlot, + timeTaken: route.timeTaken, + }; + } + + /** + * Get a swap transaction for quote + */ + public async getSwap({ + inputMint, + outputMint, + amount, + userPublicKey, + maxAccounts = 50, // 50 is an estimated amount with buffer + slippageBps, + swapMode, + onlyDirectRoutes, + excludeDexes, + sizeConstraint, + accountsLimitWritable, + }: { + inputMint: PublicKey; + outputMint: PublicKey; + amount: BN; + userPublicKey: PublicKey; + maxAccounts?: number; + slippageBps?: number; + swapMode?: SwapMode; + onlyDirectRoutes?: boolean; + excludeDexes?: string[]; + sizeConstraint?: number; + accountsLimitWritable?: number; + }): Promise<{ + transactionMessage: TransactionMessage; + lookupTables: AddressLookupTableAccount[]; + }> { + const params = new URLSearchParams({ + inputMint: inputMint.toString(), + outputMint: outputMint.toString(), + amount: amount.toString(), + userPublicKey: userPublicKey.toString(), + ...(slippageBps && { slippageBps: slippageBps.toString() }), + ...(swapMode && { swapMode: swapMode }), + ...(maxAccounts && { accountsLimitTotal: maxAccounts.toString() }), + ...(excludeDexes && { excludeDexes: excludeDexes.join(',') }), + ...(onlyDirectRoutes && { + onlyDirectRoutes: onlyDirectRoutes.toString(), + }), + ...(sizeConstraint && { sizeConstraint: sizeConstraint.toString() }), + ...(accountsLimitWritable && { + accountsLimitWritable: accountsLimitWritable.toString(), + }), + }); + + const response = await fetch( + `${this.url}/api/v1/quote/swap?${params.toString()}`, + { + headers: { + Accept: 'application/vnd.msgpack', + 'Accept-Encoding': 'gzip, deflate, br', + Authorization: `Bearer ${this.authToken}`, + }, + } + ); + + if (!response.ok) { + if (response.status === 404) { + throw new Error('No routes available'); + } + throw new Error( + `Titan API error: ${response.status} ${response.statusText}` + ); + } + + const buffer = await response.arrayBuffer(); + const data = decode(buffer) as SwapQuotes; + + const route = + data.quotes[ + Object.keys(data.quotes).find((key) => key.toLowerCase() === 'titan') || + '' + ]; + + if (!route) { + throw new Error('No routes available'); + } + + if (route.instructions && route.instructions.length > 0) { + try { + const { transactionMessage, lookupTables } = + await this.getTransactionMessageAndLookupTables(route, userPublicKey); + return { transactionMessage, lookupTables }; + } catch (err) { + throw new Error( + 'Something went wrong with creating the Titan swap transaction. Please try again.' + ); + } + } + throw new Error('No instructions provided in the route'); + } + + /** + * Get the titan instructions from transaction by filtering out instructions to compute budget and associated token programs + * @param transactionMessage the transaction message + * @param inputMint the input mint + * @param outputMint the output mint + */ + public getTitanInstructions({ + transactionMessage, + inputMint, + outputMint, + }: { + transactionMessage: TransactionMessage; + inputMint: PublicKey; + outputMint: PublicKey; + }): TransactionInstruction[] { + // Filter out common system instructions that can be handled by DriftClient + const filteredInstructions = transactionMessage.instructions.filter( + (instruction) => { + const programId = instruction.programId.toString(); + + // Filter out system programs + if (programId === 'ComputeBudget111111111111111111111111111111') { + return false; + } + + if (programId === 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA') { + return false; + } + + if (programId === '11111111111111111111111111111111') { + return false; + } + + // Filter out Associated Token Account creation for input/output mints + if (programId === 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL') { + if (instruction.keys.length > 3) { + const mint = instruction.keys[3].pubkey; + if (mint.equals(inputMint) || mint.equals(outputMint)) { + return false; + } + } + } + + return true; + } + ); + return filteredInstructions; + } + + private async getTransactionMessageAndLookupTables( + route: SwapRoute, + userPublicKey: PublicKey + ): Promise<{ + transactionMessage: TransactionMessage; + lookupTables: AddressLookupTableAccount[]; + }> { + const solanaInstructions: TransactionInstruction[] = route.instructions.map( + (instruction) => ({ + programId: new PublicKey(instruction.p), + keys: instruction.a.map((meta) => ({ + pubkey: new PublicKey(meta.p), + isSigner: meta.s, + isWritable: meta.w, + })), + data: Buffer.from(instruction.d), + }) + ); + + // Get recent blockhash + const { blockhash } = await this.connection.getLatestBlockhash(); + + // Build address lookup tables if provided + const addressLookupTables: AddressLookupTableAccount[] = []; + if (route.addressLookupTables && route.addressLookupTables.length > 0) { + for (const altPubkey of route.addressLookupTables) { + try { + const altAccount = await this.connection.getAddressLookupTable( + new PublicKey(altPubkey) + ); + if (altAccount.value) { + addressLookupTables.push(altAccount.value); + } + } catch (err) { + console.warn(`Failed to fetch address lookup table:`, err); + } + } + } + + const transactionMessage = new TransactionMessage({ + payerKey: userPublicKey, + recentBlockhash: blockhash, + instructions: solanaInstructions, + }); + + return { transactionMessage, lookupTables: addressLookupTables }; + } +} diff --git a/sdk/src/types.ts b/sdk/src/types.ts index f263e2b9cc..fd0d9368e5 100644 --- a/sdk/src/types.ts +++ b/sdk/src/types.ts @@ -31,6 +31,8 @@ export enum ExchangeStatus { export enum FeatureBitFlags { MM_ORACLE_UPDATE = 1, MEDIAN_TRIGGER_PRICE = 2, + BUILDER_CODES = 4, + BUILDER_REFERRAL = 8, } export class MarketStatus { @@ -582,6 +584,10 @@ export type SpotBankruptcyRecord = { ifPayment: BN; }; +export class LiquidationBitFlag { + static readonly IsolatedPosition = 1; +} + export type SettlePnlRecord = { ts: BN; user: PublicKey; @@ -765,6 +771,7 @@ export type LPSwapRecord = { outMarketTargetWeight: BN; inSwapId: BN; outSwapId: BN; + lpPool: PublicKey; }; export type LPMintRedeemRecord = { @@ -787,6 +794,7 @@ export type LPMintRedeemRecord = { lastAumSlot: BN; inMarketCurrentWeight: BN; inMarketTargetWeight: BN; + lpPool: PublicKey; }; export type LPSettleRecord = { @@ -801,6 +809,20 @@ export type LPSettleRecord = { perpAmmExFeeDelta: BN; lpAum: BN; lpPrice: BN; + lpPool: PublicKey; +}; + +export type LPBorrowLendDepositRecord = { + ts: BN; + slot: BN; + spotMarketIndex: number; + constituentIndex: number; + direction: DepositDirection; + tokenBalance: BN; + lastTokenBalance: BN; + interestAccruedTokenAmount: BN; + amountDepositWithdraw: BN; + lpPool: PublicKey; }; export type StateAccount = { @@ -876,6 +898,12 @@ export type PerpMarketAccount = { protectedMakerLimitPriceDivisor: number; protectedMakerDynamicDivisor: number; lastFillPrice: BN; + + lpPoolId: number; + lpFeeTransferScalar: number; + lpExchangeFeeExcluscionScalar: number; + lpStatus: number; + lpPausedOperations: number; }; export type HistoricalOracleData = { @@ -1084,11 +1112,13 @@ export type AMM = { quoteAssetAmountWithUnsettledLp: BN; referencePriceOffset: number; - takerSpeedBumpOverride: number; + oracleLowRiskSlotDelayOverride: number; + oracleSlotDelayOverride: number; ammSpreadAdjustment: number; ammInventorySpreadAdjustment: number; lastFundingOracleTwap: BN; + referencePriceOffsetDeadbandPct: number; }; // # User Account Types @@ -1110,8 +1140,8 @@ export type PerpPosition = { lastBaseAssetAmountPerLp: BN; lastQuoteAssetAmountPerLp: BN; perLpBase: number; - isolatedPositionScaledBalance: BN; positionFlag: number; + isolatedPositionScaledBalance: BN; }; export type UserStatsAccount = { @@ -1264,6 +1294,12 @@ export class OrderParamsBitFlag { static readonly UpdateHighLeverageMode = 2; } +export class PositionFlag { + static readonly IsolatedPosition = 1; + static readonly BeingLiquidated = 2; + static readonly Bankruptcy = 3; +} + export type NecessaryOrderParams = { orderType: OrderType; marketIndex: number; @@ -1275,6 +1311,10 @@ export type OptionalOrderParams = { [Property in keyof OrderParams]?: OrderParams[Property]; } & NecessaryOrderParams; +export type PerpOrderIsolatedExtras = { + isolatedPositionDepositAmount?: BN; +}; + export type ModifyOrderParams = { [Property in keyof OrderParams]?: OrderParams[Property] | null; } & { policy?: ModifyOrderPolicy }; @@ -1312,6 +1352,9 @@ export type SignedMsgOrderParamsMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + builderIdx?: number | null; + builderFeeTenthBps?: number | null; + isolatedPositionDeposit?: BN | null; }; export type SignedMsgOrderParamsDelegateMessage = { @@ -1322,6 +1365,9 @@ export type SignedMsgOrderParamsDelegateMessage = { takeProfitOrderParams: SignedMsgTriggerOrderParams | null; stopLossOrderParams: SignedMsgTriggerOrderParams | null; maxMarginRatio?: number | null; + builderIdx?: number | null; + builderFeeTenthBps?: number | null; + isolatedPositionDeposit?: BN | null; }; export type SignedMsgTriggerOrderParams = { @@ -1406,6 +1452,10 @@ export interface IVersionedWallet { payer?: Keypair; } +export interface IWalletV2 extends IWallet { + signMessage(message: Uint8Array): Promise; +} + export type FeeStructure = { feeTiers: FeeTier[]; fillerRewardStructure: OrderFillerRewardStructure; @@ -1443,6 +1493,17 @@ export type OracleGuardRails = { }; }; +export enum OracleValidity { + NonPositive = 0, + TooVolatile = 1, + TooUncertain = 2, + StaleForMargin = 3, + InsufficientDataPoints = 4, + StaleForAMMLowRisk = 5, + isStaleForAmmImmediate = 6, + Valid = 7, +} + export type PrelaunchOracle = { price: BN; maxPrice: BN; @@ -1640,3 +1701,212 @@ export type SignedMsgUserOrdersAccount = { authorityPubkey: PublicKey; signedMsgOrderData: SignedMsgOrderId[]; }; + +export type RevenueShareAccount = { + authority: PublicKey; + totalReferrerRewards: BN; + totalBuilderRewards: BN; + padding: number[]; +}; + +export type RevenueShareEscrowAccount = { + authority: PublicKey; + referrer: PublicKey; + referrerBoostExpireTs: number; + referrerRewardOffset: number; + refereeFeeNumeratorOffset: number; + referrerBoostNumerator: number; + reservedFixed: number[]; + orders: RevenueShareOrder[]; + approvedBuilders: BuilderInfo[]; +}; + +export type RevenueShareOrder = { + feesAccrued: BN; + orderId: number; + feeTenthBps: number; + marketIndex: number; + subAccountId: number; + builderIdx: number; + bitFlags: number; + userOrderIndex: number; + marketType: MarketType; + padding: number[]; +}; + +export type BuilderInfo = { + authority: PublicKey; + maxFeeTenthBps: number; + padding: number[]; +}; + +export type RevenueShareSettleRecord = { + ts: number; + builder: PublicKey | null; + referrer: PublicKey | null; + feeSettled: BN; + marketIndex: number; + marketType: MarketType; + builderTotalReferrerRewards: BN; + builderTotalBuilderRewards: BN; + builderSubAccountId: number; +}; + +export type AddAmmConstituentMappingDatum = { + constituentIndex: number; + perpMarketIndex: number; + weight: BN; +}; + +export type AmmConstituentDatum = AddAmmConstituentMappingDatum & { + lastSlot: BN; +}; + +export type AmmConstituentMapping = { + lpPool: PublicKey; + bump: number; + weights: AmmConstituentDatum[]; +}; + +export type TargetDatum = { + costToTradeBps: number; + lastOracleSlot: BN; + lastPositionSlot: BN; + targetBase: BN; +}; + +export type ConstituentTargetBaseAccount = { + lpPool: PublicKey; + bump: number; + targets: TargetDatum[]; +}; + +export type ConstituentCorrelations = { + lpPool: PublicKey; + bump: number; + correlations: BN[]; +}; + +export type LPPoolAccount = { + lpPoolId: number; + pubkey: PublicKey; + mint: PublicKey; + whitelistMint: PublicKey; + constituentTargetBase: PublicKey; + constituentCorrelations: PublicKey; + maxAum: BN; + lastAum: BN; + cumulativeQuoteSentToPerpMarkets: BN; + cumulativeQuoteReceivedFromPerpMarkets: BN; + totalMintRedeemFeesPaid: BN; + lastAumSlot: BN; + maxSettleQuoteAmount: BN; + mintRedeemId: BN; + settleId: BN; + minMintFee: BN; + tokenSupply: BN; + volatility: BN; + constituents: number; + quoteConstituentIndex: number; + bump: number; + gammaExecution: number; + xi: number; +}; + +export type ConstituentSpotBalance = { + scaledBalance: BN; + cumulativeDeposits: BN; + marketIndex: number; + balanceType: SpotBalanceType; +}; + +export type InitializeConstituentParams = { + spotMarketIndex: number; + decimals: number; + maxWeightDeviation: BN; + swapFeeMin: BN; + swapFeeMax: BN; + maxBorrowTokenAmount: BN; + oracleStalenessThreshold: BN; + costToTrade: number; + derivativeWeight: BN; + constituentDerivativeIndex?: number; + constituentDerivativeDepegThreshold?: BN; + constituentCorrelations: BN[]; + volatility: BN; + gammaExecution?: number; + gammaInventory?: number; + xi?: number; +}; + +export enum ConstituentStatus { + ACTIVE = 0, + REDUCE_ONLY = 1, + DECOMMISSIONED = 2, +} +export enum ConstituentLpOperation { + Swap = 0b00000001, + Deposit = 0b00000010, + Withdraw = 0b00000100, +} + +export type ConstituentAccount = { + pubkey: PublicKey; + mint: PublicKey; + lpPool: PublicKey; + vault: PublicKey; + totalSwapFees: BN; + spotBalance: ConstituentSpotBalance; + lastSpotBalanceTokenAmount: BN; + cumulativeSpotInterestAccruedTokenAmount: BN; + maxWeightDeviation: BN; + swapFeeMin: BN; + swapFeeMax: BN; + maxBorrowTokenAmount: BN; + vaultTokenBalance: BN; + lastOraclePrice: BN; + lastOracleSlot: BN; + oracleStalenessThreshold: BN; + flashLoanInitialTokenAmount: BN; + nextSwapId: BN; + derivativeWeight: BN; + volatility: BN; + constituentDerivativeDepegThreshold: BN; + constituentDerivativeIndex: number; + spotMarketIndex: number; + constituentIndex: number; + decimals: number; + bump: number; + vaultBump: number; + gammaInventory: number; + gammaExecution: number; + xi: number; + status: number; + pausedOperations: number; +}; + +export type CacheInfo = { + oracle: PublicKey; + lastFeePoolTokenAmount: BN; + lastNetPnlPoolTokenAmount: BN; + lastExchangeFees: BN; + lastSettleAmmExFees: BN; + lastSettleAmmPnl: BN; + position: BN; + slot: BN; + lastSettleAmount: BN; + lastSettleSlot: BN; + lastSettleTs: BN; + quoteOwedFromLpPool: BN; + ammInventoryLimit: BN; + oraclePrice: BN; + oracleSlot: BN; + oracleSource: number; + oracleValidity: number; + lpStatusForPerpMarket: number; + ammPositionScalar: number; +}; + +export type AmmCache = { + cache: CacheInfo[]; +}; diff --git a/sdk/src/user.ts b/sdk/src/user.ts index 7b78495e21..50cf587598 100644 --- a/sdk/src/user.ts +++ b/sdk/src/user.ts @@ -68,6 +68,7 @@ import { getUser30dRollingVolumeEstimate } from './math/trade'; import { MarketType, PositionDirection, + PositionFlag, SpotBalanceType, SpotMarketAccount, } from './types'; @@ -106,6 +107,9 @@ import { StrictOraclePrice } from './oracles/strictOraclePrice'; import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel'; import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber'; +import { MarginCalculation, MarginContext } from './marginCalculation'; + +export type MarginType = 'Cross' | 'Isolated'; export class User { driftClient: DriftClient; @@ -137,15 +141,22 @@ export class User { } else if (config.accountSubscription?.type === 'custom') { this.accountSubscriber = config.accountSubscription.userAccountSubscriber; } else if (config.accountSubscription?.type === 'grpc') { - this.accountSubscriber = new grpcUserAccountSubscriber( - config.accountSubscription.grpcConfigs, - config.driftClient.program, - config.userAccountPublicKey, - { - resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, - logResubMessages: config.accountSubscription?.logResubMessages, - } - ); + if (config.accountSubscription.grpcMultiUserAccountSubscriber) { + this.accountSubscriber = + config.accountSubscription.grpcMultiUserAccountSubscriber.forUser( + config.userAccountPublicKey + ); + } else { + this.accountSubscriber = new grpcUserAccountSubscriber( + config.accountSubscription.grpcConfigs, + config.driftClient.program, + config.userAccountPublicKey, + { + resubTimeoutMs: config.accountSubscription?.resubTimeoutMs, + logResubMessages: config.accountSubscription?.logResubMessages, + } + ); + } } else { if ( config.accountSubscription?.type === 'websocket' && @@ -331,6 +342,8 @@ export class User { lastQuoteAssetAmountPerLp: ZERO, perLpBase: 0, maxMarginRatio: 0, + positionFlag: 0, + isolatedPositionScaledBalance: ZERO, }; } @@ -465,7 +478,8 @@ export class User { public getPerpBuyingPower( marketIndex: number, collateralBuffer = ZERO, - enterHighLeverageMode = undefined + enterHighLeverageMode = undefined, + maxMarginRatio = undefined ): BN { const perpPosition = this.getPerpPositionOrEmpty(marketIndex); @@ -489,7 +503,7 @@ export class User { freeCollateral, worstCaseBaseAssetAmount, enterHighLeverageMode, - perpPosition + maxMarginRatio || perpPosition.maxMarginRatio ); } @@ -498,17 +512,17 @@ export class User { freeCollateral: BN, baseAssetAmount: BN, enterHighLeverageMode = undefined, - perpPosition?: PerpPosition + perpMarketMaxMarginRatio = undefined ): BN { - const userCustomMargin = Math.max( - perpPosition?.maxMarginRatio ?? 0, + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, this.getUserAccount().maxMarginRatio ); const marginRatio = calculateMarketMarginRatio( this.driftClient.getPerpMarketAccount(marketIndex), baseAssetAmount, 'Initial', - userCustomMargin, + maxMarginRatio, enterHighLeverageMode || this.isHighLeverageMode('Initial') ); @@ -521,62 +535,113 @@ export class User { */ public getFreeCollateral( marginCategory: MarginCategory = 'Initial', - enterHighLeverageMode = undefined + enterHighLeverageMode = false, + perpMarketIndex?: number ): BN { - const totalCollateral = this.getTotalCollateral(marginCategory, true); - const marginRequirement = - marginCategory === 'Initial' - ? this.getInitialMarginRequirement(enterHighLeverageMode) - : this.getMaintenanceMarginRequirement(); - const freeCollateral = totalCollateral.sub(marginRequirement); - return freeCollateral.gte(ZERO) ? freeCollateral : ZERO; + const marginCalc = this.getMarginCalculation(marginCategory, { + enteringHighLeverage: enterHighLeverageMode, + }); + + if (perpMarketIndex !== undefined) { + return marginCalc.getIsolatedFreeCollateral(perpMarketIndex); + } else { + return marginCalc.getCrossFreeCollateral(); + } } /** - * @returns The margin requirement of a certain type (Initial or Maintenance) in USDC. : QUOTE_PRECISION + * @deprecated Use the overload that includes { marginType, perpMarketIndex } */ public getMarginRequirement( marginCategory: MarginCategory, liquidationBuffer?: BN, - strict = false, - includeOpenOrders = true, - enteringHighLeverage = undefined + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean + ): BN; + + /** + * Calculates the margin requirement based on the specified parameters. + * + * @param marginCategory - The category of margin to calculate ('Initial' or 'Maintenance'). + * @param liquidationBuffer - Optional buffer amount to consider during liquidation scenarios. + * @param strict - Optional flag to enforce strict margin calculations. + * @param includeOpenOrders - Optional flag to include open orders in the margin calculation. + * @param enteringHighLeverage - Optional flag indicating if the user is entering high leverage mode. + * @param perpMarketIndex - Optional index of the perpetual market. Required if marginType is 'Isolated'. + * + * @returns The calculated margin requirement as a BN (BigNumber). + */ + public getMarginRequirement( + marginCategory: MarginCategory, + liquidationBuffer?: BN, + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean, + perpMarketIndex?: number + ): BN; + + public getMarginRequirement( + marginCategory: MarginCategory, + liquidationBuffer?: BN, + strict?: boolean, + includeOpenOrders?: boolean, + enteringHighLeverage?: boolean, + perpMarketIndex?: number ): BN { - return this.getTotalPerpPositionLiability( - marginCategory, - liquidationBuffer, - includeOpenOrders, + const marginCalc = this.getMarginCalculation(marginCategory, { strict, - enteringHighLeverage - ).add( - this.getSpotMarketLiabilityValue( - undefined, - marginCategory, - liquidationBuffer, - includeOpenOrders, - strict - ) - ); + includeOpenOrders, + enteringHighLeverage, + liquidationBuffer, + }); + + // If perpMarketIndex is provided, compute only for that market index + if (perpMarketIndex !== undefined) { + const isolatedMarginCalculation = + marginCalc.isolatedMarginCalculations.get(perpMarketIndex); + const { marginRequirement } = isolatedMarginCalculation; + + return marginRequirement; + } + + // Default: Cross margin requirement + // TODO: should we be using plus buffer sometimes? + return marginCalc.marginRequirement; } /** * @returns The initial margin requirement in USDC. : QUOTE_PRECISION */ - public getInitialMarginRequirement(enterHighLeverageMode = undefined): BN { + public getInitialMarginRequirement( + enterHighLeverageMode = false, + perpMarketIndex?: number + ): BN { return this.getMarginRequirement( 'Initial', undefined, - true, + false, undefined, - enterHighLeverageMode + enterHighLeverageMode, + perpMarketIndex ); } /** * @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION */ - public getMaintenanceMarginRequirement(liquidationBuffer?: BN): BN { - return this.getMarginRequirement('Maintenance', liquidationBuffer); + public getMaintenanceMarginRequirement( + liquidationBuffer?: BN, + perpMarketIndex?: number + ): BN { + return this.getMarginRequirement( + 'Maintenance', + liquidationBuffer, + true, // strict default + true, // includeOpenOrders default + false, // enteringHighLeverage default + perpMarketIndex + ); } public getActivePerpPositionsForUserAccount( @@ -648,6 +713,7 @@ export class User { const market = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); + if (!market) return unrealizedPnl; const oraclePriceData = this.getMMOracleDataForPerpMarket( market.marketIndex ); @@ -1160,22 +1226,21 @@ export class User { marginCategory: MarginCategory = 'Initial', strict = false, includeOpenOrders = true, - liquidationBuffer?: BN + liquidationBuffer?: BN, + perpMarketIndex?: number ): BN { - return this.getSpotMarketAssetValue( - undefined, - marginCategory, + const marginCalc = this.getMarginCalculation(marginCategory, { + strict, includeOpenOrders, - strict - ).add( - this.getUnrealizedPNL( - true, - undefined, - marginCategory, - strict, - liquidationBuffer - ) - ); + liquidationBuffer, + }); + + if (perpMarketIndex !== undefined) { + return marginCalc.isolatedMarginCalculations.get(perpMarketIndex) + .totalCollateral; + } + + return marginCalc.totalCollateral; } public getLiquidationBuffer(): BN | undefined { @@ -1193,13 +1258,26 @@ export class User { * calculates User Health by comparing total collateral and maint. margin requirement * @returns : number (value from [0, 100]) */ - public getHealth(): number { - if (this.isBeingLiquidated()) { + public getHealth(perpMarketIndex?: number): number { + const marginCalc = this.getMarginCalculation('Maintenance'); + if (this.isCrossMarginBeingLiquidated(marginCalc) && !perpMarketIndex) { return 0; } - const totalCollateral = this.getTotalCollateral('Maintenance'); - const maintenanceMarginReq = this.getMaintenanceMarginRequirement(); + let totalCollateral: BN; + let maintenanceMarginReq: BN; + + if (perpMarketIndex) { + const isolatedMarginCalc = + marginCalc.isolatedMarginCalculations.get(perpMarketIndex); + if (isolatedMarginCalc) { + totalCollateral = isolatedMarginCalc.totalCollateral; + maintenanceMarginReq = isolatedMarginCalc.marginRequirement; + } + } else { + totalCollateral = marginCalc.totalCollateral; + maintenanceMarginReq = marginCalc.marginRequirement; + } let health: number; @@ -1235,6 +1313,8 @@ export class User { perpPosition.marketIndex ); + if (!market) return ZERO; + let valuationPrice = this.getOracleDataForPerpMarket( market.marketIndex ).price; @@ -1264,7 +1344,10 @@ export class User { } if (marginCategory) { - const userCustomMargin = this.getUserAccount().maxMarginRatio; + const userCustomMargin = Math.max( + perpPosition.maxMarginRatio, + this.getUserAccount().maxMarginRatio + ); let marginRatio = new BN( calculateMarketMarginRatio( market, @@ -1495,9 +1578,9 @@ export class User { * calculates current user leverage which is (total liability size) / (net asset value) * @returns : Precision TEN_THOUSAND */ - public getLeverage(includeOpenOrders = true): BN { + public getLeverage(includeOpenOrders = true, perpMarketIndex?: number): BN { return this.calculateLeverageFromComponents( - this.getLeverageComponents(includeOpenOrders) + this.getLeverageComponents(includeOpenOrders, undefined, perpMarketIndex) ); } @@ -1525,13 +1608,61 @@ export class User { getLeverageComponents( includeOpenOrders = true, - marginCategory: MarginCategory = undefined + marginCategory: MarginCategory = undefined, + perpMarketIndex?: number ): { perpLiabilityValue: BN; perpPnl: BN; spotAssetValue: BN; spotLiabilityValue: BN; } { + if (perpMarketIndex) { + const perpPosition = this.getPerpPositionOrEmpty(perpMarketIndex); + const perpLiability = this.calculateWeightedPerpPositionLiability( + perpPosition, + marginCategory, + undefined, + includeOpenOrders + ); + const perpMarket = this.driftClient.getPerpMarketAccount( + perpPosition.marketIndex + ); + + const oraclePriceData = this.getOracleDataForPerpMarket( + perpPosition.marketIndex + ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + perpMarket.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + perpMarket.quoteSpotMarketIndex + ); + const strictOracle = new StrictOraclePrice( + quoteOraclePriceData.price, + quoteOraclePriceData.twap + ); + + const positionUnrealizedPnl = calculatePositionPNL( + perpMarket, + perpPosition, + true, + oraclePriceData + ); + + const spotAssetValue = getStrictTokenValue( + perpPosition.isolatedPositionScaledBalance, + quoteSpotMarket.decimals, + strictOracle + ); + + return { + perpLiabilityValue: perpLiability, + perpPnl: positionUnrealizedPnl, + spotAssetValue, + spotLiabilityValue: ZERO, + }; + } + const perpLiability = this.getTotalPerpPositionLiability( marginCategory, undefined, @@ -1825,35 +1956,113 @@ export class User { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN; + liquidationStatuses: Map< + 'cross' | number, + { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN } + >; } { - const liquidationBuffer = this.getLiquidationBuffer(); + // Deprecated signature retained for backward compatibility in type only + // but implementation now delegates to the new Map-based API and returns cross margin status. + const map = this.getLiquidationStatuses(); + const cross = map.get('cross'); + return cross + ? { ...cross, liquidationStatuses: map } + : { + canBeLiquidated: false, + marginRequirement: ZERO, + totalCollateral: ZERO, + liquidationStatuses: map, + }; + } - const totalCollateral = this.getTotalCollateral( - 'Maintenance', - undefined, - undefined, - liquidationBuffer - ); + /** + * New API: Returns liquidation status for cross and each isolated perp position. + * Map keys: + * - 'cross' for cross margin + * - marketIndex (number) for each isolated perp position + */ + public getLiquidationStatuses( + marginCalc?: MarginCalculation + ): Map< + 'cross' | number, + { canBeLiquidated: boolean; marginRequirement: BN; totalCollateral: BN } + > { + // If not provided, use buffer-aware calc for canBeLiquidated checks + if (!marginCalc) { + const liquidationBuffer = this.getLiquidationBuffer(); + marginCalc = this.getMarginCalculation('Maintenance', { + liquidationBuffer, + }); + } - const marginRequirement = - this.getMaintenanceMarginRequirement(liquidationBuffer); - const canBeLiquidated = totalCollateral.lt(marginRequirement); + const result = new Map< + 'cross' | number, + { + canBeLiquidated: boolean; + marginRequirement: BN; + totalCollateral: BN; + } + >(); + + // Cross margin status + const crossTotalCollateral = marginCalc.totalCollateral; + const crossMarginRequirement = marginCalc.marginRequirement; + result.set('cross', { + canBeLiquidated: crossTotalCollateral.lt(crossMarginRequirement), + marginRequirement: crossMarginRequirement, + totalCollateral: crossTotalCollateral, + }); - return { - canBeLiquidated, - marginRequirement, - totalCollateral, - }; + // Isolated positions status + for (const [ + marketIndex, + isoCalc, + ] of marginCalc.isolatedMarginCalculations) { + const isoTotalCollateral = isoCalc.totalCollateral; + const isoMarginRequirement = isoCalc.marginRequirement; + result.set(marketIndex, { + canBeLiquidated: isoTotalCollateral.lt(isoMarginRequirement), + marginRequirement: isoMarginRequirement, + totalCollateral: isoTotalCollateral, + }); + } + + return result; } - public isBeingLiquidated(): boolean { - return ( + public isBeingLiquidated(marginCalc?: MarginCalculation): boolean { + // Consider on-chain flags OR computed margin status (cross or any isolated) + const hasOnChainFlag = (this.getUserAccount().status & (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > - 0 + 0; + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + return ( + hasOnChainFlag || + this.isCrossMarginBeingLiquidated(calc) || + this.isIsolatedMarginBeingLiquidated(calc) ); } + /** Returns true if cross margin is currently below maintenance requirement (no buffer). */ + public isCrossMarginBeingLiquidated(marginCalc?: MarginCalculation): boolean { + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + return calc.totalCollateral.lt(calc.marginRequirement); + } + + /** Returns true if any isolated perp position is currently below its maintenance requirement (no buffer). */ + public isIsolatedMarginBeingLiquidated( + marginCalc?: MarginCalculation + ): boolean { + const calc = marginCalc ?? this.getMarginCalculation('Maintenance'); + for (const [, isoCalc] of calc.isolatedMarginCalculations) { + if (isoCalc.totalCollateral.lt(isoCalc.marginRequirement)) { + return true; + } + } + return false; + } + public hasStatus(status: UserStatus): boolean { return (this.getUserAccount().status & status) > 0; } @@ -2010,8 +2219,61 @@ export class User { marginCategory: MarginCategory = 'Maintenance', includeOpenOrders = false, offsetCollateral = ZERO, - enteringHighLeverage = undefined + enteringHighLeverage = false, + marginType?: MarginType ): BN { + const market = this.driftClient.getPerpMarketAccount(marketIndex); + + const oracle = + this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle; + + const oraclePrice = + this.driftClient.getOracleDataForPerpMarket(marketIndex).price; + + const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); + + if (marginType === 'Isolated') { + const marginCalculation = this.getMarginCalculation(marginCategory, { + strict: false, + includeOpenOrders, + enteringHighLeverage, + }); + const isolatedMarginCalculation = + marginCalculation.isolatedMarginCalculations.get(marketIndex); + const { totalCollateral, marginRequirement } = isolatedMarginCalculation; + + const freeCollateral = BN.max( + ZERO, + totalCollateral.sub(marginRequirement) + ).add(offsetCollateral); + + const freeCollateralDelta = this.calculateFreeCollateralDeltaForPerp( + market, + currentPerpPosition, + positionBaseSizeChange, + oraclePrice, + marginCategory, + includeOpenOrders, + enteringHighLeverage + ); + + if (freeCollateralDelta.eq(ZERO)) { + return new BN(-1); + } + + const liqPriceDelta = freeCollateral + .mul(QUOTE_PRECISION) + .div(freeCollateralDelta); + + const liqPrice = oraclePrice.sub(liqPriceDelta); + + if (liqPrice.lt(ZERO)) { + return new BN(-1); + } + + return liqPrice; + } + const totalCollateral = this.getTotalCollateral( marginCategory, false, @@ -2029,15 +2291,6 @@ export class User { totalCollateral.sub(marginRequirement) ).add(offsetCollateral); - const oracle = - this.driftClient.getPerpMarketAccount(marketIndex).amm.oracle; - - const oraclePrice = - this.driftClient.getOracleDataForPerpMarket(marketIndex).price; - - const market = this.driftClient.getPerpMarketAccount(marketIndex); - const currentPerpPosition = this.getPerpPositionOrEmpty(marketIndex); - positionBaseSizeChange = standardizeBaseAssetAmount( positionBaseSizeChange, market.amm.orderStepSize @@ -2362,13 +2615,18 @@ export class User { public getMarginUSDCRequiredForTrade( targetMarketIndex: number, baseSize: BN, - estEntryPrice?: BN + estEntryPrice?: BN, + perpMarketMaxMarginRatio?: number ): BN { + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, + this.getUserAccount().maxMarginRatio + ); return calculateMarginUSDCRequiredForTrade( this.driftClient, targetMarketIndex, baseSize, - this.getUserAccount().maxMarginRatio, + maxMarginRatio, undefined, estEntryPrice ); @@ -2377,14 +2635,19 @@ export class User { public getCollateralDepositRequiredForTrade( targetMarketIndex: number, baseSize: BN, - collateralIndex: number + collateralIndex: number, + perpMarketMaxMarginRatio?: number ): BN { + const maxMarginRatio = Math.max( + perpMarketMaxMarginRatio, + this.getUserAccount().maxMarginRatio + ); return calculateCollateralDepositRequiredForTrade( this.driftClient, targetMarketIndex, baseSize, collateralIndex, - this.getUserAccount().maxMarginRatio, + maxMarginRatio, false // assume user cant be high leverage if they havent created user account ? ); } @@ -2402,7 +2665,8 @@ export class User { targetMarketIndex: number, tradeSide: PositionDirection, isLp = false, - enterHighLeverageMode = undefined + enterHighLeverageMode = undefined, + maxMarginRatio = undefined ): { tradeSize: BN; oppositeSideTradeSize: BN } { let tradeSize = ZERO; let oppositeSideTradeSize = ZERO; @@ -2441,7 +2705,8 @@ export class User { const maxPositionSize = this.getPerpBuyingPower( targetMarketIndex, lpBuffer, - enterHighLeverageMode + enterHighLeverageMode, + maxMarginRatio ); if (maxPositionSize.gte(ZERO)) { @@ -2468,8 +2733,12 @@ export class User { const marginRequirement = this.getInitialMarginRequirement( enterHighLeverageMode ); + const marginRatio = Math.max( + currentPosition.maxMarginRatio, + this.getUserAccount().maxMarginRatio + ); const marginFreedByClosing = perpLiabilityValue - .mul(new BN(market.marginRatioInitial)) + .mul(new BN(marginRatio)) .div(MARGIN_PRECISION); const marginRequirementAfterClosing = marginRequirement.sub(marginFreedByClosing); @@ -2485,7 +2754,8 @@ export class User { this.getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount( targetMarketIndex, freeCollateralAfterClose, - ZERO + ZERO, + currentPosition.maxMarginRatio ); oppositeSideTradeSize = perpLiabilityValue; tradeSize = buyingPowerAfterClose; @@ -3586,11 +3856,13 @@ export class User { perpPosition, oraclePriceData, quoteOraclePriceData, + includeOpenOrders = true, }: { marginCategory: MarginCategory; perpPosition: PerpPosition; oraclePriceData?: OraclePriceData; quoteOraclePriceData?: OraclePriceData; + includeOpenOrders?: boolean; }): HealthComponent { const perpMarket = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex @@ -3599,14 +3871,25 @@ export class User { oraclePriceData || this.driftClient.getOracleDataForPerpMarket(perpMarket.marketIndex); const oraclePrice = _oraclePriceData.price; - const { - worstCaseBaseAssetAmount: worstCaseBaseAmount, - worstCaseLiabilityValue, - } = calculateWorstCasePerpLiabilityValue( - perpPosition, - perpMarket, - oraclePrice - ); + + let worstCaseBaseAmount; + let worstCaseLiabilityValue; + if (includeOpenOrders) { + const worstCaseIncludeOrders = calculateWorstCasePerpLiabilityValue( + perpPosition, + perpMarket, + oraclePrice + ); + worstCaseBaseAmount = worstCaseIncludeOrders.worstCaseBaseAssetAmount; + worstCaseLiabilityValue = worstCaseIncludeOrders.worstCaseLiabilityValue; + } else { + worstCaseBaseAmount = perpPosition.baseAssetAmount; + worstCaseLiabilityValue = calculatePerpLiabilityValue( + perpPosition.baseAssetAmount, + oraclePrice, + isVariant(perpMarket.contractType, 'prediction') + ); + } const userCustomMargin = Math.max( perpPosition.maxMarginRatio, @@ -3889,4 +4172,287 @@ export class User { activeSpotPositions: activeSpotMarkets, }; } + + /** + * Compute a consolidated margin snapshot once, without caching. + * Consumers can use this to avoid duplicating work across separate calls. + */ + // TODO: need another param to tell it give it back leverage compnents + // TODO: change get leverage functions need to pull the right values from + public getMarginCalculation( + marginCategory: MarginCategory = 'Initial', + opts?: { + strict?: boolean; // mirror StrictOraclePrice application + includeOpenOrders?: boolean; + enteringHighLeverage?: boolean; + liquidationBuffer?: BN; // margin_buffer analog for buffer mode + marginRatioOverride?: number; // mirrors context.margin_ratio_override + } + ): MarginCalculation { + const strict = opts?.strict ?? false; + const enteringHighLeverage = opts?.enteringHighLeverage ?? false; + const includeOpenOrders = opts?.includeOpenOrders ?? true; // TODO: remove this ?? + const marginBuffer = opts?.liquidationBuffer; // treat as MARGIN_BUFFER ratio if provided + const marginRatioOverride = opts?.marginRatioOverride; + + // Equivalent to on-chain user_custom_margin_ratio + let userCustomMarginRatio = + marginCategory === 'Initial' ? this.getUserAccount().maxMarginRatio : 0; + if (marginRatioOverride !== undefined) { + userCustomMarginRatio = Math.max( + userCustomMarginRatio, + marginRatioOverride + ); + } + + // Initialize calc via JS mirror of Rust MarginCalculation + const ctx = MarginContext.standard(marginCategory) + .strictMode(strict) + .setMarginBuffer(marginBuffer) + .setMarginRatioOverride(userCustomMarginRatio); + const calc = new MarginCalculation(ctx); + + // SPOT POSITIONS + // TODO: include open orders in the worst-case simulation in the same way on both spot and perp positions + for (const spotPosition of this.getUserAccount().spotPositions) { + if (isSpotPositionAvailable(spotPosition)) continue; + + const spotMarket = this.driftClient.getSpotMarketAccount( + spotPosition.marketIndex + ); + const oraclePriceData = this.getOracleDataForSpotMarket( + spotPosition.marketIndex + ); + const twap5 = strict + ? calculateLiveOracleTwap( + spotMarket.historicalOracleData, + oraclePriceData, + new BN(Math.floor(Date.now() / 1000)), + FIVE_MINUTE + ) + : undefined; + const strictOracle = new StrictOraclePrice(oraclePriceData.price, twap5); + + if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX) { + const tokenAmount = getSignedTokenAmount( + getTokenAmount( + spotPosition.scaledBalance, + spotMarket, + spotPosition.balanceType + ), + spotPosition.balanceType + ); + if (isVariant(spotPosition.balanceType, 'deposit')) { + // add deposit value to total collateral + const tokenValue = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ); + calc.addCrossMarginTotalCollateral(tokenValue); + } else { + // borrow on quote contributes to margin requirement + const tokenValueAbs = getStrictTokenValue( + tokenAmount, + spotMarket.decimals, + strictOracle + ).abs(); + calc.addCrossMarginRequirement(tokenValueAbs, tokenValueAbs); + calc.addSpotLiability(); + } + continue; + } + + // Non-quote spot: worst-case simulation + const { + tokenAmount: worstCaseTokenAmount, + ordersValue: worstCaseOrdersValue, + tokenValue: worstCaseTokenValue, + weightedTokenValue: worstCaseWeightedTokenValue, + } = getWorstCaseTokenAmounts( + spotPosition, + spotMarket, + strictOracle, + marginCategory, + userCustomMarginRatio, + includeOpenOrders + ); + + // open order IM + calc.addCrossMarginRequirement( + new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT), + ZERO + ); + + if (worstCaseTokenAmount.gt(ZERO)) { + // asset side increases total collateral (weighted) + calc.addCrossMarginTotalCollateral(worstCaseWeightedTokenValue); + } else if (worstCaseTokenAmount.lt(ZERO)) { + // liability side increases margin requirement (weighted >= abs(token_value)) + const liabilityWeighted = worstCaseWeightedTokenValue.abs(); + calc.addCrossMarginRequirement( + liabilityWeighted, + worstCaseTokenValue.abs() + ); + calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); + } else if (spotPosition.openOrders !== 0) { + calc.addSpotLiability(); + calc.addSpotLiabilityValue(worstCaseTokenValue.abs()); + } + + // orders value contributes to collateral or requirement + if (worstCaseOrdersValue.gt(ZERO)) { + calc.addCrossMarginTotalCollateral(worstCaseOrdersValue); + } else if (worstCaseOrdersValue.lt(ZERO)) { + const absVal = worstCaseOrdersValue.abs(); + calc.addCrossMarginRequirement(absVal, absVal); + } + } + + // PERP POSITIONS + for (const marketPosition of this.getActivePerpPositions()) { + const market = this.driftClient.getPerpMarketAccount( + marketPosition.marketIndex + ); + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const oraclePriceData = this.getOracleDataForPerpMarket( + market.marketIndex + ); + + // Worst-case perp liability and weighted pnl + const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = + calculateWorstCasePerpLiabilityValue( + marketPosition, + market, + oraclePriceData.price, + includeOpenOrders + ); + + // margin ratio for this perp + const customMarginRatio = Math.max( + this.getUserAccount().maxMarginRatio, + marketPosition.maxMarginRatio + ); + let marginRatio = new BN( + calculateMarketMarginRatio( + market, + worstCaseBaseAssetAmount.abs(), + marginCategory, + customMarginRatio, + this.isHighLeverageMode(marginCategory) || enteringHighLeverage + ) + ); + if (isVariant(market.status, 'settlement')) { + marginRatio = ZERO; + } + + // convert liability to quote value and apply margin ratio + const quotePrice = strict + ? BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ) + : quoteOraclePriceData.price; + let perpMarginRequirement = worstCaseLiabilityValue + .mul(quotePrice) + .div(PRICE_PRECISION) + .mul(marginRatio) + .div(MARGIN_PRECISION); + // add open orders IM + perpMarginRequirement = perpMarginRequirement.add( + new BN(marketPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) + ); + + // weighted unrealized pnl + let positionUnrealizedPnl = calculatePositionPNL( + market, + marketPosition, + true, + oraclePriceData + ); + let pnlQuotePrice: BN; + if (strict && positionUnrealizedPnl.gt(ZERO)) { + pnlQuotePrice = BN.min( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else if (strict && positionUnrealizedPnl.lt(ZERO)) { + pnlQuotePrice = BN.max( + quoteOraclePriceData.price, + quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + ); + } else { + pnlQuotePrice = quoteOraclePriceData.price; + } + positionUnrealizedPnl = positionUnrealizedPnl + .mul(pnlQuotePrice) + .div(PRICE_PRECISION); + + // Add perp contribution: isolated vs cross + const isIsolated = this.isPerpPositionIsolated(marketPosition); + if (isIsolated) { + // derive isolated quote deposit value, mirroring on-chain logic + let depositValue = ZERO; + if (marketPosition.isolatedPositionScaledBalance.gt(ZERO)) { + const quoteSpotMarket = this.driftClient.getSpotMarketAccount( + market.quoteSpotMarketIndex + ); + const quoteOraclePriceData = this.getOracleDataForSpotMarket( + market.quoteSpotMarketIndex + ); + const strictQuote = new StrictOraclePrice( + quoteOraclePriceData.price, + strict + ? quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min + : undefined + ); + const quoteTokenAmount = getTokenAmount( + marketPosition.isolatedPositionScaledBalance, + quoteSpotMarket, + SpotBalanceType.DEPOSIT + ); + depositValue = getStrictTokenValue( + quoteTokenAmount, + quoteSpotMarket.decimals, + strictQuote + ); + } + calc.addIsolatedMarginCalculation( + market.marketIndex, + depositValue, + positionUnrealizedPnl, + worstCaseLiabilityValue, + perpMarginRequirement + ); + calc.addPerpLiability(); + calc.addPerpLiabilityValue(worstCaseLiabilityValue); + } else { + // cross: add to global requirement and collateral + calc.addCrossMarginRequirement( + perpMarginRequirement, + worstCaseLiabilityValue + ); + calc.addCrossMarginTotalCollateral(positionUnrealizedPnl); + const hasPerpLiability = + !marketPosition.baseAssetAmount.eq(ZERO) || + marketPosition.quoteAssetAmount.lt(ZERO) || + marketPosition.openOrders !== 0; + if (hasPerpLiability) { + calc.addPerpLiability(); + } + } + } + + return calc; + } + + private isPerpPositionIsolated(perpPosition: PerpPosition): boolean { + return (perpPosition.positionFlag & PositionFlag.IsolatedPosition) !== 0; + } } diff --git a/sdk/src/userConfig.ts b/sdk/src/userConfig.ts index d5ba2147b9..9067833a78 100644 --- a/sdk/src/userConfig.ts +++ b/sdk/src/userConfig.ts @@ -4,6 +4,7 @@ import { BulkAccountLoader } from './accounts/bulkAccountLoader'; import { GrpcConfigs, UserAccountSubscriber } from './accounts/types'; import { WebSocketProgramAccountSubscriber } from './accounts/webSocketProgramAccountSubscriber'; import { UserAccount } from './types'; +import { grpcMultiUserAccountSubscriber } from './accounts/grpcMultiUserAccountSubscriber'; export type UserConfig = { accountSubscription?: UserSubscriptionConfig; @@ -17,6 +18,7 @@ export type UserSubscriptionConfig = resubTimeoutMs?: number; logResubMessages?: boolean; grpcConfigs: GrpcConfigs; + grpcMultiUserAccountSubscriber?: grpcMultiUserAccountSubscriber; } | { type: 'websocket'; diff --git a/sdk/src/userMap/revenueShareEscrowMap.ts b/sdk/src/userMap/revenueShareEscrowMap.ts new file mode 100644 index 0000000000..fb56628a23 --- /dev/null +++ b/sdk/src/userMap/revenueShareEscrowMap.ts @@ -0,0 +1,306 @@ +import { PublicKey, RpcResponseAndContext } from '@solana/web3.js'; +import { DriftClient } from '../driftClient'; +import { RevenueShareEscrowAccount } from '../types'; +import { getRevenueShareEscrowAccountPublicKey } from '../addresses/pda'; +import { getRevenueShareEscrowFilter } from '../memcmp'; + +export class RevenueShareEscrowMap { + /** + * map from authority pubkey to RevenueShareEscrow account data. + */ + private authorityEscrowMap = new Map(); + private driftClient: DriftClient; + private parallelSync: boolean; + + private fetchPromise?: Promise; + private fetchPromiseResolver: () => void; + + /** + * Creates a new RevenueShareEscrowMap instance. + * + * @param {DriftClient} driftClient - The DriftClient instance. + * @param {boolean} parallelSync - Whether to sync accounts in parallel. + */ + constructor(driftClient: DriftClient, parallelSync?: boolean) { + this.driftClient = driftClient; + this.parallelSync = parallelSync !== undefined ? parallelSync : true; + } + + /** + * Subscribe to all RevenueShareEscrow accounts. + */ + public async subscribe() { + if (this.size() > 0) { + return; + } + + await this.driftClient.subscribe(); + await this.sync(); + } + + public has(authorityPublicKey: string): boolean { + return this.authorityEscrowMap.has(authorityPublicKey); + } + + public get( + authorityPublicKey: string + ): RevenueShareEscrowAccount | undefined { + return this.authorityEscrowMap.get(authorityPublicKey); + } + + /** + * Enforce that a RevenueShareEscrow will exist for the given authorityPublicKey, + * reading one from the blockchain if necessary. + * @param authorityPublicKey + * @returns + */ + public async mustGet( + authorityPublicKey: string + ): Promise { + if (!this.has(authorityPublicKey)) { + await this.addRevenueShareEscrow(authorityPublicKey); + } + return this.get(authorityPublicKey); + } + + public async addRevenueShareEscrow(authority: string) { + const escrowAccountPublicKey = getRevenueShareEscrowAccountPublicKey( + this.driftClient.program.programId, + new PublicKey(authority) + ); + + try { + const accountInfo = await this.driftClient.connection.getAccountInfo( + escrowAccountPublicKey, + 'processed' + ); + + if (accountInfo && accountInfo.data) { + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + accountInfo.data + ) as RevenueShareEscrowAccount; + + this.authorityEscrowMap.set(authority, escrow); + } + } catch (error) { + // RevenueShareEscrow account doesn't exist for this authority, which is normal + console.debug( + `No RevenueShareEscrow account found for authority: ${authority}` + ); + } + } + + public size(): number { + return this.authorityEscrowMap.size; + } + + public async sync(): Promise { + if (this.fetchPromise) { + return this.fetchPromise; + } + + this.fetchPromise = new Promise((resolver) => { + this.fetchPromiseResolver = resolver; + }); + + try { + await this.syncAll(); + } finally { + this.fetchPromiseResolver(); + this.fetchPromise = undefined; + } + } + + /** + * A slow, bankrun test friendly version of sync(), uses getAccountInfo on every cached account to refresh data + * @returns + */ + public async slowSync(): Promise { + if (this.fetchPromise) { + return this.fetchPromise; + } + for (const authority of this.authorityEscrowMap.keys()) { + const accountInfo = await this.driftClient.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + this.driftClient.program.programId, + new PublicKey(authority) + ), + 'confirmed' + ); + const escrowNew = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + accountInfo.data + ) as RevenueShareEscrowAccount; + this.authorityEscrowMap.set(authority, escrowNew); + } + } + + public async syncAll(): Promise { + const rpcRequestArgs = [ + this.driftClient.program.programId.toBase58(), + { + commitment: this.driftClient.opts.commitment, + filters: [getRevenueShareEscrowFilter()], + encoding: 'base64', + withContext: true, + }, + ]; + + const rpcJSONResponse: any = + // @ts-ignore + await this.driftClient.connection._rpcRequest( + 'getProgramAccounts', + rpcRequestArgs + ); + + const rpcResponseAndContext: RpcResponseAndContext< + Array<{ + pubkey: string; + account: { + data: [string, string]; + }; + }> + > = rpcJSONResponse.result; + + const batchSize = 100; + for (let i = 0; i < rpcResponseAndContext.value.length; i += batchSize) { + const batch = rpcResponseAndContext.value.slice(i, i + batchSize); + + if (this.parallelSync) { + await Promise.all( + batch.map(async (programAccount) => { + try { + // @ts-ignore + const buffer = Buffer.from( + programAccount.account.data[0], + programAccount.account.data[1] + ); + + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + buffer + ) as RevenueShareEscrowAccount; + + // Extract authority from the account data + const authorityKey = escrow.authority.toBase58(); + this.authorityEscrowMap.set(authorityKey, escrow); + } catch (error) { + console.warn( + `Failed to decode RevenueShareEscrow account ${programAccount.pubkey}:`, + error + ); + } + }) + ); + } else { + for (const programAccount of batch) { + try { + // @ts-ignore + const buffer = Buffer.from( + programAccount.account.data[0], + programAccount.account.data[1] + ); + + const escrow = + this.driftClient.program.account.revenueShareEscrow.coder.accounts.decode( + 'RevenueShareEscrow', + buffer + ) as RevenueShareEscrowAccount; + + // Extract authority from the account data + const authorityKey = escrow.authority.toBase58(); + this.authorityEscrowMap.set(authorityKey, escrow); + } catch (error) { + console.warn( + `Failed to decode RevenueShareEscrow account ${programAccount.pubkey}:`, + error + ); + } + } + } + + // Add a small delay between batches to avoid overwhelming the RPC + await new Promise((resolve) => setTimeout(resolve, 10)); + } + } + + /** + * Get all RevenueShareEscrow accounts + */ + public getAll(): Map { + return new Map(this.authorityEscrowMap); + } + + /** + * Get all authorities that have RevenueShareEscrow accounts + */ + public getAuthorities(): string[] { + return Array.from(this.authorityEscrowMap.keys()); + } + + /** + * Get RevenueShareEscrow accounts that have approved referrers + */ + public getEscrowsWithApprovedReferrers(): Map< + string, + RevenueShareEscrowAccount + > { + const result = new Map(); + for (const [authority, escrow] of this.authorityEscrowMap) { + if (escrow.approvedBuilders && escrow.approvedBuilders.length > 0) { + result.set(authority, escrow); + } + } + return result; + } + + /** + * Get RevenueShareEscrow accounts that have active orders + */ + public getEscrowsWithOrders(): Map { + const result = new Map(); + for (const [authority, escrow] of this.authorityEscrowMap) { + if (escrow.orders && escrow.orders.length > 0) { + result.set(authority, escrow); + } + } + return result; + } + + /** + * Get RevenueShareEscrow account by referrer + */ + public getByReferrer( + referrerPublicKey: string + ): RevenueShareEscrowAccount | undefined { + for (const escrow of this.authorityEscrowMap.values()) { + if (escrow.referrer.toBase58() === referrerPublicKey) { + return escrow; + } + } + return undefined; + } + + /** + * Get all RevenueShareEscrow accounts for a specific referrer + */ + public getAllByReferrer( + referrerPublicKey: string + ): RevenueShareEscrowAccount[] { + const result: RevenueShareEscrowAccount[] = []; + for (const escrow of this.authorityEscrowMap.values()) { + if (escrow.referrer.toBase58() === referrerPublicKey) { + result.push(escrow); + } + } + return result; + } + + public async unsubscribe() { + this.authorityEscrowMap.clear(); + } +} diff --git a/sdk/src/userStats.ts b/sdk/src/userStats.ts index 8fdb30d2bf..a045f533c0 100644 --- a/sdk/src/userStats.ts +++ b/sdk/src/userStats.ts @@ -54,9 +54,14 @@ export class UserStats { }, config.accountSubscription.commitment ); + } else if (config.accountSubscription?.type === 'custom') { + this.accountSubscriber = + config.accountSubscription.userStatsAccountSubscriber; } else { + const exhaustiveCheck: never = config.accountSubscription; + throw new Error( - `Unknown user stats account subscription type: ${config.accountSubscription?.type}` + `Unknown user stats account subscription type: ${exhaustiveCheck}` ); } } diff --git a/sdk/src/userStatsConfig.ts b/sdk/src/userStatsConfig.ts index 693de80e85..2a8f1ce813 100644 --- a/sdk/src/userStatsConfig.ts +++ b/sdk/src/userStatsConfig.ts @@ -1,7 +1,7 @@ import { DriftClient } from './driftClient'; import { Commitment, PublicKey } from '@solana/web3.js'; import { BulkAccountLoader } from './accounts/bulkAccountLoader'; -import { GrpcConfigs } from './accounts/types'; +import { GrpcConfigs, UserStatsAccountSubscriber } from './accounts/types'; export type UserStatsConfig = { accountSubscription?: UserStatsSubscriptionConfig; @@ -22,6 +22,7 @@ export type UserStatsSubscriptionConfig = } | { type: 'custom'; + userStatsAccountSubscriber: UserStatsAccountSubscriber; } | { type: 'grpc'; diff --git a/sdk/src/wallet.ts b/sdk/src/wallet.ts index c7ad92f4a2..05c5759a17 100644 --- a/sdk/src/wallet.ts +++ b/sdk/src/wallet.ts @@ -5,6 +5,7 @@ import { VersionedTransaction, } from '@solana/web3.js'; import { IWallet, IVersionedWallet } from './types'; +import nacl from 'tweetnacl'; export class Wallet implements IWallet, IVersionedWallet { constructor(readonly payer: Keypair) {} @@ -41,3 +42,13 @@ export class Wallet implements IWallet, IVersionedWallet { return this.payer.publicKey; } } + +export class WalletV2 extends Wallet { + constructor(readonly payer: Keypair) { + super(payer); + } + + async signMessage(message: Uint8Array): Promise { + return Buffer.from(nacl.sign.detached(message, this.payer.secretKey)); + } +} diff --git a/sdk/tests/amm/test.ts b/sdk/tests/amm/test.ts index ab849c57d6..b4377f4066 100644 --- a/sdk/tests/amm/test.ts +++ b/sdk/tests/amm/test.ts @@ -279,7 +279,7 @@ describe('AMM Tests', () => { longIntensity, shortIntensity, volume24H, - 0 + 0, ); const l1 = spreads[0]; const s1 = spreads[1]; diff --git a/sdk/tests/dlob/helpers.ts b/sdk/tests/dlob/helpers.ts index d1b68abe8c..d39141b1e2 100644 --- a/sdk/tests/dlob/helpers.ts +++ b/sdk/tests/dlob/helpers.ts @@ -44,6 +44,9 @@ export const mockPerpPosition: PerpPosition = { lastBaseAssetAmountPerLp: new BN(0), lastQuoteAssetAmountPerLp: new BN(0), perLpBase: 0, + positionFlag: 0, + isolatedPositionScaledBalance: new BN(0), + maxMarginRatio: 1, }; export const mockAMM: AMM = { @@ -144,7 +147,7 @@ export const mockAMM: AMM = { quoteAssetAmountWithUnsettledLp: new BN(0), referencePriceOffset: 0, - takerSpeedBumpOverride: 0, + oracleLowRiskSlotDelayOverride: 0, ammSpreadAdjustment: 0, ammInventorySpreadAdjustment: 0, mmOracleSequenceId: new BN(0), @@ -669,6 +672,7 @@ export class MockUserMap implements UserMapInterface { wallet: new Wallet(new Keypair()), programID: PublicKey.default, }); + this.eventEmitter = new EventEmitter(); } public async subscribe(): Promise {} diff --git a/sdk/tests/events/parseLogsForCuUsage.ts b/sdk/tests/events/parseLogsForCuUsage.ts new file mode 100644 index 0000000000..5308664e84 --- /dev/null +++ b/sdk/tests/events/parseLogsForCuUsage.ts @@ -0,0 +1,139 @@ +import { expect } from 'chai'; +import { parseLogsForCuUsage } from '../../src/events/parse'; + +// if you used the '@types/mocha' method to install mocha type definitions, uncomment the following line +// import 'mocha'; + +describe('parseLogsForCuUsage Tests', () => { + it('can parse single ix', () => { + const logs = [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: UpdateFundingRate', + 'Program log: correcting mark twap update (oracle previously invalid for 1625 seconds)', + 'Program data: Ze4o5EYuPXWjSrNoAAAAADkUAAAAAAAAZTIKAAAAAAAAAAAAAAAAAGwIAGYvdqgAAAAAAAAAAAAxpQahVMKdAAAAAAAAAAAAK0cfnMcFowAAAAAAAAAAAGUyCgAAAAAAAAAAAAAAAAAbXpy4n6WoAAAAAAAAAAAAGAAXbsHunQAAAAAAAAAAAObum9evM6MAAAAAAAAAAAAA9PA5+UYKAAAAAAAAAAAAAKhuDZ0RCgAAAAAAAAAAAABMgixcNQAAAAAAAAAAAAA9NqYKSgAAAAAAAAAAAAAA4nylcy0AAAAAAAAAAAAAAMvCAAAAAAAAAAAAAAAAAACOjAkAAAAAAET3AgAAAAAAAAAAAAAAAAAMAQAAHgA=', + 'Program data: RAP/GoVbk/6jSrNoAAAAAA0rAAAAAAAAHgBxcgEAAAAAAHFyAQAAAAAAAAAAAAAAAABxcgEAAAAAAAAAAAAAAAAAZRuYAQAAAAAAAAAAAAAAAJMNmAEAAAAAAAAAAAAAAAC0eAkAAAAAAByBCQAAAAAAWUbv4v////8ATIIsXDUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 102636 of 143817 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + ]; + const cuUsage = parseLogsForCuUsage(logs); + expect(cuUsage).to.deep.equal([ + { + name: 'CuUsage', + data: { + instruction: 'UpdateFundingRate', + cuUsage: 102636, + }, + }, + ]); + }); + + it('can parse multiple ixs', () => { + const logs = [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: PostPythLazerOracleUpdate', + 'Program log: Skipping new lazer update. current ts 1756622092550000 >= next ts 1756622092000000', + 'Program log: Skipping new lazer update. current ts 1756622092550000 >= next ts 1756622092000000', + 'Program log: Skipping new lazer update. current ts 1756622092550000 >= next ts 1756622092000000', + 'Program log: Price updated to 433158894', + 'Program log: Posting new lazer update. current ts 1756622079000000 < next ts 1756622092000000', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 29242 of 199700 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: UpdatePerpBidAskTwap', + 'Program log: estimated_bid = None estimated_ask = None', + 'Program log: after amm bid twap = 204332308 -> 204328128 \n ask twap = 204350474 -> 204347149 \n ts = 1756622080 -> 1756622092', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 71006 of 170458 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + ]; + const cuUsage = parseLogsForCuUsage(logs); + expect(cuUsage).to.deep.equal([ + { + name: 'CuUsage', + data: { + instruction: 'PostPythLazerOracleUpdate', + cuUsage: 29242, + }, + }, + { + name: 'CuUsage', + data: { + instruction: 'UpdatePerpBidAskTwap', + cuUsage: 71006, + }, + }, + ]); + }); + + it('can parse ixs with CPI (swaps)', () => { + const logs = [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: BeginSwap', + 'Program data: t7rLuuG7X4K7X+loAAAAAAAAoDz72UK0JwQAAAAAAAAAAMS7r8ACAAAAAAAAAAAAAAB36Aiv26cZAwAAAAAAAAAArDDOLAMAAAAAAAAAAAAAAAA1DAAUzQAAoLsNAA==', + 'Program data: t7rLuuG7X4K7X+loAAAAAAEASQcRhBUVAQAAAAAAAAAAAG3WGn8CAAAAAAAAAAAAAADBBakRTIoAAAAAAAAAAAAAFfFDwgIAAAAAAAAAAAAAAAA1DACghgEAYOMWAA==', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 1336324 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 79071 of 1399700 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [1]', + 'Program log: Instruction: Route', + 'Program SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF invoke [2]', + 'Program data: S3VCwUhV8CXSyrcV3EtPUNCsQJvXpBqCGUobEJZVRnl5bVAAAAAAAFUFNBYAAAAAAAAAAAAAAAAAAAAAAAAAAA==', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4645 of 1255262 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 1249195 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF consumed 69257 of 1311915 compute units', + 'Program SV2EYYJyRz2YhfXwXnhNAevDEui5Q6yrfyo13WtupPF success', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [2]', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 199 of 1241147 compute units', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 consumed 81059 of 1320629 compute units', + 'Program return: JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 Z/tXxXEAAAA=', + 'Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH invoke [1]', + 'Program log: Instruction: EndSwap', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]', + 'Program log: Instruction: Transfer', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 4736 of 1187840 compute units', + 'Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success', + 'Program data: ort7woo4+vG7X+loAAAAAJ1Bg8Gp9WhWrw9VRm1UiC0KW6LRC2am2mjhfd3lzm6WZ/tXxXEAAAAA6HZIFwAAAAEAAAAJdDEMAAAAANFBDwAAAAAAAAAAAAAAAAA=', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH consumed 156076 of 1239570 compute units', + 'Program dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH success', + ]; + const cuUsage = parseLogsForCuUsage(logs); + expect(cuUsage).to.deep.equal([ + { + name: 'CuUsage', + data: { + instruction: 'BeginSwap', + cuUsage: 79071, + }, + }, + { + name: 'CuUsage', + data: { + instruction: 'EndSwap', + cuUsage: 156076, + }, + }, + ]); + }); +}); diff --git a/sdk/tests/user/getMarginCalculation.ts b/sdk/tests/user/getMarginCalculation.ts new file mode 100644 index 0000000000..0df348a19a --- /dev/null +++ b/sdk/tests/user/getMarginCalculation.ts @@ -0,0 +1,405 @@ +import { + BN, + ZERO, + User, + UserAccount, + PublicKey, + PerpMarketAccount, + SpotMarketAccount, + PRICE_PRECISION, + OraclePriceData, + BASE_PRECISION, + QUOTE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + OPEN_ORDER_MARGIN_REQUIREMENT, + SPOT_MARKET_WEIGHT_PRECISION, + PositionFlag, +} from '../../src'; +import { MockUserMap, mockPerpMarkets, mockSpotMarkets } from '../dlob/helpers'; +import { assert } from '../../src/assert/assert'; +import { mockUserAccount as baseMockUserAccount } from './helpers'; +import * as _ from 'lodash'; + +async function makeMockUser( + myMockPerpMarkets: Array, + myMockSpotMarkets: Array, + myMockUserAccount: UserAccount, + perpOraclePriceList: number[], + spotOraclePriceList: number[] +): Promise { + const umap = new MockUserMap(); + const mockUser: User = await umap.mustGet('1'); + mockUser._isSubscribed = true; + mockUser.driftClient._isSubscribed = true; + mockUser.driftClient.accountSubscriber.isSubscribed = true; + + const oraclePriceMap: Record = {}; + for (let i = 0; i < myMockPerpMarkets.length; i++) { + oraclePriceMap[myMockPerpMarkets[i].amm.oracle.toString()] = + perpOraclePriceList[i] ?? 1; + } + for (let i = 0; i < myMockSpotMarkets.length; i++) { + oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = + spotOraclePriceList[i] ?? 1; + } + + function getMockUserAccount(): UserAccount { + return myMockUserAccount; + } + function getMockPerpMarket(marketIndex: number): PerpMarketAccount { + return myMockPerpMarkets[marketIndex]; + } + function getMockSpotMarket(marketIndex: number): SpotMarketAccount { + return myMockSpotMarkets[marketIndex]; + } + function getMockOracle(oracleKey: PublicKey) { + const data: OraclePriceData = { + price: new BN( + (oraclePriceMap[oracleKey.toString()] ?? 1) * PRICE_PRECISION.toNumber() + ), + slot: new BN(0), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + return { data, slot: 0 }; + } + function getOracleDataForPerpMarket(marketIndex: number) { + const oracle = getMockPerpMarket(marketIndex).amm.oracle; + return getMockOracle(oracle).data; + } + function getOracleDataForSpotMarket(marketIndex: number) { + const oracle = getMockSpotMarket(marketIndex).oracle; + return getMockOracle(oracle).data; + } + + mockUser.getUserAccount = getMockUserAccount; + mockUser.driftClient.getPerpMarketAccount = getMockPerpMarket as any; + mockUser.driftClient.getSpotMarketAccount = getMockSpotMarket as any; + mockUser.driftClient.getOraclePriceDataAndSlot = getMockOracle as any; + mockUser.driftClient.getOracleDataForPerpMarket = + getOracleDataForPerpMarket as any; + mockUser.driftClient.getOracleDataForSpotMarket = + getOracleDataForSpotMarket as any; + return mockUser; +} + +describe('getMarginCalculation snapshot', () => { + it('empty account returns zeroed snapshot', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(ZERO)); + assert(calc.numSpotLiabilities === 0); + assert(calc.numPerpLiabilities === 0); + }); + + it('quote deposit increases totalCollateral, no requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 10000 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expected = new BN('10000000000'); // $10k + assert(calc.totalCollateral.eq(expected)); + assert(calc.marginRequirement.eq(ZERO)); + }); + + it('quote borrow increases requirement and buffer applies', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Borrow 100 quote + myMockUserAccount.spotPositions[0].balanceType = SpotBalanceType.BORROW; + myMockUserAccount.spotPositions[0].scaledBalance = new BN( + 100 * SPOT_MARKET_BALANCE_PRECISION.toNumber() + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const tenPercent = new BN(1000); + const calc = user.getMarginCalculation('Initial', { + liquidationBuffer: tenPercent, + }); + const liability = new BN(100).mul(QUOTE_PRECISION); // $100 + assert(calc.totalCollateral.eq(ZERO)); + assert(calc.marginRequirement.eq(liability)); + assert( + calc.marginRequirementPlusBuffer.eq( + liability.div(new BN(10)).add(calc.marginRequirement) // 10% of liability + margin requirement + ) + ); + assert(calc.numSpotLiabilities === 1); + }); + + it('non-quote spot open orders add IM', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // Market 1 (e.g., SOL) with 2 open orders + myMockUserAccount.spotPositions[1].marketIndex = 1; + myMockUserAccount.spotPositions[1].openOrders = 2; + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Initial'); + const expectedIM = new BN(2).mul(OPEN_ORDER_MARGIN_REQUIREMENT); + assert(calc.marginRequirement.eq(expectedIM)); + }); + + it('perp long liability reflects maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + // 20 base long, -$10 quote (liability) + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN(-10).mul( + QUOTE_PRECISION + ); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + // From existing liquidation test expectations: 2_000_000 + assert(calc.marginRequirement.eq(new BN('2000000'))); + }); + + it.only('maker position reducing: collateral equals maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + const myMockUserAccount = _.cloneDeep(baseMockUserAccount); + + myMockUserAccount.perpPositions[0].baseAssetAmount = new BN(200000000).mul( + BASE_PRECISION + ); + myMockUserAccount.perpPositions[0].quoteAssetAmount = new BN( + -180000000 + ).mul(QUOTE_PRECISION); + + const user: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + myMockUserAccount, + [1, 1, 1, 1, 1, 1, 1, 1], + [1, 1, 1, 1, 1, 1, 1, 1] + ); + + const calc = user.getMarginCalculation('Maintenance'); + console.log('calc.marginRequirement', calc.marginRequirement.toString()); + console.log('calc.totalCollateral', calc.totalCollateral.toString()); + assert(calc.marginRequirement.eq(calc.totalCollateral)); + }); + + it('maker reducing after simulated fill: collateral equals maintenance requirement', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + + // Build maker and taker accounts + const makerAccount = _.cloneDeep(baseMockUserAccount); + const takerAccount = _.cloneDeep(baseMockUserAccount); + + // Oracle price = 1 for perp and spot + const perpOracles = [1, 1, 1, 1, 1, 1, 1, 1]; + const spotOracles = [1, 1, 1, 1, 1, 1, 1, 1]; + + // Pre-fill: maker has 21 base long at entry 1 ($21 notional), taker flat + makerAccount.perpPositions[0].baseAssetAmount = new BN(21).mul( + BASE_PRECISION + ); + makerAccount.perpPositions[0].quoteEntryAmount = new BN(-21).mul( + QUOTE_PRECISION + ); + makerAccount.perpPositions[0].quoteBreakEvenAmount = new BN(-21).mul( + QUOTE_PRECISION + ); + // Provide exactly $2 in quote collateral to equal 10% maintenance of 20 notional post-fill + makerAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + makerAccount.spotPositions[0].scaledBalance = new BN(2).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + + // Simulate fill: maker sells 1 base to taker at price = oracle = 1 + // Post-fill maker position: 20 base long with zero unrealized PnL + const maker: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + makerAccount, + perpOracles, + spotOracles + ); + const taker: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + takerAccount, + perpOracles, + spotOracles + ); + + // Apply synthetic trade deltas to both user accounts + // Maker: base 21 -> 20; taker: base 0 -> 1. Use quote deltas consistent with price 1, fee 0 + maker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(20).mul( + BASE_PRECISION + ); + maker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + maker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN( + -20 + ).mul(QUOTE_PRECISION); + // Align quoteAssetAmount with base value so unrealized PnL = 0 at price 1 + maker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-20).mul( + QUOTE_PRECISION + ); + + taker.getUserAccount().perpPositions[0].baseAssetAmount = new BN(1).mul( + BASE_PRECISION + ); + taker.getUserAccount().perpPositions[0].quoteEntryAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + taker.getUserAccount().perpPositions[0].quoteBreakEvenAmount = new BN( + -1 + ).mul(QUOTE_PRECISION); + // Also set taker's quoteAssetAmount consistently + taker.getUserAccount().perpPositions[0].quoteAssetAmount = new BN(-1).mul( + QUOTE_PRECISION + ); + + const makerCalc = maker.getMarginCalculation('Maintenance'); + assert(makerCalc.marginRequirement.eq(makerCalc.totalCollateral)); + assert(makerCalc.marginRequirement.gt(ZERO)); + }); + + it('isolated position margin requirement (SDK parity)', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const myMockSpotMarkets = _.cloneDeep(mockSpotMarkets); + myMockSpotMarkets[0].oracle = new PublicKey(2); + myMockSpotMarkets[1].oracle = new PublicKey(5); + myMockPerpMarkets[0].amm.oracle = new PublicKey(5); + + // Configure perp market 0 ratios to match on-chain test + myMockPerpMarkets[0].marginRatioInitial = 1000; // 10% + myMockPerpMarkets[0].marginRatioMaintenance = 500; // 5% + + // Configure spot market 1 (e.g., SOL) weights to match on-chain test + myMockSpotMarkets[1].initialAssetWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 8) / 10; // 0.8 + myMockSpotMarkets[1].maintenanceAssetWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 9) / 10; // 0.9 + myMockSpotMarkets[1].initialLiabilityWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 12) / 10; // 1.2 + myMockSpotMarkets[1].maintenanceLiabilityWeight = + (SPOT_MARKET_WEIGHT_PRECISION.toNumber() * 11) / 10; // 1.1 + + // ---------- Cross margin only (spot positions) ---------- + const crossAccount = _.cloneDeep(baseMockUserAccount); + // USDC deposit: $20,000 + crossAccount.spotPositions[0].marketIndex = 0; + crossAccount.spotPositions[0].balanceType = SpotBalanceType.DEPOSIT; + crossAccount.spotPositions[0].scaledBalance = new BN(20000).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + // SOL borrow: 100 units + crossAccount.spotPositions[1].marketIndex = 1; + crossAccount.spotPositions[1].balanceType = SpotBalanceType.BORROW; + crossAccount.spotPositions[1].scaledBalance = new BN(100).mul( + SPOT_MARKET_BALANCE_PRECISION + ); + // No perp exposure in cross calc + crossAccount.perpPositions[0].baseAssetAmount = new BN( + 100 * BASE_PRECISION.toNumber() + ); + crossAccount.perpPositions[0].quoteAssetAmount = new BN( + -11000 * QUOTE_PRECISION.toNumber() + ); + crossAccount.perpPositions[0].positionFlag = PositionFlag.IsolatedPosition; + crossAccount.perpPositions[0].isolatedPositionScaledBalance = new BN( + 100 + ).mul(SPOT_MARKET_BALANCE_PRECISION); + + const userCross: User = await makeMockUser( + myMockPerpMarkets, + myMockSpotMarkets, + crossAccount, + [100, 1, 1, 1, 1, 1, 1, 1], // perp oracle for market 0 = 100 + [1, 100, 1, 1, 1, 1, 1, 1] // spot oracle: usdc=1, sol=100 + ); + + const crossCalc = userCross.getMarginCalculation('Initial'); + const isolatedMarginCalc = crossCalc.isolatedMarginCalculations.get(0); + // Expect: cross MR from SOL borrow: 100 * $100 = $10,000 * 1.2 = $12,000 + assert(crossCalc.marginRequirement.eq(new BN('12000000000'))); + // Expect: cross total collateral from USDC deposit only = $20,000 + assert(crossCalc.totalCollateral.eq(new BN('20000000000'))); + // Meets cross margin requirement + assert(crossCalc.marginRequirement.lte(crossCalc.totalCollateral)); + + assert(isolatedMarginCalc?.marginRequirement.eq(new BN('1000000000'))); + assert(isolatedMarginCalc?.totalCollateral.eq(new BN('-900000000'))); + // With 10% buffer + const tenPct = new BN(1000); + const crossCalcBuf = userCross.getMarginCalculation('Initial', { + liquidationBuffer: tenPct, + }); + assert(crossCalcBuf.marginRequirementPlusBuffer.eq(new BN('13000000000'))); // replicate 10% buffer + const crossTotalPlusBuffer = crossCalcBuf.totalCollateral.add( + crossCalcBuf.totalCollateralBuffer + ); + assert(crossTotalPlusBuffer.eq(new BN('20000000000'))); + + const isoPosition = crossCalcBuf.isolatedMarginCalculations.get(0); + assert(isoPosition?.marginRequirementPlusBuffer.eq(new BN('2000000000'))); + assert( + isoPosition?.totalCollateralBuffer + .add(isoPosition?.totalCollateral) + .eq(new BN('-1000000000')) + ); + }); +}); diff --git a/sdk/tests/user/test.ts b/sdk/tests/user/test.ts index 6c431b7226..990abc473e 100644 --- a/sdk/tests/user/test.ts +++ b/sdk/tests/user/test.ts @@ -49,7 +49,6 @@ async function makeMockUser( oraclePriceMap[myMockSpotMarkets[i].oracle.toString()] = spotOraclePriceList[i]; } - // console.log(oraclePriceMap); function getMockUserAccount(): UserAccount { return myMockUserAccount; @@ -61,12 +60,6 @@ async function makeMockUser( return myMockSpotMarkets[marketIndex]; } function getMockOracle(oracleKey: PublicKey) { - // console.log('oracleKey.toString():', oracleKey.toString()); - // console.log( - // 'oraclePriceMap[oracleKey.toString()]:', - // oraclePriceMap[oracleKey.toString()] - // ); - const QUOTE_ORACLE_PRICE_DATA: OraclePriceData = { price: new BN( oraclePriceMap[oracleKey.toString()] * PRICE_PRECISION.toNumber() diff --git a/sdk/yarn.lock b/sdk/yarn.lock index 4013c2cc05..85552471dc 100644 --- a/sdk/yarn.lock +++ b/sdk/yarn.lock @@ -164,12 +164,12 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@grpc/grpc-js@1.12.6": - version "1.12.6" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.12.6.tgz#a3586ffdfb6a1f5cd5b4866dec9074c4a1e65472" - integrity sha512-JXUj6PI0oqqzTGvKtzOkxtpsyPRNsrmhh41TtIz/zEB6J+AUiZZ0dxWzcMwO9Ns5rmSPuMdghlTbUuqIM48d3Q== +"@grpc/grpc-js@1.14.0": + version "1.14.0" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.0.tgz#a3c47e7816ca2b4d5490cba9e06a3cf324e675ad" + integrity sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg== dependencies: - "@grpc/proto-loader" "^0.7.13" + "@grpc/proto-loader" "^0.8.0" "@js-sdsl/ordered-map" "^4.4.2" "@grpc/grpc-js@^1.8.0", "@grpc/grpc-js@^1.8.13": @@ -190,6 +190,16 @@ protobufjs "^7.2.5" yargs "^17.7.2" +"@grpc/proto-loader@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz#b6c324dd909c458a0e4aa9bfd3d69cf78a4b9bd8" + integrity sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.5.3" + yargs "^17.7.2" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -318,6 +328,11 @@ snake-case "^3.0.4" spok "^1.4.3" +"@msgpack/msgpack@^3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.1.2.tgz#fdd25cc2202297519798bbaf4689152ad9609e19" + integrity sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ== + "@noble/curves@^1.4.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911" @@ -1139,10 +1154,10 @@ buffer "^6.0.3" js-yaml "^4.1.0" -"@triton-one/yellowstone-grpc@1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@triton-one/yellowstone-grpc/-/yellowstone-grpc-1.3.0.tgz#7caa7006b525149b4780d1295c7d4c34bc6a6ff6" - integrity sha512-tuwHtoYzvqnahsMrecfNNkQceCYwgiY0qKS8RwqtaxvDEgjm0E+0bXwKz2eUD3ZFYifomJmRKDmSBx9yQzAeMQ== +"@triton-one/yellowstone-grpc@1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@triton-one/yellowstone-grpc/-/yellowstone-grpc-1.4.1.tgz#b50434f68c3d73c6ce3e1f225656064c080f4b25" + integrity sha512-ZN49vooxFbOqWttll8u7AOsIVnX+srqX9ddhZ9ttE+OcehUo8c2p2suK8Gr2puab49cgsV0VGjiTn9Gua/ntIw== dependencies: "@grpc/grpc-js" "^1.8.0" @@ -1281,6 +1296,13 @@ dependencies: undici-types "~5.26.4" +"@types/protobufjs@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/protobufjs/-/protobufjs-6.0.0.tgz#aeabb43f9507bb19c8adfb479584c151082353e4" + integrity sha512-A27RDExpAf3rdDjIrHKiJK6x8kqqJ4CmoChwtipfhVAn1p7+wviQFFP7dppn8FslSbHtQeVPvi8wNKkDjSYjHw== + dependencies: + protobufjs "*" + "@types/semver@^7.5.0": version "7.7.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.0.tgz#64c441bdae033b378b6eef7d0c3d77c329b9378e" @@ -2820,6 +2842,51 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== +helius-laserstream-darwin-arm64@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-darwin-arm64/-/helius-laserstream-darwin-arm64-0.1.8.tgz#d78ad15e6cd16dc9379a9a365f9fcb3f958e6c01" + integrity sha512-p/K2Mi3wZnMxEYSLCvu858VyMvtJFonhdF8cLaMcszFv04WWdsK+hINNZpVRfakypvDfDPbMudEhL1Q9USD5+w== + +helius-laserstream-darwin-x64@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-darwin-x64/-/helius-laserstream-darwin-x64-0.1.8.tgz#e57bc8f03135fd3b5c01a5aebd7b87c42129da50" + integrity sha512-Hd5irFyfOqQZLdoj5a+OV7vML2YfySSBuKlOwtisMHkUuIXZ4NpAexslDmK7iP5VWRI+lOv9X/tA7BhxW7RGSQ== + +helius-laserstream-linux-arm64-gnu@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-arm64-gnu/-/helius-laserstream-linux-arm64-gnu-0.1.8.tgz#1b3c8440804d143f650166842620fc334f9c319b" + integrity sha512-PlPm1dvOvTGBL1nuzK98Xe40BJq1JWNREXlBHKDVA/B+KCGQnIMJ1s6e1MevSvFE7SOix5i1BxhYIxGioK2GMg== + +helius-laserstream-linux-arm64-musl@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-arm64-musl/-/helius-laserstream-linux-arm64-musl-0.1.8.tgz#28e0645bebc3253d2a136cf0bd13f8cb5256f47b" + integrity sha512-LFadfMRuTd1zo6RZqLTgHQapo3gJYioS7wFMWFoBOFulG0BpAqHEDNobkxx0002QArw+zX29MQ/5OaOCf8kKTA== + +helius-laserstream-linux-x64-gnu@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-x64-gnu/-/helius-laserstream-linux-x64-gnu-0.1.8.tgz#e59990ca0bcdc27e46f71a8fc2c18fddbe6f07e3" + integrity sha512-IZWK/OQIe0647QqfYikLb1DFK+nYtXLJiMcpj24qnNVWBOtMXmPc1hL6ebazdEiaKt7fxNd5IiM1RqeaqZAZMw== + +helius-laserstream-linux-x64-musl@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream-linux-x64-musl/-/helius-laserstream-linux-x64-musl-0.1.8.tgz#42aa0919ef266c40f50ac74d6f9d871d4e2e7c9c" + integrity sha512-riTS6VgxDae1fHOJ2XC/o/v1OZRbEv/3rcoa3NlAOnooDKp5HDgD0zJTcImjQHpYWwGaejx1oX/Ht53lxNoijw== + +helius-laserstream@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/helius-laserstream/-/helius-laserstream-0.1.8.tgz#6ee5e0bc9fe2560c03a0d2c9079b9f875c3e6bb7" + integrity sha512-jXQkwQRWiowbVPGQrGacOkI5shKPhrEixCu93OpoMtL5fs9mnhtD7XKMPi8CX0W8gsqsJjwR4NlaR+EflyANbQ== + dependencies: + "@types/protobufjs" "^6.0.0" + protobufjs "^7.5.3" + optionalDependencies: + helius-laserstream-darwin-arm64 "0.1.8" + helius-laserstream-darwin-x64 "0.1.8" + helius-laserstream-linux-arm64-gnu "0.1.8" + helius-laserstream-linux-arm64-musl "0.1.8" + helius-laserstream-linux-x64-gnu "0.1.8" + helius-laserstream-linux-x64-musl "0.1.8" + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -3673,6 +3740,24 @@ pretty-ms@^7.0.1: dependencies: parse-ms "^2.1.0" +protobufjs@*, protobufjs@^7.5.3: + version "7.5.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.4.tgz#885d31fe9c4b37f25d1bb600da30b1c5b37d286a" + integrity sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + protobufjs@^7.2.5, protobufjs@^7.4.0: version "7.5.3" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.5.3.tgz#13f95a9e3c84669995ec3652db2ac2fb00b89363" diff --git a/test-scripts/run-anchor-tests.sh b/test-scripts/run-anchor-tests.sh index a352caf9ef..2ef7a67138 100644 --- a/test-scripts/run-anchor-tests.sh +++ b/test-scripts/run-anchor-tests.sh @@ -22,6 +22,7 @@ test_files=( # updateK.ts # postOnlyAmmFulfillment.ts # TODO BROKEN ^^ + builderCodes.ts decodeUser.ts fuel.ts fuelSweep.ts @@ -43,6 +44,8 @@ test_files=( liquidatePerpPnlForDeposit.ts liquidateSpot.ts liquidateSpotSocialLoss.ts + lpPool.ts + lpPoolSwap.ts marketOrder.ts marketOrderBaseAssetAmount.ts maxDeposit.ts @@ -94,4 +97,4 @@ test_files=( for test_file in ${test_files[@]}; do ts-mocha -t 300000 ./tests/${test_file} || exit 1 -done \ No newline at end of file +done diff --git a/test-scripts/run-til-failure.sh b/test-scripts/run-til-failure.sh new file mode 100644 index 0000000000..d832743171 --- /dev/null +++ b/test-scripts/run-til-failure.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +count=0 +trap 'echo -e "\nStopped after $count runs"; exit 0' INT + +while true; do + if ! bash test-scripts/single-anchor-test.sh --skip-build; then + echo "Test failed after $count successful runs!" + exit 1 + fi + count=$((count + 1)) + echo "Test passed ($count), running again..." +done diff --git a/test-scripts/single-anchor-test.sh b/test-scripts/single-anchor-test.sh index a9f48da728..f3fb157085 100755 --- a/test-scripts/single-anchor-test.sh +++ b/test-scripts/single-anchor-test.sh @@ -7,7 +7,8 @@ fi export ANCHOR_WALLET=~/.config/solana/id.json test_files=( - spotDepositWithdraw22ScaledUI.ts + lpPool.ts + lpPoolSwap.ts ) for test_file in ${test_files[@]}; do diff --git a/tests/admin.ts b/tests/admin.ts index 7eb87c396a..c397b3f350 100644 --- a/tests/admin.ts +++ b/tests/admin.ts @@ -472,6 +472,19 @@ describe('admin', () => { assert(perpMarket.amm.ammSpreadAdjustment == ammSpreadAdjustment); }); + it('update perp market reference offset deadband pct', async () => { + const referenceOffsetDeadbandPct = 5; + await driftClient.updatePerpMarketReferencePriceOffsetDeadbandPct( + 0, + referenceOffsetDeadbandPct + ); + const perpMarket = driftClient.getPerpMarketAccount(0); + assert( + perpMarket.amm.referencePriceOffsetDeadbandPct == + referenceOffsetDeadbandPct + ); + }); + it('update pnl pool', async () => { const quoteVault = driftClient.getSpotMarketAccount(0).vault; diff --git a/tests/builderCodes.ts b/tests/builderCodes.ts new file mode 100644 index 0000000000..4f26cd0476 --- /dev/null +++ b/tests/builderCodes.ts @@ -0,0 +1,1612 @@ +import * as anchor from '@coral-xyz/anchor'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; + +import { + TestClient, + OracleSource, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + assert, + getRevenueShareAccountPublicKey, + getRevenueShareEscrowAccountPublicKey, + RevenueShareAccount, + RevenueShareEscrowAccount, + BASE_PRECISION, + BN, + PRICE_PRECISION, + getMarketOrderParams, + PositionDirection, + PostOnlyParams, + MarketType, + OrderParams, + PEG_PRECISION, + ZERO, + isVariant, + hasBuilder, + parseLogs, + RevenueShareEscrowMap, + getTokenAmount, + RevenueShareSettleRecord, + getLimitOrderParams, + SignedMsgOrderParamsMessage, + QUOTE_PRECISION, +} from '../sdk/src'; + +import { + createUserWithUSDCAccount, + initializeQuoteSpotMarket, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccount, + printTxLogs, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_STORAGE_DATA } from './pythLazerData'; +import { nanoid } from 'nanoid'; +import { + isBuilderOrderCompleted, + isBuilderOrderReferral, +} from '../sdk/src/math/builder'; +import { createTransferInstruction } from '@solana/spl-token'; + +dotenv.config(); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +function buildMsg( + marketIndex: number, + baseAssetAmount: BN, + userOrderId: number, + feeBps: number, + slot: BN +) { + const params = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + return { + signedMsgOrderParams: params, + subAccountId: 0, + slot, + uuid: Uint8Array.from(Buffer.from(nanoid(8))), + builderIdx: 0, + builderFeeTenthBps: feeBps, + takeProfitOrderParams: null, + stopLossOrderParams: null, + } as SignedMsgOrderParamsMessage; +} + +describe('builder codes', () => { + const chProgram = anchor.workspace.Drift as Program; + + let usdcMint: Keypair; + + let builderClient: TestClient; + let builderUSDCAccount: Keypair = null; + + let makerClient: TestClient; + let makerUSDCAccount: PublicKey = null; + + let userUSDCAccount: PublicKey = null; + let userClient: TestClient; + + // user without RevenueShareEscrow + let user2USDCAccount: PublicKey = null; + let user2Client: TestClient; + + let escrowMap: RevenueShareEscrowMap; + let bulkAccountLoader: TestBulkAccountLoader; + let bankrunContextWrapper: BankrunContextWrapper; + + let solUsd: PublicKey; + let marketIndexes; + let spotMarketIndexes; + let oracleInfos; + + const usdcAmount = new BN(10000 * 10 ** 6); + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 224.3); + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + marketIndexes = [0, 1]; + spotMarketIndexes = [0, 1]; + oracleInfos = [{ publicKey: solUsd, source: OracleSource.PYTH }]; + + builderClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: bankrunContextWrapper.provider.wallet, + programID: chProgram.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + perpMarketIndexes: marketIndexes, + spotMarketIndexes: spotMarketIndexes, + subAccountIds: [], + oracleInfos, + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + }); + await builderClient.initialize(usdcMint.publicKey, true); + await builderClient.subscribe(); + + await builderClient.updateFeatureBitFlagsBuilderCodes(true); + // await builderClient.updateFeatureBitFlagsBuilderReferral(true); + + await initializeQuoteSpotMarket(builderClient, usdcMint.publicKey); + + const periodicity = new BN(0); + await builderClient.initializePerpMarket( + 0, + solUsd, + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await builderClient.initializePerpMarket( + 1, + solUsd, + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + new BN(10 * 10 ** 13).mul(new BN(Math.sqrt(PRICE_PRECISION.toNumber()))), + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + builderUSDCAccount = await mockUserUSDCAccount( + usdcMint, + usdcAmount.add(new BN(1e9).mul(QUOTE_PRECISION)), + bankrunContextWrapper, + builderClient.wallet.publicKey + ); + await builderClient.initializeUserAccountAndDepositCollateral( + usdcAmount, + builderUSDCAccount.publicKey + ); + + // top up pnl pool for mkt 0 and mkt 1 + const spotMarket = builderClient.getSpotMarketAccount(0); + const pnlPoolTopupAmount = new BN(500).mul(QUOTE_PRECISION); + + const transferIx0 = createTransferInstruction( + builderUSDCAccount.publicKey, + spotMarket.vault, + builderClient.wallet.publicKey, + pnlPoolTopupAmount.toNumber() + ); + const tx0 = new Transaction().add(transferIx0); + tx0.recentBlockhash = ( + await bankrunContextWrapper.connection.getLatestBlockhash() + ).blockhash; + tx0.sign(builderClient.wallet.payer); + await bankrunContextWrapper.connection.sendTransaction(tx0); + + // top up pnl pool for mkt 1 + const transferIx1 = createTransferInstruction( + builderUSDCAccount.publicKey, + spotMarket.vault, + builderClient.wallet.publicKey, + pnlPoolTopupAmount.toNumber() + ); + const tx1 = new Transaction().add(transferIx1); + tx1.recentBlockhash = ( + await bankrunContextWrapper.connection.getLatestBlockhash() + ).blockhash; + tx1.sign(builderClient.wallet.payer); + await bankrunContextWrapper.connection.sendTransaction(tx1); + + await builderClient.updatePerpMarketPnlPool(0, pnlPoolTopupAmount); + await builderClient.updatePerpMarketPnlPool(1, pnlPoolTopupAmount); + + // await builderClient.depositIntoPerpMarketFeePool( + // 0, + // new BN(1e6).mul(QUOTE_PRECISION), + // builderUSDCAccount.publicKey + // ); + + [userClient, userUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + } + ); + await userClient.deposit( + usdcAmount, + 0, + userUSDCAccount, + undefined, + false, + undefined, + true + ); + + [user2Client, user2USDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + } + ); + await user2Client.deposit( + usdcAmount, + 0, + user2USDCAccount, + undefined, + false, + undefined, + true + ); + + [makerClient, makerUSDCAccount] = await createUserWithUSDCAccount( + bankrunContextWrapper, + usdcMint, + chProgram, + usdcAmount, + marketIndexes, + spotMarketIndexes, + oracleInfos, + bulkAccountLoader + ); + await makerClient.deposit( + usdcAmount, + 0, + makerUSDCAccount, + undefined, + false, + undefined, + true + ); + + escrowMap = new RevenueShareEscrowMap(userClient, false); + }); + + after(async () => { + await builderClient.unsubscribe(); + await userClient.unsubscribe(); + await user2Client.unsubscribe(); + await makerClient.unsubscribe(); + }); + + it('builder can create builder', async () => { + await builderClient.initializeRevenueShare(builderClient.wallet.publicKey); + + const builderAccountInfo = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + + const builderAcc: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfo.data + ); + assert( + builderAcc.authority.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + assert(builderAcc.totalBuilderRewards.toNumber() === 0); + assert(builderAcc.totalReferrerRewards.toNumber() === 0); + }); + + it('user can initialize a RevenueShareEscrow', async () => { + const numOrders = 2; + + // Test the instruction creation + const ix = await userClient.getInitializeRevenueShareEscrowIx( + userClient.wallet.publicKey, + numOrders + ); + + assert(ix !== null, 'Instruction should be created'); + assert(ix.programId.toBase58() === userClient.program.programId.toBase58()); + + // Test the full transaction + await userClient.initializeRevenueShareEscrow( + userClient.wallet.publicKey, + numOrders + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + assert(accountInfo !== null, 'RevenueShareEscrow account should exist'); + assert( + accountInfo.owner.toBase58() === userClient.program.programId.toBase58() + ); + + const revShareEscrow: RevenueShareEscrowAccount = + builderClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + assert( + revShareEscrow.authority.toBase58() === + userClient.wallet.publicKey.toBase58() + ); + // assert( + // revShareEscrow.referrer.toBase58() === + // builderClient.wallet.publicKey.toBase58() + // ); + assert(revShareEscrow.orders.length === numOrders); + assert(revShareEscrow.approvedBuilders.length === 0); + }); + + it('user can resize RevenueShareEscrow account', async () => { + const newNumOrders = 10; + + // Test the instruction creation + const ix = await userClient.getResizeRevenueShareEscrowOrdersIx( + userClient.wallet.publicKey, + newNumOrders + ); + + assert(ix !== null, 'Instruction should be created'); + assert(ix.programId.toBase58() === userClient.program.programId.toBase58()); + + // Test the full transaction + await userClient.resizeRevenueShareEscrowOrders( + userClient.wallet.publicKey, + newNumOrders + ); + + const accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + assert( + accountInfo !== null, + 'RevenueShareEscrow account should exist after resize' + ); + assert( + accountInfo.owner.toBase58() === userClient.program.programId.toBase58() + ); + + const revShareEscrow: RevenueShareEscrowAccount = + builderClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + assert( + revShareEscrow.authority.toBase58() === + userClient.wallet.publicKey.toBase58() + ); + // assert( + // revShareEscrow.referrer.toBase58() === + // builderClient.wallet.publicKey.toBase58() + // ); + assert(revShareEscrow.orders.length === newNumOrders); + }); + + it('user can add/update/remove approved builder from RevenueShareEscrow', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; // 1.5% + + // First add a builder + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + true // add + ); + + // Verify the builder was added + let accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + let revShareEscrow: RevenueShareEscrowAccount = + userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const addedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + addedBuilder !== undefined, + 'Builder should be in approved builders list before removal' + ); + assert( + revShareEscrow.approvedBuilders.length === 1, + 'Approved builders list should contain 1 builder' + ); + assert( + addedBuilder.maxFeeTenthBps === maxFeeBps, + 'Builder should have correct max fee bps before removal' + ); + + // update the user fee + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps * 2, + true // update existing builder + ); + + // Verify the builder was updated + accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + revShareEscrow = userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const updatedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + updatedBuilder !== undefined, + 'Builder should be in approved builders list after update' + ); + assert( + updatedBuilder.maxFeeTenthBps === maxFeeBps * 2, + 'Builder should have correct max fee bps after update' + ); + + // Now remove the builder + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + false // remove + ); + + // Verify the builder was removed + accountInfo = await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareEscrowAccountPublicKey( + userClient.program.programId, + userClient.wallet.publicKey + ) + ); + + revShareEscrow = userClient.program.coder.accounts.decodeUnchecked( + 'RevenueShareEscrow', + accountInfo.data + ); + const removedBuilder = revShareEscrow.approvedBuilders.find( + (b) => b.authority.toBase58() === builder.publicKey.toBase58() + ); + assert( + removedBuilder.maxFeeTenthBps === 0, + 'Builder should have 0 max fee bps after removal' + ); + }); + + it('user with no RevenueShareEscrow can place and fill order with no builder', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount: baseAssetAmount.muln(2), + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + + let userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const takerOrderParamsMessage: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: { + triggerPrice: new BN(235).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + stopLossOrderParams: { + triggerPrice: new BN(220).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + builderIdx: null, + builderFeeTenthBps: null, + }; + + const signedOrderParams = user2Client.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage, + false + ); + + await builderClient.placeSignedMsgTakerOrder( + signedOrderParams, + marketIndex, + { + taker: await user2Client.getUserAccountPublicKey(), + takerUserAccount: user2Client.getUserAccount(), + takerStats: user2Client.getUserStatsAccountPublicKey(), + signingAuthority: user2Client.wallet.publicKey, + }, + undefined, + 2 + ); + + await user2Client.fetchAccounts(); + + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 3); + assert(userOrders[0].orderId === 1); + assert(userOrders[0].reduceOnly === true); + assert(hasBuilder(userOrders[0]) === false); + assert(userOrders[1].orderId === 2); + assert(userOrders[1].reduceOnly === true); + assert(hasBuilder(userOrders[1]) === false); + assert(userOrders[2].orderId === 3); + assert(userOrders[2].reduceOnly === false); + assert(hasBuilder(userOrders[2]) === false); + + await user2Client.fetchAccounts(); + + // fill order with vamm + await builderClient.fetchAccounts(); + const fillTx = await makerClient.fillPerpOrder( + await user2Client.getUserAccountPublicKey(), + user2Client.getUserAccount(), + { + marketIndex, + orderId: 3, + }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTx + ); + const events = parseLogs(builderClient.program, logs); + assert(events[0].name === 'OrderActionRecord'); + const fillQuoteAssetAmount = events[0].data['quoteAssetAmountFilled'] as BN; + const builderFee = events[0].data['builderFee'] as BN | null; + const takerFee = events[0].data['takerFee'] as BN; + const totalFeePaid = takerFee; + const referrerReward = new BN(events[0].data['referrerReward'] as number); + assert(builderFee === null); + assert(referrerReward.gt(ZERO)); + + await user2Client.fetchAccounts(); + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + await bankrunContextWrapper.moveTimeForward(100); + + // cancel remaining orders + await user2Client.cancelOrders(); + await user2Client.fetchAccounts(); + + userOrders = user2Client.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const perpPos = user2Client.getUser().getPerpPosition(0); + assert( + perpPos.quoteAssetAmount.eq(fillQuoteAssetAmount.add(totalFeePaid).neg()) + ); + + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBeforeSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfterSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert(builderUsdcAfterSettle.eq(builderUsdcBeforeSettle)); + }); + + it('user can place and fill order with builder', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + // approve builder again + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; // 1.5% + await userClient.changeApprovedBuilder( + builder.publicKey, + maxFeeBps, + true // update existing builder + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const takerOrderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount: baseAssetAmount.muln(2), + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 1, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + + // Should fail if we try first without encoding properly + + let userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const builderFeeBps = 7 * 10; + const takerOrderParamsMessage: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: takerOrderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: { + triggerPrice: new BN(235).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + stopLossOrderParams: { + triggerPrice: new BN(220).mul(PRICE_PRECISION), + baseAssetAmount: takerOrderParams.baseAssetAmount, + }, + builderIdx: 0, + builderFeeTenthBps: builderFeeBps, + }; + + const signedOrderParams = userClient.signSignedMsgOrderParamsMessage( + takerOrderParamsMessage, + false + ); + + await builderClient.placeSignedMsgTakerOrder( + signedOrderParams, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + + await userClient.fetchAccounts(); + + // try to revoke builder with open orders + try { + await userClient.changeApprovedBuilder( + builder.publicKey, + 0, + false // remove + ); + assert( + false, + 'should throw error when revoking builder with open orders' + ); + } catch (e) { + assert(e.message.includes('0x18b3')); // CannotRevokeBuilderWithOpenOrders + } + + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 3); + assert(userOrders[0].orderId === 1); + assert(userOrders[0].reduceOnly === true); + assert(hasBuilder(userOrders[0]) === true); + assert(userOrders[1].orderId === 2); + assert(userOrders[1].reduceOnly === true); + assert(hasBuilder(userOrders[1]) === true); + assert(userOrders[2].orderId === 3); + assert(userOrders[2].reduceOnly === false); + assert(hasBuilder(userOrders[2]) === true); + + await escrowMap.slowSync(); + let escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + + // check the corresponding revShareEscrow orders are added + for (let i = 0; i < userOrders.length; i++) { + assert(escrow.orders[i]!.builderIdx === 0); + assert(escrow.orders[i]!.feesAccrued.eq(ZERO)); + assert( + escrow.orders[i]!.feeTenthBps === builderFeeBps, + `builderFeeBps ${escrow.orders[i]!.feeTenthBps} !== ${builderFeeBps}` + ); + assert( + escrow.orders[i]!.orderId === i + 1, + `orderId ${i} is ${escrow.orders[i]!.orderId}` + ); + assert(isVariant(escrow.orders[i]!.marketType, 'perp')); + assert(escrow.orders[i]!.marketIndex === marketIndex); + } + + assert(escrow.approvedBuilders[0]!.authority.equals(builder.publicKey)); + assert(escrow.approvedBuilders[0]!.maxFeeTenthBps === maxFeeBps); + + await userClient.fetchAccounts(); + + // fill order with vamm + await builderClient.fetchAccounts(); + const fillTx = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { + marketIndex, + orderId: 3, + }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTx + ); + const events = parseLogs(builderClient.program, logs); + assert(events[0].name === 'OrderActionRecord'); + const fillQuoteAssetAmount = events[0].data['quoteAssetAmountFilled'] as BN; + const builderFee = events[0].data['builderFee'] as BN; + const takerFee = events[0].data['takerFee'] as BN; + // const referrerReward = events[0].data['referrerReward'] as number; + assert( + builderFee.eq(fillQuoteAssetAmount.muln(builderFeeBps).divn(100000)) + ); + + await userClient.fetchAccounts(); + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + const pos = userClient.getUser().getPerpPosition(0); + const takerOrderCumulativeQuoteAssetAmountFilled = events[0].data[ + 'takerOrderCumulativeQuoteAssetAmountFilled' + ] as BN; + assert( + pos.quoteEntryAmount.abs().eq(takerOrderCumulativeQuoteAssetAmountFilled), + `pos.quoteEntryAmount ${pos.quoteEntryAmount.toNumber()} !== takerOrderCumulativeQuoteAssetAmountFilled ${takerOrderCumulativeQuoteAssetAmountFilled.toNumber()}` + ); + + const builderFeePaidBps = + (builderFee.toNumber() / Math.abs(pos.quoteEntryAmount.toNumber())) * + 10_000; + assert( + Math.round(builderFeePaidBps) === builderFeeBps / 10, + `builderFeePaidBps ${builderFeePaidBps} !== builderFeeBps ${ + builderFeeBps / 10 + }` + ); + + // expect 9.5 bps (taker fee - discount) + 7 bps (builder fee) + const takerFeePaidBps = + (takerFee.toNumber() / Math.abs(pos.quoteEntryAmount.toNumber())) * + 10_000; + assert( + Math.round(takerFeePaidBps * 10) === 165, + `takerFeePaidBps ${takerFeePaidBps} !== 16.5 bps` + ); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + assert(escrow.orders[2].orderId === 3); + assert(escrow.orders[2].feesAccrued.gt(ZERO)); + assert(isBuilderOrderCompleted(escrow.orders[2])); + + // cancel remaining orders + await userClient.cancelOrders(); + await userClient.fetchAccounts(); + + userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 0); + + const perpPos = userClient.getUser().getPerpPosition(0); + assert( + perpPos.quoteAssetAmount.eq(fillQuoteAssetAmount.add(takerFee).neg()) + ); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + assert(escrow.orders[2].bitFlags === 3); + assert(escrow.orders[2].feesAccrued.eq(builderFee)); + + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBeforeSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + const settleTx = await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + + const settleLogs = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + settleTx + ); + const settleEvents = parseLogs(builderClient.program, settleLogs); + const builderSettleEvents = settleEvents + .filter((e) => e.name === 'RevenueShareSettleRecord') + .map((e) => e.data) as RevenueShareSettleRecord[]; + + assert(builderSettleEvents.length === 1); + assert(builderSettleEvents[0].builder.equals(builder.publicKey)); + assert(builderSettleEvents[0].referrer == null); + assert(builderSettleEvents[0].feeSettled.eq(builderFee)); + assert(builderSettleEvents[0].marketIndex === marketIndex); + assert(isVariant(builderSettleEvents[0].marketType, 'perp')); + assert(builderSettleEvents[0].builderTotalReferrerRewards.eq(ZERO)); + assert(builderSettleEvents[0].builderTotalBuilderRewards.eq(builderFee)); + + // assert(builderSettleEvents[1].builder === null); + // assert(builderSettleEvents[1].referrer.equals(builder.publicKey)); + // assert(builderSettleEvents[1].feeSettled.eq(new BN(referrerReward))); + // assert(builderSettleEvents[1].marketIndex === marketIndex); + // assert(isVariant(builderSettleEvents[1].marketType, 'spot')); + // assert( + // builderSettleEvents[1].builderTotalReferrerRewards.eq( + // new BN(referrerReward) + // ) + // ); + // assert(builderSettleEvents[1].builderTotalBuilderRewards.eq(builderFee)); + + await escrowMap.slowSync(); + escrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrow.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfterSettle = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + const finalBuilderFee = builderUsdcAfterSettle.sub(builderUsdcBeforeSettle); + // .sub(new BN(referrerReward)) + assert( + finalBuilderFee.eq(builderFee), + `finalBuilderFee ${finalBuilderFee.toString()} !== builderFee ${builderFee.toString()}` + ); + }); + + it('user can place and cancel with no fill (no fees accrued, escrow unchanged)', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + await escrowMap.slowSync(); + const beforeEscrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const beforeTotalFees = beforeEscrow.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + const orderParams = getMarketOrderParams({ + marketIndex, + direction: PositionDirection.LONG, + baseAssetAmount, + price: new BN(230).mul(PRICE_PRECISION), + auctionStartPrice: new BN(226).mul(PRICE_PRECISION), + auctionEndPrice: new BN(230).mul(PRICE_PRECISION), + auctionDuration: 10, + userOrderId: 7, + postOnly: PostOnlyParams.NONE, + marketType: MarketType.PERP, + }) as OrderParams; + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const uuid = Uint8Array.from(Buffer.from(nanoid(8))); + const builderFeeBps = 5; + const msg: SignedMsgOrderParamsMessage = { + signedMsgOrderParams: orderParams, + subAccountId: 0, + slot, + uuid, + takeProfitOrderParams: null, + stopLossOrderParams: null, + builderIdx: 0, + builderFeeTenthBps: builderFeeBps, + }; + + const signed = userClient.signSignedMsgOrderParamsMessage(msg, false); + await builderClient.placeSignedMsgTakerOrder( + signed, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + + await userClient.cancelOrders(); + await userClient.fetchAccounts(); + assert(userClient.getUser().getOpenOrders().length === 0); + + await escrowMap.slowSync(); + const afterEscrow = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const afterTotalFees = afterEscrow.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + assert(afterTotalFees.eq(beforeTotalFees)); + }); + + it('user can place and fill multiple orders (fees accumulate and settle)', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + + await escrowMap.slowSync(); + const escrowStart = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesInEscrowStart = escrowStart.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const feeBpsA = 6; + const feeBpsB = 9; + + const signedA = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 10, feeBpsA, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedA, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const signedB = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 11, feeBpsB, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedB, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 2); + + // Fill both orders + const fillTxA = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[0].orderId }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsA = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxA + ); + const eventsA = parseLogs(builderClient.program, logsA); + const fillEventA = eventsA.find((e) => e.name === 'OrderActionRecord'); + assert(fillEventA !== undefined); + const builderFeeA = fillEventA.data['builderFee'] as BN; + // const referrerRewardA = new BN(fillEventA.data['referrerReward'] as number); + + const fillTxB = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[1].orderId }, + undefined, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsB = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxB + ); + const eventsB = parseLogs(builderClient.program, logsB); + const fillEventB = eventsB.find((e) => e.name === 'OrderActionRecord'); + assert(fillEventB !== undefined); + const builderFeeB = fillEventB.data['builderFee'] as BN; + // const referrerRewardB = new BN(fillEventB.data['referrerReward'] as number); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + const escrowAfterFills = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesAccrued = escrowAfterFills.orders.reduce( + (sum, o) => sum.add(o.feesAccrued ?? ZERO), + ZERO + ); + const expectedTotal = builderFeeA.add(builderFeeB); + // .add(referrerRewardA) + // .add(referrerRewardB); + assert( + totalFeesAccrued.sub(totalFeesInEscrowStart).eq(expectedTotal), + `totalFeesAccrued: ${totalFeesAccrued.toString()}, expectedTotal: ${expectedTotal.toString()}` + ); + + // Settle and verify fees swept to builder + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBefore = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + + await escrowMap.slowSync(); + const escrowAfterSettle = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrowAfterSettle.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfter = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + const usdcDiff = builderUsdcAfter.sub(builderUsdcBefore); + assert( + usdcDiff.eq(expectedTotal), + `usdcDiff: ${usdcDiff.toString()}, expectedTotal: ${expectedTotal.toString()}` + ); + }); + + it('user can place and fill with multiple maker orders', async () => { + const builder = builderClient.wallet; + const maxFeeBps = 150 * 10; + await userClient.changeApprovedBuilder(builder.publicKey, maxFeeBps, true); + + const builderAccountInfoBefore = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + const builderAccBefore: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfoBefore.data + ); + + const marketIndex = 0; + const baseAssetAmount = BASE_PRECISION; + + // place maker orders + await makerClient.placeOrders([ + getLimitOrderParams({ + marketIndex: 0, + baseAssetAmount: baseAssetAmount.divn(3), + direction: PositionDirection.SHORT, + price: new BN(223000000), + marketType: MarketType.PERP, + postOnly: PostOnlyParams.SLIDE, + }) as OrderParams, + getLimitOrderParams({ + marketIndex: 0, + baseAssetAmount: baseAssetAmount.divn(3), + direction: PositionDirection.SHORT, + price: new BN(223500000), + marketType: MarketType.PERP, + postOnly: PostOnlyParams.SLIDE, + }) as OrderParams, + ]); + await makerClient.fetchAccounts(); + const makerOrders = makerClient.getUser().getOpenOrders(); + assert(makerOrders.length === 2); + + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + const feeBpsA = 6; + + const signedA = userClient.signSignedMsgOrderParamsMessage( + buildMsg(marketIndex, baseAssetAmount, 10, feeBpsA, slot), + false + ); + await builderClient.placeSignedMsgTakerOrder( + signedA, + marketIndex, + { + taker: await userClient.getUserAccountPublicKey(), + takerUserAccount: userClient.getUserAccount(), + takerStats: userClient.getUserStatsAccountPublicKey(), + signingAuthority: userClient.wallet.publicKey, + }, + undefined, + 2 + ); + await userClient.fetchAccounts(); + + const userOrders = userClient.getUser().getOpenOrders(); + assert(userOrders.length === 1); + + // Fill taker against maker orders + const fillTxA = await makerClient.fillPerpOrder( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + { marketIndex, orderId: userOrders[0].orderId }, + { + maker: await makerClient.getUserAccountPublicKey(), + makerStats: makerClient.getUserStatsAccountPublicKey(), + makerUserAccount: makerClient.getUserAccount(), + // order?: Order; + }, + { + referrer: await builderClient.getUserAccountPublicKey(), + referrerStats: builderClient.getUserStatsAccountPublicKey(), + }, + undefined, + undefined, + undefined, + true + ); + const logsA = await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + fillTxA + ); + const eventsA = parseLogs(builderClient.program, logsA); + const fillEventA = eventsA.filter((e) => e.name === 'OrderActionRecord'); + assert(fillEventA !== undefined); + const builderFeeA = fillEventA.reduce( + (sum, e) => sum.add(e.data['builderFee'] as BN), + ZERO + ); + // const referrerRewardA = fillEventA.reduce( + // (sum, e) => sum.add(new BN(e.data['referrerReward'] as number)), + // ZERO + // ); + + await bankrunContextWrapper.moveTimeForward(100); + + await escrowMap.slowSync(); + const escrowAfterFills = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + const totalFeesAccrued = escrowAfterFills.orders + .filter((o) => !isBuilderOrderReferral(o)) + .reduce((sum, o) => sum.add(o.feesAccrued ?? ZERO), ZERO); + assert( + totalFeesAccrued.eq(builderFeeA), + `totalFeesAccrued: ${totalFeesAccrued.toString()}, builderFeeA: ${builderFeeA.toString()}` + ); + + // Settle and verify fees swept to builder + await builderClient.fetchAccounts(); + let usdcPos = builderClient.getSpotPosition(0); + const builderUsdcBefore = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + + await userClient.fetchAccounts(); + const settleTx = await builderClient.settlePNL( + await userClient.getUserAccountPublicKey(), + userClient.getUserAccount(), + marketIndex, + undefined, + undefined, + escrowMap + ); + await printTxLogs( + bankrunContextWrapper.connection.toConnection(), + settleTx + ); + + await escrowMap.slowSync(); + const escrowAfterSettle = (await escrowMap.mustGet( + userClient.wallet.publicKey.toBase58() + )) as RevenueShareEscrowAccount; + for (const order of escrowAfterSettle.orders) { + assert(order.feesAccrued.eq(ZERO)); + } + + await builderClient.fetchAccounts(); + usdcPos = builderClient.getSpotPosition(0); + const builderUsdcAfter = getTokenAmount( + usdcPos.scaledBalance, + builderClient.getSpotMarketAccount(0), + usdcPos.balanceType + ); + assert( + builderUsdcAfter.sub(builderUsdcBefore).eq(builderFeeA), + // .add(referrerRewardA) + `builderUsdcAfter: ${builderUsdcAfter.toString()} !== builderUsdcBefore ${builderUsdcBefore.toString()} + builderFeeA ${builderFeeA.toString()}` + ); + + const builderAccountInfoAfter = + await bankrunContextWrapper.connection.getAccountInfo( + getRevenueShareAccountPublicKey( + builderClient.program.programId, + builderClient.wallet.publicKey + ) + ); + const builderAccAfter: RevenueShareAccount = + builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + 'RevenueShare', + builderAccountInfoAfter.data + ); + assert( + builderAccAfter.authority.toBase58() === + builderClient.wallet.publicKey.toBase58() + ); + + const builderFeeChange = builderAccAfter.totalBuilderRewards.sub( + builderAccBefore.totalBuilderRewards + ); + assert( + builderFeeChange.eq(builderFeeA), + `builderFeeChange: ${builderFeeChange.toString()}, builderFeeA: ${builderFeeA.toString()}` + ); + + // const referrerRewardChange = builderAccAfter.totalReferrerRewards.sub( + // builderAccBefore.totalReferrerRewards + // ); + // assert(referrerRewardChange.eq(referrerRewardA)); + }); + + // it('can track referral rewards for 2 markets', async () => { + // const builderAccountInfoBefore = + // await bankrunContextWrapper.connection.getAccountInfo( + // getRevenueShareAccountPublicKey( + // builderClient.program.programId, + // builderClient.wallet.publicKey + // ) + // ); + // const builderAccBefore: RevenueShareAccount = + // builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + // 'RevenueShare', + // builderAccountInfoBefore.data + // ); + // // await escrowMap.slowSync(); + // // const escrowBeforeFills = (await escrowMap.mustGet( + // // userClient.wallet.publicKey.toBase58() + // // )) as RevenueShareEscrowAccount; + + // const slot = new BN( + // await bankrunContextWrapper.connection.toConnection().getSlot() + // ); + + // // place 2 orders in different markets + + // const signedA = userClient.signSignedMsgOrderParamsMessage( + // buildMsg(0, BASE_PRECISION, 1, 5, slot), + // false + // ); + // await builderClient.placeSignedMsgTakerOrder( + // signedA, + // 0, + // { + // taker: await userClient.getUserAccountPublicKey(), + // takerUserAccount: userClient.getUserAccount(), + // takerStats: userClient.getUserStatsAccountPublicKey(), + // signingAuthority: userClient.wallet.publicKey, + // }, + // undefined, + // 2 + // ); + + // const signedB = userClient.signSignedMsgOrderParamsMessage( + // buildMsg(1, BASE_PRECISION, 2, 5, slot), + // false + // ); + // await builderClient.placeSignedMsgTakerOrder( + // signedB, + // 1, + // { + // taker: await userClient.getUserAccountPublicKey(), + // takerUserAccount: userClient.getUserAccount(), + // takerStats: userClient.getUserStatsAccountPublicKey(), + // signingAuthority: userClient.wallet.publicKey, + // }, + // undefined, + // 2 + // ); + + // await userClient.fetchAccounts(); + // const openOrders = userClient.getUser().getOpenOrders(); + + // const fillTxA = await makerClient.fillPerpOrder( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // { + // marketIndex: 0, + // orderId: openOrders.find( + // (o) => isVariant(o.status, 'open') && o.marketIndex === 0 + // )!.orderId, + // }, + // undefined, + // { + // referrer: await builderClient.getUserAccountPublicKey(), + // referrerStats: builderClient.getUserStatsAccountPublicKey(), + // }, + // undefined, + // undefined, + // undefined, + // true + // ); + // const logsA = await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // fillTxA + // ); + // const eventsA = parseLogs(builderClient.program, logsA); + // const fillsA = eventsA.filter((e) => e.name === 'OrderActionRecord'); + // const fillAReferrerReward = fillsA[0]['data']['referrerReward'] as number; + // assert(fillsA.length > 0); + // // debug: fillsA[0]['data'] + + // const fillTxB = await makerClient.fillPerpOrder( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // { + // marketIndex: 1, + // orderId: openOrders.find( + // (o) => isVariant(o.status, 'open') && o.marketIndex === 1 + // )!.orderId, + // }, + // undefined, + // { + // referrer: await builderClient.getUserAccountPublicKey(), + // referrerStats: builderClient.getUserStatsAccountPublicKey(), + // }, + // undefined, + // undefined, + // undefined, + // true + // ); + // const logsB = await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // fillTxB + // ); + // const eventsB = parseLogs(builderClient.program, logsB); + // const fillsB = eventsB.filter((e) => e.name === 'OrderActionRecord'); + // assert(fillsB.length > 0); + // const fillBReferrerReward = fillsB[0]['data']['referrerReward'] as number; + // // debug: fillsB[0]['data'] + + // await escrowMap.slowSync(); + // const escrowAfterFills = (await escrowMap.mustGet( + // userClient.wallet.publicKey.toBase58() + // )) as RevenueShareEscrowAccount; + + // const referrerOrdersMarket0 = escrowAfterFills.orders.filter( + // (o) => o.marketIndex === 0 && isBuilderOrderReferral(o) + // ); + // const referrerOrdersMarket1 = escrowAfterFills.orders.filter( + // (o) => o.marketIndex === 1 && isBuilderOrderReferral(o) + // ); + // assert(referrerOrdersMarket0[0].marketIndex === 0); + // assert( + // referrerOrdersMarket0[0].feesAccrued.eq(new BN(fillAReferrerReward)) + // ); + // assert(referrerOrdersMarket1[0].marketIndex === 1); + // assert( + // referrerOrdersMarket1[0].feesAccrued.eq(new BN(fillBReferrerReward)) + // ); + + // // settle pnl + // const settleTxA = await builderClient.settleMultiplePNLs( + // await userClient.getUserAccountPublicKey(), + // userClient.getUserAccount(), + // [0, 1], + // SettlePnlMode.MUST_SETTLE, + // escrowMap + // ); + // await printTxLogs( + // bankrunContextWrapper.connection.toConnection(), + // settleTxA + // ); + + // await escrowMap.slowSync(); + // const escrowAfterSettle = (await escrowMap.mustGet( + // userClient.wallet.publicKey.toBase58() + // )) as RevenueShareEscrowAccount; + // const referrerOrdersMarket0AfterSettle = escrowAfterSettle.orders.filter( + // (o) => o.marketIndex === 0 && isBuilderOrderReferral(o) + // ); + // const referrerOrdersMarket1AfterSettle = escrowAfterSettle.orders.filter( + // (o) => o.marketIndex === 1 && isBuilderOrderReferral(o) + // ); + // assert(referrerOrdersMarket0AfterSettle.length === 1); + // assert(referrerOrdersMarket1AfterSettle.length === 1); + // assert(referrerOrdersMarket0AfterSettle[0].feesAccrued.eq(ZERO)); + // assert(referrerOrdersMarket1AfterSettle[0].feesAccrued.eq(ZERO)); + + // const builderAccountInfoAfter = + // await bankrunContextWrapper.connection.getAccountInfo( + // getRevenueShareAccountPublicKey( + // builderClient.program.programId, + // builderClient.wallet.publicKey + // ) + // ); + // const builderAccAfter: RevenueShareAccount = + // builderClient.program.account.revenueShare.coder.accounts.decodeUnchecked( + // 'RevenueShare', + // builderAccountInfoAfter.data + // ); + // const referrerRewards = builderAccAfter.totalReferrerRewards.sub( + // builderAccBefore.totalReferrerRewards + // ); + // assert( + // referrerRewards.eq(new BN(fillAReferrerReward + fillBReferrerReward)) + // ); + // }); +}); diff --git a/tests/fixtures/token_2022.so b/tests/fixtures/token_2022.so new file mode 100755 index 0000000000..23c12ecb2f Binary files /dev/null and b/tests/fixtures/token_2022.so differ diff --git a/tests/insuranceFundStake.ts b/tests/insuranceFundStake.ts index c90a1795ce..0f67d1814c 100644 --- a/tests/insuranceFundStake.ts +++ b/tests/insuranceFundStake.ts @@ -29,6 +29,7 @@ import { unstakeSharesToAmount, MarketStatus, LIQUIDATION_PCT_PRECISION, + getUserStatsAccountPublicKey, } from '../sdk/src'; import { @@ -40,6 +41,7 @@ import { sleep, mockOracleNoProgram, setFeedPriceNoProgram, + mintUSDCToUser, } from './testHelpers'; import { ContractTier, PERCENTAGE_PRECISION, UserStatus } from '../sdk'; import { startAnchor } from 'solana-bankrun'; @@ -1163,6 +1165,33 @@ describe('insurance fund stake', () => { // assert(usdcBefore.eq(usdcAfter)); }); + it('admin deposit into insurance fund stake', async () => { + await mintUSDCToUser( + usdcMint, + userUSDCAccount.publicKey, + usdcAmount, + bankrunContextWrapper + ); + const marketIndex = 0; + const insuranceFundStakePublicKey = getInsuranceFundStakeAccountPublicKey( + driftClient.program.programId, + driftClient.wallet.publicKey, + marketIndex + ); + const userStatsPublicKey = getUserStatsAccountPublicKey( + driftClient.program.programId, + driftClient.wallet.publicKey + ); + const txSig = await driftClient.depositIntoInsuranceFundStake( + marketIndex, + usdcAmount, + userStatsPublicKey, + insuranceFundStakePublicKey, + userUSDCAccount.publicKey + ); + bankrunContextWrapper.printTxLogs(txSig); + }); + // it('settle spotMarket to insurance vault', async () => { // const marketIndex = new BN(0); diff --git a/tests/lpPool.ts b/tests/lpPool.ts new file mode 100644 index 0000000000..6cd7229776 --- /dev/null +++ b/tests/lpPool.ts @@ -0,0 +1,1756 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, +} from '@solana/web3.js'; +import { + createAssociatedTokenAccountInstruction, + createInitializeMint2Instruction, + createMintToInstruction, + getAssociatedTokenAddress, + getAssociatedTokenAddressSync, + getMint, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; + +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + AmmConstituentMapping, + LPPoolAccount, + getConstituentVaultPublicKey, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_RATE_PRECISION, + getAmmCachePublicKey, + AmmCache, + ZERO, + getConstituentPublicKey, + ConstituentAccount, + PositionDirection, + getPythLazerOraclePublicKey, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + BASE_PRECISION, + SPOT_MARKET_BALANCE_PRECISION, + SpotBalanceType, + getTokenAmount, + TWO, + ConstituentLpOperation, +} from '../sdk/src'; + +import { + createWSolTokenAccountForUser, + initializeQuoteSpotMarket, + initializeSolSpotMarket, + mockAtaTokenAccountForMint, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccountWithAuthority, + overWriteMintAccount, + overWritePerpMarket, + overWriteSpotMarket, + setFeedPriceNoProgram, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_LAZER_HEX_STRING_SOL, PYTH_STORAGE_DATA } from './pythLazerData'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let userLpTokenAccount: PublicKey; + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + let spotMarketOracle2: PublicKey; + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + let solUsd: PublicKey; + let solUsdLazer: PublicKey; + + const lpPoolId = 0; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); + + let whitelistMint: PublicKey; + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200); + spotMarketOracle2 = await mockOracleNoProgram(bankrunContextWrapper, 200); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 9); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1, 2], + spotMarketIndexes: [0, 1], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(1_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + + const periodicity = new BN(0); + + solUsdLazer = getPythLazerOraclePublicKey(program.programId, 6); + await adminClient.initializePythLazerOracle(6); + + await adminClient.initializePerpMarket( + 0, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(0, 1); + + await adminClient.initializePerpMarket( + 1, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(1, 1); + + await adminClient.initializePerpMarket( + 2, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(2, 1); + + await adminClient.updatePerpAuctionDuration(new BN(0)); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const imfFactor = 0; + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + await initializeSolSpotMarket(adminClient, spotMarketOracle2); + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle2, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + await adminClient.initializeLpPool( + lpPoolId, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() + ); + + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + // Give the vamm some inventory + await adminClient.openPosition(PositionDirection.LONG, BASE_PRECISION, 0); + await adminClient.openPosition(PositionDirection.SHORT, BASE_PRECISION, 1); + assert( + adminClient + .getUser() + .getActivePerpPositions() + .filter((x) => !x.baseAssetAmount.eq(ZERO)).length == 2 + ); + + console.log('create whitelist mint'); + const whitelistKeypair = Keypair.generate(); + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: bankrunContextWrapper.provider.wallet.publicKey, + newAccountPubkey: whitelistKeypair.publicKey, + space: MINT_SIZE, + lamports: 10_000_000_000, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + whitelistKeypair.publicKey, + 0, + bankrunContextWrapper.provider.wallet.publicKey, + bankrunContextWrapper.provider.wallet.publicKey, + TOKEN_PROGRAM_ID + ) + ); + + await bankrunContextWrapper.sendTransaction(transaction, [ + whitelistKeypair, + ]); + + const whitelistMintInfo = + await bankrunContextWrapper.connection.getAccountInfo( + whitelistKeypair.publicKey + ); + console.log('whitelistMintInfo', whitelistMintInfo); + + whitelistMint = whitelistKeypair.publicKey; + }); + + after(async () => { + await adminClient.unsubscribe(); + }); + + it('can create a new LP Pool', async () => { + // check LpPool created + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminClient.wallet.publicKey + ); + + // Check amm constituent map exists + const ammConstituentMapPublicKey = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammConstituentMap = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapPublicKey + )) as AmmConstituentMapping; + expect(ammConstituentMap).to.not.be.null; + assert(ammConstituentMap.weights.length == 0); + + // check constituent target weights exists + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 0); + + // check mint created correctly + const mintInfo = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(mintInfo.decimals).to.equal(tokenDecimals); + expect(Number(mintInfo.supply)).to.equal(0); + expect(mintInfo.mintAuthority?.toBase58()).to.equal(lpPoolKey.toBase58()); + }); + + it('can add constituents to LP Pool', async () => { + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [], + }); + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.constituents == 1); + + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 1); + + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const constituentTokenVault = + await bankrunContextWrapper.connection.getAccountInfo( + constituentVaultPublicKey + ); + expect(constituentTokenVault).to.not.be.null; + + // Add second constituent representing SOL + await adminClient.initializeConstituent(lpPool.lpPoolId, { + spotMarketIndex: 1, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + derivativeWeight: ZERO, + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ZERO], + }); + }); + + it('can add amm mapping datum', async () => { + // Firt constituent is USDC, so add no mapping. We will add a second mapping though + // for the second constituent which is SOL + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 1, + constituentIndex: 1, + weight: PERCENTAGE_PRECISION, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammMapping = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.length == 1); + }); + + it('can update and remove amm constituent mapping entries', async () => { + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 2, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION, + }, + ]); + const ammConstituentMapping = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + let ammMapping = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.length == 2); + + // Update + await adminClient.updateAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 2, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION.muln(2), + }, + ]); + ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert( + ammMapping.weights + .find((x) => x.perpMarketIndex == 2) + .weight.eq(PERCENTAGE_PRECISION.muln(2)) + ); + + // Remove + await adminClient.removeAmmConstituentMappingData(lpPoolId, 2, 0); + ammMapping = (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapping + )) as AmmConstituentMapping; + expect(ammMapping).to.not.be.null; + assert(ammMapping.weights.find((x) => x.perpMarketIndex == 2) == undefined); + assert(ammMapping.weights.length === 1); + }); + + it('can crank amm info into the cache', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + expect(ammCache).to.not.be.null; + assert(ammCache.cache.length == 3); + assert(ammCache.cache[0].oracle.equals(solUsd)); + assert(ammCache.cache[0].oraclePrice.eq(new BN(200000000))); + }); + + it('can update constituent properties and correlations', async () => { + const constituentPublicKey = getConstituentPublicKey( + program.programId, + lpPoolKey, + 0 + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + constituentPublicKey + )) as ConstituentAccount; + + await adminClient.updateConstituentParams(lpPoolId, constituentPublicKey, { + costToTradeBps: 10, + }); + const constituentTargetBase = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const targets = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBase + )) as ConstituentTargetBaseAccount; + expect(targets).to.not.be.null; + assert(targets.targets[constituent.constituentIndex].costToTradeBps == 10); + + await adminClient.updateConstituentCorrelationData( + lpPoolId, + 0, + 1, + PERCENTAGE_PRECISION.muln(87).divn(100) + ); + + await adminClient.updateConstituentCorrelationData( + lpPoolId, + 0, + 1, + PERCENTAGE_PRECISION + ); + }); + + it('fails adding datum with bad params', async () => { + // Bad perp market index + try { + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 3, + constituentIndex: 0, + weight: PERCENTAGE_PRECISION, + }, + ]); + expect.fail('should have failed'); + } catch (e) { + console.log(e.message); + expect(e.message).to.contain('0x18b6'); // InvalidAmmConstituentMappingArgument + } + + // Bad constituent index + try { + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: 0, + constituentIndex: 5, + weight: PERCENTAGE_PRECISION, + }, + ]); + expect.fail('should have failed'); + } catch (e) { + expect(e.message).to.contain('0x18b6'); // InvalidAmmConstituentMappingArgument + } + }); + + it('fails to add liquidity if aum not updated atomically', async () => { + try { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + await adminClient.lpPoolAddLiquidity({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + }); + expect.fail('should have failed'); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18bd')); // LpPoolAumDelayed + } + }); + + it('fails to add liquidity if a paused operation', async () => { + await adminClient.updateConstituentPausedOperations( + getConstituentPublicKey(program.programId, lpPoolKey, 0), + ConstituentLpOperation.Deposit + ); + try { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c5')); // InvalidConstituentOperation + } + await adminClient.updateConstituentPausedOperations( + getConstituentPublicKey(program.programId, lpPoolKey, 0), + 0 + ); + }); + + it('can update pool aum', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.constituents == 2); + + const createAtaIx = + adminClient.createAssociatedTokenAccountIdempotentInstruction( + await getAssociatedTokenAddress( + lpPool.mint, + adminClient.wallet.publicKey, + true + ), + adminClient.wallet.publicKey, + adminClient.wallet.publicKey, + lpPool.mint + ); + + await adminClient.sendTransaction(new Transaction().add(createAtaIx), []); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + console.log(lpPool.lastAum.toString()); + assert(lpPool.lastAum.eq(new BN(1000).mul(QUOTE_PRECISION))); + + // Should fail if we dont pass in the second constituent + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 1) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + try { + await adminClient.updateLpPoolAum(lpPool, [0]); + expect.fail('should have failed'); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18ba')); // WrongNumberOfConstituents + } + }); + + it('can update constituent target weights', async () => { + await adminClient.postPythLazerOracleUpdate([6], PYTH_LAZER_HEX_STRING_SOL); + await adminClient.updatePerpMarketOracle( + 0, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updatePerpMarketOracle( + 1, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updatePerpMarketOracle( + 2, + solUsdLazer, + OracleSource.PYTH_LAZER + ); + await adminClient.updateAmmCache([0, 1, 2]); + + await adminClient.overrideAmmCacheInfo(0, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + await adminClient.overrideAmmCacheInfo(2, { + ammPositionScalar: 100, + ammInventoryLimit: BASE_PRECISION.muln(5000), + }); + + let tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + tx.add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ]) + ); + await adminClient.sendTransaction(tx); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 2); + assert( + constituentTargetBase.targets.filter((x) => x.targetBase.eq(ZERO)) + .length !== constituentTargetBase.targets.length + ); + + // Make sure the target base respects the cache scalar + const cacheValueBefore = constituentTargetBase.targets[1].targetBase; + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 50, + }); + tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + tx.add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + ]) + ); + await adminClient.sendTransaction(tx); + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + console.log(cacheValueBefore.toString()); + expect( + constituentTargetBase.targets[1].targetBase.toNumber() + ).to.approximately(cacheValueBefore.muln(50).divn(100).toNumber(), 1); + + await adminClient.overrideAmmCacheInfo(1, { + ammPositionScalar: 100, + }); + }); + + it('can add constituent to LP Pool thats a derivative and behave correctly', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.initializeConstituent(lpPool.lpPoolId, { + spotMarketIndex: 2, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + oracleStalenessThreshold: new BN(400), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(2), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ZERO, PERCENTAGE_PRECISION.muln(87).divn(100)], + constituentDerivativeIndex: 1, + }); + + await adminClient.updateAmmCache([0, 1, 2]); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + assert(!constituent.lastOraclePrice.eq(ZERO)); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) + ); + await adminClient.sendTransaction(tx); + + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + expect(constituentTargetBase).to.not.be.null; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect( + constituentTargetBase.targets[1].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[2].targetBase.toNumber(), + 10 + ); + + // Move the oracle price to be double, so it should have half of the target base + const derivativeBalanceBefore = constituentTargetBase.targets[2].targetBase; + const derivative = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + await setFeedPriceNoProgram(bankrunContextWrapper, 400, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + const tx2 = new Transaction(); + tx2 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) + ); + await adminClient.sendTransaction(tx2); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + const derivativeBalanceAfter = constituentTargetBase.targets[2].targetBase; + + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + + expect(derivativeBalanceAfter.toNumber()).to.be.approximately( + derivativeBalanceBefore.toNumber() / 2, + 20 + ); + + // Move the oracle price to be half, so its target base should go to zero + const parentBalanceBefore = constituentTargetBase.targets[1].targetBase; + await setFeedPriceNoProgram(bankrunContextWrapper, 100, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + const tx3 = new Transaction(); + tx3 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + ]) + ); + await adminClient.sendTransaction(tx3); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + const parentBalanceAfter = constituentTargetBase.targets[1].targetBase; + + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect(parentBalanceAfter.toNumber()).to.be.approximately( + parentBalanceBefore.toNumber() * 2, + 10 + ); + await setFeedPriceNoProgram(bankrunContextWrapper, 200, spotMarketOracle2); + await adminClient.updateConstituentOracleInfo(derivative); + }); + + it('can settle pnl from perp markets into the usdc account', async () => { + await adminClient.updateFeatureBitFlagsSettleLpPool(true); + + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + // Exclude 25% of exchange fees, put 100 dollars there to make sure that the + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(0, 100, 25); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(1, 100, 0); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(2, 100, 0); + + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.totalExchangeFee = perpMarket.amm.totalExchangeFee.add( + QUOTE_PRECISION.muln(100) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + await adminClient.depositIntoPerpMarketFeePool( + 0, + new BN(100).mul(QUOTE_PRECISION), + await adminClient.getAssociatedTokenAccount(0) + ); + + await adminClient.depositIntoPerpMarketFeePool( + 1, + new BN(100).mul(QUOTE_PRECISION), + await adminClient.getAssociatedTokenAccount(0) + ); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterDeposit = lpPool.lastAum; + + // Make sure the amount recorded goes into the cache and that the quote amount owed is adjusted + // for new influx in fees + const ammCacheBeforeAdjust = ammCache; + // Test pausing tracking for market 0 + await adminClient.updatePerpMarketLpPoolPausedOperations(0, 1); + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + assert(ammCache.cache[0].lastFeePoolTokenAmount.eq(ZERO)); + assert( + ammCache.cache[0].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool + ) + ); + assert(ammCache.cache[1].lastFeePoolTokenAmount.eq(new BN(100000000))); + assert( + ammCache.cache[1].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[1].quoteOwedFromLpPool.sub( + new BN(100).mul(QUOTE_PRECISION) + ) + ) + ); + + // Market 0 on the amm cache will update now that tracking is permissioned again + await adminClient.updatePerpMarketLpPoolPausedOperations(0, 0); + await adminClient.updateAmmCache([0, 1, 2]); + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + assert(ammCache.cache[0].lastFeePoolTokenAmount.eq(new BN(100000000))); + assert( + ammCache.cache[0].quoteOwedFromLpPool.eq( + ammCacheBeforeAdjust.cache[0].quoteOwedFromLpPool.sub( + new BN(75).mul(QUOTE_PRECISION) + ) + ) + ); + + const usdcBefore = constituent.vaultTokenBalance; + // Update Amm Cache to update the aum + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterUpdateCacheBeforeSettle = lpPool.lastAum; + assert( + lpAumAfterUpdateCacheBeforeSettle.eq( + lpAumAfterDeposit.add(new BN(175).mul(QUOTE_PRECISION)) + ) + ); + + // Calculate the expected transfer amount which is the increase in fee pool - amount owed, + // but we have to consider the fee pool limitations + const pnlPoolBalance0 = getTokenAmount( + adminClient.getPerpMarketAccount(0).pnlPool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const feePoolBalance0 = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + + const pnlPoolBalance1 = getTokenAmount( + adminClient.getPerpMarketAccount(1).pnlPool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const feePoolBalance1 = getTokenAmount( + adminClient.getPerpMarketAccount(1).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + + // Expected transfers per pool are capital constrained by the actual balances + const expectedTransfer0 = BN.min( + ammCache.cache[0].quoteOwedFromLpPool.muln(-1), + pnlPoolBalance0.add(feePoolBalance0).sub(QUOTE_PRECISION.muln(25)) + ); + const expectedTransfer1 = BN.min( + ammCache.cache[1].quoteOwedFromLpPool.muln(-1), + pnlPoolBalance1.add(feePoolBalance1) + ); + const expectedTransferAmount = expectedTransfer0.add(expectedTransfer1); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) + ); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + await adminClient.sendTransaction(settleTx); + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpAumAfterSettle = lpPool.lastAum; + assert(lpAumAfterSettle.eq(lpAumAfterUpdateCacheBeforeSettle)); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const usdcAfter = constituent.vaultTokenBalance; + const feePoolBalanceAfter = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + console.log('usdcBefore', usdcBefore.toString()); + console.log('usdcAfter', usdcAfter.toString()); + + // Verify the expected usdc transfer amount + assert(usdcAfter.sub(usdcBefore).eq(expectedTransferAmount)); + console.log('feePoolBalanceBefore', feePoolBalance0.toString()); + console.log('feePoolBalanceAfter', feePoolBalanceAfter.toString()); + // Fee pool can cover it all in first perp market + expect( + feePoolBalance0.sub(feePoolBalanceAfter).toNumber() + ).to.be.approximately(expectedTransfer0.toNumber(), 1); + + // Constituent sync worked successfully + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const constituentVault = + await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + assert( + new BN(constituentVault.amount.toString()).eq( + constituent.vaultTokenBalance + ) + ); + }); + + it('will settle gracefully when trying to settle pnl from constituents to perp markets if not enough usdc in the constituent vault', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const constituentVaultPublicKey = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + + /// First remove some liquidity so DLP doesnt have enought to transfer + const lpTokenBalance = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + const tx = new Transaction(); + tx.add( + ...(await adminClient.getAllSettlePerpToLpPoolIxs( + lpPool.lpPoolId, + [0, 1, 2] + )) + ); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + tx.add( + ...(await adminClient.getLpPoolRemoveLiquidityIx({ + outMarketIndex: 0, + lpToBurn: new BN(lpTokenBalance.amount.toString()), + minAmountOut: new BN(1000).mul(QUOTE_PRECISION), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(tx); + + let constituentVault = + await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + + const expectedTransferAmount = getTokenAmount( + adminClient.getPerpMarketAccount(0).amm.feePool.scaledBalance, + adminClient.getQuoteSpotMarketAccount(), + SpotBalanceType.DEPOSIT + ); + const constituentUSDCBalanceBefore = constituentVault.amount; + + // Temporarily overwrite perp market to have taken a loss on the fee pool + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const spotMarket = adminClient.getSpotMarketAccount(0); + const perpMarket = adminClient.getPerpMarketAccount(0); + spotMarket.depositBalance = spotMarket.depositBalance.sub( + perpMarket.amm.feePool.scaledBalance.add( + spotMarket.cumulativeDepositInterest.muln(10 ** 3) + ) + ); + await overWriteSpotMarket( + adminClient, + bankrunContextWrapper, + spotMarket.pubkey, + spotMarket + ); + perpMarket.amm.feePool.scaledBalance = ZERO; + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + /// Now finally try and settle Perp to LP Pool + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) + ); + await adminClient.sendTransaction(settleTx); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + constituentVault = await bankrunContextWrapper.connection.getTokenAccount( + constituentVaultPublicKey + ); + + // Should have written fee pool amount owed to the amm cache and new constituent usdc balane should just be the quote precision to leave aum > 0 + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + // No more usdc left in the constituent vault + console.log('constituentVault.amount', constituentVault.amount.toString()); + assert(constituent.vaultTokenBalance.eq(QUOTE_PRECISION)); + assert(new BN(constituentVault.amount.toString()).eq(QUOTE_PRECISION)); + + // Should have recorded the amount left over to the amm cache and increased the amount in the fee pool + assert( + ammCache.cache[0].lastFeePoolTokenAmount.eq( + new BN(constituentUSDCBalanceBefore.toString()).sub(QUOTE_PRECISION) + ) + ); + expect( + ammCache.cache[0].quoteOwedFromLpPool.toNumber() + ).to.be.approximately( + expectedTransferAmount + .sub(new BN(constituentUSDCBalanceBefore.toString())) + .add(QUOTE_PRECISION) + .toNumber(), + 1 + ); + assert( + adminClient + .getPerpMarketAccount(0) + .amm.feePool.scaledBalance.eq( + new BN(constituentUSDCBalanceBefore.toString()) + .sub(QUOTE_PRECISION) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ) + ); + + // Update the LP pool AUM + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + assert(lpPool.lastAum.eq(QUOTE_PRECISION)); + }); + + it('perp market will not transfer with the constituent vault if it is owed from dlp', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; + + // Give the perp market half of its owed amount + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.feePool.scaledBalance = + perpMarket.amm.feePool.scaledBalance.add( + owedAmount + .div(TWO) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) + ); + await adminClient.sendTransaction(settleTx); + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + expect( + ammCache.cache[0].quoteOwedFromLpPool.toNumber() + ).to.be.approximately(owedAmount.divn(2).toNumber(), 1); + assert(constituent.vaultTokenBalance.eq(QUOTE_PRECISION)); + assert(lpPool.lastAum.eq(QUOTE_PRECISION)); + + // Deposit here to DLP to make sure aum calc work with perp market debt + await overWriteMintAccount( + bankrunContextWrapper, + lpPool.mint, + BigInt(lpPool.lastAum.toNumber()) + ); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + await adminClient.sendTransaction(tx); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2]); + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + let aum = new BN(0); + for (let i = 0; i <= 2; i++) { + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + aum = aum.add( + constituent.vaultTokenBalance + .mul(constituent.lastOraclePrice) + .div(QUOTE_PRECISION) + ); + } + + // Overwrite the amm cache with amount owed + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + for (let i = 0; i <= ammCache.cache.length - 1; i++) { + aum = aum.sub(ammCache.cache[i].quoteOwedFromLpPool); + } + assert(lpPool.lastAum.eq(aum)); + }); + + it('perp market will transfer with the constituent vault if it should send more than its owed', async () => { + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const aumBefore = lpPool.lastAum; + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + const balanceBefore = constituent.vaultTokenBalance; + const owedAmount = ammCache.cache[0].quoteOwedFromLpPool; + + // Give the perp market double of its owed amount + const perpMarket = adminClient.getPerpMarketAccount(0); + perpMarket.amm.feePool.scaledBalance = + perpMarket.amm.feePool.scaledBalance.add( + owedAmount + .mul(TWO) + .mul(SPOT_MARKET_BALANCE_PRECISION.div(QUOTE_PRECISION)) + ); + await overWritePerpMarket( + adminClient, + bankrunContextWrapper, + perpMarket.pubkey, + perpMarket + ); + + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(lpPoolId, [0, 1, 2]) + ); + settleTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2])); + await adminClient.sendTransaction(settleTx); + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(ammCache.cache[0].quoteOwedFromLpPool.eq(ZERO)); + assert(constituent.vaultTokenBalance.eq(balanceBefore.add(owedAmount))); + assert(lpPool.lastAum.eq(aumBefore.add(owedAmount.muln(2)))); + }); + + it('can work with multiple derivatives on the same parent', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + await adminClient.initializeConstituent(lpPool.lpPoolId, { + spotMarketIndex: 3, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(4), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [ + ZERO, + PERCENTAGE_PRECISION.muln(87).divn(100), + PERCENTAGE_PRECISION, + ], + constituentDerivativeIndex: 1, + }); + + await adminClient.updateConstituentParams( + lpPool.lpPoolId, + getConstituentPublicKey(program.programId, lpPoolKey, 2), + { + derivativeWeight: PERCENTAGE_PRECISION.divn(4), + } + ); + + await adminClient.updateAmmCache([0, 1, 2]); + + let constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 3) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 3) + )) as ConstituentAccount; + assert(!constituent.lastOraclePrice.eq(ZERO)); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])).add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ]) + ); + await adminClient.sendTransaction(tx); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + let constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + expect(constituentTargetBase).to.not.be.null; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + expect( + constituentTargetBase.targets[2].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[3].targetBase.toNumber(), + 10 + ); + expect( + constituentTargetBase.targets[3].targetBase.toNumber() + ).to.be.approximately( + constituentTargetBase.targets[1].targetBase.toNumber() / 2, + 10 + ); + + // Set the derivative weights to 0 + await adminClient.updateConstituentParams( + lpPool.lpPoolId, + getConstituentPublicKey(program.programId, lpPoolKey, 2), + { + derivativeWeight: ZERO, + } + ); + + await adminClient.updateConstituentParams( + lpPool.lpPoolId, + getConstituentPublicKey(program.programId, lpPoolKey, 3), + { + derivativeWeight: ZERO, + } + ); + + const parentTargetBaseBefore = constituentTargetBase.targets[1].targetBase; + const tx2 = new Transaction(); + tx2 + .add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])) + .add( + await adminClient.getUpdateLpConstituentTargetBaseIx(lpPoolId, [ + getConstituentPublicKey(program.programId, lpPoolKey, 0), + getConstituentPublicKey(program.programId, lpPoolKey, 1), + getConstituentPublicKey(program.programId, lpPoolKey, 2), + getConstituentPublicKey(program.programId, lpPoolKey, 3), + ]) + ); + await adminClient.sendTransaction(tx2); + await adminClient.updateLpPoolAum(lpPool, [0, 1, 2, 3]); + + constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + console.log( + 'constituentTargetBase.targets', + constituentTargetBase.targets.map((x) => x.targetBase.toString()) + ); + + const parentTargetBaseAfter = constituentTargetBase.targets[1].targetBase; + + expect(parentTargetBaseAfter.toNumber()).to.be.approximately( + parentTargetBaseBefore.toNumber() * 2, + 10 + ); + }); + + it('cant withdraw more than constituent limit', async () => { + await adminClient.updateConstituentParams( + lpPoolId, + getConstituentPublicKey(program.programId, lpPoolKey, 0), + { + maxBorrowTokenAmount: new BN(10).muln(10 ** 6), + } + ); + + try { + await adminClient.withdrawFromProgramVault( + lpPoolId, + 0, + new BN(100).mul(QUOTE_PRECISION) + ); + } catch (e) { + console.log(e); + assert(e.toString().includes('0x18bf')); // invariant failed + } + }); + + it('cant disable lp pool settling', async () => { + await adminClient.updateFeatureBitFlagsSettleLpPool(false); + + try { + await adminClient.settlePerpToLpPool(lpPoolId, [0, 1, 2]); + assert(false, 'Should have thrown'); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c2')); // SettleLpPoolDisabled + } + + await adminClient.updateFeatureBitFlagsSettleLpPool(true); + }); + + it('can do spot vault withdraws when there are borrows', async () => { + // First deposit into wsol account from subaccount 1 + await adminClient.initializeUserAccount(1); + const pubkey = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + new BN(7_000).mul(new BN(10 ** 9)) + ); + await adminClient.deposit(new BN(1000).mul(new BN(10 ** 9)), 2, pubkey, 1); + const lpPool = await adminClient.getLpPoolAccount(lpPoolId); + + // Deposit into LP pool some balance + const ixs = []; + ixs.push(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + ixs.push( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 2, + minMintAmount: new BN(1), + lpPool, + inAmount: new BN(100).mul(new BN(10 ** 9)), + })) + ); + await adminClient.sendTransaction(new Transaction().add(...ixs)); + await adminClient.depositToProgramVault( + lpPool.lpPoolId, + 2, + new BN(100).mul(new BN(10 ** 9)) + ); + + const spotMarket = adminClient.getSpotMarketAccount(2); + spotMarket.depositBalance = new BN(1_186_650_830_132); + spotMarket.borrowBalance = new BN(320_916_317_572); + spotMarket.cumulativeBorrowInterest = new BN(697_794_836_247_770); + spotMarket.cumulativeDepositInterest = new BN(188_718_954_233_794); + await overWriteSpotMarket( + adminClient, + bankrunContextWrapper, + spotMarket.pubkey, + spotMarket + ); + + // const curClock = + // await bankrunContextWrapper.provider.context.banksClient.getClock(); + // bankrunContextWrapper.provider.context.setClock( + // new Clock( + // curClock.slot, + // curClock.epochStartTimestamp, + // curClock.epoch, + // curClock.leaderScheduleEpoch, + // curClock.unixTimestamp + BigInt(60 * 60 * 24 * 365 * 10) + // ) + // ); + + await adminClient.withdrawFromProgramVault( + lpPoolId, + 2, + new BN(500).mul(new BN(10 ** 9)) + ); + }); + + it('whitelist mint', async () => { + await adminClient.updateLpPoolParams(lpPoolId, { + whitelistMint: whitelistMint, + }); + + const lpPool = await adminClient.getLpPoolAccount(lpPoolId); + assert(lpPool.whitelistMint.equals(whitelistMint)); + + console.log('lpPool.whitelistMint', lpPool.whitelistMint.toString()); + + const tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + try { + await adminClient.sendTransaction(tx); + assert(false, 'Should have thrown'); + } catch (e) { + assert(e.toString().includes('0x1789')); // invalid whitelist token + } + + const whitelistMintAta = getAssociatedTokenAddressSync( + whitelistMint, + adminClient.wallet.publicKey + ); + const ix = createAssociatedTokenAccountInstruction( + bankrunContextWrapper.context.payer.publicKey, + whitelistMintAta, + adminClient.wallet.publicKey, + whitelistMint + ); + const mintToIx = createMintToInstruction( + whitelistMint, + whitelistMintAta, + bankrunContextWrapper.provider.wallet.publicKey, + 1 + ); + await bankrunContextWrapper.sendTransaction( + new Transaction().add(ix, mintToIx) + ); + + const txAfter = new Transaction(); + txAfter.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1, 2, 3])); + txAfter.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + lpPool, + inAmount: new BN(1000).mul(QUOTE_PRECISION), + minMintAmount: new BN(1), + inMarketIndex: 0, + })) + ); + + // successfully call add liquidity + await adminClient.sendTransaction(txAfter); + }); + + it('can initialize multiple lp pools', async () => { + const newLpPoolId = 1; + + await adminClient.initializeLpPool( + newLpPoolId, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() + ); + await adminClient.initializeConstituent(newLpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION.divn(2), + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + volatility: new BN(10).mul(PERCENTAGE_PRECISION), + constituentCorrelations: [], + constituentDerivativeIndex: -1, + }); + + const oldLpPool = await adminClient.getLpPoolAccount(lpPoolId); + // cant settle a perp market to the new lp pool with a different id + + try { + const settleTx = new Transaction(); + settleTx.add(await adminClient.getUpdateAmmCacheIx([0, 1, 2])); + settleTx.add( + await adminClient.getSettlePerpToLpPoolIx(newLpPoolId, [0, 1, 2]) + ); + settleTx.add( + await adminClient.getUpdateLpPoolAumIxs(oldLpPool, [0, 1, 2]) + ); + await adminClient.sendTransaction(settleTx); + assert.fail('Should have thrown'); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18')); + } + }); + + // it('can delete amm cache and then init and realloc and update', async () => { + // const ammCacheKey = getAmmCachePublicKey(program.programId); + // const ammCacheBefore = (await adminClient.program.account.ammCache.fetch( + // ammCacheKey + // )) as AmmCache; + + // await adminClient.deleteAmmCache(); + // await adminClient.resizeAmmCache(); + // await adminClient.updateInitialAmmCacheInfo([0, 1, 2]); + // await adminClient.updateAmmCache([0, 1, 2]); + + // const ammCacheAfter = (await adminClient.program.account.ammCache.fetch( + // ammCacheKey + // )) as AmmCache; + + // for (let i = 0; i < ammCacheBefore.cache.length; i++) { + // assert( + // ammCacheBefore.cache[i].position.eq(ammCacheAfter.cache[i].position) + // ); + // } + // }); +}); diff --git a/tests/lpPoolCUs.ts b/tests/lpPoolCUs.ts new file mode 100644 index 0000000000..d256c56ed3 --- /dev/null +++ b/tests/lpPoolCUs.ts @@ -0,0 +1,658 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; + +import { Program } from '@coral-xyz/anchor'; + +import { + AccountInfo, + AddressLookupTableProgram, + ComputeBudgetProgram, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, + Transaction, + TransactionMessage, + VersionedTransaction, +} from '@solana/web3.js'; +import { + createInitializeMint2Instruction, + getMint, + MINT_SIZE, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token'; + +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getAmmConstituentMappingPublicKey, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + AmmConstituentMapping, + LPPoolAccount, + OracleSource, + SPOT_MARKET_WEIGHT_PRECISION, + SPOT_MARKET_RATE_PRECISION, + getAmmCachePublicKey, + AmmCache, + ZERO, + getConstituentPublicKey, + ConstituentAccount, + PositionDirection, + PYTH_LAZER_STORAGE_ACCOUNT_KEY, + PTYH_LAZER_PROGRAM_ID, + BASE_PRECISION, +} from '../sdk/src'; + +import { + initializeQuoteSpotMarket, + mockAtaTokenAccountForMint, + mockOracleNoProgram, + mockUSDCMint, + mockUserUSDCAccountWithAuthority, + overwriteConstituentAccount, + sleep, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { PYTH_STORAGE_DATA } from './pythLazerData'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +const NUMBER_OF_CONSTITUENTS = 10; +const NUMBER_OF_PERP_MARKETS = 60; +const NUMBER_OF_USERS = Math.ceil(NUMBER_OF_PERP_MARKETS / 8); + +const PERP_MARKET_INDEXES = Array.from( + { length: NUMBER_OF_PERP_MARKETS }, + (_, i) => i +); +const SPOT_MARKET_INDEXES = Array.from( + { length: NUMBER_OF_CONSTITUENTS + 2 }, + (_, i) => i +); +const CONSTITUENT_INDEXES = Array.from( + { length: NUMBER_OF_CONSTITUENTS }, + (_, i) => i +); + +const PYTH_STORAGE_ACCOUNT_INFO: AccountInfo = { + executable: false, + lamports: LAMPORTS_PER_SOL, + owner: new PublicKey(PTYH_LAZER_PROGRAM_ID), + rentEpoch: 0, + data: Buffer.from(PYTH_STORAGE_DATA, 'base64'), +}; + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let _userLpTokenAccount: PublicKey; + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle2: PublicKey; + + let adminKeypair: Keypair; + + let lutAddress: PublicKey; + + const userClients: TestClient[] = []; + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(100 * 10 ** 13).mul( + mantissaSqrtScale + ); + let solUsd: PublicKey; + + const lpPoolId = 0; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const imfFactor = 0; + + before(async () => { + const context = await startAnchor( + '', + [], + [ + { + address: PYTH_LAZER_STORAGE_ACCOUNT_KEY, + info: PYTH_STORAGE_ACCOUNT_INFO, + }, + ] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle2 = await mockOracleNoProgram(bankrunContextWrapper, 200); + + const keypair = new Keypair(); + adminKeypair = keypair; + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 12); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + + solUsd = await mockOracleNoProgram(bankrunContextWrapper, 200); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1, 2], + spotMarketIndexes: [0, 1], + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(1_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + + await adminClient.initializePythLazerOracle(6); + + await adminClient.updatePerpAuctionDuration(new BN(0)); + + console.log('create whitelist mint'); + const whitelistKeypair = Keypair.generate(); + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: bankrunContextWrapper.provider.wallet.publicKey, + newAccountPubkey: whitelistKeypair.publicKey, + space: MINT_SIZE, + lamports: 10_000_000_000, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction( + whitelistKeypair.publicKey, + 0, + bankrunContextWrapper.provider.wallet.publicKey, + bankrunContextWrapper.provider.wallet.publicKey, + TOKEN_PROGRAM_ID + ) + ); + + await bankrunContextWrapper.sendTransaction(transaction, [ + whitelistKeypair, + ]); + + const whitelistMintInfo = + await bankrunContextWrapper.connection.getAccountInfo( + whitelistKeypair.publicKey + ); + console.log('whitelistMintInfo', whitelistMintInfo); + }); + + after(async () => { + await adminClient.unsubscribe(); + for (const userClient of userClients) { + await userClient.unsubscribe(); + } + }); + + it('can create a new LP Pool', async () => { + await adminClient.initializeLpPool( + lpPoolId, + ZERO, + new BN(1_000_000_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + new Keypair() + ); + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + // check LpPool created + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + _userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminClient.wallet.publicKey + ); + + // Check amm constituent map exists + const ammConstituentMapPublicKey = getAmmConstituentMappingPublicKey( + program.programId, + lpPoolKey + ); + const ammConstituentMap = + (await adminClient.program.account.ammConstituentMapping.fetch( + ammConstituentMapPublicKey + )) as AmmConstituentMapping; + expect(ammConstituentMap).to.not.be.null; + assert(ammConstituentMap.weights.length == 0); + + // check constituent target weights exists + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 0); + + // check mint created correctly + const mintInfo = await getMint( + bankrunContextWrapper.connection.toConnection(), + lpPool.mint as PublicKey + ); + expect(mintInfo.decimals).to.equal(tokenDecimals); + expect(Number(mintInfo.supply)).to.equal(0); + expect(mintInfo.mintAuthority?.toBase58()).to.equal(lpPoolKey.toBase58()); + }); + + it('can add constituents to LP Pool', async () => { + // USDC Constituent + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [], + }); + + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle2, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + await sleep(50); + } + await adminClient.unsubscribe(); + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(adminKeypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1], + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.subscribe(); + await sleep(50); + + const correlations = [ZERO]; + for (let i = 1; i < NUMBER_OF_CONSTITUENTS; i++) { + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: i, + decimals: 6, + maxWeightDeviation: new BN(10).mul(PERCENTAGE_PRECISION), + swapFeeMin: new BN(1).mul(PERCENTAGE_PRECISION), + swapFeeMax: new BN(2).mul(PERCENTAGE_PRECISION), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(400), + costToTrade: 1, + constituentDerivativeDepegThreshold: + PERCENTAGE_PRECISION.divn(10).muln(9), + derivativeWeight: ZERO, + volatility: PERCENTAGE_PRECISION.muln( + Math.floor(Math.random() * 10) + ).divn(100), + constituentCorrelations: correlations, + }); + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + assert(lpPool.constituents == i + 1); + + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == i + 1); + + correlations.push(new BN(Math.floor(Math.random() * 100)).divn(100)); + } + }); + + it('can initialize many perp markets and given some inventory', async () => { + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + await adminClient.initializePerpMarket( + i, + solUsd, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + new BN(0), + new BN(200 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(i, 1); + await adminClient.updatePerpMarketLpPoolFeeTransferScalar(i, 100); + await sleep(50); + } + + await adminClient.unsubscribe(); + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(adminKeypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: PERP_MARKET_INDEXES, + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.subscribe(); + }); + + it('can initialize all the different extra users', async () => { + for (let i = 0; i < NUMBER_OF_USERS; i++) { + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 10 ** 9); + await sleep(100); + const userClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: PERP_MARKET_INDEXES, + spotMarketIndexes: SPOT_MARKET_INDEXES, + oracleInfos: [{ publicKey: solUsd, source: OracleSource.PYTH }], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + coder: new CustomBorshCoder(program.idl), + }); + await userClient.subscribe(); + await sleep(100); + + const userUSDCAccount = await mockUserUSDCAccountWithAuthority( + usdcMint, + new BN(100_000_000).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair + ); + await sleep(100); + + await userClient.initializeUserAccountAndDepositCollateral( + new BN(10_000_000).mul(QUOTE_PRECISION), + userUSDCAccount + ); + await sleep(100); + userClients.push(userClient); + } + + let userIndex = 0; + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + // Give the vamm some inventory + const userClient = userClients[userIndex]; + await userClient.openPosition(PositionDirection.LONG, BASE_PRECISION, i); + await sleep(50); + if ( + userClient + .getUser() + .getActivePerpPositions() + .filter((x) => !x.baseAssetAmount.eq(ZERO)).length == 8 + ) { + userIndex++; + } + } + }); + + it('can add lots of mapping data', async () => { + // Assume that constituent 0 is USDC + for (let i = 0; i < NUMBER_OF_PERP_MARKETS; i++) { + for (let j = 1; j <= 3; j++) { + await adminClient.addAmmConstituentMappingData(lpPoolId, [ + { + perpMarketIndex: i, + constituentIndex: j, + weight: PERCENTAGE_PRECISION.divn(3), + }, + ]); + await sleep(50); + } + } + }); + + it('can add all addresses to lookup tables', async () => { + const slot = new BN( + await bankrunContextWrapper.connection.toConnection().getSlot() + ); + + const [lookupTableInst, lookupTableAddress] = + AddressLookupTableProgram.createLookupTable({ + authority: adminClient.wallet.publicKey, + payer: adminClient.wallet.publicKey, + recentSlot: slot.toNumber() - 10, + }); + + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + payer: adminClient.wallet.publicKey, + authority: adminClient.wallet.publicKey, + lookupTable: lookupTableAddress, + addresses: CONSTITUENT_INDEXES.map((i) => + getConstituentPublicKey(program.programId, lpPoolKey, i) + ), + }); + + const tx = new Transaction().add(lookupTableInst).add(extendInstruction); + await adminClient.sendTransaction(tx); + lutAddress = lookupTableAddress; + + const chunkies = chunks( + adminClient.getPerpMarketAccounts().map((account) => account.pubkey), + 20 + ); + for (const chunk of chunkies) { + const extendTx = new Transaction(); + const extendInstruction = AddressLookupTableProgram.extendLookupTable({ + payer: adminClient.wallet.publicKey, + authority: adminClient.wallet.publicKey, + lookupTable: lookupTableAddress, + addresses: chunk, + }); + extendTx.add(extendInstruction); + await adminClient.sendTransaction(extendTx); + } + }); + + it('can crank amm info into the cache', async () => { + let ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + + for (const chunk of chunks(PERP_MARKET_INDEXES, 20)) { + const txSig = await adminClient.updateAmmCache(chunk); + const cus = + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); + console.log(cus); + assert(cus < 200_000); + } + + ammCache = (await adminClient.program.account.ammCache.fetch( + getAmmCachePublicKey(program.programId) + )) as AmmCache; + expect(ammCache).to.not.be.null; + assert(ammCache.cache.length == NUMBER_OF_PERP_MARKETS); + }); + + it('can update target balances', async () => { + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, i) + )) as ConstituentAccount; + + await adminClient.updateConstituentOracleInfo(constituent); + } + + const cuIx = ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_400_000, + }); + const ammCacheIxs = await Promise.all( + chunks(PERP_MARKET_INDEXES, 50).map( + async (chunk) => await adminClient.getUpdateAmmCacheIx(chunk) + ) + ); + const updateBaseIx = await adminClient.getUpdateLpConstituentTargetBaseIx( + lpPoolId, + [getConstituentPublicKey(program.programId, lpPoolKey, 1)] + ); + + const txMessage = new TransactionMessage({ + payerKey: adminClient.wallet.publicKey, + recentBlockhash: (await adminClient.connection.getLatestBlockhash()) + .blockhash, + instructions: [cuIx, ...ammCacheIxs, updateBaseIx], + }); + + const lookupTableAccount = ( + await bankrunContextWrapper.connection.getAddressLookupTable(lutAddress) + ).value; + const message = txMessage.compileToV0Message([lookupTableAccount]); + + const txSig = await adminClient.connection.sendTransaction( + new VersionedTransaction(message) + ); + + const cus = Number( + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig) + ); + console.log(cus); + + // assert(+cus.toString() < 100_000); + }); + + it('can update AUM with high balances', async () => { + const lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + for (let i = 0; i < NUMBER_OF_CONSTITUENTS; i++) { + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + getConstituentPublicKey(program.programId, lpPoolKey, i), + [['vaultTokenBalance', QUOTE_PRECISION.muln(1000)]] + ); + } + + const tx = new Transaction(); + tx.add( + await adminClient.getUpdateLpPoolAumIxs(lpPool, CONSTITUENT_INDEXES) + ); + const txSig = await adminClient.sendTransaction(tx); + const cus = Number( + bankrunContextWrapper.connection.findComputeUnitConsumption(txSig.txSig) + ); + console.log(cus); + }); +}); + +const chunks = (array: readonly T[], size: number): T[][] => { + return new Array(Math.ceil(array.length / size)) + .fill(null) + .map((_, index) => index * size) + .map((begin) => array.slice(begin, begin + size)); +}; diff --git a/tests/lpPoolSwap.ts b/tests/lpPoolSwap.ts new file mode 100644 index 0000000000..4361c9a948 --- /dev/null +++ b/tests/lpPoolSwap.ts @@ -0,0 +1,998 @@ +import * as anchor from '@coral-xyz/anchor'; +import { expect, assert } from 'chai'; +import { Program } from '@coral-xyz/anchor'; +import { + Account, + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + Transaction, +} from '@solana/web3.js'; +import { + BN, + TestClient, + QUOTE_PRECISION, + getLpPoolPublicKey, + getConstituentTargetBasePublicKey, + PERCENTAGE_PRECISION, + PRICE_PRECISION, + PEG_PRECISION, + ConstituentTargetBaseAccount, + OracleSource, + SPOT_MARKET_RATE_PRECISION, + SPOT_MARKET_WEIGHT_PRECISION, + LPPoolAccount, + convertToNumber, + getConstituentVaultPublicKey, + getConstituentPublicKey, + ConstituentAccount, + ZERO, + getSerumSignerPublicKey, + BN_MAX, + isVariant, + ConstituentStatus, + getSignedTokenAmount, + getTokenAmount, +} from '../sdk/src'; +import { + initializeQuoteSpotMarket, + mockUSDCMint, + mockUserUSDCAccount, + mockOracleNoProgram, + setFeedPriceNoProgram, + overWriteTokenAccountBalance, + overwriteConstituentAccount, + mockAtaTokenAccountForMint, + overWriteMintAccount, + createWSolTokenAccountForUser, + initializeSolSpotMarket, + createUserWithUSDCAndWSOLAccount, + sleep, +} from './testHelpers'; +import { startAnchor } from 'solana-bankrun'; +import { TestBulkAccountLoader } from '../sdk/src/accounts/testBulkAccountLoader'; +import { BankrunContextWrapper } from '../sdk/src/bankrun/bankrunConnection'; +import dotenv from 'dotenv'; +import { DexInstructions, Market, OpenOrders } from '@project-serum/serum'; +import { listMarket, SERUM, makePlaceOrderTransaction } from './serumHelper'; +import { NATIVE_MINT } from '@solana/spl-token'; +import { + CustomBorshAccountsCoder, + CustomBorshCoder, +} from '../sdk/src/decode/customCoder'; +dotenv.config(); + +describe('LP Pool', () => { + const program = anchor.workspace.Drift as Program; + // Align account (de)serialization with on-chain zero-copy layouts + // @ts-ignore + program.coder.accounts = new CustomBorshAccountsCoder(program.idl); + let bankrunContextWrapper: BankrunContextWrapper; + let bulkAccountLoader: TestBulkAccountLoader; + + let adminClient: TestClient; + let usdcMint: Keypair; + let spotTokenMint: Keypair; + let spotMarketOracle: PublicKey; + + let serumMarketPublicKey: PublicKey; + + let serumDriftClient: TestClient; + let serumWSOL: PublicKey; + let serumUSDC: PublicKey; + let serumKeypair: Keypair; + + let adminSolAta: PublicKey; + + let openOrdersAccount: PublicKey; + + const usdcAmount = new BN(500 * 10 ** 6); + const solAmount = new BN(2 * 10 ** 9); + + const mantissaSqrtScale = new BN(Math.sqrt(PRICE_PRECISION.toNumber())); + const ammInitialQuoteAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + const ammInitialBaseAssetReserve = new anchor.BN(10 * 10 ** 13).mul( + mantissaSqrtScale + ); + + const lpPoolId = 0; + const tokenDecimals = 6; + const lpPoolKey = getLpPoolPublicKey(program.programId, lpPoolId); + + let userUSDCAccount: Keypair; + let serumMarket: Market; + + before(async () => { + const context = await startAnchor( + '', + [ + { + name: 'serum_dex', + programId: new PublicKey( + 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX' + ), + }, + ], + [] + ); + + // @ts-ignore + bankrunContextWrapper = new BankrunContextWrapper(context); + + bulkAccountLoader = new TestBulkAccountLoader( + bankrunContextWrapper.connection, + 'processed', + 1 + ); + + usdcMint = await mockUSDCMint(bankrunContextWrapper); + spotTokenMint = await mockUSDCMint(bankrunContextWrapper); + spotMarketOracle = await mockOracleNoProgram(bankrunContextWrapper, 200.1); + + const keypair = new Keypair(); + await bankrunContextWrapper.fundKeypair(keypair, 50 * LAMPORTS_PER_SOL); + + adminClient = new TestClient({ + connection: bankrunContextWrapper.connection.toConnection(), + wallet: new anchor.Wallet(keypair), + programID: program.programId, + opts: { + commitment: 'confirmed', + }, + activeSubAccountId: 0, + subAccountIds: [], + perpMarketIndexes: [0, 1], + spotMarketIndexes: [0, 1, 2], + oracleInfos: [ + { + publicKey: spotMarketOracle, + source: OracleSource.PYTH, + }, + ], + accountSubscription: { + type: 'polling', + accountLoader: bulkAccountLoader, + }, + // Ensure the client uses the same custom coder + coder: new CustomBorshCoder(program.idl), + }); + await adminClient.initialize(usdcMint.publicKey, true); + await adminClient.subscribe(); + await initializeQuoteSpotMarket(adminClient, usdcMint.publicKey); + + userUSDCAccount = await mockUserUSDCAccount( + usdcMint, + new BN(10).mul(QUOTE_PRECISION), + bankrunContextWrapper, + keypair.publicKey + ); + + await adminClient.initializeUserAccountAndDepositCollateral( + new BN(10).mul(QUOTE_PRECISION), + userUSDCAccount.publicKey + ); + + const periodicity = new BN(0); + + await adminClient.initializePerpMarket( + 0, + spotMarketOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(0, 1); + + await adminClient.initializePerpMarket( + 1, + spotMarketOracle, + ammInitialBaseAssetReserve, + ammInitialQuoteAssetReserve, + periodicity, + new BN(224 * PEG_PRECISION.toNumber()) + ); + await adminClient.updatePerpMarketLpPoolStatus(1, 1); + + const optimalUtilization = SPOT_MARKET_RATE_PRECISION.div( + new BN(2) + ).toNumber(); // 50% utilization + const optimalRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const maxRate = SPOT_MARKET_RATE_PRECISION.toNumber(); + const initialAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceAssetWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const initialLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const maintenanceLiabilityWeight = SPOT_MARKET_WEIGHT_PRECISION.toNumber(); + const imfFactor = 0; + + await adminClient.initializeSpotMarket( + spotTokenMint.publicKey, + optimalUtilization, + optimalRate, + maxRate, + spotMarketOracle, + OracleSource.PYTH, + initialAssetWeight, + maintenanceAssetWeight, + initialLiabilityWeight, + maintenanceLiabilityWeight, + imfFactor + ); + + adminSolAta = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + new BN(20 * 10 ** 9) // 10 SOL + ); + + await adminClient.initializeLpPool( + lpPoolId, + new BN(100), // 1 bps + new BN(100_000_000).mul(QUOTE_PRECISION), + new BN(1_000_000).mul(QUOTE_PRECISION), + Keypair.generate() // dlp mint + ); + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 0, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: PERCENTAGE_PRECISION, + volatility: ZERO, + constituentCorrelations: [], + }); + await adminClient.updateFeatureBitFlagsMintRedeemLpPool(true); + + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 1, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: PERCENTAGE_PRECISION.muln(4).divn(100), + constituentCorrelations: [ZERO], + }); + + await initializeSolSpotMarket(adminClient, spotMarketOracle); + await adminClient.updateSpotMarketStepSizeAndTickSize( + 2, + new BN(100000000), + new BN(100) + ); + await adminClient.updateSpotAuctionDuration(0); + + await adminClient.deposit( + new BN(5 * 10 ** 9), // 10 SOL + 2, // market index + adminSolAta // user token account + ); + + await adminClient.depositIntoSpotMarketVault( + 2, + new BN(4 * 10 ** 9), // 4 SOL + adminSolAta + ); + + [serumDriftClient, serumWSOL, serumUSDC, serumKeypair] = + await createUserWithUSDCAndWSOLAccount( + bankrunContextWrapper, + usdcMint, + program, + solAmount, + usdcAmount, + [], + [0, 1], + [ + { + publicKey: spotMarketOracle, + source: OracleSource.PYTH, + }, + ], + bulkAccountLoader + ); + + await bankrunContextWrapper.fundKeypair( + serumKeypair, + 50 * LAMPORTS_PER_SOL + ); + await serumDriftClient.deposit(usdcAmount, 0, serumUSDC); + }); + + after(async () => { + await adminClient.unsubscribe(); + await serumDriftClient.unsubscribe(); + }); + + it('LP Pool init properly', async () => { + let lpPool: LPPoolAccount; + try { + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + expect(lpPool).to.not.be.null; + } catch (e) { + expect.fail('LP Pool should have been created'); + } + + try { + const constituentTargetBasePublicKey = getConstituentTargetBasePublicKey( + program.programId, + lpPoolKey + ); + const constituentTargetBase = + (await adminClient.program.account.constituentTargetBase.fetch( + constituentTargetBasePublicKey + )) as ConstituentTargetBaseAccount; + expect(constituentTargetBase).to.not.be.null; + assert(constituentTargetBase.targets.length == 2); + } catch (e) { + expect.fail('Amm constituent map should have been created'); + } + }); + + it('lp pool swap', async () => { + let spotOracle = adminClient.getOracleDataForSpotMarket(1); + const price1 = convertToNumber(spotOracle.price); + + await setFeedPriceNoProgram(bankrunContextWrapper, 224.3, spotMarketOracle); + + await adminClient.fetchAccounts(); + + spotOracle = adminClient.getOracleDataForSpotMarket(1); + const price2 = convertToNumber(spotOracle.price); + assert(price2 > price1); + + const const0TokenAccount = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 0 + ); + const const1TokenAccount = getConstituentVaultPublicKey( + program.programId, + lpPoolKey, + 1 + ); + + const const0Key = getConstituentPublicKey(program.programId, lpPoolKey, 0); + const const1Key = getConstituentPublicKey(program.programId, lpPoolKey, 1); + + const c0TokenBalance = new BN(224_300_000_000); + const c1TokenBalance = new BN(1_000_000_000); + + await overWriteTokenAccountBalance( + bankrunContextWrapper, + const0TokenAccount, + BigInt(c0TokenBalance.toString()) + ); + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + const0Key, + [['vaultTokenBalance', c0TokenBalance]] + ); + + await overWriteTokenAccountBalance( + bankrunContextWrapper, + const1TokenAccount, + BigInt(c1TokenBalance.toString()) + ); + await overwriteConstituentAccount( + bankrunContextWrapper, + adminClient.program, + const1Key, + [['vaultTokenBalance', c1TokenBalance]] + ); + + // check fields overwritten correctly + const c0 = (await adminClient.program.account.constituent.fetch( + const0Key + )) as ConstituentAccount; + expect(c0.vaultTokenBalance.toString()).to.equal(c0TokenBalance.toString()); + + const c1 = (await adminClient.program.account.constituent.fetch( + const1Key + )) as ConstituentAccount; + expect(c1.vaultTokenBalance.toString()).to.equal(c1TokenBalance.toString()); + + await adminClient.updateConstituentOracleInfo(c1); + await adminClient.updateConstituentOracleInfo(c0); + + const prec = new BN(10).pow(new BN(tokenDecimals)); + console.log( + `const0 balance: ${convertToNumber(c0.vaultTokenBalance, prec)}` + ); + console.log( + `const1 balance: ${convertToNumber(c1.vaultTokenBalance, prec)}` + ); + + const lpPool1 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + expect(lpPool1.lastAumSlot.toNumber()).to.be.equal(0); + + await adminClient.updateLpPoolAum(lpPool1, [1, 0]); + + const lpPool2 = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + + expect(lpPool2.lastAumSlot.toNumber()).to.be.greaterThan(0); + expect(lpPool2.lastAum.gt(lpPool1.lastAum)).to.be.true; + console.log(`AUM: ${convertToNumber(lpPool2.lastAum, QUOTE_PRECISION)}`); + + // swap c0 for c1 + + const adminAuth = adminClient.wallet.publicKey; + + // mint some tokens for user + const c0UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + usdcMint.publicKey, + new BN(224_300_000_000), + adminAuth + ); + const c1UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + spotTokenMint.publicKey, + new BN(1_000_000_000), + adminAuth + ); + + const inTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const outTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c1UserTokenAccount + ); + + // in = 0, out = 1 + const swapTx = new Transaction(); + swapTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool2, [0, 1])); + swapTx.add( + await adminClient.getLpPoolSwapIx( + 0, + 1, + new BN(224_300_000), + new BN(0), + lpPoolKey, + adminAuth + ) + ); + + // Should throw since we havnet enabled swaps yet + try { + await adminClient.sendTransaction(swapTx); + assert(false, 'Should have thrown'); + } catch (error) { + assert(error.message.includes('0x17f1')); + } + + // Enable swaps + await adminClient.updateFeatureBitFlagsSwapLpPool(true); + + // Send swap + await adminClient.sendTransaction(swapTx); + + const inTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const outTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c1UserTokenAccount + ); + const diffInToken = + inTokenBalanceAfter.amount - inTokenBalanceBefore.amount; + const diffOutToken = + outTokenBalanceAfter.amount - outTokenBalanceBefore.amount; + + expect(Number(diffInToken)).to.be.equal(-224_300_000); + expect(Number(diffOutToken)).to.be.approximately(1001298, 1); + + console.log( + `in Token: ${inTokenBalanceBefore.amount} -> ${ + inTokenBalanceAfter.amount + } (${Number(diffInToken) / 1e6})` + ); + console.log( + `out Token: ${outTokenBalanceBefore.amount} -> ${ + outTokenBalanceAfter.amount + } (${Number(diffOutToken) / 1e6})` + ); + }); + + it('lp pool add and remove liquidity: usdc', async () => { + // add c0 liquidity + const adminAuth = adminClient.wallet.publicKey; + const c0UserTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + usdcMint.publicKey, + new BN(1_000_000_000_000), + adminAuth + ); + let lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + await adminClient.updateLpPoolAum(lpPool, [0, 1]); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpPoolAumBefore = lpPool.lastAum; + + const userLpTokenAccount = await mockAtaTokenAccountForMint( + bankrunContextWrapper, + lpPool.mint, + new BN(0), + adminAuth + ); + + // check fields overwritten correctly + const c0 = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + const c1 = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 1) + )) as ConstituentAccount; + await adminClient.updateConstituentOracleInfo(c1); + await adminClient.updateConstituentOracleInfo(c0); + + const userC0TokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceBefore = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + await overWriteMintAccount( + bankrunContextWrapper, + lpPool.mint, + BigInt(lpPool.lastAum.toNumber()) + ); + + const tokensAdded = new BN(1_000_000_000_000); + let tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 0, + inAmount: tokensAdded, + minMintAmount: new BN(1), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(tx); + + // Should fail to add more liquidity if it's in redulce only mode; + await adminClient.updateConstituentStatus( + c0.pubkey, + ConstituentStatus.REDUCE_ONLY + ); + tx = new Transaction(); + tx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + tx.add( + ...(await adminClient.getLpPoolAddLiquidityIx({ + inMarketIndex: 0, + inAmount: tokensAdded, + minMintAmount: new BN(1), + lpPool: lpPool, + })) + ); + try { + await adminClient.sendTransaction(tx); + } catch (e) { + console.log(e.message); + assert(e.message.includes('0x18c5')); // InvalidConstituentOperation + } + await adminClient.updateConstituentStatus( + c0.pubkey, + ConstituentStatus.ACTIVE + ); + + await sleep(500); + + const userC0TokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceAfter = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + lpPool = (await adminClient.program.account.lpPool.fetch( + lpPoolKey + )) as LPPoolAccount; + const lpPoolAumAfter = lpPool.lastAum; + const lpPoolAumDiff = lpPoolAumAfter.sub(lpPoolAumBefore); + expect(lpPoolAumDiff.toString()).to.be.equal(tokensAdded.toString()); + + const userC0TokenBalanceDiff = + Number(userC0TokenBalanceAfter.amount) - + Number(userC0TokenBalanceBefore.amount); + expect(Number(userC0TokenBalanceDiff)).to.be.equal( + -1 * tokensAdded.toNumber() + ); + + const userLpTokenBalanceDiff = + Number(userLpTokenBalanceAfter.amount) - + Number(userLpTokenBalanceBefore.amount); + expect(userLpTokenBalanceDiff).to.be.equal( + (((tokensAdded.toNumber() * 9997) / 10000) * 9999) / 10000 + ); // max weight deviation: expect min swap% fee on constituent, + 0.01% lp mint fee + + const constituentBalanceBefore = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + console.log(`constituentBalanceBefore: ${constituentBalanceBefore}`); + + // remove liquidity + const removeTx = new Transaction(); + removeTx.add(await adminClient.getUpdateLpPoolAumIxs(lpPool, [0, 1])); + removeTx.add( + await adminClient.getDepositToProgramVaultIx( + lpPoolId, + 0, + new BN(constituentBalanceBefore) + ) + ); + removeTx.add( + ...(await adminClient.getLpPoolRemoveLiquidityIx({ + outMarketIndex: 0, + lpToBurn: new BN(userLpTokenBalanceAfter.amount.toString()), + minAmountOut: new BN(1), + lpPool: lpPool, + })) + ); + await adminClient.sendTransaction(removeTx); + + const constituentAfterRemoveLiquidity = + (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 0) + )) as ConstituentAccount; + + const blTokenAmountAfterRemoveLiquidity = getSignedTokenAmount( + getTokenAmount( + constituentAfterRemoveLiquidity.spotBalance.scaledBalance, + adminClient.getSpotMarketAccount(0), + constituentAfterRemoveLiquidity.spotBalance.balanceType + ), + constituentAfterRemoveLiquidity.spotBalance.balanceType + ); + + const withdrawFromProgramVaultTx = new Transaction(); + withdrawFromProgramVaultTx.add( + await adminClient.getWithdrawFromProgramVaultIx( + lpPoolId, + 0, + blTokenAmountAfterRemoveLiquidity.abs() + ) + ); + await adminClient.sendTransaction(withdrawFromProgramVaultTx); + + const userC0TokenBalanceAfterBurn = + await bankrunContextWrapper.connection.getTokenAccount( + c0UserTokenAccount + ); + const userLpTokenBalanceAfterBurn = + await bankrunContextWrapper.connection.getTokenAccount( + userLpTokenAccount + ); + + const userC0TokenBalanceAfterBurnDiff = + Number(userC0TokenBalanceAfterBurn.amount) - + Number(userC0TokenBalanceAfter.amount); + + expect(userC0TokenBalanceAfterBurnDiff).to.be.greaterThan(0); + expect(Number(userLpTokenBalanceAfterBurn.amount)).to.be.equal(0); + + const totalC0TokensLost = new BN( + userC0TokenBalanceAfterBurn.amount.toString() + ).sub(tokensAdded); + const totalC0TokensLostPercent = + Number(totalC0TokensLost) / Number(tokensAdded); + expect(totalC0TokensLostPercent).to.be.approximately(-0.0006, 0.0001); // lost about 7bps swapping in an out + }); + + it('Add Serum Market', async () => { + serumMarketPublicKey = await listMarket({ + context: bankrunContextWrapper, + wallet: bankrunContextWrapper.provider.wallet, + baseMint: NATIVE_MINT, + quoteMint: usdcMint.publicKey, + baseLotSize: 100000000, + quoteLotSize: 100, + dexProgramId: SERUM, + feeRateBps: 0, + }); + + serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'confirmed' }, + SERUM + ); + + await adminClient.initializeSerumFulfillmentConfig( + 2, + serumMarketPublicKey, + SERUM + ); + + serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'recent' }, + SERUM + ); + + const serumOpenOrdersAccount = new Account(); + const createOpenOrdersIx = await OpenOrders.makeCreateAccountTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket.address, + serumDriftClient.wallet.publicKey, + serumOpenOrdersAccount.publicKey, + serumMarket.programId + ); + await serumDriftClient.sendTransaction( + new Transaction().add(createOpenOrdersIx), + [serumOpenOrdersAccount] + ); + + const adminOpenOrdersAccount = new Account(); + const adminCreateOpenOrdersIx = + await OpenOrders.makeCreateAccountTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket.address, + adminClient.wallet.publicKey, + adminOpenOrdersAccount.publicKey, + serumMarket.programId + ); + await adminClient.sendTransaction( + new Transaction().add(adminCreateOpenOrdersIx), + [adminOpenOrdersAccount] + ); + + openOrdersAccount = adminOpenOrdersAccount.publicKey; + }); + + it('swap sol for usdc', async () => { + // Initialize new constituent for market 2 + await adminClient.initializeConstituent(lpPoolId, { + spotMarketIndex: 2, + decimals: 6, + maxWeightDeviation: PERCENTAGE_PRECISION.divn(10), // 10% max dev, + swapFeeMin: PERCENTAGE_PRECISION.divn(10000), // min fee 1 bps, + swapFeeMax: PERCENTAGE_PRECISION.divn(100), + maxBorrowTokenAmount: new BN(1_000_000).muln(10 ** 6), + oracleStalenessThreshold: new BN(100), + costToTrade: 1, + derivativeWeight: ZERO, + volatility: ZERO, + constituentCorrelations: [ZERO, PERCENTAGE_PRECISION], + }); + + const beforeSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + console.log(`beforeSOLBalance: ${beforeSOLBalance}`); + const beforeUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + console.log(`beforeUSDCBalance: ${beforeUSDCBalance}`); + + const serumMarket = await Market.load( + bankrunContextWrapper.connection.toConnection(), + serumMarketPublicKey, + { commitment: 'recent' }, + SERUM + ); + + const adminSolAccount = await createWSolTokenAccountForUser( + bankrunContextWrapper, + adminClient.wallet.payer, + ZERO + ); + + // place ask to sell 1 sol for 100 usdc + const { transaction, signers } = await makePlaceOrderTransaction( + bankrunContextWrapper.connection.toConnection(), + serumMarket, + { + owner: serumDriftClient.wallet, + payer: serumWSOL, + side: 'sell', + price: 100, + size: 1, + orderType: 'postOnly', + clientId: undefined, // todo? + openOrdersAddressKey: undefined, + openOrdersAccount: undefined, + feeDiscountPubkey: null, + selfTradeBehavior: 'abortTransaction', + maxTs: BN_MAX, + } + ); + + const signerKeypairs = signers.map((signer) => { + return Keypair.fromSecretKey(signer.secretKey); + }); + + await serumDriftClient.sendTransaction(transaction, signerKeypairs); + + const amountIn = new BN(200).muln( + 10 ** adminClient.getSpotMarketAccount(0).decimals + ); + + const { beginSwapIx, endSwapIx } = await adminClient.getSwapIx( + { + lpPoolId: lpPoolId, + amountIn: amountIn, + inMarketIndex: 0, + outMarketIndex: 2, + inTokenAccount: userUSDCAccount.publicKey, + outTokenAccount: adminSolAccount, + }, + true + ); + + const serumBidIx = serumMarket.makePlaceOrderInstruction( + bankrunContextWrapper.connection.toConnection(), + { + owner: adminClient.wallet.publicKey, + payer: userUSDCAccount.publicKey, + side: 'buy', + price: 100, + size: 2, // larger than maker orders so that entire maker order is taken + orderType: 'ioc', + clientId: new BN(1), // todo? + openOrdersAddressKey: openOrdersAccount, + feeDiscountPubkey: null, + selfTradeBehavior: 'abortTransaction', + } + ); + + const serumConfig = await adminClient.getSerumV3FulfillmentConfig( + serumMarket.publicKey + ); + const settleFundsIx = DexInstructions.settleFunds({ + market: serumMarket.publicKey, + openOrders: openOrdersAccount, + owner: adminClient.wallet.publicKey, + // @ts-ignore + baseVault: serumConfig.serumBaseVault, + // @ts-ignore + quoteVault: serumConfig.serumQuoteVault, + baseWallet: adminSolAccount, + quoteWallet: userUSDCAccount.publicKey, + vaultSigner: getSerumSignerPublicKey( + serumMarket.programId, + serumMarket.publicKey, + serumConfig.serumSignerNonce + ), + programId: serumMarket.programId, + }); + + const tx = new Transaction() + .add(beginSwapIx) + .add(serumBidIx) + .add(settleFundsIx) + .add(endSwapIx); + + // Should fail if usdc is in reduce only + const c0pubkey = getConstituentPublicKey(program.programId, lpPoolKey, 0); + await adminClient.updateConstituentStatus( + c0pubkey, + ConstituentStatus.REDUCE_ONLY + ); + try { + await adminClient.sendTransaction(tx); + } catch (e) { + assert(e.message.includes('0x18c0')); + } + await adminClient.updateConstituentStatus( + c0pubkey, + ConstituentStatus.ACTIVE + ); + + const { txSig } = await adminClient.sendTransaction(tx); + + bankrunContextWrapper.printTxLogs(txSig); + + // Balances should be accuarate after swap + const afterSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const afterUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + const solDiff = afterSOLBalance - beforeSOLBalance; + const usdcDiff = afterUSDCBalance - beforeUSDCBalance; + + console.log( + `in Token: ${beforeUSDCBalance} -> ${afterUSDCBalance} (${usdcDiff})` + ); + console.log( + `out Token: ${beforeSOLBalance} -> ${afterSOLBalance} (${solDiff})` + ); + + expect(usdcDiff).to.be.equal(-100040000); + expect(solDiff).to.be.equal(1000000000); + }); + + it('deposit and withdraw atomically before swapping', async () => { + const beforeSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const beforeUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + await adminClient.depositWithdrawToProgramVault( + lpPoolId, + 0, + 2, + new BN(400).mul(QUOTE_PRECISION), // 100 USDC + new BN(2 * 10 ** 9) // 100 USDC + ); + + const afterSOLBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 2) + ) + ).amount.toString(); + const afterUSDCBalance = +( + await bankrunContextWrapper.connection.getTokenAccount( + getConstituentVaultPublicKey(program.programId, lpPoolKey, 0) + ) + ).amount.toString(); + + const solDiff = afterSOLBalance - beforeSOLBalance; + const usdcDiff = afterUSDCBalance - beforeUSDCBalance; + + console.log( + `in Token: ${beforeUSDCBalance} -> ${afterUSDCBalance} (${usdcDiff})` + ); + console.log( + `out Token: ${beforeSOLBalance} -> ${afterSOLBalance} (${solDiff})` + ); + + expect(usdcDiff).to.be.equal(-400000000); + expect(solDiff).to.be.equal(2000000000); + + const constituent = (await adminClient.program.account.constituent.fetch( + getConstituentPublicKey(program.programId, lpPoolKey, 2) + )) as ConstituentAccount; + + assert(constituent.spotBalance.scaledBalance.eq(new BN(2000000001))); + assert(isVariant(constituent.spotBalance.balanceType, 'borrow')); + }); +}); diff --git a/tests/placeAndMakeSignedMsgBankrun.ts b/tests/placeAndMakeSignedMsgBankrun.ts index 3b23722445..5971501251 100644 --- a/tests/placeAndMakeSignedMsgBankrun.ts +++ b/tests/placeAndMakeSignedMsgBankrun.ts @@ -1564,7 +1564,7 @@ describe('place and make signedMsg order', () => { ); assert.fail('should fail'); } catch (e) { - assert(e.toString().includes('0x1776')); + assert(e.toString().includes('Error: Invalid option')); const takerOrders = takerDriftClient.getUser().getOpenOrders(); assert(takerOrders.length == 0); } diff --git a/tests/subaccounts.ts b/tests/subaccounts.ts index 2f6f5c5cd2..fe0c63acc4 100644 --- a/tests/subaccounts.ts +++ b/tests/subaccounts.ts @@ -158,6 +158,7 @@ describe('subaccounts', () => { undefined, donationAmount ); + await driftClient.fetchAccounts(); await driftClient.addUser(1); await driftClient.switchActiveUser(1); diff --git a/tests/switchboardTxCus.ts b/tests/switchboardTxCus.ts index 40e6a33277..9c46994d99 100644 --- a/tests/switchboardTxCus.ts +++ b/tests/switchboardTxCus.ts @@ -219,6 +219,6 @@ describe('switchboard place orders cus', () => { const cus = bankrunContextWrapper.connection.findComputeUnitConsumption(txSig); console.log(cus); - assert(cus < 410000); + assert(cus < 415000); }); }); diff --git a/tests/testHelpers.ts b/tests/testHelpers.ts index df4e742abe..b4a1ca83a6 100644 --- a/tests/testHelpers.ts +++ b/tests/testHelpers.ts @@ -43,7 +43,10 @@ import { PositionDirection, DriftClient, OrderType, -} from '../sdk'; + ReferrerInfo, + ConstituentAccount, + SpotMarketAccount, +} from '../sdk/src'; import { TestClient, SPOT_MARKET_RATE_PRECISION, @@ -401,7 +404,8 @@ export async function initializeAndSubscribeDriftClient( marketIndexes: number[], bankIndexes: number[], oracleInfos: OracleInfo[] = [], - accountLoader?: TestBulkAccountLoader + accountLoader?: TestBulkAccountLoader, + referrerInfo?: ReferrerInfo ): Promise { const driftClient = new TestClient({ connection, @@ -426,7 +430,7 @@ export async function initializeAndSubscribeDriftClient( }, }); await driftClient.subscribe(); - await driftClient.initializeUserAccount(); + await driftClient.initializeUserAccount(0, undefined, referrerInfo); return driftClient; } @@ -438,7 +442,8 @@ export async function createUserWithUSDCAccount( marketIndexes: number[], bankIndexes: number[], oracleInfos: OracleInfo[] = [], - accountLoader?: TestBulkAccountLoader + accountLoader?: TestBulkAccountLoader, + referrerInfo?: ReferrerInfo ): Promise<[TestClient, PublicKey, Keypair]> { const userKeyPair = await createFundedKeyPair(context); const usdcAccount = await createUSDCAccountForUser( @@ -454,7 +459,8 @@ export async function createUserWithUSDCAccount( marketIndexes, bankIndexes, oracleInfos, - accountLoader + accountLoader, + referrerInfo ); return [driftClient, usdcAccount, userKeyPair]; @@ -557,7 +563,6 @@ export async function printTxLogs( const tx = await connection.getTransaction(txSig, { commitment: 'confirmed', }); - console.log('tx logs', tx.meta.logMessages); return tx.meta.logMessages; } @@ -1205,6 +1210,23 @@ export async function overWritePerpMarket( }); } +export async function overWriteSpotMarket( + driftClient: TestClient, + bankrunContextWrapper: BankrunContextWrapper, + spotMarketKey: PublicKey, + spotMarket: SpotMarketAccount +) { + bankrunContextWrapper.context.setAccount(spotMarketKey, { + executable: false, + owner: driftClient.program.programId, + lamports: LAMPORTS_PER_SOL, + data: await driftClient.program.account.spotMarket.coder.accounts.encode( + 'SpotMarket', + spotMarket + ), + }); +} + export async function getPerpMarketDecoded( driftClient: TestClient, bankrunContextWrapper: BankrunContextWrapper, @@ -1359,3 +1381,29 @@ export async function placeAndFillVammTrade({ console.error(e); } } + +export async function overwriteConstituentAccount( + bankrunContextWrapper: BankrunContextWrapper, + program: Program, + constituentPublicKey: PublicKey, + overwriteFields: Array<[key: keyof ConstituentAccount, value: any]> +) { + const acc = await program.account.constituent.fetch(constituentPublicKey); + if (!acc) { + throw new Error( + `Constituent account ${constituentPublicKey.toBase58()} not found` + ); + } + for (const [key, value] of overwriteFields) { + acc[key] = value; + } + bankrunContextWrapper.context.setAccount(constituentPublicKey, { + executable: false, + owner: program.programId, + lamports: LAMPORTS_PER_SOL, + data: await program.account.constituent.coder.accounts.encode( + 'Constituent', + acc + ), + }); +} diff --git a/yarn.lock b/yarn.lock index 41c9b1a9d6..2678bc1b59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -895,12 +895,12 @@ anchor-bankrun@0.3.0: resolved "https://registry.yarnpkg.com/anchor-bankrun/-/anchor-bankrun-0.3.0.tgz#3789fcecbc201a2334cff228b99cc0da8ef0167e" integrity sha512-PYBW5fWX+iGicIS5MIM/omhk1tQPUc0ELAnI/IkLKQJ6d75De/CQRh8MF2bU/TgGyFi6zEel80wUe3uRol9RrQ== -ansi-regex@^5.0.1: +ansi-regex@5.0.1, ansi-regex@^5.0.1, ansi-regex@^6.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@4.3.0, ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -938,6 +938,11 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -967,6 +972,11 @@ axios@^1.5.1, axios@^1.8.3, axios@^1.9.0: form-data "^4.0.0" proxy-from-env "^1.1.0" +backslash@<0.2.1: + version "0.2.0" + resolved "https://registry.yarnpkg.com/backslash/-/backslash-0.2.0.tgz#6c3c1fce7e7e714ccfc10fd74f0f73410677375f" + integrity sha512-Avs+8FUZ1HF/VFP4YWwHQZSGzRPm37ukU1JQYQWijuHhtXdOuAzcZ8PcAzfIw898a8PyBzdn+RtnKA6MzW0X2A== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1167,7 +1177,14 @@ chai@4.4.1: pathval "^1.1.1" type-detect "^4.0.8" -chalk@^4.0.0, chalk@~4.1.2: +chalk-template@<1.1.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/chalk-template/-/chalk-template-1.1.0.tgz#ffc55db6dd745e9394b85327c8ac8466edb7a7b1" + integrity sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg== + dependencies: + chalk "^5.2.0" + +chalk@4.1.2, chalk@^4.0.0, chalk@^5.2.0, chalk@^5.3.0, chalk@^5.4.1, chalk@~4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -1175,11 +1192,6 @@ chalk@^4.0.0, chalk@~4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@^5.3.0, chalk@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" - integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== - check-error@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" @@ -1196,17 +1208,24 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== +color-convert@<3.1.1, color-convert@^2.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-3.1.0.tgz#ce16ebb832f9d7522649ed9e11bc0ccb9433a524" + integrity sha512-TVoqAq8ZDIpK5lsQY874DDnu65CSsc9vzq0wLpNQ6UMBq81GSZocVazPiBbYGzngzBOIRahpkTzCLVe2at4MfA== dependencies: - color-name "~1.1.4" + color-name "^2.0.0" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@<2.0.1, color-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.0.0.tgz#03ff6b1b5aec9bb3cf1ed82400c2790dfcd01d2d" + integrity sha512-SbtvAMWvASO5TE2QP07jHBMXKafgdZz8Vrsrn96fiL+O92/FN/PLARzUW5sKt013fjAprK2d2iCn2hk2Xb5oow== + +color-string@<2.1.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-2.1.0.tgz#a1cc4bb16a23032ff1048a2458a170323b15a23f" + integrity sha512-gNVoDzpaSwvftp6Y8nqk97FtZoXP9Yj7KGYB8yIXuv0JcfqbYihTrd1OU5iZW9btfXde4YAOCRySBHT7O910MA== + dependencies: + color-name "^2.0.0" combined-stream@^1.0.8: version "1.0.8" @@ -1270,7 +1289,7 @@ csvtojson@2.0.10: lodash "^4.17.3" strip-bom "^2.0.0" -debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@<4.4.2, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.4.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== @@ -1373,6 +1392,13 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +error-ex@<1.3.3: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" @@ -1761,11 +1787,23 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +has-ansi@<6.0.1: + version "6.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-6.0.0.tgz#8118b2fb548c062f9356c7d5013b192a238ce3b3" + integrity sha512-1AYj+gqAskFf9Skb7xuEYMfJqkW3TJ8lukw4Fczw+Y6jRkgxvcE4JiFWuTO4DsoleMvvHudryolA9ObJHJKHWQ== + dependencies: + ansi-regex "^6.0.1" + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-flag@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-5.0.1.tgz#5483db2ae02a472d1d0691462fc587d1843cd940" + integrity sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA== + has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" @@ -1848,6 +1886,11 @@ is-arguments@^1.0.4: call-bound "^1.0.2" has-tostringtag "^1.0.2" +is-arrayish@<0.3.3, is-arrayish@^0.2.1, is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -2491,11 +2534,27 @@ shiki@^0.11.1: vscode-oniguruma "^1.6.1" vscode-textmate "^6.0.0" +simple-swizzle@<0.2.3: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -2572,7 +2631,7 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -2619,13 +2678,21 @@ superstruct@^2.0.2: resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-2.0.2.tgz#3f6d32fbdc11c357deff127d591a39b996300c54" integrity sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A== -supports-color@^7.1.0: +supports-color@7.2.0, supports-color@^10.0.0, supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== dependencies: has-flag "^4.0.0" +supports-hyperlinks@<4.1.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-4.1.0.tgz#f006d9e2f6b9b6672f86c86c6f76bf52a69f4d91" + integrity sha512-6lY0rDZ5bbZhAPrwpz/nMR6XmeaFmh2itk7YnIyph2jblPmDcKMCPkSdLFTlaX8snBvg7OJmaOL3WRLqMEqcJQ== + dependencies: + has-flag "^5.0.1" + supports-color "^10.0.0" + text-encoding-utf-8@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz#585b62197b0ae437e3c7b5d0af27ac1021e10d13" @@ -2803,7 +2870,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -wrap-ansi@^7.0.0: +wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==