├── stable-swap-program ├── sdk │ ├── .prettierrc │ ├── .eslintignore │ ├── jest.config.ts │ ├── babel.config.js │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── README.md │ ├── package.json │ ├── .gitignore │ ├── test │ │ ├── helpers.ts │ │ ├── deployTestTokens.ts │ │ └── admin.int.test.ts │ └── src │ │ └── cli │ │ └── index.ts ├── scripts │ ├── stableswap │ ├── deployment_config.json │ ├── deploy-test-pools.sh │ ├── deploy-mainnet-pools.sh │ ├── deploy-program.sh │ └── _common.sh ├── program │ ├── src │ │ ├── lib.rs │ │ ├── entrypoint.rs │ │ └── processor │ │ │ ├── utils.rs │ │ │ ├── logging.rs │ │ │ ├── mod.rs │ │ │ ├── macros.rs │ │ │ ├── token.rs │ │ │ └── checks.rs │ ├── Cargo.toml │ ├── README.md │ └── proptest-regressions │ │ └── curve.txt ├── do.sh └── README.md ├── fuzz ├── .gitignore ├── src │ ├── lib.rs │ ├── native_account_data.rs │ ├── native_token.rs │ └── native_processor.rs ├── README.md ├── Cargo.toml └── targets │ └── swaps_only.rs ├── assets └── banner.png ├── audit └── bramah-systems.pdf ├── .envrc ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── libraries.yml │ ├── prebuild.yml │ ├── fuzzing.yml │ ├── release.yml │ └── program.yml ├── stable-swap-math ├── src │ ├── lib.rs │ ├── pool_converter.rs │ ├── bn.rs │ ├── math.rs │ └── price.rs ├── README.md ├── sim │ ├── Cargo.toml │ ├── README.md │ ├── simulation.py │ └── src │ │ └── lib.rs ├── proptest-regressions │ └── curve.txt └── Cargo.toml ├── stable-swap-client ├── README.md ├── src │ ├── lib.rs │ ├── fees.rs │ ├── error.rs │ └── state.rs └── Cargo.toml ├── stable-swap-anchor ├── README.md ├── Cargo.toml └── src │ ├── lib.rs │ ├── state.rs │ ├── accounts.rs │ └── instructions.rs ├── Cargo.toml ├── Anchor.toml ├── flake.nix ├── flake.lock ├── README.md └── LICENSE /stable-swap-program/sdk/.prettierrc: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | corpus/ 3 | -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saber-hq/stable-swap/HEAD/assets/banner.png -------------------------------------------------------------------------------- /audit/bramah-systems.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saber-hq/stable-swap/HEAD/audit/bramah-systems.pdf -------------------------------------------------------------------------------- /stable-swap-program/sdk/.eslintignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | lib/ 3 | jest.config.ts 4 | .eslintrc.js 5 | babel.config.js 6 | -------------------------------------------------------------------------------- /stable-swap-program/scripts/stableswap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | $(dirname $0)/../sdk/bin/stableswap "${@:1}" 4 | -------------------------------------------------------------------------------- /fuzz/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod native_account_data; 2 | pub mod native_processor; 3 | pub mod native_stable_swap; 4 | pub mod native_token; 5 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | watch_file flake.nix 2 | watch_file flake.lock 3 | mkdir -p .direnv 4 | eval "$(nix print-dev-env --profile "$(direnv_layout_dir)/flake-profile")" 5 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // The test environment that will be used for testing 3 | testEnvironment: "node", 4 | }; 5 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/babel.config.js: -------------------------------------------------------------------------------- 1 | // this file is needed for jest 2 | module.exports = { 3 | presets: [ 4 | ["@babel/preset-env", { targets: { node: "current" } }], 5 | "@babel/preset-typescript", 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .env 3 | .vscode 4 | bin 5 | config.json 6 | node_modules 7 | ./package-lock.json 8 | lib/client/lib/* 9 | lib/client/yarn-error.log 10 | last-deploy.json 11 | 12 | test-ledger 13 | .anchor/ 14 | docker-target/ 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "cargo" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /stable-swap-program/scripts/deployment_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "program_id": "SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ", 3 | "admin_account": "GSmjrpT8zNtp6Ke8y2xS5P1kREEjqZCjwxF8VbxDJAV8", 4 | "upgrade_authority": "GSmjrpT8zNtp6Ke8y2xS5P1kREEjqZCjwxF8VbxDJAV8" 5 | } 6 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ignorePatterns: ["dist/", "*.js"], 3 | parserOptions: { 4 | tsconfigRootDir: __dirname, 5 | project: "tsconfig.json", 6 | }, 7 | extends: ["@saberhq"], 8 | env: { 9 | node: true, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /stable-swap-math/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Math utilities for stable-swap. 2 | #![deny(rustdoc::all)] 3 | #![allow(rustdoc::missing_doc_code_examples)] 4 | #![deny(missing_docs)] 5 | #![deny(clippy::unwrap_used)] 6 | #![deny(clippy::integer_arithmetic)] 7 | 8 | pub mod bn; 9 | pub mod curve; 10 | pub mod math; 11 | pub mod pool_converter; 12 | pub mod price; 13 | -------------------------------------------------------------------------------- /stable-swap-client/README.md: -------------------------------------------------------------------------------- 1 | # stable-swap-client 2 | 3 | StableSwap Rust client. 4 | 5 | ## Documentation 6 | 7 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 8 | 9 | ## License 10 | 11 | Saber StableSwap is licensed under the Apache License, Version 2.0. 12 | -------------------------------------------------------------------------------- /stable-swap-math/README.md: -------------------------------------------------------------------------------- 1 | # stable-swap-math 2 | 3 | Calculations for the StableSwap invariant. 4 | 5 | ## Documentation 6 | 7 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 8 | 9 | ## License 10 | 11 | Saber StableSwap is licensed under the Apache License, Version 2.0. 12 | -------------------------------------------------------------------------------- /stable-swap-anchor/README.md: -------------------------------------------------------------------------------- 1 | # stable-swap-anchor 2 | 3 | Anchor bindings for the StableSwap Rust client. 4 | 5 | ## Documentation 6 | 7 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 8 | 9 | ## License 10 | 11 | Saber StableSwap is licensed under the Apache License, Version 2.0. 12 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "fuzz", 4 | "stable-swap-anchor/", 5 | "stable-swap-client/", 6 | "stable-swap-math/", 7 | "stable-swap-math/sim", 8 | "stable-swap-program/program" 9 | ] 10 | 11 | [profile.release] 12 | lto = "fat" 13 | codegen-units = 1 14 | 15 | [profile.release.build-override] 16 | opt-level = 3 17 | incremental = false 18 | codegen-units = 1 19 | -------------------------------------------------------------------------------- /stable-swap-program/scripts/deploy-test-pools.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | cd $(dirname $0) 5 | source _common.sh 6 | 7 | setup_solana_cluster $1 8 | 9 | echo "Deploying test pools to Solana cluster $CLUSTER with program $SWAP_PROGRAM_ID and admin $SWAP_ADMIN_ACCOUNT" 10 | ./stableswap deploy-pool \ 11 | --cluster $CLUSTER \ 12 | --admin_account $SWAP_ADMIN_ACCOUNT \ 13 | --program_id $SWAP_PROGRAM_ID ${@:2} 14 | -------------------------------------------------------------------------------- /stable-swap-anchor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stable-swap-anchor" 3 | version = "1.8.2" 4 | description = "Anchor bindings for the StableSwap Rust client." 5 | license = "Apache-2.0" 6 | authors = ["michaelhly "] 7 | homepage = "https://saber.so" 8 | repository = "https://github.com/saber-hq/stable-swap" 9 | edition = "2021" 10 | keywords = ["solana", "saber", "anchor"] 11 | 12 | [dependencies] 13 | anchor-lang = ">=0.22.0" 14 | stable-swap-client = { path = "../stable-swap-client", version = "^1" } 15 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "strict": true, 6 | "strictFunctionTypes": true, 7 | "noImplicitOverride": true, 8 | "esModuleInterop": true, 9 | "noUncheckedIndexedAccess": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true, 13 | 14 | "noEmit": true 15 | }, 16 | "include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /stable-swap-math/sim/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stable-swap-sim" 3 | version = "0.1.4" 4 | authors = ["michaelhly "] 5 | description = "Simulations of the StableSwap invariant compared to Curve's reference implementation." 6 | license = "Apache-2.0" 7 | homepage = "https://saber.so" 8 | repository = "https://github.com/saber-hq/stable-swap" 9 | edition = "2021" 10 | keywords = ["solana", "saber", "curve", "sim"] 11 | 12 | [lib] 13 | name = "sim" 14 | 15 | [dependencies] 16 | pyo3 = { version = "^0.16.4", features = ["auto-initialize"] } 17 | -------------------------------------------------------------------------------- /fuzz/README.md: -------------------------------------------------------------------------------- 1 | # `stable-swap-fuzz` 2 | 3 | Fuzz tests for the Saber StableSwap program. 4 | 5 | ## Running 6 | 7 | If you are using a recent nightly version, use the following command: 8 | 9 | ``` 10 | RUSTFLAGS="-Znew-llvm-pass-manager=no" cargo fuzz run --dev fuzz_test 11 | ``` 12 | 13 | ## Documentation 14 | 15 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 16 | 17 | ## License 18 | 19 | Saber StableSwap is licensed under the Apache License, Version 2.0. 20 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/README.md: -------------------------------------------------------------------------------- 1 | # Saber StableSwap tests 2 | 3 | End-to-end integration tests of the StableSwap program. 4 | 5 | 6 | If you are looking to integrate Saber into TypeScript, view the [@saberhq/stableswap-sdk here](https://github.com/saber-hq/saber-common/tree/master/packages/stableswap-sdk). 7 | 8 | ## Documentation 9 | 10 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 11 | 12 | ## License 13 | 14 | Saber StableSwap is licensed under the Apache License, Version 2.0. 15 | -------------------------------------------------------------------------------- /stable-swap-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Curve-like program for the Solana blockchain. 2 | #![deny(clippy::unwrap_used)] 3 | #![deny(rustdoc::all)] 4 | #![allow(rustdoc::missing_doc_code_examples)] 5 | #![deny(missing_docs)] 6 | 7 | pub mod error; 8 | pub mod fees; 9 | pub mod instruction; 10 | pub mod state; 11 | 12 | // Export current solana-program types for downstream users who may also be 13 | // building with a different solana-program version 14 | pub use solana_program; 15 | 16 | // The library uses this to verify the keys 17 | solana_program::declare_id!("SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ"); 18 | -------------------------------------------------------------------------------- /stable-swap-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stable-swap-client" 3 | version = "1.8.2" 4 | description = "StableSwap Rust client." 5 | license = "Apache-2.0" 6 | authors = ["michaelhly "] 7 | homepage = "https://saber.so" 8 | repository = "https://github.com/saber-hq/stable-swap" 9 | edition = "2021" 10 | keywords = ["solana", "saber"] 11 | 12 | [features] 13 | fuzz = ["arbitrary"] 14 | 15 | [dependencies] 16 | arbitrary = { version = "1.1.3", features = ["derive"], optional = true } 17 | arrayref = "0.3.6" 18 | num-derive = "0.3" 19 | num-traits = "0.2" 20 | solana-program = "^1.7" 21 | thiserror = "1.0" 22 | -------------------------------------------------------------------------------- /stable-swap-math/proptest-regressions/curve.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc b340c86fe850c5795a66fd7ba1f606ecedb37fd819b16176c8bf0ed8c5c82eee # shrinks to current_ts = 0, amp_factor = 1, deposit_amount_a = 1080629387744087693, deposit_amount_b = 511085650789627788, swap_token_a_amount = 20266855739835941, swap_token_b_amount = 32097373911537805, pool_token_supply = 598173393203946206 8 | -------------------------------------------------------------------------------- /stable-swap-math/sim/README.md: -------------------------------------------------------------------------------- 1 | # `stable-swap-sim` 2 | 3 | Simulations of the StableSwap invariant compared to Curve's reference implementation. 4 | 5 | ## Background 6 | 7 | The Saber StableSwap program is built to be faithful to Curve's StableSwap invariant. This package contains a simulator which ensures that the output of the Saber Rust code is equivalent to that of Curve's Python reference implementation. 8 | 9 | ## Documentation 10 | 11 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 12 | 13 | ## License 14 | 15 | Saber StableSwap is licensed under the Apache License, Version 2.0. 16 | -------------------------------------------------------------------------------- /stable-swap-math/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stable-swap-math" 3 | version = "1.8.2" 4 | description = "Calculations for the StableSwap invariant" 5 | license = "Apache-2.0" 6 | authors = ["michaelhly "] 7 | homepage = "https://saber.so" 8 | repository = "https://github.com/saber-hq/stable-swap" 9 | edition = "2021" 10 | keywords = ["solana", "saber", "math"] 11 | 12 | [dependencies] 13 | borsh = "0.9.2" 14 | num-traits = "0.2" 15 | stable-swap-client = { path = "../stable-swap-client", version = "^1" } 16 | uint = { version = "0.9", default-features = false } 17 | 18 | [dev-dependencies] 19 | proptest = "1.0.0" 20 | rand = "0.8.4" 21 | stable-swap-sim = { path = "./sim", version = "^0.1" } 22 | -------------------------------------------------------------------------------- /stable-swap-program/program/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An automated market maker for mean-reverting trading pairs. 2 | #![deny(rustdoc::all)] 3 | #![allow(rustdoc::missing_doc_code_examples)] 4 | #![deny(clippy::unwrap_used)] 5 | #![deny(clippy::integer_arithmetic)] 6 | #![deny(missing_docs)] 7 | 8 | pub mod entrypoint; 9 | pub mod processor; 10 | 11 | pub use stable_swap_client::{error, fees, instruction, state}; 12 | pub use stable_swap_math::{curve, math, pool_converter}; 13 | 14 | /// Export current solana-program types for downstream users who may also be 15 | /// building with a different solana-program version 16 | pub use solana_program; 17 | 18 | solana_program::declare_id!("SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ"); 19 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | anchor_version = "0.24.2" 2 | solana_version = "1.11.10" 3 | 4 | [workspace] 5 | members = [ 6 | "fuzz", 7 | "stable-swap-anchor", 8 | "stable-swap-client", 9 | "stable-swap-math", 10 | "stable-swap-math/sim", 11 | "stable-swap-program/program" 12 | ] 13 | 14 | [provider] 15 | cluster = "localnet" 16 | wallet = "~/.config/solana/id.json" 17 | 18 | [programs.mainnet] 19 | stable_swap = "SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ" 20 | 21 | [programs.devnet] 22 | stable_swap = "SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ" 23 | 24 | [programs.testnet] 25 | stable_swap = "SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ" 26 | 27 | [programs.localnet] 28 | stable_swap = "SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ" 29 | -------------------------------------------------------------------------------- /stable-swap-program/program/src/entrypoint.rs: -------------------------------------------------------------------------------- 1 | //! Program entrypoint definitions 2 | #![cfg(not(feature = "no-entrypoint"))] 3 | 4 | use crate::{error::SwapError, processor::Processor}; 5 | use solana_program::{ 6 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, 7 | program_error::PrintProgramError, pubkey::Pubkey, 8 | }; 9 | 10 | entrypoint!(process_instruction); 11 | fn process_instruction<'a>( 12 | program_id: &Pubkey, 13 | accounts: &'a [AccountInfo<'a>], 14 | instruction_data: &[u8], 15 | ) -> ProgramResult { 16 | if let Err(error) = Processor::process(program_id, accounts, instruction_data) { 17 | // catch the error so we can print it 18 | error.print::(); 19 | return Err(error); 20 | } 21 | Ok(()) 22 | } 23 | -------------------------------------------------------------------------------- /stable-swap-program/program/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stable-swap" 3 | version = "1.8.2" 4 | authors = ["michaelhly "] 5 | edition = "2021" 6 | description = "Saber StableSwap program." 7 | homepage = "https://saber.so" 8 | repository = "https://github.com/saber-hq/stable-swap" 9 | keywords = ["solana", "saber", "program"] 10 | license = "Apache-2.0" 11 | 12 | [features] 13 | program = [] 14 | no-entrypoint = [] 15 | fuzz = ["stable-swap-client/fuzz"] 16 | 17 | [dependencies] 18 | solana-program = "^1.6.10" 19 | spl-token = { version = "^3", features = ["no-entrypoint"] } 20 | stable-swap-client = { path = "../../stable-swap-client", version = "^1" } 21 | stable-swap-math = { path = "../../stable-swap-math", version = "^1" } 22 | 23 | [dev-dependencies] 24 | solana-sdk = "^1.9.18" 25 | 26 | [lib] 27 | name = "stable_swap" 28 | crate-type = ["cdylib", "lib"] 29 | -------------------------------------------------------------------------------- /stable-swap-program/program/README.md: -------------------------------------------------------------------------------- 1 | # `stable-swap` 2 | 3 | ![StableSwap by Saber](https://raw.githubusercontent.com/saber-hq/stable-swap/master/assets/banner.png) 4 | 5 | An automated market maker for mean-reverting trading pairs. 6 | 7 | ## About 8 | 9 | Saber is the leading StableSwap exchange on [Solana](https://solana.com/https://solana.com/). 10 | 11 | ### Programs 12 | 13 | **Program ID:** [`SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ`](https://explorer.solana.com/address/SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ) 14 | 15 | The program is deployed at the same address on `mainnet-beta`, `devnet`, and `testnet`. 16 | 17 | ## Documentation 18 | 19 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 20 | 21 | ## License 22 | 23 | Saber StableSwap is licensed under the Apache License, Version 2.0. 24 | -------------------------------------------------------------------------------- /stable-swap-program/program/src/processor/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utility methods 2 | 3 | use crate::error::SwapError; 4 | use solana_program::program_pack::Pack; 5 | use solana_program::pubkey::Pubkey; 6 | use spl_token::state::{Account, Mint}; 7 | 8 | /// Calculates the authority id by generating a program address. 9 | pub fn authority_id(program_id: &Pubkey, my_info: &Pubkey, nonce: u8) -> Result { 10 | Pubkey::create_program_address(&[&my_info.to_bytes()[..32], &[nonce]], program_id) 11 | .or(Err(SwapError::InvalidProgramAddress)) 12 | } 13 | 14 | /// Unpacks a spl_token `Account`. 15 | pub fn unpack_token_account(data: &[u8]) -> Result { 16 | Account::unpack(data).map_err(|_| SwapError::ExpectedAccount) 17 | } 18 | 19 | /// Unpacks a spl_token `Mint`. 20 | pub fn unpack_mint(data: &[u8]) -> Result { 21 | Mint::unpack(data).map_err(|_| SwapError::ExpectedMint) 22 | } 23 | -------------------------------------------------------------------------------- /stable-swap-anchor/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Anchor-compatible SDK for the StableSwap program. 2 | #![deny(missing_docs)] 3 | #![deny(rustdoc::all)] 4 | #![allow(rustdoc::missing_doc_code_examples)] 5 | #![allow(clippy::nonstandard_macro_braces)] 6 | 7 | mod accounts; 8 | mod instructions; 9 | mod state; 10 | 11 | pub use accounts::*; 12 | pub use instructions::*; 13 | pub use state::*; 14 | 15 | use anchor_lang::prelude::*; 16 | 17 | declare_id!("SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ"); 18 | 19 | /// The StableSwap program. 20 | #[derive(Clone)] 21 | pub struct StableSwap; 22 | 23 | impl anchor_lang::AccountDeserialize for StableSwap { 24 | fn try_deserialize(buf: &mut &[u8]) -> Result { 25 | StableSwap::try_deserialize_unchecked(buf) 26 | } 27 | 28 | fn try_deserialize_unchecked(_buf: &mut &[u8]) -> Result { 29 | Ok(StableSwap) 30 | } 31 | } 32 | 33 | impl anchor_lang::Id for StableSwap { 34 | fn id() -> Pubkey { 35 | ID 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stable-swap-fuzz" 3 | version = "1.8.2" 4 | authors = ["michaelhly "] 5 | edition = "2021" 6 | description = "Fuzz tests for the Saber StableSwap program." 7 | homepage = "https://saber.so" 8 | repository = "https://github.com/saber-hq/stable-swap" 9 | license = "Apache-2.0" 10 | keywords = ["solana", "saber", "fuzz"] 11 | 12 | [package.metadata] 13 | cargo-fuzz = true 14 | 15 | [lib] 16 | name = "fuzz" 17 | 18 | [dependencies] 19 | arbitrary = "1.1.3" 20 | chrono = "0.4" 21 | lazy_static = "1.4.0" 22 | libfuzzer-sys = "0.4.4" 23 | rand = "0.8.4" 24 | solana-program = "^1.9" 25 | spl-token = { version = "^3", features = ["no-entrypoint"] } 26 | stable-swap = { path = "../stable-swap-program/program", features = [ 27 | "fuzz", 28 | "program", 29 | "no-entrypoint" 30 | ], version = "^1" } 31 | 32 | [[bin]] 33 | name = "fuzz_test" 34 | path = "targets/full.rs" 35 | test = false 36 | doc = false 37 | 38 | [[bin]] 39 | name = "swaps_only_test" 40 | path = "targets/swaps_only.rs" 41 | test = false 42 | doc = false 43 | -------------------------------------------------------------------------------- /.github/workflows/libraries.yml: -------------------------------------------------------------------------------- 1 | name: Libraries 2 | 3 | on: 4 | push: 5 | branches: [master, exchange_rate_override] 6 | pull_request: 7 | branches: [master, exchange_rate_override] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_TOOLCHAIN: "1.59.0" 12 | 13 | jobs: 14 | doc: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Install Rust nightly 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | override: true 22 | profile: minimal 23 | toolchain: ${{ env.RUST_TOOLCHAIN }} 24 | components: rustfmt, clippy 25 | - name: Cache dependencies 26 | uses: Swatinem/rust-cache@v2 27 | - run: cargo doc 28 | 29 | test: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v3 33 | - name: Install Rust nightly 34 | uses: actions-rs/toolchain@v1 35 | with: 36 | override: true 37 | profile: minimal 38 | toolchain: ${{ env.RUST_TOOLCHAIN }} 39 | components: rustfmt, clippy 40 | - name: Cache dependencies 41 | uses: Swatinem/rust-cache@v2 42 | - run: cargo test --workspace --exclude stable-swap 43 | -------------------------------------------------------------------------------- /stable-swap-program/do.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | cd "$(dirname "$0")" 5 | 6 | export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH" 7 | 8 | usage() { 9 | cat < 11 | Supported actions: 12 | e2e-test 13 | test 14 | EOF 15 | } 16 | 17 | perform_action() { 18 | case "$1" in 19 | e2e-test) 20 | ( 21 | rm -rf scripts/tmp 22 | anchor build --program-name stable_swap 23 | solana-test-validator --quiet --bpf-program SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ ../target/deploy/stable_swap.so & 24 | yarn --cwd sdk install 25 | yarn --cwd sdk test-int ${@:2} 26 | ) 27 | ;; 28 | help) 29 | usage 30 | exit 31 | ;; 32 | test) 33 | cargo test-bpf --manifest-path program/Cargo.toml -- --test-threads 1 ${@:2} 34 | ;; 35 | esac 36 | } 37 | 38 | if [[ $1 == "update" ]]; then 39 | perform_action "$1" 40 | exit 41 | else 42 | if [[ "$#" -lt 1 ]]; then 43 | usage 44 | exit 45 | fi 46 | if ! hash solana 2>/dev/null; then 47 | solana-install update 48 | fi 49 | fi 50 | 51 | perform_action "$1" "${@:2}" 52 | -------------------------------------------------------------------------------- /stable-swap-program/scripts/deploy-mainnet-pools.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0) 4 | source _common.sh 5 | setup_solana_cluster mainnet-beta 6 | 7 | echo "Using admin $SWAP_ADMIN_ACCOUNT" 8 | 9 | # Check to see if all mints exist 10 | USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v 11 | USDT_MINT=Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB 12 | WUSDC_MINT=FVsXUnbhifqJ4LiXQEbpUtXVdB8T5ADLKqSs5t1oc54F 13 | WUSDT_MINT=9w97GdWUYYaamGwdKMKZgGzPduZJkiFizq4rz5CPXRv2 14 | 15 | MINTS=( 16 | $USDC_MINT 17 | $USDT_MINT 18 | $WUSDC_MINT 19 | $WUSDT_MINT 20 | ) 21 | 22 | # Ensure we have enough of each token 23 | for MINT in $MINTS; do 24 | spl-token account-info 25 | done 26 | 27 | # Deploy the pools 28 | deploy_pool() { 29 | echo "Deploying $1" 30 | ./stableswap deploy-pool \ 31 | --cluster mainnet-beta \ 32 | --admin_account $SWAP_ADMIN_ACCOUNT \ 33 | --program_id $SWAP_PROGRAM_ID \ 34 | "${@:2}" 35 | } 36 | 37 | deploy_pool "2pool (USDC-USDT)" \ 38 | --token_a_mint $USDC_MINT \ 39 | --token_b_mint $USDT_MINT 40 | 41 | deploy_pool "WUSDC" \ 42 | --token_a_mint $USDC_MINT \ 43 | --token_b_mint $WUSDC_MINT 44 | 45 | deploy_pool "WUSDT" \ 46 | --token_a_mint $USDT_MINT \ 47 | --token_b_mint $WUSDT_MINT 48 | -------------------------------------------------------------------------------- /.github/workflows/prebuild.yml: -------------------------------------------------------------------------------- 1 | name: Pre-build Checks 2 | 3 | on: 4 | push: 5 | branches: [master, exchange_rate_override] 6 | pull_request: 7 | branches: [master, exchange_rate_override] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_TOOLCHAIN: "1.59.0" 12 | 13 | jobs: 14 | lint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Install Rust nightly 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | override: true 22 | profile: minimal 23 | toolchain: ${{ env.RUST_TOOLCHAIN }} 24 | components: rustfmt, clippy 25 | - name: Cache dependencies 26 | uses: Swatinem/rust-cache@v2 27 | 28 | - name: Run cargo fmt 29 | run: cargo fmt -- --check 30 | - name: Run clippy 31 | uses: actions-rs/clippy-check@v1 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | args: --all-features --all-targets -- --deny=warnings 35 | 36 | check: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: Install Rust nightly 41 | uses: actions-rs/toolchain@v1 42 | with: 43 | override: true 44 | profile: minimal 45 | toolchain: ${{ env.RUST_TOOLCHAIN }} 46 | components: rustfmt, clippy 47 | - name: Cache dependencies 48 | uses: Swatinem/rust-cache@v2 49 | - run: cargo check 50 | -------------------------------------------------------------------------------- /stable-swap-program/program/proptest-regressions/curve.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc 84a0d20bd770308daaabcf4df5ed7b5318519e72fce71e9219d0516c2d5c1b73 # shrinks to current_ts = 0, start_ramp_ts = 1, stop_ramp_ts = 1, amp_factor = 1, amount_a = 0, amount_b = 0 8 | cc 4117faecd8dc4e889e6f3da4416e9793f44ec34d40a7931636d3d55da3688df8 # shrinks to current_ts = 0, amp_factor = 1, pool_token_supply = 0, pool_token_amount = 0, swap_base_amount = 0, swap_quote_amount = 0 9 | cc d9702dda1a9b45ed692afb6cdc11ace45f7e13da6705cae3464fd0b014808111 # shrinks to current_ts = 0, amp_factor = 1, (pool_token_supply, pool_token_amount) = (27, 12), base_token_amount = 5755454262278, quote_token_amount = 9224025265226 10 | cc 9539f4847e5369f3f0a9f02edb56bd34c5c1564eeb8591cbd4f12ba29a951cb3 # shrinks to current_ts = 0, amp_factor = 1, deposit_amount_a = 2619121132546205556, deposit_amount_b = 1575504837621298384, swap_token_a_amount = 318647138643886077, swap_token_b_amount = 151432214323253812, pool_token_supply = 1828457411458969820 11 | cc 4dd1a7fc7b389d16b1f1d64b62a0f0a76aa45d3a6223b2dcc25edfb85ea37218 # shrinks to current_ts = 0, amp_factor = 1, pool_token_amount = 21192873611586, swap_base_amount = 859727996002481980, swap_quote_amount = 85090802858096108 12 | -------------------------------------------------------------------------------- /.github/workflows/fuzzing.yml: -------------------------------------------------------------------------------- 1 | name: The Daily Fuzz 2 | 3 | on: 4 | schedule: 5 | - cron: "0 10 * * *" 6 | push: 7 | branches: [master] 8 | paths: 9 | - ".github/workflows/fuzzing.yml" 10 | 11 | jobs: 12 | nightly: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Install Rust nightly 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | override: true 21 | profile: minimal 22 | toolchain: nightly-2021-12-25 23 | - name: Cache dependencies 24 | uses: Swatinem/rust-cache@v2 25 | 26 | # Install Cachix 27 | - uses: cachix/install-nix-action@v22 28 | with: 29 | install_url: https://nixos-nix-install-tests.cachix.org/serve/i6laym9jw3wg9mw6ncyrk6gjx4l34vvx/install 30 | install_options: "--tarball-url-prefix https://nixos-nix-install-tests.cachix.org/serve" 31 | extra_nix_config: | 32 | experimental-features = nix-command flakes 33 | - name: Setup Cachix 34 | uses: cachix/cachix-action@v12 35 | with: 36 | name: saber 37 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 38 | 39 | - name: Build 40 | run: nix shell .#ci --command cargo fuzz build --dev fuzz_test 41 | env: 42 | RUSTFLAGS: "-Znew-llvm-pass-manager=no" 43 | - name: Run fuzz test 44 | run: nix shell .#ci --command cargo fuzz run --dev fuzz_test -- -max_total_time=10800 45 | env: 46 | RUSTFLAGS: "-Znew-llvm-pass-manager=no" 47 | -------------------------------------------------------------------------------- /stable-swap-anchor/src/state.rs: -------------------------------------------------------------------------------- 1 | //! State structs for swaps. 2 | 3 | use anchor_lang::prelude::*; 4 | use anchor_lang::solana_program::program_pack::Pack; 5 | use std::ops::Deref; 6 | 7 | /// StableSwap account wrapper for Anchor programs. 8 | /// 9 | /// *For more info, see [stable_swap_client::state::SwapInfo].* 10 | #[derive(Clone, Copy, Debug, PartialEq)] 11 | pub struct SwapInfo(stable_swap_client::state::SwapInfo); 12 | 13 | impl SwapInfo { 14 | /// The length, in bytes, of the packed representation 15 | pub const LEN: usize = stable_swap_client::state::SwapInfo::LEN; 16 | 17 | /// Computes the minimum rent exempt balance of a [SwapInfo]. 18 | pub fn minimum_rent_exempt_balance() -> Result { 19 | Ok(Rent::get()?.minimum_balance(Self::LEN)) 20 | } 21 | } 22 | 23 | impl Owner for SwapInfo { 24 | fn owner() -> Pubkey { 25 | crate::ID 26 | } 27 | } 28 | 29 | impl Deref for SwapInfo { 30 | type Target = stable_swap_client::state::SwapInfo; 31 | 32 | fn deref(&self) -> &Self::Target { 33 | &self.0 34 | } 35 | } 36 | 37 | impl anchor_lang::AccountSerialize for SwapInfo { 38 | fn try_serialize(&self, _writer: &mut W) -> Result<()> { 39 | // no-op 40 | Ok(()) 41 | } 42 | } 43 | 44 | impl anchor_lang::AccountDeserialize for SwapInfo { 45 | fn try_deserialize(buf: &mut &[u8]) -> Result { 46 | SwapInfo::try_deserialize_unchecked(buf) 47 | } 48 | 49 | fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result { 50 | Ok(stable_swap_client::state::SwapInfo::unpack(buf).map(SwapInfo)?) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /fuzz/src/native_account_data.rs: -------------------------------------------------------------------------------- 1 | use solana_program::{account_info::AccountInfo, clock::Epoch, pubkey::Pubkey}; 2 | 3 | #[derive(Clone)] 4 | pub struct NativeAccountData { 5 | pub key: Pubkey, 6 | pub lamports: u64, 7 | pub data: Vec, 8 | pub program_id: Pubkey, 9 | pub is_signer: bool, 10 | } 11 | 12 | impl NativeAccountData { 13 | pub fn new(size: usize, program_id: Pubkey) -> Self { 14 | Self { 15 | key: Pubkey::new_unique(), 16 | lamports: 0, 17 | data: vec![0; size], 18 | program_id, 19 | is_signer: false, 20 | } 21 | } 22 | 23 | pub fn new_signer(size: usize, program_id: Pubkey) -> Self { 24 | Self { 25 | key: Pubkey::new_unique(), 26 | lamports: 0, 27 | data: vec![0; size], 28 | program_id, 29 | is_signer: true, 30 | } 31 | } 32 | 33 | pub fn new_from_account_info(account_info: &AccountInfo) -> Self { 34 | Self { 35 | key: *account_info.key, 36 | lamports: **account_info.lamports.borrow(), 37 | data: account_info.data.borrow().to_vec(), 38 | program_id: *account_info.owner, 39 | is_signer: account_info.is_signer, 40 | } 41 | } 42 | 43 | pub fn as_account_info(&mut self) -> AccountInfo { 44 | AccountInfo::new( 45 | &self.key, 46 | self.is_signer, 47 | false, 48 | &mut self.lamports, 49 | &mut self.data[..], 50 | &self.program_id, 51 | false, 52 | Epoch::default(), 53 | ) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: {} 5 | push: 6 | tags: 7 | - "v*.*.*" 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_TOOLCHAIN: "1.59.0" 12 | 13 | jobs: 14 | release-crate: 15 | runs-on: ubuntu-latest 16 | name: Release crate on crates.io 17 | steps: 18 | - uses: actions/checkout@v3 19 | - uses: cachix/install-nix-action@v22 20 | - name: Setup Cachix 21 | uses: cachix/cachix-action@v12 22 | with: 23 | name: saber 24 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 25 | - name: Install Rust nightly 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | override: true 29 | profile: minimal 30 | toolchain: ${{ env.RUST_TOOLCHAIN }} 31 | - uses: Swatinem/rust-cache@v2 32 | - name: Publish crates 33 | run: nix shell .#ci --command cargo ws publish --from-git --yes --skip-published --token ${{ secrets.CARGO_PUBLISH_TOKEN }} 34 | 35 | release-binaries: 36 | runs-on: ubuntu-latest 37 | name: Release verifiable binaries 38 | steps: 39 | - uses: actions/checkout@v3 40 | - uses: cachix/install-nix-action@v22 41 | - name: Setup Cachix 42 | uses: cachix/cachix-action@v12 43 | with: 44 | name: saber 45 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 46 | 47 | - name: Build programs 48 | run: nix shell .#ci --command anchor build --verifiable --program-name stable_swap 49 | - name: Release 50 | uses: softprops/action-gh-release@v1 51 | with: 52 | files: | 53 | target/deploy/* 54 | target/idl/* 55 | target/verifiable/* 56 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Saber development environment."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | saber-overlay.url = "github:saber-hq/saber-overlay"; 7 | flake-utils.url = "github:numtide/flake-utils"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, saber-overlay, flake-utils }: 11 | flake-utils.lib.eachSystem [ 12 | "aarch64-darwin" 13 | "x86_64-linux" 14 | "x86_64-darwin" 15 | ] 16 | (system: 17 | let 18 | pkgs = import nixpkgs { inherit system; } 19 | // saber-overlay.packages.${system}; 20 | ci = pkgs.buildEnv { 21 | name = "ci"; 22 | paths = with pkgs; 23 | (pkgs.lib.optionals pkgs.stdenv.isLinux ([ udev ])) ++ [ 24 | anchor-0_24_2 25 | cargo-workspaces 26 | cargo-fuzz 27 | 28 | # sdk 29 | nodejs 30 | yarn 31 | python3 32 | 33 | pkgconfig 34 | openssl 35 | jq 36 | gnused 37 | 38 | solana-basic 39 | 40 | libiconv 41 | ] ++ (pkgs.lib.optionals pkgs.stdenv.isDarwin 42 | (with pkgs.darwin.apple_sdk.frameworks; [ 43 | AppKit 44 | IOKit 45 | Foundation 46 | ])); 47 | }; 48 | in 49 | { 50 | packages.ci = ci; 51 | devShell = pkgs.stdenvNoCC.mkDerivation { 52 | name = "devshell"; 53 | buildInputs = with pkgs; [ 54 | ci 55 | rustup 56 | cargo-outdated 57 | cargo-deps 58 | gh 59 | spl-token-cli 60 | ]; 61 | }; 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saber-hq/stableswap-tests", 3 | "private": true, 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/saber-hq/stableswap.git" 7 | }, 8 | "version": "1.12.3", 9 | "main": "dist/index.js", 10 | "bin": "bin/stableswap", 11 | "license": "UNLICENSED", 12 | "scripts": { 13 | "clean": "rm -rf dist", 14 | "lint": "eslint . --cache", 15 | "lint:ci": "eslint . --max-warnings=0", 16 | "lint:fix": "eslint . --fix --cache", 17 | "test": "jest", 18 | "prettier": "prettier -w src test", 19 | "test-int": "jest int.test.ts --force-exit --testTimeout=10000", 20 | "prepublishOnly": "npm run clean && npm run build" 21 | }, 22 | "engines": { 23 | "node": ">=12.x" 24 | }, 25 | "devDependencies": { 26 | "@babel/preset-env": "^7.16.11", 27 | "@babel/preset-typescript": "^7.16.7", 28 | "@saberhq/eslint-config": "^1.12.36", 29 | "@saberhq/solana-contrib": "^1.12.36", 30 | "@types/bs58": "^4.0.1", 31 | "@types/jest": "^27.4.0", 32 | "@types/node": "^17.0.15", 33 | "b58": "^4.0.3", 34 | "eslint": "^8.8.0", 35 | "jest": "^27.5.0", 36 | "lint-staged": ">=12.3.3", 37 | "prettier": "^2.5.1", 38 | "ts-node": "^10.4.0", 39 | "typescript": "^4.5.5" 40 | }, 41 | "dependencies": { 42 | "@saberhq/stableswap-sdk": "^1.12.36", 43 | "@saberhq/token-utils": "^1.12.36", 44 | "@solana/web3.js": "^1.33.0", 45 | "@types/bn.js": "^5.1.0", 46 | "@types/yargs": "^17.0.8", 47 | "bn.js": "^5.2.0", 48 | "jsbi": "^4.1.0", 49 | "yargs": "^17.3.1" 50 | }, 51 | "lint-staged": { 52 | "*.{js,css,ts,tsx}": "prettier --write", 53 | "../**/*.{md,yml,yaml}": "prettier --write" 54 | }, 55 | "resolutions": { 56 | "@solana/buffer-layout": "^4" 57 | }, 58 | "files": [ 59 | "bin/", 60 | "dist/", 61 | "src/" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /stable-swap-program/program/src/processor/logging.rs: -------------------------------------------------------------------------------- 1 | //! Logging related helpers. 2 | 3 | use solana_program::log::sol_log_64; 4 | use solana_program::msg; 5 | use solana_program::pubkey::Pubkey; 6 | 7 | /// Event enum 8 | #[derive(Debug)] 9 | pub enum Event { 10 | /// Burn event 11 | Burn, 12 | /// Deposit event 13 | Deposit, 14 | /// Swap event A -> B 15 | SwapAToB, 16 | /// Swap event B -> A 17 | SwapBToA, 18 | /// Withdraw event (A) 19 | WithdrawA, 20 | /// Withdraw event (B) 21 | WithdrawB, 22 | } 23 | 24 | /// Log event 25 | pub fn log_event( 26 | event: Event, 27 | token_a_amount: u64, 28 | token_b_amount: u64, 29 | pool_token_amount: u64, 30 | fee: u64, 31 | ) { 32 | msg!(match event { 33 | Event::Burn => "Event: Burn", 34 | Event::Deposit => "Event: Deposit", 35 | Event::SwapAToB => "Event: SwapAToB", 36 | Event::SwapBToA => "Event: SwapBToA", 37 | Event::WithdrawA => "Event: WithdrawA", 38 | Event::WithdrawB => "Event: WithdrawB", 39 | }); 40 | 41 | solana_program::log::sol_log_64( 42 | event as u64, 43 | token_a_amount, 44 | token_b_amount, 45 | pool_token_amount, 46 | fee, 47 | ); 48 | } 49 | 50 | pub fn log_keys_mismatch(msg: &str, left: Pubkey, right: Pubkey) { 51 | msg!(msg); 52 | msg!("Left:"); 53 | left.log(); 54 | msg!("Right:"); 55 | right.log(); 56 | } 57 | 58 | pub fn log_keys_mismatch_optional(msg: &str, left: Option, right: Option) { 59 | msg!(msg); 60 | msg!("Left:"); 61 | if let Some(left_inner) = left { 62 | left_inner.log(); 63 | } else { 64 | msg!("left: missing"); 65 | } 66 | msg!("Right:"); 67 | if let Some(right_inner) = right { 68 | right_inner.log(); 69 | } else { 70 | msg!("right: missing"); 71 | } 72 | } 73 | 74 | /// Log slippage error 75 | pub fn log_slippage_error(minimum_amount: u64, computed_amount: u64) { 76 | sol_log_64(0, 0, 0, minimum_amount, computed_amount); 77 | } 78 | -------------------------------------------------------------------------------- /stable-swap-program/scripts/deploy-program.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | # Program deployer script. 5 | 6 | cd $(dirname $0) 7 | source _common.sh 8 | 9 | CLUSTER=$1 10 | 11 | # reset on localnet 12 | if [[ $CLUSTER == "localnet" ]]; then 13 | echo "On localnet, clearing old program deploy." 14 | rm -fr $HOME/stableswap_deployments/localnet/ 15 | fi 16 | 17 | setup_solana_cluster $CLUSTER 18 | 19 | if [ ! -f "$SWAP_PROGRAM_ID_PATH" ]; then 20 | echo "Swap program ID path $SWAP_PROGRAM_ID_PATH does not exist. Cannot deploy." 21 | exit 1 22 | fi 23 | 24 | if solana program show $SWAP_PROGRAM_ID; then 25 | echo "Program already deployed at $SWAP_PROGRAM_ID. Did you mean to upgrade?" 26 | exit 1 27 | fi 28 | 29 | SOL_BALANCE=$(solana balance $DEPLOYER_KP --lamports | awk '{ print $1 }') 30 | 31 | # We need about 8 SOL for a successful deploy 32 | MIN_SOL_BALANCE_LAMPORTS=8000000000 33 | if [[ "$SOL_BALANCE" -lt "$MIN_SOL_BALANCE_LAMPORTS" ]]; then 34 | if [[ $CLUSTER == "localnet" ]]; then 35 | echo "On localnet. Requesting SOL airdrop." 36 | solana airdrop 10 $DEPLOYER_ADDRESS 37 | elif [[ $CLUSTER != "mainnet-beta" ]]; then 38 | echo "Not on mainnet. Requesting SOL airdrop." 39 | for i in {1..8}; do 40 | solana airdrop 1 $DEPLOYER_ADDRESS 41 | done 42 | else 43 | echo "Not enough SOL in the deployer. Need 8 SOL to deploy." 44 | echo "Deployer address:" 45 | solana address 46 | exit 1 47 | fi 48 | fi 49 | 50 | echo "Deploying program to $SWAP_PROGRAM_ID..." 51 | solana program deploy \ 52 | --program-id $SWAP_PROGRAM_ID_PATH \ 53 | ../target/deploy/stable_swap.so 54 | 55 | echo "StableSwap deployed to ProgramID: $SWAP_PROGRAM_ID" 56 | 57 | echo "Changing upgrade authority to $UPGRADE_AUTHORITY" 58 | solana program set-upgrade-authority $SWAP_PROGRAM_ID --new-upgrade-authority $UPGRADE_AUTHORITY 59 | 60 | echo "Program deploy complete. Saving deployment result." 61 | OUTFILE=$CLUSTER_DEPLOYMENT_DIR/program.json 62 | solana program show $SWAP_PROGRAM_ID --output json > $OUTFILE 63 | cat $OUTFILE | jq . 64 | -------------------------------------------------------------------------------- /fuzz/src/native_token.rs: -------------------------------------------------------------------------------- 1 | use crate::native_account_data::NativeAccountData; 2 | 3 | use spl_token::state::{Account as TokenAccount, AccountState as TokenAccountState, Mint}; 4 | 5 | use solana_program::{program_option::COption, program_pack::Pack, pubkey::Pubkey}; 6 | 7 | pub fn create_mint(owner: &Pubkey) -> NativeAccountData { 8 | let mut account_data = NativeAccountData::new(Mint::LEN, spl_token::id()); 9 | let mint = Mint { 10 | is_initialized: true, 11 | mint_authority: COption::Some(*owner), 12 | ..Default::default() 13 | }; 14 | Mint::pack(mint, &mut account_data.data[..]).unwrap(); 15 | account_data 16 | } 17 | 18 | pub fn create_token_account( 19 | mint_account: &mut NativeAccountData, 20 | owner: &Pubkey, 21 | amount: u64, 22 | ) -> NativeAccountData { 23 | let mut mint = Mint::unpack(&mint_account.data).unwrap(); 24 | let mut account_data = NativeAccountData::new(TokenAccount::LEN, spl_token::id()); 25 | let account = TokenAccount { 26 | state: TokenAccountState::Initialized, 27 | mint: mint_account.key, 28 | owner: *owner, 29 | amount, 30 | ..Default::default() 31 | }; 32 | mint.supply += amount; 33 | Mint::pack(mint, &mut mint_account.data[..]).unwrap(); 34 | TokenAccount::pack(account, &mut account_data.data[..]).unwrap(); 35 | account_data 36 | } 37 | 38 | pub fn get_mint_supply(account_data: &NativeAccountData) -> u64 { 39 | let mint = Mint::unpack_from_slice(&account_data.data).unwrap(); 40 | mint.supply 41 | } 42 | 43 | pub fn get_token_balance(account_data: &NativeAccountData) -> u64 { 44 | let account = TokenAccount::unpack(&account_data.data).unwrap(); 45 | account.amount 46 | } 47 | 48 | pub fn transfer( 49 | from_account: &mut NativeAccountData, 50 | to_account: &mut NativeAccountData, 51 | amount: u64, 52 | ) { 53 | let mut from = TokenAccount::unpack(&from_account.data).unwrap(); 54 | let mut to = TokenAccount::unpack(&to_account.data).unwrap(); 55 | assert_eq!(from.mint, to.mint); 56 | from.amount -= amount; 57 | to.amount += amount; 58 | TokenAccount::pack(from, &mut from_account.data[..]).unwrap(); 59 | TokenAccount::pack(to, &mut to_account.data[..]).unwrap(); 60 | } 61 | -------------------------------------------------------------------------------- /stable-swap-program/README.md: -------------------------------------------------------------------------------- 1 | # StableSwap Program 2 | 3 | An adaptation of the Solana [token-swap](https://github.com/solana-labs/solana-program-library/tree/master/token-swap/program) program implementing Curve's [StableSwap](https://www.curve.fi/stableswap-paper.pdf) invariant. 4 | 5 | Click [here](https://stableswap.pro) to try it out live on the Solana testnet! 6 | 7 | ## Development 8 | 9 | _We recommend using the included Nix flake to develop within this repo._ 10 | 11 | Download or update the Solana SDK by running: 12 | 13 | ```bash 14 | solana-install init 1.11.10 15 | ``` 16 | 17 | To build the program, run: 18 | 19 | ```bash 20 | anchor build --program-name stable_swap 21 | ``` 22 | 23 | ### Testing 24 | 25 | Run the unit tests contained within the project via: 26 | 27 | ```bash 28 | ./do.sh test 29 | ``` 30 | 31 | Running end-to-end tests: 32 | 33 | ``` 34 | ./do.sh e2e-test 35 | ``` 36 | 37 | [View instructions for running fuzz tests here.](../fuzz) 38 | 39 | ### Clippy 40 | 41 | Run the [Clippy linter](https://github.com/rust-lang/rust-clippy) via: 42 | 43 | ```bash 44 | cargo clippy 45 | ``` 46 | 47 | ## Deployment 48 | 49 | To deploy, run: 50 | 51 | ```bash 52 | # On Vagrant/build environment only 53 | anchor build --program-name stable_swap 54 | 55 | # On your machine 56 | ./scripts/deploy-program.sh 57 | ./scripts/deploy-test-pools.sh 58 | 59 | # If mainnet 60 | ./scripts/deploy-mainnet-pools.sh 61 | ``` 62 | 63 | ### Upgrades 64 | 65 | To upgrade the program, run: 66 | 67 | ``` 68 | # Write the buffer. This returns the buffer address. 69 | solana program write-buffer ./target/deploy/stable_swap.so 70 | 71 | # Change the buffer authority to the same address as the upgrade authority. (Ledger) 72 | solana program set-buffer-authority $BUFFER_ADDR --new-buffer-authority $UPGRADE_AUTHORITY 73 | 74 | # Swap out the program for the new buffer. 75 | solana program deploy --buffer $BUFFER_ADDR --program-id $PROGRAM_ID --keypair $UPGRADE_AUTHORITY_KEYPAIR 76 | ``` 77 | 78 | ## TODO 79 | 80 | - [ ] Generalize swap pool to support `n` tokens 81 | - [ ] Implement [`remove_liquidity_imbalance`](https://github.com/curvefi/curve-contract/blob/4aa3832a4871b1c5b74af7f130c5b32bdf703af5/contracts/pool-templates/base/SwapTemplateBase.vy#L539) 82 | 83 | ## Documentation 84 | 85 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 86 | 87 | ## License 88 | 89 | Saber StableSwap is licensed under the Apache License, Version 2.0. 90 | -------------------------------------------------------------------------------- /stable-swap-program/program/src/processor/mod.rs: -------------------------------------------------------------------------------- 1 | //! Program state processor 2 | 3 | #[macro_use] 4 | mod macros; 5 | 6 | mod admin; 7 | mod checks; 8 | mod logging; 9 | mod swap; 10 | mod token; 11 | mod utils; 12 | 13 | #[cfg(test)] 14 | #[allow(clippy::unwrap_used)] 15 | mod test_utils; 16 | 17 | use crate::instruction::AdminInstruction; 18 | 19 | use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey}; 20 | 21 | /// Program state handler. (and general curve params) 22 | pub struct Processor {} 23 | 24 | impl Processor { 25 | /// Processes an [Instruction](enum.Instruction.html). 26 | pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { 27 | let instruction = AdminInstruction::unpack(input)?; 28 | match instruction { 29 | None => swap::process_swap_instruction(program_id, accounts, input), 30 | Some(admin_instruction) => { 31 | admin::process_admin_instruction(&admin_instruction, accounts) 32 | } 33 | } 34 | } 35 | } 36 | 37 | #[cfg(test)] 38 | #[allow(clippy::unwrap_used)] 39 | mod tests { 40 | use super::*; 41 | use crate::processor::test_utils::*; 42 | use solana_program::program_error::ProgramError; 43 | use solana_sdk::account::Account; 44 | use spl_token::instruction::mint_to; 45 | 46 | #[test] 47 | fn test_token_program_id_error() { 48 | let swap_key = pubkey_rand(); 49 | let mut mint = (pubkey_rand(), Account::default()); 50 | let mut destination = (pubkey_rand(), Account::default()); 51 | let token_program = (spl_token::id(), Account::default()); 52 | let (authority_key, nonce) = 53 | Pubkey::find_program_address(&[&swap_key.to_bytes()[..]], &SWAP_PROGRAM_ID); 54 | let mut authority = (authority_key, Account::default()); 55 | let swap_bytes = swap_key.to_bytes(); 56 | let authority_signature_seeds = [&swap_bytes[..32], &[nonce]]; 57 | let signers = &[&authority_signature_seeds[..]]; 58 | let ix = mint_to( 59 | &token_program.0, 60 | &mint.0, 61 | &destination.0, 62 | &authority.0, 63 | &[], 64 | 10, 65 | ) 66 | .unwrap(); 67 | let mint = (&mut mint).into(); 68 | let destination = (&mut destination).into(); 69 | let authority = (&mut authority).into(); 70 | 71 | let err = 72 | solana_program::program::invoke_signed(&ix, &[mint, destination, authority], signers) 73 | .unwrap_err(); 74 | assert_eq!(err, ProgramError::InvalidAccountData); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | .env.test 82 | .env*.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | 91 | # Nuxt.js build / generate output 92 | .nuxt 93 | dist 94 | 95 | # Storybook build outputs 96 | .out 97 | .storybook-out 98 | storybook-static 99 | 100 | # rollup.js default build output 101 | dist/ 102 | 103 | # Gatsby files 104 | .cache/ 105 | # Comment in the public line in if your project uses Gatsby and not Next.js 106 | # https://nextjs.org/blog/next-9-1#public-directory-support 107 | # public 108 | 109 | # vuepress build output 110 | .vuepress/dist 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # Temporary folders 128 | tmp/ 129 | temp/ 130 | 131 | # End of https://www.toptal.com/developers/gitignore/api/node 132 | 133 | lib/ 134 | -------------------------------------------------------------------------------- /stable-swap-program/scripts/_common.sh: -------------------------------------------------------------------------------- 1 | setup_solana_cluster() { 2 | if [ -f "../.env" ]; then 3 | source ../.env 4 | elif [[ $CLUSTER == "localnet" ]]; then 5 | echo "Warning: .env does not exist. A new keypair for the upgrade authority will be generated." 6 | elif [ -z "$UPGRADE_AUTHORITY_KEYPAIR" ]; then 7 | echo "UPGRADE_AUTHORITY_KEYPAIR required." 8 | exit 1 9 | fi 10 | 11 | export CLUSTER=$1 12 | if [ -z "$CLUSTER" ]; then 13 | echo "No cluster specified." 14 | exit 1 15 | fi 16 | 17 | # Set up the network config 18 | SOLANA_ENDPOINT=$CLUSTER 19 | if [[ $CLUSTER == "localnet" ]]; then 20 | SOLANA_ENDPOINT="localhost" 21 | fi 22 | solana config set --url $SOLANA_ENDPOINT || { 23 | echo "Invalid cluster $CLUSTER" 24 | exit 1 25 | } 26 | solana config set --commitment confirmed 27 | 28 | export ALL_DEPLOYMENTS_DIR=$HOME/stableswap_deployments 29 | export CLUSTER_DEPLOYMENT_DIR=$ALL_DEPLOYMENTS_DIR/$CLUSTER 30 | mkdir -p $CLUSTER_DEPLOYMENT_DIR 31 | mkdir -p $ALL_DEPLOYMENTS_DIR/program_ids/ 32 | 33 | # Get the program ID 34 | CLUSTER_PROGRAM_INFO=$CLUSTER_DEPLOYMENT_DIR/program.json 35 | if [ -f "$CLUSTER_PROGRAM_INFO" ]; then 36 | export SWAP_PROGRAM_ID=$(cat $CLUSTER_PROGRAM_INFO | jq -r .programId) 37 | else 38 | if [[ $CLUSTER == "localnet" ]]; then 39 | mkdir -p tmp 40 | TMP_SWAP_PROGRAM_ID_PATH=tmp/localnet-programid-$RANDOM.json 41 | solana-keygen new --no-passphrase --outfile $TMP_SWAP_PROGRAM_ID_PATH 42 | export SWAP_PROGRAM_ID=$(solana address --keypair $TMP_SWAP_PROGRAM_ID_PATH) 43 | mv $TMP_SWAP_PROGRAM_ID_PATH $ALL_DEPLOYMENTS_DIR/program_ids/$SWAP_PROGRAM_ID.json 44 | echo "On localnet and keyfile not found; generated new program id $SWAP_PROGRAM_ID." 45 | else 46 | echo "Previous program deployment not found. Reading from deployment_config.json." 47 | export SWAP_PROGRAM_ID=$(cat ./deployment_config.json | jq -r .program_id) 48 | fi 49 | fi 50 | export SWAP_PROGRAM_ID_PATH=$ALL_DEPLOYMENTS_DIR/program_ids/$SWAP_PROGRAM_ID.json 51 | 52 | # Set up the deployer key 53 | export DEPLOYER_KP=$CLUSTER_DEPLOYMENT_DIR/deployer.keypair.json 54 | if [ ! -f "$DEPLOYER_KP" ]; then 55 | echo "Generating a keypair for this deploy..." 56 | solana-keygen new --no-passphrase --outfile $DEPLOYER_KP 57 | fi 58 | export DEPLOYER_ADDRESS=$(solana address --keypair $DEPLOYER_KP) 59 | echo "Deployer address: $DEPLOYER_ADDRESS" 60 | 61 | solana config set --keypair $DEPLOYER_KP 62 | 63 | export SWAP_ADMIN_ACCOUNT=$(cat ./deployment_config.json | jq -r .admin_account) 64 | export UPGRADE_AUTHORITY=$(cat ./deployment_config.json | jq -r .upgrade_authority) 65 | 66 | if [ ! -f "$DEPLOYER_KP" ]; then 67 | echo "Deployer keypair $DEPLOYER_KP does not exist. Exiting." 68 | exit 1 69 | fi 70 | } 71 | -------------------------------------------------------------------------------- /stable-swap-program/program/src/processor/macros.rs: -------------------------------------------------------------------------------- 1 | /// Checks if two pubkeys are equal, and if not, throws an error. 2 | macro_rules! check_keys_equal { 3 | ($left:expr, $right:expr, $msg:expr, $err:expr) => { 4 | check_keys_condition!($left, $right, $msg, $err, $left == $right); 5 | }; 6 | } 7 | 8 | /// Checks if two pubkeys are not equal, and if not, throws an error. 9 | macro_rules! check_keys_not_equal { 10 | ($left:expr, $right:expr, $msg:expr, $err:expr) => { 11 | check_keys_condition!($left, $right, $msg, $err, $left != $right); 12 | }; 13 | } 14 | 15 | macro_rules! check_keys_condition { 16 | ($left:expr, $right:expr, $msg:expr, $err:expr, $continueExpr:expr) => { 17 | if !$continueExpr { 18 | $crate::processor::logging::log_keys_mismatch( 19 | concat!($msg, " mismatch:"), 20 | $left, 21 | $right, 22 | ); 23 | return Err::<(), ProgramError>($err.into()); 24 | } 25 | }; 26 | } 27 | 28 | /// Checks if two pubkeys are equal and exist, and if not, throws an error. 29 | macro_rules! check_keys_equal_optional { 30 | ($left:expr, $right:expr, $msg:expr, $err:expr) => { 31 | check_keys_condition_optional!($left, $right, $msg, $err, $left == $right); 32 | }; 33 | } 34 | 35 | macro_rules! check_keys_condition_optional { 36 | ($left:expr, $right:expr, $msg:expr, $err:expr, $continueExpr:expr) => { 37 | match ($left.into(), $right.into()) { 38 | (Some(_), Some(_)) if $continueExpr => (), 39 | (left, right) => { 40 | $crate::processor::logging::log_keys_mismatch_optional( 41 | concat!($msg, " mismatch:"), 42 | left, 43 | right, 44 | ); 45 | return Err::<(), ProgramError>($err.into()); 46 | } 47 | } 48 | }; 49 | } 50 | 51 | /// Checks if two pubkeys are equal, and if not, throws an error. 52 | macro_rules! check_token_keys_equal { 53 | ($token: expr,$left:expr, $right:expr, $msg:expr, $err:expr) => { 54 | check_token_keys_condition!($token, $left, $right, $msg, $err, $left == $right); 55 | }; 56 | } 57 | 58 | /// Checks if two pubkeys are not equal, and if not, throws an error. 59 | macro_rules! check_token_keys_not_equal { 60 | ($token: expr,$left:expr, $right:expr, $msg:expr, $err:expr) => { 61 | check_token_keys_condition!($token, $left, $right, $msg, $err, $left != $right); 62 | }; 63 | } 64 | 65 | macro_rules! check_token_keys_condition { 66 | ($token:expr, $left:expr, $right:expr, $msg:expr, $err:expr, $continueExpr:expr) => { 67 | if ($token.index == 0) { 68 | check_keys_condition!($left, $right, concat!($msg, " A"), $err, $continueExpr); 69 | } else if ($token.index == 1) { 70 | check_keys_condition!($left, $right, concat!($msg, " B"), $err, $continueExpr); 71 | } else { 72 | check_keys_condition!($left, $right, concat!($msg), $err, $continueExpr); 73 | } 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /stable-swap-program/program/src/processor/token.rs: -------------------------------------------------------------------------------- 1 | //! Token helpers 2 | 3 | use solana_program::{ 4 | account_info::AccountInfo, program::invoke_signed, program_error::ProgramError, pubkey::Pubkey, 5 | }; 6 | 7 | /// Issue a spl_token `Burn` instruction. 8 | pub fn burn<'a>( 9 | token_program: AccountInfo<'a>, 10 | burn_account: AccountInfo<'a>, 11 | mint: AccountInfo<'a>, 12 | user_authority: AccountInfo<'a>, 13 | amount: u64, 14 | ) -> Result<(), ProgramError> { 15 | let ix = spl_token::instruction::burn( 16 | token_program.key, 17 | burn_account.key, 18 | mint.key, 19 | user_authority.key, 20 | &[], 21 | amount, 22 | )?; 23 | solana_program::program::invoke(&ix, &[token_program, burn_account, mint, user_authority]) 24 | } 25 | 26 | /// Issue a spl_token `MintTo` instruction. 27 | pub fn mint_to<'a>( 28 | swap: &Pubkey, 29 | token_program: AccountInfo<'a>, 30 | mint: AccountInfo<'a>, 31 | destination: AccountInfo<'a>, 32 | authority: AccountInfo<'a>, 33 | nonce: u8, 34 | amount: u64, 35 | ) -> Result<(), ProgramError> { 36 | let swap_bytes = swap.to_bytes(); 37 | let authority_signature_seeds = [&swap_bytes[..32], &[nonce]]; 38 | let signers = &[&authority_signature_seeds[..]]; 39 | let ix = spl_token::instruction::mint_to( 40 | token_program.key, 41 | mint.key, 42 | destination.key, 43 | authority.key, 44 | &[], 45 | amount, 46 | )?; 47 | 48 | invoke_signed(&ix, &[mint, destination, authority, token_program], signers) 49 | } 50 | 51 | /// Issue a spl_token `Transfer` instruction signed by the authority. 52 | pub fn transfer_as_swap<'a>( 53 | swap: &Pubkey, 54 | token_program: AccountInfo<'a>, 55 | source: AccountInfo<'a>, 56 | destination: AccountInfo<'a>, 57 | program_authority: AccountInfo<'a>, 58 | nonce: u8, 59 | amount: u64, 60 | ) -> Result<(), ProgramError> { 61 | let swap_bytes = swap.to_bytes(); 62 | let authority_signature_seeds = [&swap_bytes[..32], &[nonce]]; 63 | let signers = &[&authority_signature_seeds[..]]; 64 | let ix = spl_token::instruction::transfer( 65 | token_program.key, 66 | source.key, 67 | destination.key, 68 | program_authority.key, 69 | &[], 70 | amount, 71 | )?; 72 | 73 | invoke_signed( 74 | &ix, 75 | &[token_program, source, destination, program_authority], 76 | signers, 77 | ) 78 | } 79 | 80 | /// Issue a spl_token `Transfer` instruction as the user. 81 | pub fn transfer_as_user<'a>( 82 | token_program: AccountInfo<'a>, 83 | source: AccountInfo<'a>, 84 | destination: AccountInfo<'a>, 85 | user_authority: AccountInfo<'a>, 86 | amount: u64, 87 | ) -> Result<(), ProgramError> { 88 | let ix = spl_token::instruction::transfer( 89 | token_program.key, 90 | source.key, 91 | destination.key, 92 | user_authority.key, 93 | &[], 94 | amount, 95 | )?; 96 | solana_program::program::invoke(&ix, &[token_program, source, destination, user_authority]) 97 | } 98 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/test/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { Network } from "@saberhq/solana-contrib"; 2 | import { DEFAULT_NETWORK_CONFIG_MAP } from "@saberhq/solana-contrib"; 3 | import type { Fees } from "@saberhq/stableswap-sdk"; 4 | import { DEFAULT_FEE } from "@saberhq/stableswap-sdk"; 5 | import { Percent } from "@saberhq/token-utils"; 6 | import type { 7 | Cluster, 8 | Connection, 9 | PublicKey, 10 | Signer, 11 | Transaction, 12 | TransactionSignature, 13 | } from "@solana/web3.js"; 14 | import { 15 | Keypair, 16 | LAMPORTS_PER_SOL, 17 | sendAndConfirmTransaction as realSendAndConfirmTransaction, 18 | } from "@solana/web3.js"; 19 | 20 | // Initial amount in each swap token 21 | export const INITIAL_TOKEN_A_AMOUNT = LAMPORTS_PER_SOL; 22 | export const INITIAL_TOKEN_B_AMOUNT = LAMPORTS_PER_SOL; 23 | 24 | // Cluster configs 25 | export const CLUSTER_URL = "http://localhost:8899"; 26 | export const BOOTSTRAP_TIMEOUT = 300000; 27 | // Pool configs 28 | export const AMP_FACTOR = 100; 29 | export const FEES: Fees = { 30 | adminTrade: DEFAULT_FEE, 31 | adminWithdraw: DEFAULT_FEE, 32 | trade: new Percent(1, 4), 33 | withdraw: DEFAULT_FEE, 34 | }; 35 | 36 | export function sleep(ms: number): Promise { 37 | return new Promise((resolve) => setTimeout(resolve, ms)); 38 | } 39 | 40 | export const requestAirdrop = async ( 41 | connection: Connection, 42 | account: PublicKey, 43 | lamports: number = LAMPORTS_PER_SOL 44 | ): Promise => { 45 | const txSig = await connection.requestAirdrop(account, lamports); 46 | await connection.confirmTransaction(txSig); 47 | }; 48 | 49 | export async function newKeypairWithLamports( 50 | connection: Connection, 51 | lamports = LAMPORTS_PER_SOL 52 | ): Promise { 53 | const keypair = Keypair.generate(); 54 | await requestAirdrop(connection, keypair.publicKey, lamports); 55 | return keypair; 56 | } 57 | 58 | export const isCluster = (clusterRaw?: string): clusterRaw is Cluster => 59 | clusterRaw !== undefined && 60 | (Object.keys(DEFAULT_NETWORK_CONFIG_MAP) as readonly string[]).includes( 61 | clusterRaw 62 | ); 63 | 64 | export const CLUSTER_API_URLS: { [C in Network]: string } = { 65 | "mainnet-beta": "https://api.mainnet-beta.solana.com", 66 | devnet: "https://api.devnet.solana.com", 67 | testnet: "https://api.testnet.solana.com", 68 | localnet: "http://127.0.0.1:8899", 69 | }; 70 | 71 | export const sendAndConfirmTransactionWithTitle = async ( 72 | title: string, 73 | connection: Connection, 74 | transaction: Transaction, 75 | ...signers: Signer[] 76 | ): Promise => { 77 | console.info(`Sending ${title} transaction`); 78 | const txSig = await realSendAndConfirmTransaction( 79 | connection, 80 | transaction, 81 | signers, 82 | { 83 | skipPreflight: false, 84 | commitment: connection.commitment ?? "recent", 85 | preflightCommitment: connection.commitment ?? "recent", 86 | } 87 | ); 88 | console.info(`TxSig: ${txSig}`); 89 | const txReceipt = await connection.getConfirmedTransaction( 90 | txSig, 91 | "confirmed" 92 | ); 93 | console.log(`Result: ${txReceipt?.meta?.logMessages?.join("\n") ?? "--"}`); 94 | return txSig; 95 | }; 96 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/test/deployTestTokens.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from "@saberhq/solana-contrib"; 2 | import type { ISeedPoolAccountsFn } from "@saberhq/stableswap-sdk"; 3 | import { DEFAULT_TOKEN_DECIMALS } from "@saberhq/stableswap-sdk"; 4 | import { 5 | createInitMintInstructions, 6 | SPLToken, 7 | TOKEN_PROGRAM_ID, 8 | } from "@saberhq/token-utils"; 9 | import type { PublicKey, Signer, TransactionSignature } from "@solana/web3.js"; 10 | import { Account, Keypair } from "@solana/web3.js"; 11 | 12 | import { 13 | DEFAULT_INITIAL_TOKEN_A_AMOUNT, 14 | DEFAULT_INITIAL_TOKEN_B_AMOUNT, 15 | } from "../src/cli"; 16 | 17 | /** 18 | * Creates a new token mint 19 | * @returns 20 | */ 21 | const createMint = async ({ 22 | provider, 23 | mintAuthority, 24 | freezeAuthority, 25 | name, 26 | decimals, 27 | }: { 28 | name: string; 29 | provider: Provider; 30 | mintAuthority: PublicKey; 31 | freezeAuthority?: PublicKey; 32 | decimals: number; 33 | }): Promise<{ mint: PublicKey; txSig: TransactionSignature }> => { 34 | const mintSigner = Keypair.generate(); 35 | const tx = await createInitMintInstructions({ 36 | provider, 37 | mintKP: mintSigner, 38 | mintAuthority, 39 | freezeAuthority, 40 | decimals, 41 | }); 42 | console.log(`Create Mint ${name}`); 43 | const txSig = (await tx.confirm()).signature; 44 | return { mint: mintSigner.publicKey, txSig }; 45 | }; 46 | 47 | export const deployTestTokens = async ({ 48 | provider, 49 | minterSigner = Keypair.generate(), 50 | initialTokenAAmount = DEFAULT_INITIAL_TOKEN_A_AMOUNT, 51 | initialTokenBAmount = DEFAULT_INITIAL_TOKEN_B_AMOUNT, 52 | }: { 53 | provider: Provider; 54 | minterSigner?: Signer; 55 | initialTokenAAmount?: number; 56 | initialTokenBAmount?: number; 57 | }): Promise<{ 58 | minterSigner: Signer; 59 | mintA: PublicKey; 60 | mintB: PublicKey; 61 | seedPoolAccounts: ISeedPoolAccountsFn; 62 | }> => { 63 | console.log("Deploying test tokens."); 64 | const [mintA, mintB] = (await Promise.all( 65 | ["Token A", "Token B"].map(async (tokenName) => { 66 | return ( 67 | await createMint({ 68 | provider, 69 | mintAuthority: minterSigner.publicKey, 70 | name: tokenName, 71 | decimals: DEFAULT_TOKEN_DECIMALS, 72 | }) 73 | ).mint; 74 | }) 75 | )) as [PublicKey, PublicKey]; 76 | 77 | // seed the pool accounts with mints 78 | const seedPoolAccounts: ISeedPoolAccountsFn = ({ 79 | tokenAAccount, 80 | tokenBAccount, 81 | }) => ({ 82 | instructions: [ 83 | SPLToken.createMintToInstruction( 84 | TOKEN_PROGRAM_ID, 85 | mintA, 86 | tokenAAccount, 87 | minterSigner.publicKey, 88 | [new Account(minterSigner.secretKey)], 89 | initialTokenAAmount 90 | ), 91 | SPLToken.createMintToInstruction( 92 | TOKEN_PROGRAM_ID, 93 | mintB, 94 | tokenBAccount, 95 | minterSigner.publicKey, 96 | [new Account(minterSigner.secretKey)], 97 | initialTokenBAmount 98 | ), 99 | ], 100 | signers: [minterSigner], 101 | }); 102 | 103 | return { minterSigner, mintA, mintB, seedPoolAccounts }; 104 | }; 105 | -------------------------------------------------------------------------------- /stable-swap-program/program/src/processor/checks.rs: -------------------------------------------------------------------------------- 1 | //! Checks for processing instructions. 2 | 3 | use crate::{ 4 | error::SwapError, 5 | processor::utils, 6 | state::{SwapInfo, SwapTokenInfo}, 7 | }; 8 | 9 | use solana_program::{ 10 | account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError, 11 | pubkey::Pubkey, 12 | }; 13 | 14 | use super::logging::log_slippage_error; 15 | 16 | /// Checks if the reserve of the swap is the given key. 17 | fn check_reserves_match(token: &SwapTokenInfo, reserves_info_key: &Pubkey) -> ProgramResult { 18 | check_token_keys_equal!( 19 | token, 20 | *reserves_info_key, 21 | token.reserves, 22 | "Reserves", 23 | SwapError::IncorrectSwapAccount 24 | ); 25 | Ok(()) 26 | } 27 | 28 | /// Access control for admin only instructions 29 | pub fn check_has_admin_signer( 30 | expected_admin_key: &Pubkey, 31 | admin_account_info: &AccountInfo, 32 | ) -> ProgramResult { 33 | check_keys_equal!( 34 | *expected_admin_key, 35 | *admin_account_info.key, 36 | "Admin signer", 37 | SwapError::Unauthorized 38 | ); 39 | if !admin_account_info.is_signer { 40 | return Err(ProgramError::MissingRequiredSignature); 41 | } 42 | Ok(()) 43 | } 44 | 45 | pub fn check_deposit_token_accounts( 46 | token: &SwapTokenInfo, 47 | source_key: &Pubkey, 48 | reserves_info_key: &Pubkey, 49 | ) -> ProgramResult { 50 | check_token_keys_not_equal!( 51 | token, 52 | *source_key, 53 | token.reserves, 54 | "Source account cannot be swap token account of token", 55 | SwapError::InvalidInput 56 | ); 57 | check_reserves_match(token, reserves_info_key)?; 58 | Ok(()) 59 | } 60 | 61 | pub fn check_can_withdraw_token( 62 | rate: Option<(u64, u64, u64)>, 63 | minimum_token_amount: u64, 64 | ) -> Result<(u64, u64, u64), ProgramError> { 65 | let (amount, fee, admin_fee) = rate.ok_or(SwapError::CalculationFailure)?; 66 | if amount < minimum_token_amount { 67 | log_slippage_error(minimum_token_amount, amount); 68 | return Err(SwapError::ExceededSlippage.into()); 69 | } 70 | 71 | Ok((amount, fee, admin_fee)) 72 | } 73 | 74 | /// Checks that the withdraw token accounts are correct. 75 | pub fn check_withdraw_token_accounts( 76 | token: &SwapTokenInfo, 77 | reserves_info_key: &Pubkey, 78 | admin_fee_dest_key: &Pubkey, 79 | ) -> ProgramResult { 80 | check_reserves_match(token, reserves_info_key)?; 81 | check_keys_equal!( 82 | *admin_fee_dest_key, 83 | token.admin_fees, 84 | "Admin fee dest", 85 | SwapError::InvalidAdmin 86 | ); 87 | Ok(()) 88 | } 89 | 90 | pub fn check_swap_authority( 91 | token_swap: &SwapInfo, 92 | swap_info_key: &Pubkey, 93 | program_id: &Pubkey, 94 | swap_authority_key: &Pubkey, 95 | ) -> ProgramResult { 96 | let swap_authority = utils::authority_id(program_id, swap_info_key, token_swap.nonce)?; 97 | check_keys_equal!( 98 | *swap_authority_key, 99 | swap_authority, 100 | "Swap authority", 101 | SwapError::InvalidProgramAddress 102 | ); 103 | Ok(()) 104 | } 105 | 106 | /// Checks that the destination of the swap has the correct accounts. 107 | pub fn check_swap_token_destination_accounts( 108 | token: &SwapTokenInfo, 109 | swap_destination_info_key: &Pubkey, 110 | admin_destination_info_key: &Pubkey, 111 | ) -> ProgramResult { 112 | check_keys_equal!( 113 | *swap_destination_info_key, 114 | token.reserves, 115 | "Incorrect destination, expected", 116 | SwapError::IncorrectSwapAccount 117 | ); 118 | check_keys_equal!( 119 | *admin_destination_info_key, 120 | token.admin_fees, 121 | "Admin fee", 122 | SwapError::InvalidAdmin 123 | ); 124 | Ok(()) 125 | } 126 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1649676176, 6 | "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "flake-utils_2": { 19 | "locked": { 20 | "lastModified": 1649676176, 21 | "narHash": "sha256-OWKJratjt2RW151VUlJPRALb7OU2S5s+f0vLj4o1bHM=", 22 | "owner": "numtide", 23 | "repo": "flake-utils", 24 | "rev": "a4b154ebbdc88c8498a5c7b01589addc9e9cb678", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "numtide", 29 | "repo": "flake-utils", 30 | "type": "github" 31 | } 32 | }, 33 | "flake-utils_3": { 34 | "locked": { 35 | "lastModified": 1637014545, 36 | "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", 37 | "owner": "numtide", 38 | "repo": "flake-utils", 39 | "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "numtide", 44 | "repo": "flake-utils", 45 | "type": "github" 46 | } 47 | }, 48 | "nixpkgs": { 49 | "locked": { 50 | "lastModified": 1650109093, 51 | "narHash": "sha256-tqlnKrAdJktRLXTou9le0oTqrYBAFpGscV5RADdpArU=", 52 | "owner": "NixOS", 53 | "repo": "nixpkgs", 54 | "rev": "faad370edcb37162401be50d45526f52bb16a713", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "NixOS", 59 | "ref": "nixpkgs-unstable", 60 | "repo": "nixpkgs", 61 | "type": "github" 62 | } 63 | }, 64 | "nixpkgs_2": { 65 | "locked": { 66 | "lastModified": 1649961138, 67 | "narHash": "sha256-8ZCPrazs+qd2V8Elw84lIWuk0kKfVQ8Ei/19gahURhM=", 68 | "owner": "NixOS", 69 | "repo": "nixpkgs", 70 | "rev": "d08394e7cd5c7431a1e8f53b7f581e74ee909548", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "NixOS", 75 | "ref": "nixpkgs-unstable", 76 | "repo": "nixpkgs", 77 | "type": "github" 78 | } 79 | }, 80 | "root": { 81 | "inputs": { 82 | "flake-utils": "flake-utils", 83 | "nixpkgs": "nixpkgs", 84 | "saber-overlay": "saber-overlay" 85 | } 86 | }, 87 | "rust-overlay": { 88 | "inputs": { 89 | "flake-utils": "flake-utils_3", 90 | "nixpkgs": [ 91 | "saber-overlay", 92 | "nixpkgs" 93 | ] 94 | }, 95 | "locked": { 96 | "lastModified": 1649903781, 97 | "narHash": "sha256-m+3EZo0a4iS8IwHQhkM/riPuFpu76505xKqmN9j5O+E=", 98 | "owner": "oxalica", 99 | "repo": "rust-overlay", 100 | "rev": "e45696bedc4a13a5970376b8fc09660fdd0e6f6c", 101 | "type": "github" 102 | }, 103 | "original": { 104 | "owner": "oxalica", 105 | "repo": "rust-overlay", 106 | "type": "github" 107 | } 108 | }, 109 | "saber-overlay": { 110 | "inputs": { 111 | "flake-utils": "flake-utils_2", 112 | "nixpkgs": "nixpkgs_2", 113 | "rust-overlay": "rust-overlay" 114 | }, 115 | "locked": { 116 | "lastModified": 1649978970, 117 | "narHash": "sha256-hj+Yp3iacTNU/5+EhzcQ3xASiaifHP5AW3752vLMAn0=", 118 | "owner": "saber-hq", 119 | "repo": "saber-overlay", 120 | "rev": "5ec6426c8cc205d0577660fac5469f47f2dccabf", 121 | "type": "github" 122 | }, 123 | "original": { 124 | "owner": "saber-hq", 125 | "repo": "saber-overlay", 126 | "type": "github" 127 | } 128 | } 129 | }, 130 | "root": "root", 131 | "version": 7 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stable-swap 2 | 3 | [![License](https://img.shields.io/crates/l/stable-swap-anchor)](https://github.com/saber-hq/stable-swap/blob/master/LICENSE) 4 | [![Build Status](https://img.shields.io/github/workflow/status/saber-hq/stable-swap/Program/master)](https://github.com/saber-hq/stable-swap/actions/workflows/program.yml?query=branch%3Amaster) 5 | [![Contributors](https://img.shields.io/github/contributors/saber-hq/stable-swap)](https://github.com/saber-hq/stable-swap/graphs/contributors) 6 | 7 |
8 | 9 |
10 | 11 |
12 | An automated market maker for mean-reverting trading pairs. 13 |
14 | 15 | ## Documentation 16 | 17 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 18 | 19 | Automatically generated documentation for Rust Crates is available below. 20 | 21 | ## Rust Crates 22 | 23 | | Package | Description | Version | Docs | 24 | | :------------------- | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------- | 25 | | `stable-swap` | Saber StableSwap program. | [![crates](https://img.shields.io/crates/v/stable-swap)](https://crates.io/crates/stable-swap) | [![Docs.rs](https://docs.rs/stable-swap/badge.svg)](https://docs.rs/stable-swap) | 26 | | `stable-swap-anchor` | Anchor bindings for the StableSwap Rust client. | [![Crates.io](https://img.shields.io/crates/v/stable-swap-anchor)](https://crates.io/crates/stable-swap-anchor) | [![Docs.rs](https://docs.rs/stable-swap-anchor/badge.svg)](https://docs.rs/stable-swap-anchor) | 27 | | `stable-swap-client` | StableSwap Rust client. | [![crates](https://img.shields.io/crates/v/stable-swap-client)](https://crates.io/crates/stable-swap-client) | [![Docs.rs](https://docs.rs/stable-swap-client/badge.svg)](https://docs.rs/stable-swap-client) | 28 | | `stable-swap-fuzz` | Fuzz tests for the Saber StableSwap program. | [![crates](https://img.shields.io/crates/v/stable-swap-fuzz)](https://crates.io/crates/stable-swap-fuzz) | [![Docs.rs](https://docs.rs/stable-swap-fuzz/badge.svg)](https://docs.rs/stable-swap-fuzz) | 29 | | `stable-swap-math` | Calculations for the StableSwap invariant | [![crates](https://img.shields.io/crates/v/stable-swap-math)](https://crates.io/crates/stable-swap-math) | [![Docs.rs](https://docs.rs/stable-swap-math/badge.svg)](https://docs.rs/stable-swap-math) | 30 | | `stable-swap-sim` | Simulations of the StableSwap invariant compared to Curve's reference implementation | [![crates](https://img.shields.io/crates/v/stable-swap-sim)](https://crates.io/crates/stable-swap-sim) | [![Docs.rs](https://docs.rs/stable-swap-sim/badge.svg)](https://docs.rs/stable-swap-sim) | 31 | 32 | ## JavaScript/Web3.js 33 | 34 | To use StableSwap with your frontend or Node.js project, use [the JavaScript SDK.](https://github.com/saber-hq/saber-common/tree/master/packages/stableswap-sdk) 35 | 36 | ## Audit 37 | 38 | Saber's [stable-swap-program](https://github.com/saber-hq/stable-swap/tree/master/stable-swap-program) has been audited by [Bramah Systems](https://www.bramah.systems/). View the audit report [here](https://github.com/saber-hq/stable-swap/blob/master/audit/bramah-systems.pdf). 39 | 40 | ## Developing 41 | 42 | ### Tests 43 | 44 | To run the tests, run: 45 | 46 | ``` 47 | ./stable-swap-program/do.sh e2e-test 48 | ``` 49 | 50 | ## Archive 51 | 52 | The original Saber StableSwap program can be found on the [archive branch](https://github.com/saber-hq/stable-swap/tree/archive). 53 | 54 | ## License 55 | 56 | Saber StableSwap is licensed under the Apache License, Version 2.0. 57 | -------------------------------------------------------------------------------- /stable-swap-math/src/pool_converter.rs: -------------------------------------------------------------------------------- 1 | //! Conversions for pool tokens 2 | use crate::math::FeeCalculator; 3 | use num_traits::ToPrimitive; 4 | use stable_swap_client::fees::Fees; 5 | 6 | /// Converter to determine how much to deposit / withdraw, along with 7 | /// proper initialization 8 | pub struct PoolTokenConverter<'a> { 9 | /// Total supply 10 | pub supply: u64, 11 | /// Token A amount 12 | pub token_a: u64, 13 | /// Token B amount 14 | pub token_b: u64, 15 | /// Fees 16 | pub fees: &'a Fees, 17 | } 18 | 19 | impl PoolTokenConverter<'_> { 20 | /// Computes the amount of token A backing the given amount of pool tokens. 21 | pub fn token_a_rate(&self, pool_tokens: u64) -> Option<(u64, u64, u64)> { 22 | let amount = (pool_tokens as u128) 23 | .checked_mul(self.token_a as u128)? 24 | .checked_div(self.supply as u128)? 25 | .to_u64()?; 26 | let fee = self.fees.withdraw_fee(amount)?; 27 | let admin_fee = self.fees.admin_withdraw_fee(fee)?; 28 | 29 | Some((amount.checked_sub(fee)?, fee, admin_fee)) 30 | } 31 | 32 | /// Computes the amount of token B backing the given amount of pool tokens. 33 | pub fn token_b_rate(&self, pool_tokens: u64) -> Option<(u64, u64, u64)> { 34 | let amount = (pool_tokens as u128) 35 | .checked_mul(self.token_b as u128)? 36 | .checked_div(self.supply as u128)? 37 | .to_u64()?; 38 | let fee = self.fees.withdraw_fee(amount)?; 39 | let admin_fee = self.fees.admin_withdraw_fee(fee)?; 40 | 41 | Some((amount.checked_sub(fee)?, fee, admin_fee)) 42 | } 43 | 44 | /// Calculates the number of LP tokens that correspond to an amount of token A. 45 | /// This does not take withdraw fees into account. 46 | pub fn lp_tokens_for_a_excluding_fees(&self, token_a_amount: u64) -> Option { 47 | (token_a_amount as u128) 48 | .checked_mul(self.supply as u128)? 49 | .checked_div(self.token_a as u128)? 50 | .to_u64() 51 | } 52 | 53 | /// Calculates the number of LP tokens that correspond to an amount of token B. 54 | /// This does not take withdraw fees into account. 55 | pub fn lp_tokens_for_b_excluding_fees(&self, token_b_amount: u64) -> Option { 56 | (token_b_amount as u128) 57 | .checked_mul(self.supply as u128)? 58 | .checked_div(self.token_b as u128)? 59 | .to_u64() 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | #[allow(clippy::unwrap_used, clippy::integer_arithmetic)] 65 | mod tests { 66 | 67 | use crate::curve; 68 | 69 | use super::*; 70 | 71 | fn check_pool_token_a_rate( 72 | token_a: u64, 73 | token_b: u64, 74 | deposit: u64, 75 | supply: u64, 76 | expected_before_fees: Option, 77 | ) { 78 | let fees = Fees { 79 | admin_trade_fee_numerator: 0, 80 | admin_trade_fee_denominator: 1, 81 | admin_withdraw_fee_numerator: 1, 82 | admin_withdraw_fee_denominator: 2, 83 | trade_fee_numerator: 0, 84 | trade_fee_denominator: 1, 85 | withdraw_fee_numerator: 1, 86 | withdraw_fee_denominator: 2, 87 | }; 88 | let calculator = PoolTokenConverter { 89 | supply, 90 | token_a, 91 | token_b, 92 | fees: &fees, 93 | }; 94 | let expected_result = if let Some(expected_before_fees) = expected_before_fees { 95 | let expected_fees = fees.withdraw_fee(expected_before_fees).unwrap(); 96 | let expected_admin_fees = fees.admin_withdraw_fee(expected_fees).unwrap(); 97 | let expected_amount = expected_before_fees - expected_fees; 98 | Some((expected_amount, expected_fees, expected_admin_fees)) 99 | } else { 100 | None 101 | }; 102 | assert_eq!(calculator.token_a_rate(deposit), expected_result); 103 | assert_eq!(calculator.supply, supply); 104 | } 105 | 106 | #[test] 107 | fn issued_tokens() { 108 | check_pool_token_a_rate(2, 50, 5, 10, Some(1)); 109 | check_pool_token_a_rate(10, 10, 5, 10, Some(5)); 110 | check_pool_token_a_rate(5, 100, 5, 10, Some(2)); 111 | check_pool_token_a_rate(5, curve::MAX_TOKENS_IN, 5, 10, Some(2)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /stable-swap-math/src/bn.rs: -------------------------------------------------------------------------------- 1 | //! Big number types 2 | #![allow(clippy::assign_op_pattern)] 3 | #![allow(clippy::ptr_offset_with_cast)] 4 | #![allow(clippy::manual_range_contains)] 5 | #![allow(clippy::integer_arithmetic)] 6 | 7 | use borsh::{BorshDeserialize, BorshSerialize}; 8 | use stable_swap_client::error::SwapError; 9 | use std::borrow::BorrowMut; 10 | use std::io::{Error, ErrorKind, Write}; 11 | use std::mem::size_of; 12 | use uint::construct_uint; 13 | 14 | macro_rules! impl_borsh_serialize_for_bn { 15 | ($type: ident) => { 16 | impl BorshSerialize for $type { 17 | #[inline] 18 | fn serialize(&self, writer: &mut W) -> std::io::Result<()> { 19 | let bytes = self.to_le_bytes(); 20 | writer.write_all(&bytes) 21 | } 22 | } 23 | }; 24 | } 25 | 26 | macro_rules! impl_borsh_deserialize_for_bn { 27 | ($type: ident) => { 28 | impl BorshDeserialize for $type { 29 | #[inline] 30 | fn deserialize(buf: &mut &[u8]) -> std::io::Result { 31 | if buf.len() < size_of::<$type>() { 32 | return Err(Error::new( 33 | ErrorKind::InvalidInput, 34 | "Unexpected length of input", 35 | )); 36 | } 37 | let res = $type::from_le_bytes(buf[..size_of::<$type>()].try_into().unwrap()); 38 | *buf = &buf[size_of::<$type>()..]; 39 | Ok(res) 40 | } 41 | } 42 | }; 43 | } 44 | 45 | construct_uint! { 46 | /// 256-bit unsigned integer. 47 | pub struct U256(4); 48 | } 49 | 50 | impl U256 { 51 | /// Convert [U256] to u64 52 | pub fn to_u64(self) -> Option { 53 | self.try_to_u64().map_or_else(|_| None, Some) 54 | } 55 | 56 | /// Convert [U256] to u64 57 | pub fn try_to_u64(self) -> Result { 58 | self.try_into().map_err(|_| SwapError::ConversionFailure) 59 | } 60 | 61 | /// Convert [U256] to u128 62 | pub fn to_u128(self) -> Option { 63 | self.try_to_u128().map_or_else(|_| None, Some) 64 | } 65 | 66 | /// Convert [U256] to u128 67 | pub fn try_to_u128(self) -> Result { 68 | self.try_into().map_err(|_| SwapError::ConversionFailure) 69 | } 70 | 71 | /// Convert from little endian bytes 72 | pub fn from_le_bytes(bytes: [u8; 32]) -> Self { 73 | U256::from_little_endian(&bytes) 74 | } 75 | 76 | /// Convert to little endian bytes 77 | pub fn to_le_bytes(self) -> [u8; 32] { 78 | let mut buf: Vec = Vec::with_capacity(size_of::()); 79 | self.to_little_endian(buf.borrow_mut()); 80 | 81 | let mut bytes: [u8; 32] = [0u8; 32]; 82 | bytes.copy_from_slice(buf.as_slice()); 83 | bytes 84 | } 85 | } 86 | 87 | impl_borsh_deserialize_for_bn!(U256); 88 | impl_borsh_serialize_for_bn!(U256); 89 | 90 | construct_uint! { 91 | /// 192-bit unsigned integer. 92 | pub struct U192(3); 93 | } 94 | 95 | impl U192 { 96 | /// Convert [U192] to u64 97 | pub fn to_u64(self) -> Option { 98 | self.try_to_u64().map_or_else(|_| None, Some) 99 | } 100 | 101 | /// Convert [U192] to u64 102 | pub fn try_to_u64(self) -> Result { 103 | self.try_into().map_err(|_| SwapError::ConversionFailure) 104 | } 105 | 106 | /// Convert [U192] to u128 107 | pub fn to_u128(self) -> Option { 108 | self.try_to_u128().map_or_else(|_| None, Some) 109 | } 110 | 111 | /// Convert [U192] to u128 112 | pub fn try_to_u128(self) -> Result { 113 | self.try_into().map_err(|_| SwapError::ConversionFailure) 114 | } 115 | 116 | /// Convert from little endian bytes 117 | pub fn from_le_bytes(bytes: [u8; 24]) -> Self { 118 | U192::from_little_endian(&bytes) 119 | } 120 | 121 | /// Convert to little endian bytes 122 | pub fn to_le_bytes(self) -> [u8; 24] { 123 | let mut buf: Vec = Vec::with_capacity(size_of::()); 124 | self.to_little_endian(buf.borrow_mut()); 125 | 126 | let mut bytes: [u8; 24] = [0u8; 24]; 127 | bytes.copy_from_slice(buf.as_slice()); 128 | bytes 129 | } 130 | } 131 | 132 | impl_borsh_deserialize_for_bn!(U192); 133 | impl_borsh_serialize_for_bn!(U192); 134 | -------------------------------------------------------------------------------- /fuzz/src/native_processor.rs: -------------------------------------------------------------------------------- 1 | use crate::native_account_data::NativeAccountData; 2 | 3 | use lazy_static::lazy_static; 4 | use solana_program::{ 5 | account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, instruction::Instruction, 6 | program_error::ProgramError, program_stubs, pubkey::Pubkey, 7 | }; 8 | 9 | lazy_static! { 10 | static ref VERBOSE: u32 = std::env::var("FUZZ_VERBOSE") 11 | .map(|s| s.parse()) 12 | .ok() 13 | .transpose() 14 | .ok() 15 | .flatten() 16 | .unwrap_or(0); 17 | } 18 | 19 | struct TestSyscallStubs { 20 | unix_timestamp: Option, 21 | } 22 | impl program_stubs::SyscallStubs for TestSyscallStubs { 23 | fn sol_get_clock_sysvar(&self, var_addr: *mut u8) -> u64 { 24 | let clock: Option = self.unix_timestamp; 25 | unsafe { 26 | *(var_addr as *mut _ as *mut Clock) = Clock { 27 | unix_timestamp: clock.unwrap(), 28 | ..Clock::default() 29 | }; 30 | } 31 | solana_program::entrypoint::SUCCESS 32 | } 33 | 34 | fn sol_log(&self, message: &str) { 35 | if *VERBOSE >= 1 { 36 | println!("{}", message); 37 | } 38 | } 39 | 40 | fn sol_invoke_signed( 41 | &self, 42 | instruction: &Instruction, 43 | account_infos: &[AccountInfo], 44 | signers_seeds: &[&[&[u8]]], 45 | ) -> ProgramResult { 46 | let mut new_account_infos = vec![]; 47 | 48 | // mimic check for token program in accounts 49 | if !account_infos.iter().any(|x| *x.key == spl_token::id()) { 50 | return Err(ProgramError::InvalidAccountData); 51 | } 52 | 53 | for meta in instruction.accounts.iter() { 54 | for account_info in account_infos.iter() { 55 | if meta.pubkey == *account_info.key { 56 | let mut new_account_info = account_info.clone(); 57 | for seeds in signers_seeds.iter() { 58 | let signer = 59 | Pubkey::create_program_address(seeds, &stable_swap::id()).unwrap(); 60 | if *account_info.key == signer { 61 | new_account_info.is_signer = true; 62 | } 63 | } 64 | new_account_infos.push(new_account_info); 65 | } 66 | } 67 | } 68 | 69 | spl_token::processor::Processor::process( 70 | &instruction.program_id, 71 | &new_account_infos, 72 | &instruction.data, 73 | ) 74 | } 75 | } 76 | 77 | fn test_syscall_stubs(unix_timestamp: Option) { 78 | // only one test may run at a time 79 | program_stubs::set_syscall_stubs(Box::new(TestSyscallStubs { unix_timestamp })); 80 | } 81 | 82 | pub fn do_process_instruction_at_time( 83 | instruction: Instruction, 84 | accounts: &[AccountInfo], 85 | current_ts: i64, 86 | ) -> ProgramResult { 87 | do_process_instruction_maybe_at_time(instruction, accounts, Some(current_ts)) 88 | } 89 | 90 | pub fn do_process_instruction(instruction: Instruction, accounts: &[AccountInfo]) -> ProgramResult { 91 | do_process_instruction_maybe_at_time(instruction, accounts, None) 92 | } 93 | 94 | fn do_process_instruction_maybe_at_time( 95 | instruction: Instruction, 96 | accounts: &[AccountInfo], 97 | current_ts: Option, 98 | ) -> ProgramResult { 99 | test_syscall_stubs(current_ts); 100 | 101 | // approximate the logic in the actual runtime which runs the instruction 102 | // and only updates accounts if the instruction is successful 103 | let mut account_data = accounts 104 | .iter() 105 | .map(NativeAccountData::new_from_account_info) 106 | .collect::>(); 107 | let account_infos = account_data 108 | .iter_mut() 109 | .map(NativeAccountData::as_account_info) 110 | .collect::>(); 111 | let res = if instruction.program_id == stable_swap::id() { 112 | stable_swap::processor::Processor::process( 113 | &instruction.program_id, 114 | &account_infos, 115 | &instruction.data, 116 | ) 117 | } else { 118 | spl_token::processor::Processor::process( 119 | &instruction.program_id, 120 | &account_infos, 121 | &instruction.data, 122 | ) 123 | }; 124 | 125 | if res.is_ok() { 126 | let mut account_metas = instruction 127 | .accounts 128 | .iter() 129 | .zip(accounts) 130 | .map(|(account_meta, account)| (&account_meta.pubkey, account)) 131 | .collect::>(); 132 | for account_info in account_infos.iter() { 133 | for account_meta in account_metas.iter_mut() { 134 | if account_info.key == account_meta.0 { 135 | let account = &mut account_meta.1; 136 | let mut lamports = account.lamports.borrow_mut(); 137 | **lamports = **account_info.lamports.borrow(); 138 | let mut data = account.data.borrow_mut(); 139 | data.clone_from_slice(*account_info.data.borrow()); 140 | } 141 | } 142 | } 143 | } 144 | res 145 | } 146 | -------------------------------------------------------------------------------- /.github/workflows/program.yml: -------------------------------------------------------------------------------- 1 | name: Program 2 | 3 | on: 4 | push: 5 | branches: [master, exchange_rate_override] 6 | pull_request: 7 | branches: [master, exchange_rate_override] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | SOLANA_VERSION: "1.11.10" 12 | RUST_TOOLCHAIN: "1.59.0" 13 | 14 | jobs: 15 | unit-tests: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | # Install Cachix 22 | - uses: cachix/install-nix-action@v22 23 | - name: Setup Cachix 24 | uses: cachix/cachix-action@v12 25 | with: 26 | name: saber 27 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 28 | 29 | # Install Rust 30 | - name: Install Rust 31 | uses: actions-rs/toolchain@v1 32 | with: 33 | override: true 34 | profile: minimal 35 | toolchain: ${{ env.RUST_TOOLCHAIN }} 36 | 37 | # Install Solana 38 | - name: Cache Solana binaries 39 | id: solana-cache 40 | uses: actions/cache@v3 41 | with: 42 | path: | 43 | ~/.cache/solana 44 | ~/.local/share/solana/install 45 | key: ${{ runner.os }}-${{ env.SOLANA_VERSION }} 46 | - name: Install Solana 47 | if: steps.solana-cache.outputs.cache-hit != 'true' 48 | run: | 49 | nix shell .#ci --command solana-install init ${{ env.SOLANA_VERSION }} 50 | - name: Setup Solana Path 51 | run: | 52 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 53 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 54 | solana --version 55 | - name: Cache Rust 56 | uses: Swatinem/rust-cache@v2 57 | with: 58 | key: ${{ runner.os }}-${{ env.SOLANA_VERSION }} 59 | 60 | - name: Run unit tests 61 | working-directory: stable-swap-program 62 | run: ./do.sh test --nocapture 63 | 64 | e2e-test: 65 | runs-on: ubuntu-latest 66 | 67 | steps: 68 | - uses: actions/checkout@v3 69 | - uses: actions/setup-node@v3 70 | with: 71 | node-version: "14" 72 | 73 | - name: Cache SDK dependencies 74 | uses: actions/cache@v3 75 | id: cache-sdk 76 | with: 77 | path: ./sdk/node_modules 78 | key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} 79 | 80 | # Install Cachix 81 | - uses: cachix/install-nix-action@v22 82 | - name: Setup Cachix 83 | uses: cachix/cachix-action@v12 84 | with: 85 | name: saber 86 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 87 | 88 | # Install Solana 89 | - name: Cache Solana binaries 90 | id: solana-cache 91 | uses: actions/cache@v3 92 | with: 93 | path: | 94 | ~/.cache/solana 95 | ~/.local/share/solana/install 96 | key: ${{ runner.os }}-${{ env.SOLANA_VERSION }} 97 | - name: Install Solana 98 | if: steps.solana-cache.outputs.cache-hit != 'true' 99 | run: | 100 | nix shell .#ci --command solana-install init ${{ env.SOLANA_VERSION }} 101 | - name: Setup Solana Path 102 | run: | 103 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 104 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 105 | solana --version 106 | 107 | # Install Rust 108 | - name: Install Rust 109 | uses: actions-rs/toolchain@v1 110 | with: 111 | override: true 112 | profile: minimal 113 | toolchain: ${{ env.RUST_TOOLCHAIN }} 114 | - name: Cache Rust 115 | uses: Swatinem/rust-cache@v2 116 | with: 117 | key: ${{ runner.os }}-${{ env.SOLANA_VERSION }} 118 | 119 | - name: Build 120 | run: nix shell .#ci --command anchor build --program-name stable_swap 121 | - name: Run e2e tests 122 | working-directory: stable-swap-program 123 | run: nix shell ../#ci --command ./do.sh e2e-test 124 | 125 | ## FIXME(michael): ERROR: no interesting inputs were found. Is the code instrumented for coverage? Exiting. 126 | ## fuzz: 127 | ## runs-on: ubuntu-latest 128 | 129 | ## steps: 130 | ## - uses: actions/checkout@v3 131 | ## # Install Rust 132 | ## - name: Install Rust 133 | ## uses: actions-rs/toolchain@v1 134 | ## with: 135 | ## override: true 136 | ## profile: minimal 137 | ## toolchain: nightly-2022-02-01 138 | ## - name: Cache dependencies 139 | ## uses: Swatinem/rust-cache@v2 140 | 141 | ## # Install Cachix 142 | ## - uses: cachix/install-nix-action@v22 143 | ## - name: Setup Cachix 144 | ## uses: cachix/cachix-action@v12 145 | ## with: 146 | ## name: saber 147 | ## authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 148 | 149 | ## - name: Build 150 | ## run: nix shell .#ci --command cargo fuzz build --dev fuzz_test 151 | ## env: 152 | ## RUSTFLAGS: "-Znew-llvm-pass-manager=no" 153 | ## - name: Run fuzz test 154 | ## run: nix shell .#ci --command cargo fuzz run --dev fuzz_test -- -max_total_time=180 155 | ## env: 156 | ## RUSTFLAGS: "-Znew-llvm-pass-manager=no" 157 | -------------------------------------------------------------------------------- /fuzz/targets/swaps_only.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | use arbitrary::Arbitrary; 4 | use chrono::prelude::*; 5 | use fuzz::{ 6 | native_account_data::NativeAccountData, native_stable_swap::NativeStableSwap, 7 | native_token::get_token_balance, 8 | }; 9 | use lazy_static::lazy_static; 10 | use libfuzzer_sys::fuzz_target; 11 | use rand::Rng; 12 | use solana_program::system_program; 13 | use spl_token::error::TokenError; 14 | use stable_swap::{ 15 | curve::{MAX_AMP, MIN_AMP}, 16 | error::SwapError, 17 | fees::Fees, 18 | instruction::*, 19 | }; 20 | 21 | #[derive(Debug, Arbitrary, Clone)] 22 | struct SwapArgs { 23 | trade_direction: TradeDirection, 24 | instruction_data: SwapData, 25 | } 26 | 27 | /// Helper enum to tell which direction a swap is meant to go. 28 | #[derive(Debug, Arbitrary, Clone)] 29 | enum TradeDirection { 30 | AtoB, 31 | BtoA, 32 | } 33 | 34 | const INITIAL_SWAP_TOKEN_A_AMOUNT: u64 = 100_000_000_000; 35 | const INITIAL_SWAP_TOKEN_B_AMOUNT: u64 = 100_000_000_000; 36 | 37 | const INITIAL_USER_TOKEN_A_AMOUNT: u64 = 1_000_000_000; 38 | const INITIAL_USER_TOKEN_B_AMOUNT: u64 = 1_000_000_000; 39 | 40 | lazy_static! { 41 | static ref VERBOSE: u32 = std::env::var("FUZZ_VERBOSE") 42 | .map(|s| s.parse()) 43 | .ok() 44 | .transpose() 45 | .ok() 46 | .flatten() 47 | .unwrap_or(0); 48 | } 49 | 50 | fuzz_target!(|argv: Vec| { run_swaps(argv) }); 51 | 52 | fn run_swaps(argv: Vec) { 53 | let mut rng = rand::thread_rng(); 54 | let amp_factor = rng.gen_range(MIN_AMP..=MAX_AMP); 55 | 56 | if *VERBOSE >= 1 { 57 | println!("Amplification Coefficient: {}", amp_factor); 58 | if *VERBOSE >= 3 { 59 | println!("Swap args: {:?}", argv); 60 | } 61 | } 62 | 63 | let admin_trade_fee_numerator = 0; 64 | let admin_trade_fee_denominator = 10000; 65 | let admin_withdraw_fee_numerator = 0; 66 | let admin_withdraw_fee_denominator = 10000; 67 | let trade_fee_numerator = 0; 68 | let trade_fee_denominator = 1000; 69 | let withdraw_fee_numerator = 0; 70 | let withdraw_fee_denominator = 10000; 71 | let fees = Fees { 72 | admin_trade_fee_numerator, 73 | admin_trade_fee_denominator, 74 | admin_withdraw_fee_numerator, 75 | admin_withdraw_fee_denominator, 76 | trade_fee_numerator, 77 | trade_fee_denominator, 78 | withdraw_fee_numerator, 79 | withdraw_fee_denominator, 80 | }; 81 | 82 | let mut stable_swap = NativeStableSwap::new( 83 | Utc::now().timestamp(), 84 | amp_factor, 85 | INITIAL_SWAP_TOKEN_A_AMOUNT, 86 | INITIAL_SWAP_TOKEN_B_AMOUNT, 87 | fees, 88 | ); 89 | 90 | let mut user_account = NativeAccountData::new_signer(0, system_program::id()); 91 | let mut token_a_account = 92 | stable_swap.create_token_a_account(user_account.clone(), INITIAL_USER_TOKEN_A_AMOUNT); 93 | let mut token_b_account = 94 | stable_swap.create_token_b_account(user_account.clone(), INITIAL_USER_TOKEN_B_AMOUNT); 95 | 96 | let before_user_token_a = INITIAL_USER_TOKEN_A_AMOUNT; 97 | let before_user_token_b = INITIAL_USER_TOKEN_B_AMOUNT; 98 | 99 | for args in argv { 100 | run_swap( 101 | &args, 102 | &mut stable_swap, 103 | &mut user_account, 104 | &mut token_a_account, 105 | &mut token_b_account, 106 | ) 107 | } 108 | 109 | let after_user_token_a = get_token_balance(&token_a_account); 110 | let after_user_token_b = get_token_balance(&token_b_account); 111 | 112 | assert!( 113 | after_user_token_a + after_user_token_b <= before_user_token_a + before_user_token_b, 114 | "before_a: {}, before_b: {}, after_a: {}, after_b: {}", 115 | before_user_token_a, 116 | before_user_token_b, 117 | after_user_token_a, 118 | after_user_token_b 119 | ); 120 | } 121 | 122 | fn run_swap( 123 | args: &SwapArgs, 124 | stable_swap: &mut NativeStableSwap, 125 | user_account: &mut NativeAccountData, 126 | token_a_account: &mut NativeAccountData, 127 | token_b_account: &mut NativeAccountData, 128 | ) { 129 | if *VERBOSE >= 3 { 130 | println!("Current swap args: {:#?}", args); 131 | } 132 | 133 | let SwapArgs { 134 | trade_direction, 135 | instruction_data, 136 | } = args; 137 | 138 | let ix_data_with_slippage_override = SwapData { 139 | amount_in: instruction_data.amount_in, 140 | minimum_amount_out: 0, 141 | }; 142 | 143 | let result = match trade_direction { 144 | TradeDirection::AtoB => stable_swap.swap_a_to_b( 145 | Utc::now().timestamp(), 146 | user_account, 147 | token_a_account, 148 | token_b_account, 149 | ix_data_with_slippage_override, 150 | ), 151 | TradeDirection::BtoA => stable_swap.swap_b_to_a( 152 | Utc::now().timestamp(), 153 | user_account, 154 | token_a_account, 155 | token_b_account, 156 | ix_data_with_slippage_override, 157 | ), 158 | }; 159 | 160 | result 161 | .map_err(|e| { 162 | if !(e == SwapError::CalculationFailure.into() 163 | || e == TokenError::InsufficientFunds.into()) 164 | { 165 | println!("{:?}", e); 166 | Err(e).unwrap() 167 | } 168 | }) 169 | .ok(); 170 | } 171 | -------------------------------------------------------------------------------- /stable-swap-client/src/fees.rs: -------------------------------------------------------------------------------- 1 | //! Program fees 2 | 3 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 4 | use solana_program::{ 5 | program_error::ProgramError, 6 | program_pack::{Pack, Sealed}, 7 | }; 8 | 9 | /// Fees struct 10 | #[repr(C)] 11 | #[derive(Clone, Copy, Debug, Default, PartialEq)] 12 | #[cfg_attr(feature = "fuzz", derive(arbitrary::Arbitrary))] 13 | pub struct Fees { 14 | /// Admin trade fee numerator 15 | pub admin_trade_fee_numerator: u64, 16 | /// Admin trade fee denominator 17 | pub admin_trade_fee_denominator: u64, 18 | /// Admin withdraw fee numerator 19 | pub admin_withdraw_fee_numerator: u64, 20 | /// Admin withdraw fee denominator 21 | pub admin_withdraw_fee_denominator: u64, 22 | /// Trade fee numerator 23 | pub trade_fee_numerator: u64, 24 | /// Trade fee denominator 25 | pub trade_fee_denominator: u64, 26 | /// Withdraw fee numerator 27 | pub withdraw_fee_numerator: u64, 28 | /// Withdraw fee denominator 29 | pub withdraw_fee_denominator: u64, 30 | } 31 | 32 | impl Sealed for Fees {} 33 | impl Pack for Fees { 34 | const LEN: usize = 64; 35 | fn unpack_from_slice(input: &[u8]) -> Result { 36 | let input = array_ref![input, 0, 64]; 37 | #[allow(clippy::ptr_offset_with_cast)] 38 | let ( 39 | admin_trade_fee_numerator, 40 | admin_trade_fee_denominator, 41 | admin_withdraw_fee_numerator, 42 | admin_withdraw_fee_denominator, 43 | trade_fee_numerator, 44 | trade_fee_denominator, 45 | withdraw_fee_numerator, 46 | withdraw_fee_denominator, 47 | ) = array_refs![input, 8, 8, 8, 8, 8, 8, 8, 8]; 48 | Ok(Self { 49 | admin_trade_fee_numerator: u64::from_le_bytes(*admin_trade_fee_numerator), 50 | admin_trade_fee_denominator: u64::from_le_bytes(*admin_trade_fee_denominator), 51 | admin_withdraw_fee_numerator: u64::from_le_bytes(*admin_withdraw_fee_numerator), 52 | admin_withdraw_fee_denominator: u64::from_le_bytes(*admin_withdraw_fee_denominator), 53 | trade_fee_numerator: u64::from_le_bytes(*trade_fee_numerator), 54 | trade_fee_denominator: u64::from_le_bytes(*trade_fee_denominator), 55 | withdraw_fee_numerator: u64::from_le_bytes(*withdraw_fee_numerator), 56 | withdraw_fee_denominator: u64::from_le_bytes(*withdraw_fee_denominator), 57 | }) 58 | } 59 | 60 | fn pack_into_slice(&self, output: &mut [u8]) { 61 | let output = array_mut_ref![output, 0, 64]; 62 | let ( 63 | admin_trade_fee_numerator, 64 | admin_trade_fee_denominator, 65 | admin_withdraw_fee_numerator, 66 | admin_withdraw_fee_denominator, 67 | trade_fee_numerator, 68 | trade_fee_denominator, 69 | withdraw_fee_numerator, 70 | withdraw_fee_denominator, 71 | ) = mut_array_refs![output, 8, 8, 8, 8, 8, 8, 8, 8]; 72 | *admin_trade_fee_numerator = self.admin_trade_fee_numerator.to_le_bytes(); 73 | *admin_trade_fee_denominator = self.admin_trade_fee_denominator.to_le_bytes(); 74 | *admin_withdraw_fee_numerator = self.admin_withdraw_fee_numerator.to_le_bytes(); 75 | *admin_withdraw_fee_denominator = self.admin_withdraw_fee_denominator.to_le_bytes(); 76 | *trade_fee_numerator = self.trade_fee_numerator.to_le_bytes(); 77 | *trade_fee_denominator = self.trade_fee_denominator.to_le_bytes(); 78 | *withdraw_fee_numerator = self.withdraw_fee_numerator.to_le_bytes(); 79 | *withdraw_fee_denominator = self.withdraw_fee_denominator.to_le_bytes(); 80 | } 81 | } 82 | 83 | #[cfg(test)] 84 | #[allow(clippy::unwrap_used)] 85 | mod tests { 86 | use super::*; 87 | 88 | #[test] 89 | fn pack_fees() { 90 | let admin_trade_fee_numerator = 1; 91 | let admin_trade_fee_denominator = 2; 92 | let admin_withdraw_fee_numerator = 3; 93 | let admin_withdraw_fee_denominator = 4; 94 | let trade_fee_numerator = 5; 95 | let trade_fee_denominator = 6; 96 | let withdraw_fee_numerator = 7; 97 | let withdraw_fee_denominator = 8; 98 | let fees = Fees { 99 | admin_trade_fee_numerator, 100 | admin_trade_fee_denominator, 101 | admin_withdraw_fee_numerator, 102 | admin_withdraw_fee_denominator, 103 | trade_fee_numerator, 104 | trade_fee_denominator, 105 | withdraw_fee_numerator, 106 | withdraw_fee_denominator, 107 | }; 108 | 109 | let mut packed = [0u8; Fees::LEN]; 110 | Pack::pack_into_slice(&fees, &mut packed[..]); 111 | let unpacked = Fees::unpack_from_slice(&packed).unwrap(); 112 | assert_eq!(fees, unpacked); 113 | 114 | let mut packed = vec![]; 115 | packed.extend_from_slice(&admin_trade_fee_numerator.to_le_bytes()); 116 | packed.extend_from_slice(&admin_trade_fee_denominator.to_le_bytes()); 117 | packed.extend_from_slice(&admin_withdraw_fee_numerator.to_le_bytes()); 118 | packed.extend_from_slice(&admin_withdraw_fee_denominator.to_le_bytes()); 119 | packed.extend_from_slice(&trade_fee_numerator.to_le_bytes()); 120 | packed.extend_from_slice(&trade_fee_denominator.to_le_bytes()); 121 | packed.extend_from_slice(&withdraw_fee_numerator.to_le_bytes()); 122 | packed.extend_from_slice(&withdraw_fee_denominator.to_le_bytes()); 123 | let unpacked = Fees::unpack_from_slice(&packed).unwrap(); 124 | assert_eq!(fees, unpacked); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/test/admin.int.test.ts: -------------------------------------------------------------------------------- 1 | import { SignerWallet, TransactionEnvelope } from "@saberhq/solana-contrib"; 2 | import { 3 | createAdminApplyNewAdminInstruction, 4 | createAdminCommitNewAdminInstruction, 5 | createAdminSetFeeAccountInstruction, 6 | deployNewSwap, 7 | StableSwap, 8 | SWAP_PROGRAM_ID, 9 | ZERO_TS, 10 | } from "@saberhq/stableswap-sdk"; 11 | import { getOrCreateATA, u64 } from "@saberhq/token-utils"; 12 | import type { Signer, TransactionInstruction } from "@solana/web3.js"; 13 | import { 14 | Connection, 15 | Keypair, 16 | LAMPORTS_PER_SOL, 17 | PublicKey, 18 | Transaction, 19 | } from "@solana/web3.js"; 20 | 21 | import { deployTestTokens } from "./deployTestTokens"; 22 | import { 23 | AMP_FACTOR, 24 | BOOTSTRAP_TIMEOUT, 25 | CLUSTER_URL, 26 | newKeypairWithLamports, 27 | sendAndConfirmTransactionWithTitle, 28 | } from "./helpers"; 29 | 30 | describe("admin test", () => { 31 | // Cluster connection 32 | let connection: Connection; 33 | // Fee payer 34 | let payer: Signer; 35 | // owner of the user accounts 36 | let owner: Signer; 37 | // Stable swap 38 | let stableSwap: StableSwap; 39 | let stableSwapAccount: Keypair; 40 | let stableSwapProgramId: PublicKey; 41 | 42 | const newAdmin: Keypair = Keypair.generate(); 43 | 44 | beforeAll(async () => { 45 | // Bootstrap Test Environment ... 46 | connection = new Connection(CLUSTER_URL, "single"); 47 | payer = await newKeypairWithLamports(connection, LAMPORTS_PER_SOL); 48 | owner = await newKeypairWithLamports(connection, LAMPORTS_PER_SOL); 49 | 50 | const provider = new SignerWallet(payer).createProvider(connection); 51 | const { 52 | mintA: tokenAMint, 53 | mintB: tokenBMint, 54 | seedPoolAccounts, 55 | } = await deployTestTokens({ 56 | provider, 57 | minterSigner: owner, 58 | }); 59 | 60 | stableSwapProgramId = SWAP_PROGRAM_ID; 61 | stableSwapAccount = Keypair.generate(); 62 | 63 | const { swap: newSwap } = await deployNewSwap({ 64 | provider, 65 | swapProgramID: stableSwapProgramId, 66 | adminAccount: owner.publicKey, 67 | tokenAMint, 68 | tokenBMint, 69 | ampFactor: new u64(AMP_FACTOR), 70 | 71 | initialLiquidityProvider: owner.publicKey, 72 | useAssociatedAccountForInitialLP: true, 73 | seedPoolAccounts, 74 | 75 | swapAccountSigner: stableSwapAccount, 76 | }); 77 | 78 | stableSwap = newSwap; 79 | }, BOOTSTRAP_TIMEOUT); 80 | 81 | it("Set fee account", async () => { 82 | const fetchedStableSwap = await StableSwap.load( 83 | connection, 84 | stableSwapAccount.publicKey, 85 | stableSwapProgramId 86 | ); 87 | 88 | const provider = new SignerWallet(owner).createProvider(connection); 89 | const tokenOwner = Keypair.generate(); 90 | const { address: expectedFeeAccount, instruction } = await getOrCreateATA({ 91 | provider, 92 | mint: fetchedStableSwap.state.tokenA.mint, 93 | owner: tokenOwner.publicKey, 94 | }); 95 | 96 | const instructions: TransactionInstruction[] = []; 97 | if (instruction) { 98 | instructions.push(instruction); 99 | } 100 | instructions.push( 101 | createAdminSetFeeAccountInstruction({ 102 | config: fetchedStableSwap.config, 103 | state: fetchedStableSwap.state, 104 | tokenAccount: expectedFeeAccount, 105 | }) 106 | ); 107 | const txEnv = new TransactionEnvelope(provider, instructions); 108 | const pendingTx = await txEnv.send(); 109 | await pendingTx.wait(); 110 | 111 | const newSwap = await StableSwap.load( 112 | connection, 113 | stableSwap.config.swapAccount, 114 | stableSwap.config.swapProgramID 115 | ); 116 | expect(newSwap.state.tokenA.adminFeeAccount).toEqual(expectedFeeAccount); 117 | }); 118 | 119 | it("Commit new admin", async () => { 120 | const fetchedStableSwap = await StableSwap.load( 121 | connection, 122 | stableSwapAccount.publicKey, 123 | stableSwapProgramId 124 | ); 125 | 126 | const txn = new Transaction().add( 127 | createAdminCommitNewAdminInstruction({ 128 | config: fetchedStableSwap.config, 129 | state: fetchedStableSwap.state, 130 | newAdminAccount: newAdmin.publicKey, 131 | }) 132 | ); 133 | await sendAndConfirmTransactionWithTitle( 134 | "commit new admin", 135 | connection, 136 | txn, 137 | payer, 138 | owner 139 | ); 140 | 141 | const newSwap = await StableSwap.load( 142 | connection, 143 | stableSwap.config.swapAccount, 144 | stableSwap.config.swapProgramID 145 | ); 146 | expect(newSwap.state.adminAccount).toEqual(owner.publicKey); 147 | expect(newSwap.state.futureAdminAccount).toEqual(newAdmin.publicKey); 148 | expect(newSwap.state.futureAdminDeadline).not.toEqual(ZERO_TS); 149 | }); 150 | 151 | it("Apply new admin", async () => { 152 | const fetchedStableSwap = await StableSwap.load( 153 | connection, 154 | stableSwapAccount.publicKey, 155 | stableSwapProgramId 156 | ); 157 | 158 | const txn = new Transaction().add( 159 | createAdminApplyNewAdminInstruction({ 160 | config: fetchedStableSwap.config, 161 | state: fetchedStableSwap.state, 162 | }) 163 | ); 164 | await sendAndConfirmTransactionWithTitle( 165 | "commit new admin", 166 | connection, 167 | txn, 168 | payer, 169 | owner 170 | ); 171 | const newSwap = await StableSwap.load( 172 | connection, 173 | stableSwap.config.swapAccount, 174 | stableSwap.config.swapProgramID 175 | ); 176 | expect(newSwap.state.adminAccount).toEqual(newAdmin.publicKey); 177 | expect(newSwap.state.futureAdminAccount).toEqual(PublicKey.default); 178 | expect(newSwap.state.futureAdminDeadline).toEqual(ZERO_TS); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /stable-swap-math/sim/simulation.py: -------------------------------------------------------------------------------- 1 | # Source from: https://github.com/curvefi/curve-contract/blob/master/tests/simulation.py 2 | 3 | class Curve: 4 | 5 | """ 6 | Python model of Curve pool math. 7 | """ 8 | 9 | def __init__(self, A, D, n, p=None, tokens=None): 10 | """ 11 | A: Amplification coefficient 12 | D: Total deposit size 13 | n: number of currencies 14 | p: target prices 15 | """ 16 | self.A = A # actually A * n ** (n - 1) because it's an invariant 17 | self.n = n 18 | self.fee = 10 ** 7 19 | if p: 20 | self.p = p 21 | else: 22 | self.p = [10 ** 18] * n 23 | if isinstance(D, list): 24 | self.x = D 25 | else: 26 | self.x = [D // n * 10 ** 18 // _p for _p in self.p] 27 | self.tokens = tokens 28 | 29 | def xp(self): 30 | return [x * p // 10 ** 18 for x, p in zip(self.x, self.p)] 31 | 32 | def D(self): 33 | """ 34 | D invariant calculation in non-overflowing integer operations 35 | iteratively 36 | 37 | A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i)) 38 | 39 | Converging solution: 40 | D[j+1] = (A * n**n * sum(x_i) - D[j]**(n+1) / (n**n prod(x_i))) / (A * n**n - 1) 41 | """ 42 | Dprev = 0 43 | xp = self.xp() 44 | S = sum(xp) 45 | D = S 46 | Ann = self.A * self.n 47 | while abs(D - Dprev) > 1: 48 | D_P = D 49 | for x in xp: 50 | D_P = D_P * D // (self.n * x) 51 | Dprev = D 52 | D = (Ann * S + D_P * self.n) * D // ((Ann - 1) * D + (self.n + 1) * D_P) 53 | 54 | return D 55 | 56 | def y(self, i, j, x): 57 | """ 58 | Calculate x[j] if one makes x[i] = x 59 | 60 | Done by solving quadratic equation iteratively. 61 | x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) 62 | x_1**2 + b*x_1 = c 63 | 64 | x_1 = (x_1**2 + c) / (2*x_1 + b) 65 | """ 66 | D = self.D() 67 | xx = self.xp() 68 | xx[i] = x # x is quantity of underlying asset brought to 1e18 precision 69 | xx = [xx[k] for k in range(self.n) if k != j] 70 | Ann = self.A * self.n 71 | c = D 72 | for y in xx: 73 | c = c * D // (y * self.n) 74 | c = c * D // (self.n * Ann) 75 | b = sum(xx) + D // Ann - D 76 | y_prev = 0 77 | y = D 78 | while abs(y - y_prev) > 1: 79 | y_prev = y 80 | y = (y ** 2 + c) // (2 * y + b) 81 | return y # the result is in underlying units too 82 | 83 | def y_D(self, i, _D): 84 | """ 85 | Calculate x[j] if one makes x[i] = x 86 | 87 | Done by solving quadratic equation iteratively. 88 | x_1**2 + x1 * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A) 89 | x_1**2 + b*x_1 = c 90 | 91 | x_1 = (x_1**2 + c) / (2*x_1 + b) 92 | """ 93 | xx = self.xp() 94 | xx = [xx[k] for k in range(self.n) if k != i] 95 | S = sum(xx) 96 | Ann = self.A * self.n 97 | c = _D 98 | for y in xx: 99 | c = c * _D // (y * self.n) 100 | c = c * _D // (self.n * Ann) 101 | b = S + _D // Ann 102 | y_prev = 0 103 | y = _D 104 | while abs(y - y_prev) > 1: 105 | y_prev = y 106 | y = (y ** 2 + c) // (2 * y + b - _D) 107 | return y # the result is in underlying units too 108 | 109 | def dy(self, i, j, dx): 110 | # dx and dy are in underlying units 111 | xp = self.xp() 112 | return xp[j] - self.y(i, j, xp[i] + dx) 113 | 114 | def exchange(self, i, j, dx): 115 | xp = self.xp() 116 | x = xp[i] + dx 117 | y = self.y(i, j, x) 118 | dy = xp[j] - y 119 | fee = dy * self.fee // 10 ** 10 120 | assert dy > 0 121 | self.x[i] = x * 10 ** 18 // self.p[i] 122 | self.x[j] = (y + fee) * 10 ** 18 // self.p[j] 123 | return dy - fee 124 | 125 | def remove_liquidity_imbalance(self, amounts): 126 | _fee = self.fee * self.n // (4 * (self.n - 1)) 127 | 128 | old_balances = self.x 129 | new_balances = self.x[:] 130 | D0 = self.D() 131 | for i in range(self.n): 132 | new_balances[i] -= amounts[i] 133 | self.x = new_balances 134 | D1 = self.D() 135 | self.x = old_balances 136 | fees = [0] * self.n 137 | for i in range(self.n): 138 | ideal_balance = D1 * old_balances[i] // D0 139 | difference = abs(ideal_balance - new_balances[i]) 140 | fees[i] = _fee * difference // 10 ** 10 141 | new_balances[i] -= fees[i] 142 | self.x = new_balances 143 | D2 = self.D() 144 | self.x = old_balances 145 | 146 | token_amount = (D0 - D2) * self.tokens // D0 147 | 148 | return token_amount 149 | 150 | def calc_withdraw_one_coin(self, token_amount, i): 151 | xp = self.xp() 152 | xp_reduced = list(xp) 153 | 154 | D0 = self.D() 155 | D1 = D0 - token_amount * D0 // self.tokens 156 | new_y = self.y_D(i, D1) 157 | 158 | fee = self.fee * self.n // (4 * (self.n - 1)) 159 | for j in range(self.n): 160 | dx_expected = 0 161 | if j == i: 162 | dx_expected = xp[j] * D1 // D0 - new_y 163 | else: 164 | dx_expected = xp[j] - xp[j] * D1 // D0 165 | xp_reduced[j] -= fee * dx_expected // 10 ** 10 166 | 167 | self.x = [x * 10 ** 18 // p for x, p in zip(xp_reduced, self.p)] 168 | dy = xp_reduced[i] - self.y_D(i, D1) - 1 # Withdraw less to account for rounding errors 169 | self.x = [x * 10 ** 18 // p for x, p in zip(xp, self.p)] 170 | dy_0 = xp[i] - new_y 171 | 172 | return dy, dy_0 - dy 173 | -------------------------------------------------------------------------------- /stable-swap-anchor/src/accounts.rs: -------------------------------------------------------------------------------- 1 | //! Accounts structs for StableSwap. 2 | 3 | use anchor_lang::prelude::*; 4 | 5 | /// Accounts for an [crate::initialize] instruction. 6 | #[derive(Accounts, Clone)] 7 | pub struct Initialize<'info> { 8 | /// The swap. 9 | #[account(signer)] 10 | pub swap: AccountInfo<'info>, 11 | /// The authority of the swap. 12 | pub swap_authority: AccountInfo<'info>, 13 | /// The admin of the swap. 14 | pub admin: AccountInfo<'info>, 15 | /// The A token of the swap. 16 | pub token_a: InitToken<'info>, 17 | /// The B token of the swap. 18 | pub token_b: InitToken<'info>, 19 | /// The pool mint of the swap. 20 | pub pool_mint: AccountInfo<'info>, 21 | /// The output account for LP tokens. 22 | pub output_lp: AccountInfo<'info>, 23 | /// The spl_token program. 24 | pub token_program: AccountInfo<'info>, 25 | } 26 | 27 | /// Accounts for a [crate::deposit] instruction. 28 | #[derive(Accounts, Clone)] 29 | pub struct Deposit<'info> { 30 | /// The context of the user. 31 | pub user: SwapUserContext<'info>, 32 | /// The A token of the swap. 33 | pub input_a: SwapToken<'info>, 34 | /// The B token of the swap. 35 | pub input_b: SwapToken<'info>, 36 | /// The pool mint of the swap. 37 | pub pool_mint: AccountInfo<'info>, 38 | /// The output account for LP tokens. 39 | pub output_lp: AccountInfo<'info>, 40 | } 41 | 42 | /// Accounts for a [crate::swap] instruction. 43 | #[derive(Accounts, Clone)] 44 | pub struct Swap<'info> { 45 | /// The context of the user. 46 | pub user: SwapUserContext<'info>, 47 | /// Accounts for input tokens. 48 | pub input: SwapToken<'info>, 49 | /// Accounts for output tokens. 50 | pub output: SwapOutput<'info>, 51 | } 52 | 53 | /// Accounts for a [crate::withdraw_one] instruction. 54 | #[derive(Accounts, Clone)] 55 | pub struct WithdrawOne<'info> { 56 | /// The context of the user. 57 | pub user: SwapUserContext<'info>, 58 | /// The pool mint of the swap. 59 | pub pool_mint: AccountInfo<'info>, 60 | /// The input (user)'s LP token account 61 | pub input_lp: AccountInfo<'info>, 62 | /// The TokenAccount holding the swap's reserves of quote tokens; i.e., the token not being withdrawn. 63 | /// 64 | /// - If withdrawing token A, this is `swap_info.token_b.reserves`. 65 | /// - If withdrawing token B, this is `swap_info.token_a.reserves`. 66 | /// 67 | /// These reserves are needed for the withdraw_one instruction since the 68 | /// StableSwap `D` invariant requires both the base and quote reserves 69 | /// to determine how many tokens are paid out to users withdrawing from 70 | /// the swap. 71 | /// 72 | /// *For more info, see [stable_swap_client::state::SwapTokenInfo::reserves].* 73 | pub quote_reserves: AccountInfo<'info>, 74 | /// Accounts for output tokens. 75 | pub output: SwapOutput<'info>, 76 | } 77 | 78 | /// Accounts for a [crate::withdraw] instruction. 79 | #[derive(Accounts, Clone)] 80 | pub struct Withdraw<'info> { 81 | /// The context of the user. 82 | pub user: SwapUserContext<'info>, 83 | /// The input account for LP tokens. 84 | pub input_lp: AccountInfo<'info>, 85 | /// The pool mint of the swap. 86 | pub pool_mint: AccountInfo<'info>, 87 | /// The A token of the swap. 88 | pub output_a: SwapOutput<'info>, 89 | /// The B token of the swap. 90 | pub output_b: SwapOutput<'info>, 91 | } 92 | 93 | /// Accounts for a [crate::set_fee_account] instruction. 94 | #[derive(Accounts, Clone)] 95 | pub struct SetFeeAccount<'info> { 96 | /// The context of the admin user 97 | pub admin_ctx: AdminUserContext<'info>, 98 | /// The new token account for fees 99 | pub fee_account: AccountInfo<'info>, 100 | } 101 | 102 | /// Accounts for a [crate::apply_new_admin] instruction. 103 | #[derive(Accounts, Clone)] 104 | pub struct CommitNewAdmin<'info> { 105 | /// The context of the admin user. 106 | pub admin_ctx: AdminUserContext<'info>, 107 | /// The account of the new admin. 108 | pub new_admin: AccountInfo<'info>, 109 | } 110 | 111 | // -------------------------------- 112 | // Various accounts 113 | // -------------------------------- 114 | 115 | /// Token accounts for initializing a [crate::SwapInfo]. 116 | #[derive(Accounts, Clone)] 117 | pub struct InitToken<'info> { 118 | /// The token account for the pool's reserves of this token. 119 | pub reserve: AccountInfo<'info>, 120 | /// The token account for the fees associated with the token. 121 | pub fees: AccountInfo<'info>, 122 | /// The mint of the token. 123 | pub mint: AccountInfo<'info>, 124 | } 125 | 126 | /// Token accounts for a [crate::swap] instruction. 127 | #[derive(Accounts, Clone)] 128 | pub struct SwapToken<'info> { 129 | /// The token account associated with the user. 130 | pub user: AccountInfo<'info>, 131 | /// The token account for the pool's reserves of this token. 132 | pub reserve: AccountInfo<'info>, 133 | } 134 | 135 | /// Token accounts for the output of a StableSwap instruction. 136 | #[derive(Accounts, Clone)] 137 | pub struct SwapOutput<'info> { 138 | /// The token accounts of the user and the token. 139 | pub user_token: SwapToken<'info>, 140 | /// The token account for the fees associated with the token. 141 | pub fees: AccountInfo<'info>, 142 | } 143 | 144 | /// Accounts for an instruction that interacts with the swap. 145 | #[derive(Accounts, Clone)] 146 | pub struct SwapUserContext<'info> { 147 | /// The spl_token program. 148 | pub token_program: AccountInfo<'info>, 149 | /// The authority of the swap. 150 | pub swap_authority: AccountInfo<'info>, 151 | /// The authority of the user. 152 | #[account(signer)] 153 | pub user_authority: AccountInfo<'info>, 154 | /// The swap. 155 | pub swap: AccountInfo<'info>, 156 | } 157 | 158 | /// Accounts for an instruction that requires admin permission. 159 | #[derive(Accounts, Clone)] 160 | pub struct AdminUserContext<'info> { 161 | /// The public key of the admin account. 162 | /// 163 | /// *Note: must be a signer.* 164 | #[account(signer)] 165 | pub admin: AccountInfo<'info>, 166 | /// The swap. 167 | pub swap: AccountInfo<'info>, 168 | } 169 | -------------------------------------------------------------------------------- /stable-swap-math/src/math.rs: -------------------------------------------------------------------------------- 1 | //! Math helpers 2 | 3 | use num_traits::ToPrimitive; 4 | use stable_swap_client::fees::Fees; 5 | 6 | const MAX: u64 = 1 << 32; 7 | const MAX_BIG: u64 = 1 << 48; 8 | const MAX_SMALL: u64 = 1 << 16; 9 | 10 | /// Multiplies two u64s then divides by the third number. 11 | /// This function attempts to use 64 bit math if possible. 12 | #[inline(always)] 13 | pub fn mul_div(a: u64, b: u64, c: u64) -> Option { 14 | if a > MAX || b > MAX { 15 | (a as u128) 16 | .checked_mul(b as u128)? 17 | .checked_div(c as u128)? 18 | .to_u64() 19 | } else { 20 | a.checked_mul(b)?.checked_div(c) 21 | } 22 | } 23 | 24 | /// Multiplies two u64s then divides by the third number. 25 | /// This assumes that a > b. 26 | #[inline(always)] 27 | pub fn mul_div_imbalanced(a: u64, b: u64, c: u64) -> Option { 28 | if a > MAX_BIG || b > MAX_SMALL { 29 | (a as u128) 30 | .checked_mul(b as u128)? 31 | .checked_div(c as u128)? 32 | .to_u64() 33 | } else { 34 | a.checked_mul(b)?.checked_div(c) 35 | } 36 | } 37 | 38 | /// Calculates fees. 39 | pub trait FeeCalculator { 40 | /// Applies the admin trade fee. 41 | fn admin_trade_fee(&self, fee_amount: u64) -> Option; 42 | /// Applies the admin withdraw fee. 43 | fn admin_withdraw_fee(&self, fee_amount: u64) -> Option; 44 | /// Applies the trade fee. 45 | fn trade_fee(&self, trade_amount: u64) -> Option; 46 | /// Applies the withdraw fee. 47 | fn withdraw_fee(&self, withdraw_amount: u64) -> Option; 48 | /// Applies the normalized trade fee. 49 | fn normalized_trade_fee(&self, n_coins: u8, amount: u64) -> Option; 50 | } 51 | 52 | impl FeeCalculator for Fees { 53 | /// Apply admin trade fee 54 | fn admin_trade_fee(&self, fee_amount: u64) -> Option { 55 | mul_div_imbalanced( 56 | fee_amount, 57 | self.admin_trade_fee_numerator, 58 | self.admin_trade_fee_denominator, 59 | ) 60 | } 61 | 62 | /// Apply admin withdraw fee 63 | fn admin_withdraw_fee(&self, fee_amount: u64) -> Option { 64 | mul_div_imbalanced( 65 | fee_amount, 66 | self.admin_withdraw_fee_numerator, 67 | self.admin_withdraw_fee_denominator, 68 | ) 69 | } 70 | 71 | /// Compute trade fee from amount 72 | fn trade_fee(&self, trade_amount: u64) -> Option { 73 | mul_div_imbalanced( 74 | trade_amount, 75 | self.trade_fee_numerator, 76 | self.trade_fee_denominator, 77 | ) 78 | } 79 | 80 | /// Compute withdraw fee from amount 81 | fn withdraw_fee(&self, withdraw_amount: u64) -> Option { 82 | mul_div_imbalanced( 83 | withdraw_amount, 84 | self.withdraw_fee_numerator, 85 | self.withdraw_fee_denominator, 86 | ) 87 | } 88 | 89 | /// Compute normalized fee for symmetric/asymmetric deposits/withdraws 90 | fn normalized_trade_fee(&self, n_coins: u8, amount: u64) -> Option { 91 | // adjusted_fee_numerator: uint256 = self.fee * N_COINS / (4 * (N_COINS - 1)) 92 | // The number 4 comes from Curve, originating from some sort of calculus 93 | // https://github.com/curvefi/curve-contract/blob/e5fb8c0e0bcd2fe2e03634135806c0f36b245511/tests/simulation.py#L124 94 | let adjusted_trade_fee_numerator = mul_div( 95 | self.trade_fee_numerator, 96 | n_coins.into(), 97 | (n_coins.checked_sub(1)?).checked_mul(4)?.into(), 98 | )?; 99 | 100 | mul_div( 101 | amount, 102 | adjusted_trade_fee_numerator, 103 | self.trade_fee_denominator, 104 | ) 105 | } 106 | } 107 | 108 | #[cfg(test)] 109 | #[allow(clippy::unwrap_used)] 110 | mod tests { 111 | use super::*; 112 | 113 | #[test] 114 | fn fee_results() { 115 | let admin_trade_fee_numerator = 1; 116 | let admin_trade_fee_denominator = 2; 117 | let admin_withdraw_fee_numerator = 3; 118 | let admin_withdraw_fee_denominator = 4; 119 | let trade_fee_numerator = 5; 120 | let trade_fee_denominator = 6; 121 | let withdraw_fee_numerator = 7; 122 | let withdraw_fee_denominator = 8; 123 | let fees = Fees { 124 | admin_trade_fee_numerator, 125 | admin_trade_fee_denominator, 126 | admin_withdraw_fee_numerator, 127 | admin_withdraw_fee_denominator, 128 | trade_fee_numerator, 129 | trade_fee_denominator, 130 | withdraw_fee_numerator, 131 | withdraw_fee_denominator, 132 | }; 133 | 134 | let trade_amount = 1_000_000_000; 135 | let expected_trade_fee = trade_amount * trade_fee_numerator / trade_fee_denominator; 136 | let trade_fee = fees.trade_fee(trade_amount).unwrap(); 137 | assert_eq!(trade_fee, expected_trade_fee); 138 | let expected_admin_trade_fee = 139 | expected_trade_fee * admin_trade_fee_numerator / admin_trade_fee_denominator; 140 | assert_eq!( 141 | fees.admin_trade_fee(trade_fee).unwrap(), 142 | expected_admin_trade_fee 143 | ); 144 | 145 | let withdraw_amount = 100_000_000_000; 146 | let expected_withdraw_fee = 147 | withdraw_amount * withdraw_fee_numerator / withdraw_fee_denominator; 148 | let withdraw_fee = fees.withdraw_fee(withdraw_amount).unwrap(); 149 | assert_eq!(withdraw_fee, expected_withdraw_fee); 150 | let expected_admin_withdraw_fee = 151 | expected_withdraw_fee * admin_withdraw_fee_numerator / admin_withdraw_fee_denominator; 152 | assert_eq!( 153 | fees.admin_withdraw_fee(expected_withdraw_fee).unwrap(), 154 | expected_admin_withdraw_fee 155 | ); 156 | 157 | let n_coins: u8 = 2; 158 | let adjusted_trade_fee_numerator: u64 = 159 | trade_fee_numerator * (n_coins as u64) / (4 * ((n_coins as u64) - 1)); 160 | let expected_normalized_fee = 161 | trade_amount * adjusted_trade_fee_numerator / trade_fee_denominator; 162 | assert_eq!( 163 | fees.normalized_trade_fee(n_coins, trade_amount).unwrap(), 164 | expected_normalized_fee 165 | ); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /stable-swap-math/sim/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Simulations of the StableSwap invariant compared to Curve's reference implementation. 2 | 3 | use pyo3::prelude::*; 4 | use pyo3::types::PyTuple; 5 | use std::fs::File; 6 | use std::io::prelude::*; 7 | 8 | pub const MODEL_FEE_NUMERATOR: u64 = 10000000; 9 | pub const MODEL_FEE_DENOMINATOR: u64 = 10000000000; 10 | 11 | const DEFAULT_POOL_TOKENS: u64 = 0; 12 | const DEFAULT_TARGET_PRICE: u128 = 1000000000000000000; 13 | const FILE_NAME: &str = "simulation.py"; 14 | const FILE_PATH: &str = "sim/simulation.py"; 15 | const MODULE_NAME: &str = "simulation"; 16 | 17 | pub struct Model { 18 | pub py_src: String, 19 | pub amp_factor: u64, 20 | pub balances: Vec, 21 | pub n_coins: u8, 22 | pub target_prices: Vec, 23 | pub pool_tokens: u64, 24 | } 25 | 26 | impl Model { 27 | /// Constructs a new [`Model`]. 28 | pub fn new(amp_factor: u64, balances: Vec, n_coins: u8) -> Model { 29 | let src_file = File::open(FILE_PATH); 30 | let mut src_file = match src_file { 31 | Ok(file) => file, 32 | Err(error) => { 33 | panic!("{:?}\n Please run `curl -L 34 | https://raw.githubusercontent.com/curvefi/curve-contract/master/tests/simulation.py > sim/simulation.py`", error) 35 | } 36 | }; 37 | let mut src_content = String::new(); 38 | let _ = src_file.read_to_string(&mut src_content); 39 | 40 | Self { 41 | py_src: src_content, 42 | amp_factor, 43 | balances, 44 | n_coins, 45 | target_prices: vec![DEFAULT_TARGET_PRICE, DEFAULT_TARGET_PRICE], 46 | pool_tokens: DEFAULT_POOL_TOKENS, 47 | } 48 | } 49 | 50 | pub fn new_with_pool_tokens( 51 | amp_factor: u64, 52 | balances: Vec, 53 | n_coins: u8, 54 | pool_token_amount: u64, 55 | ) -> Model { 56 | let src_file = File::open(FILE_PATH); 57 | let mut src_file = match src_file { 58 | Ok(file) => file, 59 | Err(error) => { 60 | panic!("{:?}\n Please run `curl -L 61 | https://raw.githubusercontent.com/curvefi/curve-contract/master/tests/simulation.py > sim/simulation.py`", error) 62 | } 63 | }; 64 | let mut src_content = String::new(); 65 | let _ = src_file.read_to_string(&mut src_content); 66 | 67 | Self { 68 | py_src: src_content, 69 | amp_factor, 70 | balances, 71 | n_coins, 72 | target_prices: vec![DEFAULT_TARGET_PRICE, DEFAULT_TARGET_PRICE], 73 | pool_tokens: pool_token_amount, 74 | } 75 | } 76 | 77 | pub fn sim_d(&self) -> u128 { 78 | let gil = Python::acquire_gil(); 79 | return self 80 | .call0(gil.python(), "D") 81 | .unwrap() 82 | .extract(gil.python()) 83 | .unwrap(); 84 | } 85 | 86 | pub fn sim_dy(&self, i: u128, j: u128, dx: u128) -> u128 { 87 | let gil = Python::acquire_gil(); 88 | return self 89 | .call1(gil.python(), "dy", (i, j, dx)) 90 | .unwrap() 91 | .extract(gil.python()) 92 | .unwrap(); 93 | } 94 | 95 | pub fn sim_exchange(&self, i: u128, j: u128, dx: u128) -> u64 { 96 | let gil = Python::acquire_gil(); 97 | return self 98 | .call1(gil.python(), "exchange", (i, j, dx)) 99 | .unwrap() 100 | .extract(gil.python()) 101 | .unwrap(); 102 | } 103 | 104 | pub fn sim_xp(&self) -> Vec { 105 | let gil = Python::acquire_gil(); 106 | return self 107 | .call0(gil.python(), "xp") 108 | .unwrap() 109 | .extract(gil.python()) 110 | .unwrap(); 111 | } 112 | 113 | pub fn sim_y(&self, i: u128, j: u128, x: u64) -> u128 { 114 | let gil = Python::acquire_gil(); 115 | return self 116 | .call1(gil.python(), "y", (i, j, x)) 117 | .unwrap() 118 | .extract(gil.python()) 119 | .unwrap(); 120 | } 121 | 122 | pub fn sim_y_d(&self, i: u128, d: u128) -> u128 { 123 | let gil = Python::acquire_gil(); 124 | return self 125 | .call1(gil.python(), "y_D", (i, d)) 126 | .unwrap() 127 | .extract(gil.python()) 128 | .unwrap(); 129 | } 130 | 131 | pub fn sim_remove_liquidity_imbalance(&self, amounts: Vec) -> u128 { 132 | let gil = Python::acquire_gil(); 133 | return self 134 | .call1( 135 | gil.python(), 136 | "remove_liquidity_imbalance", 137 | PyTuple::new(gil.python(), amounts.to_vec()), 138 | ) 139 | .unwrap() 140 | .extract(gil.python()) 141 | .unwrap(); 142 | } 143 | 144 | pub fn sim_calc_withdraw_one_coin(&self, token_amount: u64, i: u128) -> (u64, u64) { 145 | let gil = Python::acquire_gil(); 146 | return self 147 | .call1(gil.python(), "calc_withdraw_one_coin", (token_amount, i)) 148 | .unwrap() 149 | .extract(gil.python()) 150 | .unwrap(); 151 | } 152 | 153 | fn call0(&self, py: Python, method_name: &str) -> Result { 154 | let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap(); 155 | let model = sim 156 | .getattr("Curve")? 157 | .call1(( 158 | self.amp_factor, 159 | self.balances.to_vec(), 160 | self.n_coins, 161 | self.target_prices.to_vec(), 162 | self.pool_tokens, 163 | )) 164 | .unwrap() 165 | .to_object(py); 166 | let py_ret = model.as_ref(py).call_method0(method_name); 167 | self.extract_py_ret(py, py_ret) 168 | } 169 | 170 | fn call1( 171 | &self, 172 | py: Python, 173 | method_name: &str, 174 | args: impl IntoPy>, 175 | ) -> Result { 176 | let sim = PyModule::from_code(py, &self.py_src, FILE_NAME, MODULE_NAME).unwrap(); 177 | let model = sim 178 | .getattr("Curve")? 179 | .call1(( 180 | self.amp_factor, 181 | self.balances.to_vec(), 182 | self.n_coins, 183 | self.target_prices.to_vec(), 184 | self.pool_tokens, 185 | )) 186 | .unwrap() 187 | .to_object(py); 188 | let py_ret = model.as_ref(py).call_method1(method_name, args); 189 | self.extract_py_ret(py, py_ret) 190 | } 191 | 192 | fn extract_py_ret(&self, py: Python, ret: PyResult<&PyAny>) -> Result { 193 | match ret { 194 | Ok(v) => v.extract(), 195 | Err(e) => { 196 | e.print_and_set_sys_last_vars(py); 197 | panic!("Python exeuction failed.") 198 | } 199 | } 200 | } 201 | 202 | pub fn print_src(&self) { 203 | println!("{}", self.py_src); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /stable-swap-math/src/price.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for getting the virtual price of a pool. 2 | 3 | use crate::{bn::U192, curve::StableSwap}; 4 | 5 | /// Utilities for calculating the virtual price of a Saber LP token. 6 | /// 7 | /// This is especially useful for if you want to use a Saber LP token as collateral. 8 | /// 9 | /// # Calculating liquidation value 10 | /// 11 | /// To use a Saber LP token as collateral, you will need to fetch the prices 12 | /// of both of the tokens in the pool and get the min of the two. Then, 13 | /// use the [SaberSwap::calculate_virtual_price_of_pool_tokens] function to 14 | /// get the virtual price. 15 | /// 16 | /// This virtual price is resilient to manipulations of the LP token price. 17 | /// 18 | /// Hence, `min_lp_price = min_value * virtual_price`. 19 | /// 20 | /// # Additional Reading 21 | /// - [Chainlink: Using Chainlink Oracles to Securely Utilize Curve LP Pools](https://blog.chain.link/using-chainlink-oracles-to-securely-utilize-curve-lp-pools/) 22 | #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] 23 | pub struct SaberSwap { 24 | /// Initial amp factor, or `A`. 25 | /// 26 | /// See [`StableSwap::compute_amp_factor`]. 27 | pub initial_amp_factor: u64, 28 | /// Target amp factor, or `A`. 29 | /// 30 | /// See [`StableSwap::compute_amp_factor`]. 31 | pub target_amp_factor: u64, 32 | /// Current timestmap. 33 | pub current_ts: i64, 34 | /// Start ramp timestamp for calculating the amp factor, or `A`. 35 | /// 36 | /// See [`StableSwap::compute_amp_factor`]. 37 | pub start_ramp_ts: i64, 38 | /// Stop ramp timestamp for calculating the amp factor, or `A`. 39 | /// 40 | /// See [`StableSwap::compute_amp_factor`]. 41 | pub stop_ramp_ts: i64, 42 | 43 | /// Total supply of LP tokens. 44 | /// 45 | /// This is `pool_mint.supply`, where `pool_mint` is an SPL Token Mint. 46 | pub lp_mint_supply: u64, 47 | /// Amount of token A. 48 | /// 49 | /// This is `token_a.reserve.amount`, where `token_a.reserve` is an SPL Token Token Account. 50 | pub token_a_reserve: u64, 51 | /// Amount of token B. 52 | /// 53 | /// This is `token_b.reserve.amount`, where `token_b.reserve` is an SPL Token Token Account. 54 | pub token_b_reserve: u64, 55 | } 56 | 57 | impl From<&SaberSwap> for crate::curve::StableSwap { 58 | fn from(swap: &SaberSwap) -> Self { 59 | crate::curve::StableSwap::new( 60 | swap.initial_amp_factor, 61 | swap.target_amp_factor, 62 | swap.current_ts, 63 | swap.start_ramp_ts, 64 | swap.stop_ramp_ts, 65 | ) 66 | } 67 | } 68 | 69 | impl SaberSwap { 70 | /// Calculates the amount of pool tokens represented by the given amount of virtual tokens. 71 | /// 72 | /// A virtual token is the denomination of virtual price. For example, if there is a virtual price of 1.04 73 | /// on USDC-USDT LP, then 1 virtual token maps to 1/1.04 USDC-USDT LP tokens. 74 | /// 75 | /// This is useful for building assets that are backed by LP tokens. 76 | /// An example of this is [Cashio](https://github.com/CashioApp/cashio), which 77 | /// allows users to mint $CASH tokens based on the virtual price of underlying LP tokens. 78 | /// 79 | /// # Arguments 80 | /// 81 | /// - `virtual_amount` - The number of "virtual" underlying tokens. 82 | pub fn calculate_pool_tokens_from_virtual_amount(&self, virtual_amount: u64) -> Option { 83 | U192::from(virtual_amount) 84 | .checked_mul(self.lp_mint_supply.into())? 85 | .checked_div(self.compute_d()?)? 86 | .to_u64() 87 | } 88 | 89 | /// Calculates the virtual price of the given amount of pool tokens. 90 | /// 91 | /// The virtual price is defined as the current price of the pool LP token 92 | /// relative to the underlying pool assets. 93 | /// 94 | /// The virtual price in the StableSwap algorithm is obtained through taking the invariance 95 | /// of the pool, which by default takes every token as valued at 1.00 of the underlying. 96 | /// You can get the virtual price of each pool by calling this function 97 | /// for it.[^chainlink] 98 | /// 99 | /// [^chainlink]: Source: 100 | pub fn calculate_virtual_price_of_pool_tokens(&self, pool_token_amount: u64) -> Option { 101 | self.compute_d()? 102 | .checked_mul(pool_token_amount.into())? 103 | .checked_div(self.lp_mint_supply.into())? 104 | .to_u64() 105 | } 106 | 107 | /// Computes D, which is the virtual price times the total supply of the pool. 108 | pub fn compute_d(&self) -> Option { 109 | let calculator = StableSwap::from(self); 110 | calculator.compute_d(self.token_a_reserve, self.token_b_reserve) 111 | } 112 | } 113 | 114 | #[cfg(test)] 115 | #[allow(clippy::unwrap_used)] 116 | mod tests { 117 | use proptest::prelude::*; 118 | 119 | use super::SaberSwap; 120 | 121 | prop_compose! { 122 | fn arb_swap_unsafe()( 123 | token_a_reserve in 1_u64..=u64::MAX, 124 | token_b_reserve in 1_u64..=u64::MAX, 125 | lp_mint_supply in 1_u64..=u64::MAX 126 | ) -> SaberSwap { 127 | SaberSwap { 128 | initial_amp_factor: 1, 129 | target_amp_factor: 1, 130 | current_ts: 1, 131 | start_ramp_ts: 1, 132 | stop_ramp_ts: 1, 133 | 134 | lp_mint_supply, 135 | token_a_reserve, 136 | token_b_reserve 137 | } 138 | } 139 | } 140 | 141 | prop_compose! { 142 | #[allow(clippy::integer_arithmetic)] 143 | fn arb_token_amount(decimals: u8)( 144 | amount in 1_u64..=(u64::MAX / 10u64.pow(decimals.into())), 145 | ) -> u64 { 146 | amount 147 | } 148 | } 149 | 150 | prop_compose! { 151 | fn arb_swap_reserves()( 152 | decimals in 0_u8..=19_u8, 153 | swap in arb_swap_unsafe() 154 | ) ( 155 | token_a_reserve in arb_token_amount(decimals), 156 | token_b_reserve in arb_token_amount(decimals), 157 | swap in Just(swap) 158 | ) -> SaberSwap { 159 | SaberSwap { 160 | token_a_reserve, 161 | token_b_reserve, 162 | ..swap 163 | } 164 | } 165 | } 166 | 167 | prop_compose! { 168 | fn arb_swap()( 169 | swap in arb_swap_reserves() 170 | ) ( 171 | // targeting a maximum virtual price of 4 172 | // anything higher than this is a bit ridiculous 173 | lp_mint_supply in 1_u64.max((swap.token_a_reserve.min(swap.token_b_reserve)) / 4)..=(swap.token_a_reserve.checked_add(swap.token_b_reserve).unwrap_or(u64::MAX)), 174 | swap in Just(swap) 175 | ) -> SaberSwap { 176 | SaberSwap { 177 | lp_mint_supply, 178 | ..swap 179 | } 180 | } 181 | } 182 | 183 | proptest! { 184 | #[test] 185 | fn test_invertible( 186 | swap in arb_swap(), 187 | amount in 0_u64..=u64::MAX 188 | ) { 189 | let maybe_virt = swap.calculate_virtual_price_of_pool_tokens(amount); 190 | if maybe_virt.is_none() { 191 | // ignore virt calculation failures, since they won't be used in production 192 | return Ok(()); 193 | } 194 | let virt = maybe_virt.unwrap(); 195 | if virt == 0 { 196 | // this case doesn't matter because it's a noop. 197 | return Ok(()); 198 | } 199 | 200 | let result_lp = swap.calculate_pool_tokens_from_virtual_amount(virt).unwrap(); 201 | 202 | // tokens should never be created. 203 | prop_assert!(result_lp <= amount); 204 | 205 | // these numbers should be very close to each other. 206 | prop_assert!(1.0_f64 - (result_lp as f64) / (amount as f64) < 0.001_f64); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /stable-swap-client/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error types 2 | 3 | use num_derive::FromPrimitive; 4 | use solana_program::{ 5 | decode_error::DecodeError, 6 | msg, 7 | program_error::{PrintProgramError, ProgramError}, 8 | }; 9 | use thiserror::Error; 10 | 11 | /// Errors that may be returned by the StableSwap program. 12 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Error, FromPrimitive)] 13 | pub enum SwapError { 14 | /// The account cannot be initialized because it is already being used. 15 | #[error("Swap account already in use")] 16 | AlreadyInUse, 17 | /// The address of the admin fee account is incorrect. 18 | #[error("Address of the admin fee account is incorrect")] 19 | InvalidAdmin, 20 | /// The owner of the input isn't set to the program address generated by the program. 21 | #[error("Input account owner is not the program address")] 22 | InvalidOwner, 23 | /// The owner of the pool token output is set to the program address generated by the program. 24 | #[error("Output pool account owner cannot be the program address")] 25 | InvalidOutputOwner, 26 | /// The program address provided doesn't match the value generated by the program. 27 | #[error("Invalid program address generated from nonce and key")] 28 | InvalidProgramAddress, 29 | /// The deserialization of the account returned something besides State::Mint. 30 | #[error("Deserialized account is not an SPL Token mint")] 31 | ExpectedMint, 32 | /// The deserialization of the account returned something besides State::Account. 33 | #[error("Deserialized account is not an SPL Token account")] 34 | ExpectedAccount, 35 | /// The pool supply is empty. 36 | #[error("Pool token supply is 0")] 37 | EmptyPool, 38 | /// The input token account is empty. 39 | #[error("Input token account empty")] 40 | EmptySupply, 41 | /// The pool token mint has a non-zero supply. 42 | #[error("Pool token mint has a non-zero supply")] 43 | InvalidSupply, 44 | /// The provided token account has a delegate. 45 | #[error("Token account has a delegate")] 46 | InvalidDelegate, 47 | /// The input token is invalid for swap. 48 | #[error("InvalidInput")] 49 | InvalidInput, 50 | /// Address of the provided swap token account is incorrect. 51 | #[error("Address of the provided swap token account is incorrect")] 52 | IncorrectSwapAccount, 53 | /// Address of the provided token mint is incorrect 54 | #[error("Address of the provided token mint is incorrect")] 55 | IncorrectMint, 56 | /// The calculation failed. 57 | #[error("CalculationFailure")] 58 | CalculationFailure, 59 | /// Invalid instruction number passed in. 60 | #[error("Invalid instruction")] 61 | InvalidInstruction, 62 | /// Swap input token accounts have the same mint 63 | #[error("Swap input token accounts have the same mint")] 64 | RepeatedMint, 65 | /// Swap instruction exceeds desired slippage limit 66 | #[error("Swap instruction exceeds desired slippage limit")] 67 | ExceededSlippage, 68 | /// The provided token account has a close authority. 69 | #[error("Token account has a close authority")] 70 | InvalidCloseAuthority, 71 | /// The pool token mint has a freeze authority. 72 | #[error("Pool token mint has a freeze authority")] 73 | InvalidFreezeAuthority, 74 | /// ConversionFailure 75 | #[error("Conversion to u64 failed with an overflow or underflow")] 76 | ConversionFailure, 77 | /// Unauthorized 78 | #[error("Account is not authorized to execute this instruction")] 79 | Unauthorized, 80 | /// Swap pool is paused 81 | #[error("Swap pool is paused")] 82 | IsPaused, 83 | /// Amp. coefficient change is within min ramp duration 84 | #[error("Ramp is locked in this time period")] 85 | RampLocked, 86 | /// Insufficient ramp time for the ramp operation 87 | #[error("Insufficient ramp time")] 88 | InsufficientRampTime, 89 | /// Active admin transfer in progress 90 | #[error("Active admin transfer in progress")] 91 | ActiveTransfer, 92 | /// No active admin transfer in progress 93 | #[error("No active admin transfer in progress")] 94 | NoActiveTransfer, 95 | /// Admin transfer deadline exceeded 96 | #[error("Admin transfer deadline exceeded")] 97 | AdminDeadlineExceeded, 98 | /// Token mint decimals must be the same. 99 | #[error("Token mints must have same decimals")] 100 | MismatchedDecimals, 101 | } 102 | 103 | impl From for ProgramError { 104 | fn from(e: SwapError) -> Self { 105 | ProgramError::Custom(e as u32) 106 | } 107 | } 108 | 109 | impl DecodeError for SwapError { 110 | fn type_of() -> &'static str { 111 | "Swap Error" 112 | } 113 | } 114 | 115 | impl PrintProgramError for SwapError { 116 | fn print(&self) 117 | where 118 | E: 'static 119 | + std::error::Error 120 | + DecodeError 121 | + PrintProgramError 122 | + num_traits::FromPrimitive, 123 | { 124 | match self { 125 | SwapError::AlreadyInUse => msg!("Error: Swap account already in use"), 126 | SwapError::InvalidAdmin => { 127 | msg!("Error: Address of the admin fee account is incorrect") 128 | } 129 | SwapError::InvalidOwner => { 130 | msg!("Error: The input account owner is not the program address") 131 | } 132 | SwapError::InvalidOutputOwner => { 133 | msg!("Error: Output pool account owner cannot be the program address") 134 | } 135 | SwapError::InvalidProgramAddress => { 136 | msg!("Error: Invalid program address generated from nonce and key") 137 | } 138 | SwapError::ExpectedMint => { 139 | msg!("Error: Deserialized account is not an SPL Token mint") 140 | } 141 | SwapError::ExpectedAccount => { 142 | msg!("Error: Deserialized account is not an SPL Token account") 143 | } 144 | SwapError::EmptySupply => msg!("Error: Input token account empty"), 145 | SwapError::EmptyPool => msg!("Error: Pool token supply is 0"), 146 | SwapError::InvalidSupply => msg!("Error: Pool token mint has a non-zero supply"), 147 | SwapError::RepeatedMint => msg!("Error: Swap input token accounts have the same mint"), 148 | SwapError::InvalidDelegate => msg!("Error: Token account has a delegate"), 149 | SwapError::InvalidInput => msg!("Error: InvalidInput"), 150 | SwapError::IncorrectSwapAccount => { 151 | msg!("Error: Address of the provided swap token account is incorrect") 152 | } 153 | SwapError::IncorrectMint => { 154 | msg!("Error: Address of the provided token mint is incorrect") 155 | } 156 | SwapError::CalculationFailure => msg!("Error: CalculationFailure"), 157 | SwapError::InvalidInstruction => msg!("Error: InvalidInstruction"), 158 | SwapError::ExceededSlippage => { 159 | msg!("Error: Swap instruction exceeds desired slippage limit") 160 | } 161 | SwapError::InvalidCloseAuthority => msg!("Error: Token account has a close authority"), 162 | SwapError::InvalidFreezeAuthority => { 163 | msg!("Error: Pool token mint has a freeze authority") 164 | } 165 | SwapError::ConversionFailure => msg!("Error: Conversion to or from u64 failed"), 166 | SwapError::Unauthorized => { 167 | msg!("Error: Account is not authorized to execute this instruction") 168 | } 169 | SwapError::IsPaused => msg!("Error: Swap pool is paused"), 170 | SwapError::RampLocked => msg!("Error: Ramp is locked in this time period"), 171 | SwapError::InsufficientRampTime => msg!("Error: Insufficient ramp time"), 172 | SwapError::ActiveTransfer => msg!("Error: Active admin transfer in progress"), 173 | SwapError::NoActiveTransfer => msg!("Error: No active admin transfer in progress"), 174 | SwapError::AdminDeadlineExceeded => msg!("Error: Admin transfer deadline exceeded"), 175 | SwapError::MismatchedDecimals => msg!("Error: Token mints must have same decimals"), 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Saber Labs 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /stable-swap-client/src/state.rs: -------------------------------------------------------------------------------- 1 | //! State transition types 2 | 3 | use crate::fees::Fees; 4 | use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs}; 5 | use solana_program::{ 6 | program_error::ProgramError, 7 | program_pack::{IsInitialized, Pack, Sealed}, 8 | pubkey::Pubkey, 9 | }; 10 | 11 | /// Program states. 12 | #[repr(C)] 13 | #[derive(Clone, Copy, Debug, PartialEq)] 14 | pub struct SwapInfo { 15 | /// Initialized state 16 | pub is_initialized: bool, 17 | 18 | /// Paused state 19 | pub is_paused: bool, 20 | 21 | /// Nonce used in program address 22 | /// The program address is created deterministically with the nonce, 23 | /// swap program id, and swap account pubkey. This program address has 24 | /// authority over the swap's token A account, token B account, and pool 25 | /// token mint. 26 | pub nonce: u8, 27 | 28 | /// Initial amplification coefficient (A) 29 | pub initial_amp_factor: u64, 30 | /// Target amplification coefficient (A) 31 | pub target_amp_factor: u64, 32 | /// Ramp A start timestamp 33 | pub start_ramp_ts: i64, 34 | /// Ramp A stop timestamp 35 | pub stop_ramp_ts: i64, 36 | 37 | /// Deadline to transfer admin control to future_admin_key 38 | pub future_admin_deadline: i64, 39 | /// Public key of the admin account to be applied 40 | pub future_admin_key: Pubkey, 41 | /// Public key of admin account to execute admin instructions 42 | pub admin_key: Pubkey, 43 | 44 | /// Token A 45 | pub token_a: SwapTokenInfo, 46 | /// Token B 47 | pub token_b: SwapTokenInfo, 48 | 49 | /// Pool tokens are issued when A or B tokens are deposited. 50 | /// Pool tokens can be withdrawn back to the original A or B token. 51 | pub pool_mint: Pubkey, 52 | /// Fees 53 | pub fees: Fees, 54 | } 55 | 56 | /// Information about one of the tokens. 57 | #[repr(C)] 58 | #[derive(Clone, Copy, Debug, PartialEq)] 59 | pub struct SwapTokenInfo { 60 | /// Token account for pool reserves 61 | pub reserves: Pubkey, 62 | /// Mint information for the token 63 | pub mint: Pubkey, 64 | /// Public key of the admin token account to receive trading and / or withdrawal fees for token 65 | pub admin_fees: Pubkey, 66 | /// The index of the token. Token A = 0, Token B = 1. 67 | pub index: u8, 68 | } 69 | 70 | impl Sealed for SwapInfo {} 71 | impl IsInitialized for SwapInfo { 72 | fn is_initialized(&self) -> bool { 73 | self.is_initialized 74 | } 75 | } 76 | 77 | impl Pack for SwapInfo { 78 | const LEN: usize = 395; 79 | 80 | /// Unpacks a byte buffer into a [SwapInfo](struct.SwapInfo.html). 81 | fn unpack_from_slice(input: &[u8]) -> Result { 82 | let input = array_ref![input, 0, 395]; 83 | #[allow(clippy::ptr_offset_with_cast)] 84 | let ( 85 | is_initialized, 86 | is_paused, 87 | nonce, 88 | initial_amp_factor, 89 | target_amp_factor, 90 | start_ramp_ts, 91 | stop_ramp_ts, 92 | future_admin_deadline, 93 | future_admin_key, 94 | admin_key, 95 | token_a, 96 | token_b, 97 | pool_mint, 98 | token_a_mint, 99 | token_b_mint, 100 | admin_fee_key_a, 101 | admin_fee_key_b, 102 | fees, 103 | ) = array_refs![input, 1, 1, 1, 8, 8, 8, 8, 8, 32, 32, 32, 32, 32, 32, 32, 32, 32, 64]; 104 | Ok(Self { 105 | is_initialized: match is_initialized { 106 | [0] => false, 107 | [1] => true, 108 | _ => return Err(ProgramError::InvalidAccountData), 109 | }, 110 | is_paused: match is_paused { 111 | [0] => false, 112 | [1] => true, 113 | _ => return Err(ProgramError::InvalidAccountData), 114 | }, 115 | nonce: nonce[0], 116 | initial_amp_factor: u64::from_le_bytes(*initial_amp_factor), 117 | target_amp_factor: u64::from_le_bytes(*target_amp_factor), 118 | start_ramp_ts: i64::from_le_bytes(*start_ramp_ts), 119 | stop_ramp_ts: i64::from_le_bytes(*stop_ramp_ts), 120 | future_admin_deadline: i64::from_le_bytes(*future_admin_deadline), 121 | future_admin_key: Pubkey::new_from_array(*future_admin_key), 122 | admin_key: Pubkey::new_from_array(*admin_key), 123 | token_a: SwapTokenInfo { 124 | reserves: Pubkey::new_from_array(*token_a), 125 | mint: Pubkey::new_from_array(*token_a_mint), 126 | admin_fees: Pubkey::new_from_array(*admin_fee_key_a), 127 | index: 0, 128 | }, 129 | token_b: SwapTokenInfo { 130 | reserves: Pubkey::new_from_array(*token_b), 131 | mint: Pubkey::new_from_array(*token_b_mint), 132 | admin_fees: Pubkey::new_from_array(*admin_fee_key_b), 133 | index: 1, 134 | }, 135 | pool_mint: Pubkey::new_from_array(*pool_mint), 136 | fees: Fees::unpack_from_slice(fees)?, 137 | }) 138 | } 139 | 140 | fn pack_into_slice(&self, output: &mut [u8]) { 141 | let output = array_mut_ref![output, 0, 395]; 142 | let ( 143 | is_initialized, 144 | is_paused, 145 | nonce, 146 | initial_amp_factor, 147 | target_amp_factor, 148 | start_ramp_ts, 149 | stop_ramp_ts, 150 | future_admin_deadline, 151 | future_admin_key, 152 | admin_key, 153 | token_a, 154 | token_b, 155 | pool_mint, 156 | token_a_mint, 157 | token_b_mint, 158 | admin_fee_key_a, 159 | admin_fee_key_b, 160 | fees, 161 | ) = mut_array_refs![output, 1, 1, 1, 8, 8, 8, 8, 8, 32, 32, 32, 32, 32, 32, 32, 32, 32, 64]; 162 | is_initialized[0] = self.is_initialized as u8; 163 | is_paused[0] = self.is_paused as u8; 164 | nonce[0] = self.nonce; 165 | *initial_amp_factor = self.initial_amp_factor.to_le_bytes(); 166 | *target_amp_factor = self.target_amp_factor.to_le_bytes(); 167 | *start_ramp_ts = self.start_ramp_ts.to_le_bytes(); 168 | *stop_ramp_ts = self.stop_ramp_ts.to_le_bytes(); 169 | *future_admin_deadline = self.future_admin_deadline.to_le_bytes(); 170 | future_admin_key.copy_from_slice(self.future_admin_key.as_ref()); 171 | admin_key.copy_from_slice(self.admin_key.as_ref()); 172 | token_a.copy_from_slice(self.token_a.reserves.as_ref()); 173 | token_b.copy_from_slice(self.token_b.reserves.as_ref()); 174 | pool_mint.copy_from_slice(self.pool_mint.as_ref()); 175 | token_a_mint.copy_from_slice(self.token_a.mint.as_ref()); 176 | token_b_mint.copy_from_slice(self.token_b.mint.as_ref()); 177 | admin_fee_key_a.copy_from_slice(self.token_a.admin_fees.as_ref()); 178 | admin_fee_key_b.copy_from_slice(self.token_b.admin_fees.as_ref()); 179 | self.fees.pack_into_slice(&mut fees[..]); 180 | } 181 | } 182 | 183 | #[cfg(test)] 184 | #[allow(clippy::unwrap_used)] 185 | mod tests { 186 | use super::*; 187 | 188 | #[test] 189 | fn test_swap_info_packing() { 190 | let nonce = 255; 191 | let initial_amp_factor: u64 = 1; 192 | let target_amp_factor: u64 = 1; 193 | let start_ramp_ts: i64 = i64::MAX; 194 | let stop_ramp_ts: i64 = i64::MAX; 195 | let future_admin_deadline: i64 = i64::MAX; 196 | let future_admin_key_raw = [1u8; 32]; 197 | let admin_key_raw = [2u8; 32]; 198 | let token_a_raw = [3u8; 32]; 199 | let token_b_raw = [4u8; 32]; 200 | let pool_mint_raw = [5u8; 32]; 201 | let token_a_mint_raw = [6u8; 32]; 202 | let token_b_mint_raw = [7u8; 32]; 203 | let admin_fee_key_a_raw = [8u8; 32]; 204 | let admin_fee_key_b_raw = [9u8; 32]; 205 | let admin_key = Pubkey::new_from_array(admin_key_raw); 206 | let future_admin_key = Pubkey::new_from_array(future_admin_key_raw); 207 | let token_a = Pubkey::new_from_array(token_a_raw); 208 | let token_b = Pubkey::new_from_array(token_b_raw); 209 | let pool_mint = Pubkey::new_from_array(pool_mint_raw); 210 | let token_a_mint = Pubkey::new_from_array(token_a_mint_raw); 211 | let token_b_mint = Pubkey::new_from_array(token_b_mint_raw); 212 | let admin_fee_key_a = Pubkey::new_from_array(admin_fee_key_a_raw); 213 | let admin_fee_key_b = Pubkey::new_from_array(admin_fee_key_b_raw); 214 | let admin_trade_fee_numerator = 1; 215 | let admin_trade_fee_denominator = 2; 216 | let admin_withdraw_fee_numerator = 3; 217 | let admin_withdraw_fee_denominator = 4; 218 | let trade_fee_numerator = 5; 219 | let trade_fee_denominator = 6; 220 | let withdraw_fee_numerator = 7; 221 | let withdraw_fee_denominator = 8; 222 | let fees = Fees { 223 | admin_trade_fee_numerator, 224 | admin_trade_fee_denominator, 225 | admin_withdraw_fee_numerator, 226 | admin_withdraw_fee_denominator, 227 | trade_fee_numerator, 228 | trade_fee_denominator, 229 | withdraw_fee_numerator, 230 | withdraw_fee_denominator, 231 | }; 232 | 233 | let is_initialized = true; 234 | let is_paused = false; 235 | let swap_info = SwapInfo { 236 | is_initialized, 237 | is_paused, 238 | nonce, 239 | initial_amp_factor, 240 | target_amp_factor, 241 | start_ramp_ts, 242 | stop_ramp_ts, 243 | future_admin_deadline, 244 | future_admin_key, 245 | admin_key, 246 | token_a: SwapTokenInfo { 247 | reserves: token_a, 248 | mint: token_a_mint, 249 | admin_fees: admin_fee_key_a, 250 | index: 0, 251 | }, 252 | token_b: SwapTokenInfo { 253 | reserves: token_b, 254 | mint: token_b_mint, 255 | admin_fees: admin_fee_key_b, 256 | index: 1, 257 | }, 258 | pool_mint, 259 | fees, 260 | }; 261 | 262 | let mut packed = [0u8; SwapInfo::LEN]; 263 | SwapInfo::pack(swap_info, &mut packed).unwrap(); 264 | let unpacked = SwapInfo::unpack(&packed).unwrap(); 265 | assert_eq!(swap_info, unpacked); 266 | 267 | let mut packed = vec![ 268 | 1_u8, // is_initialized 269 | 0_u8, // is_paused 270 | nonce, 271 | ]; 272 | packed.extend_from_slice(&initial_amp_factor.to_le_bytes()); 273 | packed.extend_from_slice(&target_amp_factor.to_le_bytes()); 274 | packed.extend_from_slice(&start_ramp_ts.to_le_bytes()); 275 | packed.extend_from_slice(&stop_ramp_ts.to_le_bytes()); 276 | packed.extend_from_slice(&future_admin_deadline.to_le_bytes()); 277 | packed.extend_from_slice(&future_admin_key_raw); 278 | packed.extend_from_slice(&admin_key_raw); 279 | packed.extend_from_slice(&token_a_raw); 280 | packed.extend_from_slice(&token_b_raw); 281 | packed.extend_from_slice(&pool_mint_raw); 282 | packed.extend_from_slice(&token_a_mint_raw); 283 | packed.extend_from_slice(&token_b_mint_raw); 284 | packed.extend_from_slice(&admin_fee_key_a_raw); 285 | packed.extend_from_slice(&admin_fee_key_b_raw); 286 | packed.extend_from_slice(&admin_trade_fee_numerator.to_le_bytes()); 287 | packed.extend_from_slice(&admin_trade_fee_denominator.to_le_bytes()); 288 | packed.extend_from_slice(&admin_withdraw_fee_numerator.to_le_bytes()); 289 | packed.extend_from_slice(&admin_withdraw_fee_denominator.to_le_bytes()); 290 | packed.extend_from_slice(&trade_fee_numerator.to_le_bytes()); 291 | packed.extend_from_slice(&trade_fee_denominator.to_le_bytes()); 292 | packed.extend_from_slice(&withdraw_fee_numerator.to_le_bytes()); 293 | packed.extend_from_slice(&withdraw_fee_denominator.to_le_bytes()); 294 | let unpacked = SwapInfo::unpack(&packed).unwrap(); 295 | assert_eq!(swap_info, unpacked); 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /stable-swap-anchor/src/instructions.rs: -------------------------------------------------------------------------------- 1 | //! Instruction builders and invokers for StableSwap instructions. 2 | 3 | use crate::*; 4 | use anchor_lang::{prelude::*, solana_program}; 5 | 6 | /// Creates and invokes a [stable_swap_client::instruction::initialize] instruction. 7 | /// 8 | /// # Arguments 9 | /// 10 | /// See [stable_swap_client::instruction::InitializeData]. 11 | /// 12 | /// * `nonce` - The nonce used to generate the swap_authority. 13 | /// * `amp_factor` - Amplification factor. 14 | /// * `fees` - Initial fees. 15 | pub fn initialize<'a, 'b, 'c, 'info>( 16 | ctx: CpiContext<'a, 'b, 'c, 'info, Initialize<'info>>, 17 | nonce: u8, 18 | amp_factor: u64, 19 | fees: stable_swap_client::fees::Fees, 20 | ) -> Result<()> { 21 | let ix = stable_swap_client::instruction::initialize( 22 | // token program ID is verified by the stable swap program 23 | ctx.accounts.token_program.key, 24 | ctx.accounts.swap.key, 25 | ctx.accounts.swap_authority.key, 26 | ctx.accounts.admin.key, 27 | ctx.accounts.token_a.fees.key, 28 | ctx.accounts.token_b.fees.key, 29 | ctx.accounts.token_a.mint.key, 30 | ctx.accounts.token_a.reserve.key, 31 | ctx.accounts.token_b.mint.key, 32 | ctx.accounts.token_b.reserve.key, 33 | ctx.accounts.pool_mint.key, 34 | ctx.accounts.output_lp.key, 35 | nonce, 36 | amp_factor, 37 | fees, 38 | )?; 39 | solana_program::program::invoke_signed( 40 | &ix, 41 | &[ 42 | ctx.program, 43 | ctx.accounts.swap, 44 | ctx.accounts.swap_authority, 45 | ctx.accounts.admin, 46 | ctx.accounts.token_a.fees, 47 | ctx.accounts.token_b.fees, 48 | ctx.accounts.token_a.mint, 49 | ctx.accounts.token_a.reserve, 50 | ctx.accounts.token_b.mint, 51 | ctx.accounts.token_b.reserve, 52 | ctx.accounts.pool_mint, 53 | ctx.accounts.output_lp, 54 | ctx.accounts.token_program, 55 | ], 56 | ctx.signer_seeds, 57 | )?; 58 | Ok(()) 59 | } 60 | 61 | /// Creates and invokes a [stable_swap_client::instruction::deposit] instruction. 62 | /// 63 | /// # Arguments 64 | /// 65 | /// See [stable_swap_client::instruction::DepositData]. 66 | /// 67 | /// * `token_a_amount` - Amount of tokens of [`Deposit::input_a`] to deposit. 68 | /// * `token_b_amount` - Amount of tokens of [`Deposit::input_b`] to deposit. 69 | /// * `min_mint_amount` - Minimum amount of LP tokens to mint. 70 | pub fn deposit<'a, 'b, 'c, 'info>( 71 | ctx: CpiContext<'a, 'b, 'c, 'info, Deposit<'info>>, 72 | token_a_amount: u64, 73 | token_b_amount: u64, 74 | min_mint_amount: u64, 75 | ) -> Result<()> { 76 | let ix = stable_swap_client::instruction::deposit( 77 | // token program ID is verified by the stable swap program 78 | ctx.accounts.user.token_program.key, 79 | ctx.accounts.user.swap.key, 80 | ctx.accounts.user.swap_authority.key, 81 | ctx.accounts.user.user_authority.key, 82 | ctx.accounts.input_a.user.key, 83 | ctx.accounts.input_b.user.key, 84 | ctx.accounts.input_a.reserve.key, 85 | ctx.accounts.input_b.reserve.key, 86 | ctx.accounts.pool_mint.key, 87 | ctx.accounts.output_lp.key, 88 | token_a_amount, 89 | token_b_amount, 90 | min_mint_amount, 91 | )?; 92 | solana_program::program::invoke_signed( 93 | &ix, 94 | &[ 95 | ctx.program, 96 | ctx.accounts.user.token_program, 97 | ctx.accounts.user.swap, 98 | ctx.accounts.user.swap_authority, 99 | ctx.accounts.user.user_authority, 100 | // deposit 101 | ctx.accounts.input_a.user, 102 | ctx.accounts.input_b.user, 103 | ctx.accounts.input_a.reserve, 104 | ctx.accounts.input_b.reserve, 105 | ctx.accounts.pool_mint, 106 | ctx.accounts.output_lp, 107 | ], 108 | ctx.signer_seeds, 109 | )?; 110 | Ok(()) 111 | } 112 | 113 | /// Creates and invokes a [stable_swap_client::instruction::swap] instruction. 114 | /// 115 | /// # Arguments 116 | /// 117 | /// See [stable_swap_client::instruction::SwapData]. 118 | /// 119 | /// * `amount_in` - Amount of [`Swap::input`] tokens to swap. 120 | /// * `minimum_amount_out` - Minimum amount of [`Swap::output`] tokens to receive. 121 | pub fn swap<'a, 'b, 'c, 'info>( 122 | ctx: CpiContext<'a, 'b, 'c, 'info, Swap<'info>>, 123 | amount_in: u64, 124 | minimum_amount_out: u64, 125 | ) -> Result<()> { 126 | let ix = stable_swap_client::instruction::swap( 127 | ctx.accounts.user.token_program.key, 128 | ctx.accounts.user.swap.key, 129 | ctx.accounts.user.swap_authority.key, 130 | ctx.accounts.user.user_authority.key, 131 | ctx.accounts.input.user.key, 132 | ctx.accounts.input.reserve.key, 133 | ctx.accounts.output.user_token.reserve.key, 134 | ctx.accounts.output.user_token.user.key, 135 | ctx.accounts.output.fees.key, 136 | amount_in, 137 | minimum_amount_out, 138 | )?; 139 | solana_program::program::invoke_signed( 140 | &ix, 141 | &[ 142 | ctx.program, 143 | ctx.accounts.user.token_program, 144 | ctx.accounts.user.swap, 145 | ctx.accounts.user.swap_authority, 146 | ctx.accounts.user.user_authority, 147 | // swap 148 | ctx.accounts.input.user, 149 | ctx.accounts.input.reserve, 150 | ctx.accounts.output.user_token.reserve, 151 | ctx.accounts.output.user_token.user, 152 | ctx.accounts.output.fees, 153 | ], 154 | ctx.signer_seeds, 155 | )?; 156 | Ok(()) 157 | } 158 | 159 | /// Creates and invokes a [stable_swap_client::instruction::withdraw_one] instruction. 160 | /// 161 | /// # Arguments 162 | /// 163 | /// See [stable_swap_client::instruction::WithdrawOneData]. 164 | /// 165 | /// * `pool_token_amount` - Amount of LP tokens to withdraw. 166 | /// * `minimum_token_amount` - Minimum amount of tokens of [`WithdrawOne::output`] to withdraw. 167 | pub fn withdraw_one<'a, 'b, 'c, 'info>( 168 | ctx: CpiContext<'a, 'b, 'c, 'info, WithdrawOne<'info>>, 169 | pool_token_amount: u64, 170 | minimum_token_amount: u64, 171 | ) -> Result<()> { 172 | let ix = stable_swap_client::instruction::withdraw_one( 173 | ctx.accounts.user.token_program.key, 174 | ctx.accounts.user.swap.key, 175 | ctx.accounts.user.swap_authority.key, 176 | ctx.accounts.user.user_authority.key, 177 | ctx.accounts.pool_mint.key, 178 | ctx.accounts.input_lp.key, 179 | ctx.accounts.output.user_token.reserve.key, 180 | ctx.accounts.quote_reserves.key, 181 | ctx.accounts.output.user_token.user.key, 182 | ctx.accounts.output.fees.key, 183 | pool_token_amount, 184 | minimum_token_amount, 185 | )?; 186 | solana_program::program::invoke_signed( 187 | &ix, 188 | &[ 189 | ctx.program, 190 | ctx.accounts.user.token_program, 191 | ctx.accounts.user.swap, 192 | ctx.accounts.user.swap_authority, 193 | ctx.accounts.user.user_authority, 194 | // withdraw_one 195 | ctx.accounts.pool_mint, 196 | ctx.accounts.input_lp, 197 | ctx.accounts.output.user_token.reserve, 198 | ctx.accounts.quote_reserves, 199 | ctx.accounts.output.user_token.user, 200 | ctx.accounts.output.fees, 201 | ], 202 | ctx.signer_seeds, 203 | )?; 204 | Ok(()) 205 | } 206 | 207 | /// Creates and invokes a [stable_swap_client::instruction::withdraw] instruction. 208 | /// 209 | /// # Arguments 210 | /// 211 | /// See [stable_swap_client::instruction::WithdrawData]. 212 | /// 213 | /// * `pool_token_amount` - Amount of LP tokens to withdraw. 214 | /// * `minimum_token_a_amount` - Minimum amount of tokens of [`Withdraw::output_a`] to withdraw. 215 | /// * `minimum_token_b_amount` - Minimum amount of tokens of [`Withdraw::output_b`] to withdraw. 216 | pub fn withdraw<'a, 'b, 'c, 'info>( 217 | ctx: CpiContext<'a, 'b, 'c, 'info, Withdraw<'info>>, 218 | pool_token_amount: u64, 219 | minimum_token_a_amount: u64, 220 | minimum_token_b_amount: u64, 221 | ) -> Result<()> { 222 | let ix = stable_swap_client::instruction::withdraw( 223 | // token program ID is verified by the stable swap program 224 | ctx.accounts.user.token_program.key, 225 | ctx.accounts.user.swap.key, 226 | ctx.accounts.user.swap_authority.key, 227 | ctx.accounts.user.user_authority.key, 228 | // accounts 229 | ctx.accounts.pool_mint.key, 230 | ctx.accounts.input_lp.key, 231 | ctx.accounts.output_a.user_token.reserve.key, 232 | ctx.accounts.output_b.user_token.reserve.key, 233 | ctx.accounts.output_a.user_token.user.key, 234 | ctx.accounts.output_b.user_token.user.key, 235 | ctx.accounts.output_a.fees.key, 236 | ctx.accounts.output_b.fees.key, 237 | pool_token_amount, 238 | minimum_token_a_amount, 239 | minimum_token_b_amount, 240 | )?; 241 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 242 | Ok(()) 243 | } 244 | 245 | /// Creates and invokes a [stable_swap_client::instruction::ramp_a] instruction. 246 | /// 247 | /// # Arguments 248 | /// 249 | /// See [stable_swap_client::instruction::RampAData]. 250 | /// 251 | /// * `target_amp` - Target amplification factor to ramp to. 252 | /// * `stop_ramp_ts` - Timestamp when ramp up/down should stop. 253 | pub fn ramp_a<'a, 'b, 'c, 'info>( 254 | ctx: CpiContext<'a, 'b, 'c, 'info, AdminUserContext<'info>>, 255 | target_amp: u64, 256 | stop_ramp_ts: i64, 257 | ) -> Result<()> { 258 | let ix = stable_swap_client::instruction::ramp_a( 259 | ctx.accounts.swap.key, 260 | ctx.accounts.admin.key, 261 | target_amp, 262 | stop_ramp_ts, 263 | )?; 264 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 265 | Ok(()) 266 | } 267 | 268 | /// Creates and invokes a [stable_swap_client::instruction::stop_ramp_a] instruction. 269 | pub fn stop_ramp_a<'a, 'b, 'c, 'info>( 270 | ctx: CpiContext<'a, 'b, 'c, 'info, AdminUserContext<'info>>, 271 | ) -> Result<()> { 272 | let ix = stable_swap_client::instruction::stop_ramp_a( 273 | ctx.accounts.swap.key, 274 | ctx.accounts.admin.key, 275 | )?; 276 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 277 | Ok(()) 278 | } 279 | 280 | /// Creates and invokes a [stable_swap_client::instruction::pause] instruction. 281 | pub fn pause<'a, 'b, 'c, 'info>( 282 | ctx: CpiContext<'a, 'b, 'c, 'info, AdminUserContext<'info>>, 283 | ) -> Result<()> { 284 | let ix = stable_swap_client::instruction::pause(ctx.accounts.swap.key, ctx.accounts.admin.key)?; 285 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 286 | Ok(()) 287 | } 288 | 289 | /// Creates and invokes a [stable_swap_client::instruction::unpause] instruction. 290 | pub fn unpause<'a, 'b, 'c, 'info>( 291 | ctx: CpiContext<'a, 'b, 'c, 'info, AdminUserContext<'info>>, 292 | ) -> Result<()> { 293 | let ix = 294 | stable_swap_client::instruction::unpause(ctx.accounts.swap.key, ctx.accounts.admin.key)?; 295 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 296 | Ok(()) 297 | } 298 | 299 | /// Creates and invokes a [stable_swap_client::instruction::apply_new_admin] instruction. 300 | pub fn apply_new_admin<'a, 'b, 'c, 'info>( 301 | ctx: CpiContext<'a, 'b, 'c, 'info, AdminUserContext<'info>>, 302 | ) -> Result<()> { 303 | let ix = stable_swap_client::instruction::apply_new_admin( 304 | ctx.accounts.swap.key, 305 | ctx.accounts.admin.key, 306 | )?; 307 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 308 | Ok(()) 309 | } 310 | 311 | /// Creates and invokes a [stable_swap_client::instruction::commit_new_admin] instruction 312 | /// 313 | /// # Arguments 314 | /// 315 | /// * `new_admin` - Public key of the new admin. 316 | pub fn commit_new_admin<'a, 'b, 'c, 'info>( 317 | ctx: CpiContext<'a, 'b, 'c, 'info, CommitNewAdmin<'info>>, 318 | ) -> Result<()> { 319 | let admin_ctx = &ctx.accounts.admin_ctx; 320 | let ix = stable_swap_client::instruction::commit_new_admin( 321 | admin_ctx.swap.key, 322 | admin_ctx.admin.key, 323 | ctx.accounts.new_admin.key, 324 | )?; 325 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 326 | Ok(()) 327 | } 328 | 329 | /// Creates and invokes a [stable_swap_client::instruction::set_fee_account] instruction. 330 | pub fn set_fee_account<'a, 'b, 'c, 'info>( 331 | ctx: CpiContext<'a, 'b, 'c, 'info, SetFeeAccount<'info>>, 332 | ) -> Result<()> { 333 | let ix = stable_swap_client::instruction::set_fee_account( 334 | ctx.accounts.admin_ctx.swap.key, 335 | ctx.accounts.admin_ctx.admin.key, 336 | ctx.accounts.fee_account.to_account_info().key, 337 | )?; 338 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 339 | Ok(()) 340 | } 341 | 342 | /// Creates and invokes a [stable_swap_client::instruction::set_new_fees] instruction. 343 | /// 344 | /// # Arguments 345 | /// 346 | /// * `fees` - new [`stable_swap_client::fees::Fees`]. 347 | pub fn set_new_fees<'a, 'b, 'c, 'info>( 348 | ctx: CpiContext<'a, 'b, 'c, 'info, AdminUserContext<'info>>, 349 | fees: stable_swap_client::fees::Fees, 350 | ) -> Result<()> { 351 | let ix = stable_swap_client::instruction::set_new_fees( 352 | ctx.accounts.swap.key, 353 | ctx.accounts.admin.key, 354 | fees, 355 | )?; 356 | solana_program::program::invoke_signed(&ix, &ctx.to_account_infos(), ctx.signer_seeds)?; 357 | Ok(()) 358 | } 359 | -------------------------------------------------------------------------------- /stable-swap-program/sdk/src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import type { Network, Provider } from "@saberhq/solana-contrib"; 2 | import { 3 | DEFAULT_NETWORK_CONFIG_MAP, 4 | SignerWallet, 5 | } from "@saberhq/solana-contrib"; 6 | import type { ISeedPoolAccountsFn } from "@saberhq/stableswap-sdk"; 7 | import { 8 | DEFAULT_TOKEN_DECIMALS, 9 | deployNewSwap, 10 | RECOMMENDED_FEES, 11 | } from "@saberhq/stableswap-sdk"; 12 | import type { TokenAccountData } from "@saberhq/token-utils"; 13 | import { 14 | ASSOCIATED_TOKEN_PROGRAM_ID, 15 | SPLToken, 16 | TOKEN_PROGRAM_ID, 17 | u64, 18 | } from "@saberhq/token-utils"; 19 | import type { Signer } from "@solana/web3.js"; 20 | import { 21 | Account, 22 | Connection, 23 | Keypair, 24 | LAMPORTS_PER_SOL, 25 | PublicKey, 26 | } from "@solana/web3.js"; 27 | import base58 from "bs58"; 28 | import * as fs from "fs/promises"; 29 | import * as os from "os"; 30 | import path from "path"; 31 | import yargs from "yargs"; 32 | import { hideBin } from "yargs/helpers"; 33 | 34 | import { deployTestTokens } from "../../test/deployTestTokens"; 35 | 36 | const DEFAULT_AMP_FACTOR = 100; 37 | export const DEFAULT_INITIAL_TOKEN_A_AMOUNT = 38 | 1_000_000 * Math.pow(10, DEFAULT_TOKEN_DECIMALS); 39 | export const DEFAULT_INITIAL_TOKEN_B_AMOUNT = 40 | 1_000_000 * Math.pow(10, DEFAULT_TOKEN_DECIMALS); 41 | 42 | const readKeyfile = async (path: string): Promise => 43 | Keypair.fromSecretKey( 44 | new Uint8Array(JSON.parse(await fs.readFile(path, "utf-8")) as number[]) 45 | ); 46 | 47 | const run = async ({ 48 | provider, 49 | programID, 50 | adminAccount, 51 | outfile, 52 | 53 | ampFactor, 54 | swapAccountSigner, 55 | poolTokenMintSigner, 56 | initialLiquidityProvider, 57 | useAssociatedAccountForInitialLP, 58 | tokenAMint, 59 | tokenBMint, 60 | seedPoolAccounts, 61 | 62 | minterPrivateKey, 63 | }: { 64 | provider: Provider; 65 | programID: PublicKey; 66 | adminAccount: PublicKey; 67 | outfile: string; 68 | ampFactor: number; 69 | 70 | swapAccountSigner?: Signer; 71 | poolTokenMintSigner?: Signer; 72 | initialLiquidityProvider?: PublicKey; 73 | useAssociatedAccountForInitialLP?: boolean; 74 | tokenAMint: PublicKey; 75 | tokenBMint: PublicKey; 76 | seedPoolAccounts: ISeedPoolAccountsFn; 77 | minterPrivateKey?: string; 78 | }) => { 79 | const fees = RECOMMENDED_FEES; 80 | const { swap: newSwap } = await deployNewSwap({ 81 | provider, 82 | swapProgramID: programID, 83 | adminAccount, 84 | tokenAMint, 85 | tokenBMint, 86 | ampFactor: new u64(ampFactor), 87 | fees, 88 | 89 | swapAccountSigner, 90 | poolTokenMintSigner, 91 | initialLiquidityProvider, 92 | useAssociatedAccountForInitialLP, 93 | seedPoolAccounts, 94 | }); 95 | 96 | const accounts = { 97 | ...(minterPrivateKey 98 | ? { 99 | MinterPrivateKey: minterPrivateKey, 100 | } 101 | : {}), 102 | 103 | TokenAMint: tokenAMint.toString(), 104 | TokenBMint: tokenBMint.toString(), 105 | SwapAddress: newSwap.config.swapAccount.toString(), 106 | ProgramID: newSwap.config.swapProgramID.toString(), 107 | Fees: fees, 108 | AdminAccount: adminAccount.toString(), 109 | LPTokenMint: newSwap.state.poolTokenMint.toString(), 110 | AdminFeeAccountA: newSwap.state.tokenA.adminFeeAccount.toString(), 111 | AdminFeeAccountB: newSwap.state.tokenB.adminFeeAccount.toString(), 112 | }; 113 | 114 | // write the file 115 | const jsonRepr = JSON.stringify(accounts, null, 2); 116 | await fs.mkdir(path.dirname(outfile), { recursive: true }); 117 | await fs.writeFile(outfile, jsonRepr); 118 | console.log("Swap deploy successful! Info:"); 119 | console.log(jsonRepr); 120 | console.log(`File written to ${outfile}.`); 121 | }; 122 | 123 | export default async (): Promise => { 124 | await yargs(hideBin(process.argv)) 125 | .option("cluster", { 126 | alias: "c", 127 | type: "string", 128 | description: "Solana Cluster", 129 | }) 130 | .choices("cluster", Object.keys(DEFAULT_NETWORK_CONFIG_MAP)) 131 | .demandOption("cluster") 132 | 133 | .command( 134 | "deploy-pool", 135 | "Deploys a new StableSwap pool.", 136 | // eslint-disable-next-line @typescript-eslint/no-empty-function 137 | (y) => 138 | y 139 | .option("admin_account", { 140 | type: "string", 141 | description: "Admin account public key.", 142 | }) 143 | .demandOption("admin_account") 144 | 145 | .option("initial_amp_factor", { 146 | type: "number", 147 | description: "The initial amp factor to set for the pool.", 148 | }) 149 | .default("initial_amp_factor", DEFAULT_AMP_FACTOR) 150 | .option("initial_liquidity_provider_keyfile", { 151 | type: "string", 152 | description: 153 | "Keyfile of the initial liquidity provider. This account should possess Token A and Token B.", 154 | }) 155 | .option("initial_token_a_amount", { 156 | type: "number", 157 | description: "Initial amount of token A", 158 | }) 159 | .default("initial_token_a_amount", DEFAULT_INITIAL_TOKEN_A_AMOUNT) 160 | .option("initial_token_b_amount", { 161 | type: "number", 162 | description: "Initial amount of token B", 163 | }) 164 | .default("initial_token_b_amount", DEFAULT_INITIAL_TOKEN_B_AMOUNT) 165 | 166 | .option("token_a_mint", { 167 | type: "string", 168 | description: "Mint of Token A.", 169 | }) 170 | .option("token_b_mint", { 171 | type: "string", 172 | description: "Mint of Token B.", 173 | }) 174 | .option("deploy_test_tokens", { 175 | type: "boolean", 176 | description: "Deploys test tokens. This cannot be used on mainnet.", 177 | }) 178 | .option("payer_keyfile", { 179 | type: "string", 180 | description: "Path to the JSON private key of the payer account.", 181 | }) 182 | .option("request_sol_airdrop", { 183 | type: "boolean", 184 | description: 185 | "If specified, requests an airdrop of SOL to the payer account.", 186 | }) 187 | 188 | .option("swap_account_keyfile", { 189 | type: "string", 190 | description: "Path to the JSON private key of the swap account.", 191 | }) 192 | .option("pool_token_mint_keyfile", { 193 | type: "string", 194 | description: 195 | "Path to the JSON private key of the mint of the pool token.", 196 | }) 197 | 198 | .option("program_id", { 199 | type: "string", 200 | description: "The program ID of the swap.", 201 | }) 202 | .demandOption("program_id") 203 | 204 | .option("outfile", { 205 | type: "string", 206 | description: "Path to where the accounts file should be written.", 207 | }), 208 | // eslint-disable-next-line @typescript-eslint/no-misused-promises 209 | async ({ 210 | cluster, 211 | admin_account, 212 | initial_liquidity_provider_keyfile, 213 | outfile: maybeOutfile, 214 | payer_keyfile, 215 | request_sol_airdrop, 216 | 217 | swap_account_keyfile, 218 | pool_token_mint_keyfile, 219 | 220 | initial_amp_factor, 221 | program_id, 222 | token_a_mint, 223 | token_b_mint, 224 | initial_token_a_amount, 225 | initial_token_b_amount, 226 | deploy_test_tokens, 227 | }): Promise => { 228 | const swapAccountSigner = swap_account_keyfile 229 | ? await readKeyfile(swap_account_keyfile) 230 | : undefined; 231 | const poolTokenMintSigner = pool_token_mint_keyfile 232 | ? await readKeyfile(pool_token_mint_keyfile) 233 | : undefined; 234 | 235 | const outfile = 236 | maybeOutfile ?? 237 | `${os.homedir()}/stableswap_deployments/${cluster}/pool-${ 238 | swapAccountSigner?.publicKey.toString() ?? 239 | (Math.random() * 100).toString() 240 | }.json`; 241 | if (!maybeOutfile) { 242 | console.warn(`--outfile not specified. Defaulting to ${outfile}`); 243 | } 244 | 245 | const payerSigner = payer_keyfile 246 | ? await readKeyfile(payer_keyfile) 247 | : Keypair.generate(); 248 | 249 | console.log(`Deploying to cluster ${cluster}`); 250 | const connection = new Connection( 251 | DEFAULT_NETWORK_CONFIG_MAP[cluster as Network].endpoint 252 | ); 253 | const provider = new SignerWallet(payerSigner).createProvider( 254 | connection 255 | ); 256 | 257 | if (!payer_keyfile) { 258 | if (cluster === "mainnet-beta") { 259 | console.error("Must specify `payer_keyfile` on mainnet."); 260 | return; 261 | } 262 | console.warn( 263 | "`payer_keyfile` not specified. Generating a new keypair." 264 | ); 265 | } 266 | 267 | if (request_sol_airdrop || !payer_keyfile) { 268 | if (cluster === "mainnet-beta") { 269 | console.error("Cannot request an airdrop of SOL on mainnet."); 270 | return; 271 | } 272 | console.log( 273 | `Requesting an airdrop of SOL to ${payerSigner.publicKey.toString()}` 274 | ); 275 | const txSig = await connection.requestAirdrop( 276 | payerSigner.publicKey, 277 | 10 * LAMPORTS_PER_SOL 278 | ); 279 | await connection.confirmTransaction(txSig); 280 | } 281 | 282 | // set up tokens 283 | let tokenAMint: PublicKey; 284 | let tokenBMint: PublicKey; 285 | let seedPoolAccounts: ISeedPoolAccountsFn | undefined = undefined; 286 | let minterPrivateKey: string | undefined = undefined; 287 | 288 | const shouldDeployTestTokens = 289 | deploy_test_tokens ?? 290 | (!(token_a_mint && token_b_mint) && cluster !== "mainnet-beta"); 291 | if (shouldDeployTestTokens && !deploy_test_tokens) { 292 | console.warn( 293 | "Token A and B mints not both specified. Defaulting to deploying test tokens." 294 | ); 295 | } 296 | 297 | if (shouldDeployTestTokens) { 298 | if (token_a_mint || token_b_mint) { 299 | console.error( 300 | "Mint cannot be specified with `--deploy_test_tokens`." 301 | ); 302 | return; 303 | } 304 | const testTokens = await deployTestTokens({ 305 | provider, 306 | initialTokenAAmount: initial_token_a_amount, 307 | initialTokenBAmount: initial_token_b_amount, 308 | }); 309 | tokenAMint = testTokens.mintA; 310 | tokenBMint = testTokens.mintB; 311 | seedPoolAccounts = testTokens.seedPoolAccounts; 312 | minterPrivateKey = base58.encode(testTokens.minterSigner.secretKey); 313 | } else if (!token_a_mint || !token_b_mint) { 314 | console.error("`--token_a_mint` and `--token_b_mint` are required."); 315 | return; 316 | } else { 317 | tokenAMint = new PublicKey(token_a_mint); 318 | tokenBMint = new PublicKey(token_b_mint); 319 | } 320 | 321 | // set up initial LP 322 | let initialLiquidityProvider: PublicKey | undefined; 323 | if (initial_liquidity_provider_keyfile) { 324 | const initialLiquidityProviderKP = await readKeyfile( 325 | initial_liquidity_provider_keyfile 326 | ); 327 | initialLiquidityProvider = initialLiquidityProviderKP.publicKey; 328 | const [sourceAccountA, sourceAccountB] = (await Promise.all( 329 | ([tokenAMint, tokenBMint] as const).map( 330 | async (mint) => 331 | await SPLToken.getAssociatedTokenAddress( 332 | ASSOCIATED_TOKEN_PROGRAM_ID, 333 | TOKEN_PROGRAM_ID, 334 | mint, 335 | initialLiquidityProviderKP.publicKey 336 | ) 337 | ) 338 | )) as [PublicKey, PublicKey]; 339 | 340 | const [infoA, infoB] = (await Promise.all( 341 | [ 342 | [tokenAMint, sourceAccountA] as const, 343 | [tokenBMint, sourceAccountB] as const, 344 | ].map(async ([mint, source]) => 345 | new SPLToken( 346 | connection, 347 | mint, 348 | TOKEN_PROGRAM_ID, 349 | new Account(payerSigner.secretKey) 350 | ).getAccountInfo(source) 351 | ) 352 | )) as [TokenAccountData, TokenAccountData]; 353 | 354 | // check balances for seed 355 | if (infoA.amount.lt(new u64(initial_token_a_amount))) { 356 | console.error( 357 | `Token A balance too low for LP ${initialLiquidityProvider.toString()}` 358 | ); 359 | process.exit(1); 360 | } 361 | if (infoB.amount.lt(new u64(initial_token_b_amount))) { 362 | console.error( 363 | `Token B balance too low for LP ${initialLiquidityProvider.toString()}` 364 | ); 365 | process.exit(1); 366 | } 367 | 368 | // seed accounts 369 | seedPoolAccounts = ({ tokenAAccount, tokenBAccount }) => { 370 | return { 371 | instructions: [ 372 | SPLToken.createTransferInstruction( 373 | TOKEN_PROGRAM_ID, 374 | sourceAccountA, 375 | tokenAAccount, 376 | initialLiquidityProviderKP.publicKey, 377 | [new Account(initialLiquidityProviderKP.secretKey)], 378 | initial_token_a_amount 379 | ), 380 | SPLToken.createTransferInstruction( 381 | TOKEN_PROGRAM_ID, 382 | sourceAccountB, 383 | tokenBAccount, 384 | initialLiquidityProviderKP.publicKey, 385 | [new Account(initialLiquidityProviderKP.secretKey)], 386 | initial_token_b_amount 387 | ), 388 | ], 389 | signers: [initialLiquidityProviderKP], 390 | }; 391 | }; 392 | } else if (!shouldDeployTestTokens) { 393 | console.error( 394 | "No initial LP provided, but there is also no test token deployment." 395 | ); 396 | process.exit(1); 397 | } 398 | 399 | if (!seedPoolAccounts) { 400 | console.error("Seed pool accounts not created."); 401 | process.exit(1); 402 | } 403 | 404 | try { 405 | const programID = new PublicKey(program_id); 406 | console.log( 407 | `Deploying new swap with program ID ${programID.toString()}` 408 | ); 409 | 410 | await run({ 411 | provider, 412 | programID, 413 | adminAccount: new PublicKey(admin_account), 414 | outfile, 415 | 416 | ampFactor: initial_amp_factor, 417 | swapAccountSigner, 418 | poolTokenMintSigner, 419 | initialLiquidityProvider, 420 | useAssociatedAccountForInitialLP: true, 421 | tokenAMint, 422 | tokenBMint, 423 | seedPoolAccounts, 424 | minterPrivateKey, 425 | }).catch((err) => { 426 | console.error("Error deploying new swap."); 427 | console.error(err); 428 | process.exit(1); 429 | }); 430 | } catch (e) { 431 | if (e instanceof Error && e.message.includes("ENOENT")) { 432 | console.error( 433 | `The program deployment info for cluster ${cluster} was not found.`, 434 | e 435 | ); 436 | } else { 437 | console.error(`Could not open deployment info`, e); 438 | } 439 | } 440 | } 441 | ) 442 | 443 | .demandCommand() 444 | .help().argv; 445 | }; 446 | --------------------------------------------------------------------------------