├── .artifacts_sdk_update ├── live_factory.wasm ├── old_phoenix_factory.wasm ├── old_phoenix_multihop.wasm ├── old_phoenix_pool.wasm ├── old_phoenix_pool_stable.wasm ├── old_phoenix_stake.wasm ├── old_phoenix_stake_rewards.optimized.wasm ├── old_phoenix_stake_rewards.wasm ├── old_phoenix_vesting.wasm └── old_soroban_token_contract.wasm ├── .artifacts_stake_migration_test └── old_phoenix_stake.wasm ├── .github └── workflows │ ├── basic.yml │ ├── codecov.yml │ └── release.yml ├── .gitignore ├── .wasm_binaries_mainnet ├── live_factory.wasm ├── live_multihop.wasm ├── live_pho_usdc_pool.wasm ├── live_pho_usdc_stake.wasm ├── live_token_contract.wasm ├── live_vesting.wasm ├── live_xlm_usdc_pool.wasm └── live_xlm_usdc_stake.wasm ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── MIGRATION.md ├── Makefile ├── README.md ├── contracts ├── factory │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── storage.rs │ │ ├── tests.rs │ │ ├── tests │ │ ├── admin_change.rs │ │ ├── config.rs │ │ ├── queries.rs │ │ └── setup.rs │ │ └── utils.rs ├── multihop │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── storage.rs │ │ ├── tests.rs │ │ ├── tests │ │ ├── admin_change.rs │ │ ├── query.rs │ │ ├── setup.rs │ │ └── swap.rs │ │ └── utils.rs ├── pool │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── storage.rs │ │ ├── tests.rs │ │ └── tests │ │ ├── admin_change.rs │ │ ├── config.rs │ │ ├── liquidity.rs │ │ ├── setup.rs │ │ ├── stake_deployment.rs │ │ └── swap.rs ├── pool_stable │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── math.rs │ │ ├── storage.rs │ │ ├── tests.rs │ │ └── tests │ │ ├── admin_change.rs │ │ ├── config.rs │ │ ├── liquidity.rs │ │ ├── queries.rs │ │ ├── setup.rs │ │ ├── stake_deployment.rs │ │ └── swap.rs ├── stake │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── contract.rs │ │ ├── distribution.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── msg.rs │ │ ├── storage.rs │ │ ├── tests.rs │ │ └── tests │ │ ├── admin_change.rs │ │ ├── bond.rs │ │ ├── distribution.rs │ │ ├── migration_test.sh │ │ └── setup.rs ├── token │ ├── Cargo.lock │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── admin.rs │ │ ├── allowance.rs │ │ ├── balance.rs │ │ ├── contract.rs │ │ ├── lib.rs │ │ ├── metadata.rs │ │ ├── storage_types.rs │ │ └── test.rs ├── trader │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── contract.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── storage.rs │ │ ├── tests.rs │ │ └── tests │ │ ├── admin_change.rs │ │ ├── msgs.rs │ │ └── setup.rs └── vesting │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ ├── contract.rs │ ├── error.rs │ ├── lib.rs │ ├── storage.rs │ ├── tests.rs │ ├── tests │ ├── admin_change.rs │ ├── claim.rs │ ├── instantiate.rs │ ├── minter.rs │ └── setup.rs │ └── utils.rs ├── docs ├── VAR_MoonBite_240103_OfficialR.pdf └── architecture.md ├── packages ├── curve │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ └── lib.rs ├── decimal │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ │ ├── decimal.rs │ │ ├── decimal256.rs │ │ └── lib.rs └── phoenix │ ├── Cargo.toml │ ├── Makefile │ ├── README.md │ └── src │ ├── lib.rs │ ├── ttl.rs │ └── utils.rs ├── rust-toolchain.toml └── scripts ├── deploy.sh ├── extend-queries.sh ├── testnet_migration.sh ├── update.sh ├── upgrade_mainnet.sh └── vesting_deploy.sh /.artifacts_sdk_update/live_factory.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/live_factory.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_phoenix_factory.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_phoenix_factory.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_phoenix_multihop.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_phoenix_multihop.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_phoenix_pool.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_phoenix_pool.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_phoenix_pool_stable.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_phoenix_pool_stable.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_phoenix_stake.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_phoenix_stake.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_phoenix_stake_rewards.optimized.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_phoenix_stake_rewards.optimized.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_phoenix_stake_rewards.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_phoenix_stake_rewards.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_phoenix_vesting.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_phoenix_vesting.wasm -------------------------------------------------------------------------------- /.artifacts_sdk_update/old_soroban_token_contract.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_sdk_update/old_soroban_token_contract.wasm -------------------------------------------------------------------------------- /.artifacts_stake_migration_test/old_phoenix_stake.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.artifacts_stake_migration_test/old_phoenix_stake.wasm -------------------------------------------------------------------------------- /.github/workflows/basic.yml: -------------------------------------------------------------------------------- 1 | on: [pull_request] 2 | 3 | name: Basic 4 | 5 | jobs: 6 | build: 7 | name: Build binaries 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | rust-version: [1.81.0] 12 | steps: 13 | - name: Checkout sources 14 | uses: actions/checkout@v4 15 | - name: Install stable toolchain 16 | uses: dtolnay/rust-toolchain@stable 17 | with: 18 | toolchain: ${{ matrix.rust-version }} 19 | targets: wasm32-unknown-unknown 20 | - name: Add wasm32 target 21 | run: rustup target add wasm32-unknown-unknown 22 | - name: Build 23 | run: make build 24 | 25 | test: 26 | needs: build 27 | name: Test Suite 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | rust-version: [1.81.0] 32 | steps: 33 | - name: Checkout sources 34 | uses: actions/checkout@v4 35 | - name: Install stable toolchain 36 | uses: dtolnay/rust-toolchain@stable 37 | with: 38 | toolchain: ${{ matrix.rust-version }} 39 | targets: wasm32-unknown-unknown 40 | - name: Add wasm32 target 41 | run: rustup target add wasm32-unknown-unknown 42 | - name: Run tests 43 | run: make test 44 | 45 | lints: 46 | needs: build 47 | name: Lints 48 | runs-on: ubuntu-latest 49 | strategy: 50 | matrix: 51 | rust-version: ['1.81.0'] 52 | steps: 53 | - name: Checkout sources 54 | uses: actions/checkout@v4 55 | 56 | - name: Install Rust toolchain with components 57 | uses: actions-rs/toolchain@v1 58 | with: 59 | toolchain: ${{ matrix.rust-version }} 60 | override: true 61 | components: rustfmt, clippy 62 | target: wasm32-unknown-unknown 63 | 64 | - name: Run lints 65 | run: make lints 66 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Code coverage check 7 | 8 | jobs: 9 | 10 | coverage: 11 | name: Code Coverage 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | rust-version: [1.81.0] 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v4 19 | - name: Install stable toolchain 20 | uses: dtolnay/rust-toolchain@stable 21 | with: 22 | toolchain: ${{ matrix.rust-version }} 23 | targets: wasm32-unknown-unknown 24 | - name: Add wasm32 target 25 | run: rustup target add wasm32-unknown-unknown 26 | - name: Install tarpaulin 27 | run: cargo install cargo-tarpaulin --version 0.30.0 28 | - run: make build 29 | - name: Run code coverage check with tarpaulin 30 | run: cargo tarpaulin --all-features --workspace --timeout 120 --out Xml --exclude soroban-token-contract 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v4 33 | with: 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | file: ./cobertura.xml 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Artifacts 2 | on: 3 | push: 4 | tags: 5 | - "v[0-9]+.[0-9]+.[0-9]+" # Push events to matching v*, i.e. 1.0, 20.15.10 6 | - "v[0-9]+.[0-9]+.[0-9]+-rc*" # Push events to matching v*, i.e. 1.0-rc1, 20.15.10-rc5 7 | 8 | jobs: 9 | release-artifacts: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | rust-version: [1.81.0] 14 | steps: 15 | - name: Checkout sources 16 | uses: actions/checkout@v4 17 | - name: Install stable toolchain 18 | uses: dtolnay/rust-toolchain@stable 19 | with: 20 | toolchain: ${{ matrix.rust-version }} 21 | targets: wasm32-unknown-unknown 22 | - name: Add wasm32 target 23 | run: rustup target add wasm32-unknown-unknown 24 | - name: Build artifacts 25 | run: make build 26 | - name: Generate checksums 27 | run: | 28 | cd target/wasm32-unknown-unknown/release/ 29 | sha256sum *.wasm > checksums.txt 30 | - name: Release 31 | env: 32 | GH_TOKEN: ${{ secrets.JAKUB_SECRET_CI }} 33 | run: >- 34 | gh release create ${{ github.ref_name }} 35 | target/wasm32-unknown-unknown/release/*.wasm 36 | target/wasm32-unknown-unknown/release/checksums.txt 37 | --generate-notes 38 | --title "${{ github.ref_name }}" 39 | 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Text file backups 5 | **/*.rs.bk 6 | 7 | # Build results 8 | target/ 9 | 10 | # IDEs 11 | .vscode/ 12 | .idea/ 13 | *.iml 14 | 15 | # Auto-gen 16 | .cargo-ok 17 | /artifacts/ 18 | 19 | cobertura.xml 20 | 21 | **/test_snapshots 22 | .stellar/ 23 | -------------------------------------------------------------------------------- /.wasm_binaries_mainnet/live_factory.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.wasm_binaries_mainnet/live_factory.wasm -------------------------------------------------------------------------------- /.wasm_binaries_mainnet/live_multihop.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.wasm_binaries_mainnet/live_multihop.wasm -------------------------------------------------------------------------------- /.wasm_binaries_mainnet/live_pho_usdc_pool.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.wasm_binaries_mainnet/live_pho_usdc_pool.wasm -------------------------------------------------------------------------------- /.wasm_binaries_mainnet/live_pho_usdc_stake.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.wasm_binaries_mainnet/live_pho_usdc_stake.wasm -------------------------------------------------------------------------------- /.wasm_binaries_mainnet/live_token_contract.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.wasm_binaries_mainnet/live_token_contract.wasm -------------------------------------------------------------------------------- /.wasm_binaries_mainnet/live_vesting.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.wasm_binaries_mainnet/live_vesting.wasm -------------------------------------------------------------------------------- /.wasm_binaries_mainnet/live_xlm_usdc_pool.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.wasm_binaries_mainnet/live_xlm_usdc_pool.wasm -------------------------------------------------------------------------------- /.wasm_binaries_mainnet/live_xlm_usdc_stake.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/.wasm_binaries_mainnet/live_xlm_usdc_stake.wasm -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["contracts/*", "packages/*"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "2.0.0" 7 | edition = "2021" 8 | license = "GPL-3.0" 9 | repository = "https://github.com/Phoenix-Protocol-Group/phoenix-contracts" 10 | 11 | [workspace.dependencies] 12 | curve = { path = "./packages/curve" } 13 | soroban-decimal = { path = "./packages/decimal" } 14 | phoenix = { path = "./packages/phoenix" } 15 | num-integer = { version = "0.1.45", default-features = false, features = [ 16 | "i128", 17 | ] } 18 | soroban-sdk = "22.0.7" 19 | soroban-token-sdk = "22.0.7" 20 | test-case = "3.3" 21 | pretty_assertions = "1.4.0" 22 | 23 | [workspace.lints.clippy] 24 | too_many_arguments = "allow" 25 | 26 | [workspace.lints.rust] 27 | unexpected_cfgs = { level = "warn", check-cfg = ["cfg(tarpaulin_include)"] } 28 | 29 | [profile.release] 30 | opt-level = "z" 31 | overflow-checks = true 32 | debug = 0 33 | strip = "symbols" 34 | debug-assertions = false 35 | panic = "abort" 36 | codegen-units = 1 37 | lto = true 38 | 39 | [profile.release-with-logs] 40 | inherits = "release" 41 | debug-assertions = true 42 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # MIGRATION 2 | 3 | This file shows the API changes between different versions of Phoenix-Contracts. 4 | 5 | ## 1.0.0 -> X.X.X 6 | 7 | ### factory 8 | 9 | * `initialize` function requires a new argument `stable_wasm_hash: BytesN<32>` 10 | 11 | * `create_liquidity_pool` requires a new argument `pool_type` parameter (and optional `amp` for stable pool) 12 | 13 | ### pool 14 | 15 | * `provide_liquidity`, `swap`, `withdraw_liquidity` functions now have a new argument called `deadline: Option`. We check against that if the transaction hasn't been executed after a certain timelimit. 16 | 17 | ### pool_stable 18 | 19 | * `provide_liquidity`, `swap`, `withdraw_liquidity` functions now have a new argument called `deadline: Option`. We check against that if the transaction hasn't been executed after a certain timelimit. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SUBDIRS := contracts/factory contracts/multihop contracts/pool contracts/pool_stable contracts/stake contracts/token contracts/vesting packages/phoenix packages/decimal packages/curve 2 | BUILD_FLAGS ?= 3 | 4 | default: build 5 | 6 | all: test 7 | 8 | build: 9 | @for dir in $(SUBDIRS) ; do \ 10 | $(MAKE) -C $$dir build BUILD_FLAGS=$(BUILD_FLAGS) || exit 1; \ 11 | done 12 | 13 | test: build 14 | @for dir in $(SUBDIRS) ; do \ 15 | $(MAKE) -C $$dir test BUILD_FLAGS=$(BUILD_FLAGS) || exit 1; \ 16 | done 17 | 18 | fmt: 19 | @for dir in $(SUBDIRS) ; do \ 20 | $(MAKE) -C $$dir fmt || exit 1; \ 21 | done 22 | 23 | lints: fmt 24 | @for dir in contracts/multihop contracts/pool ; do \ 25 | $(MAKE) -C $$dir build || exit 1; \ 26 | done 27 | @for dir in $(SUBDIRS) ; do \ 28 | $(MAKE) -C $$dir clippy || exit 1; \ 29 | done 30 | 31 | clean: 32 | @for dir in $(SUBDIRS) ; do \ 33 | $(MAKE) -C $$dir clean || exit 1; \ 34 | done 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/Phoenix-Protocol-Group/phoenix-contracts/branch/main/graph/badge.svg?token=BJMG2IINQB)](https://codecov.io/gh/Phoenix-Protocol-Group/phoenix-contracts) 2 | 3 | # Phoenix DEX Smart Contracts 4 | This repository contains the Rust source code for the smart contracts of the Phoenix DEX. 5 | 6 | ## Overview 7 | Phoenix will be a set of DeFi protocols hosted on Soroban platform. Directory `docs` contains brief description of architecture, including flow diagrams. 8 | 9 | ## Prerequisites 10 | The following tools are required for compiling the smart contracts: 11 | 12 | - Rust ([link](https://www.rust-lang.org/tools/install)) 13 | - make 14 | 15 | ```bash 16 | # Install rust using rustup 17 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 18 | # Then install wasm32 target 19 | rustup target add wasm32-unknown-unknown 20 | ``` 21 | 22 | ## Compilation 23 | The smart contracts can be compiled into WebAssembly (WASM) using make. The Makefile included in the repository is configured to handle the necessary steps for building the smart contracts. 24 | 25 | Navigate to the root directory of the project in your terminal and run the following command: 26 | 27 | ```bash 28 | make build 29 | ``` 30 | 31 | This will generate WASM files for each of the smart contracts in the `target/wasm32-unknown-unknown/release/` directory. 32 | 33 | ## Testing 34 | You can run tests with the following command: 35 | 36 | ```bash 37 | make test 38 | ``` 39 | 40 | ## License 41 | The smart contracts and associated code in this repository are licensed under the GPL-3.0 License. By contributing to this project, you agree that your contributions will also be licensed under the GPL-3.0 license. 42 | 43 | For the full license text, please see the LICENSE file in the root directory of this repository. 44 | 45 | ## Contact 46 | If you have any questions or issues, please create a new issue on the GitHub repository. 47 | -------------------------------------------------------------------------------- /contracts/factory/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phoenix-factory" 3 | version = { workspace = true } 4 | authors = ["Jakub "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | testutils = ["soroban-sdk/testutils"] 14 | upgrade = [] 15 | 16 | [lints] 17 | workspace = true 18 | 19 | [dependencies] 20 | soroban-sdk = { workspace = true } 21 | phoenix = { workspace = true } 22 | 23 | [dev-dependencies] 24 | soroban-sdk = { workspace = true, features = ["testutils"] } 25 | test-case = { workspace = true } 26 | -------------------------------------------------------------------------------- /contracts/factory/Makefile: -------------------------------------------------------------------------------- 1 | ifeq (,$(BUILD_FLAGS)) 2 | DEPS = ../stake ../pool ../pool_stable 3 | endif 4 | 5 | default: all 6 | 7 | all: lint build test 8 | 9 | test: build # because of token dependency 10 | cargo test 11 | 12 | build: 13 | @for dir in $(DEPS) ; do \ 14 | $(MAKE) -C $$dir build || break; \ 15 | done 16 | cargo build --target wasm32-unknown-unknown --release 17 | 18 | lint: fmt clippy 19 | 20 | fmt: 21 | cargo fmt --all 22 | 23 | clippy: build 24 | cargo clippy --all-targets -- -D warnings -A clippy::too_many_arguments 25 | 26 | clean: 27 | cargo clean 28 | -------------------------------------------------------------------------------- /contracts/factory/README.md: -------------------------------------------------------------------------------- 1 | # Dex Factory 2 | 3 | ## Main functionality 4 | 5 | The main purpose of the factory contract is to provide the tooling required for managing, creating, querying of liquidity pools. 6 | 7 | ## Messages 8 | 9 | `initialize` 10 | 11 | Params: 12 | - `admin`: `Address` of the contract administrator to be 13 | - `multihop_wasm_hash`: `BytesN<32>` hash of the multihop contract to be deployed initially 14 | 15 |
16 | 17 | `create_liquidity_pool` 18 | 19 | Params: 20 | - `lp_init_info`: `LiquidityPoolInitInfo` struct representing information for the new liquidity pool 21 | 22 | Return type: 23 | `Address` of the newly created liquidity pool 24 | 25 | Description: 26 | 27 | Creates a new liquidity pool with 'LiquidityPoolInitInfo'. After deployment of the liquidity pool it updates the liquidity pool list. 28 | 29 |
30 | 31 | `query_pools` 32 | 33 | Return type: 34 | `Vec
` of all the liquidity pools created by the factory 35 | 36 | Description: 37 | Queries for a list of all the liquidity pool addresses that have been created by the called factory contract. 38 | 39 |
40 | 41 | `query_pool_details` 42 | 43 | Params: 44 | - `pool_address`: `Address` of the liquidity pool we search for 45 | 46 | Return type: 47 | Struct `LiquidityPoolInfo` containing the information about a given liquidity pool. 48 | 49 | Description: 50 | Queries for specific liquidity pool information that has been created by the called factory contract. 51 | 52 |
53 | 54 | `query_all_pools_details` 55 | 56 | Return type: 57 | `Vec` list of structs containing the information about all liquidity pools created by the factory. 58 | 59 | Description: 60 | Queries for all liquidity pools information that have been created by the called factory contract. 61 | 62 |
63 | 64 | `query_for_pool_by_token_pair(env: Env, token_a: Address, token_b: Address)`; 65 | 66 | Params: 67 | - token_a: `Address` of the first token in the pool 68 | - token_b: `Address` of the second token in the pool 69 | 70 | Return type: 71 | `Address` of the found liquidity pool that holds the given token pair. 72 | 73 | Description: 74 | Queries for a liquidity pool address by the tokens of that pool. 75 | 76 |
77 | 78 | `get_admin` 79 | 80 | Return type: 81 | `Address` of the admin for the called factory. 82 | 83 |
84 | 85 | `get_config` 86 | 87 | Return type: 88 | Struct `Config` of the called factory. 89 | -------------------------------------------------------------------------------- /contracts/factory/src/error.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::contracterror; 2 | 3 | #[contracterror] 4 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 5 | #[repr(u32)] 6 | pub enum ContractError { 7 | AlreadyInitialized = 100, 8 | WhiteListeEmpty = 101, 9 | NotAuthorized = 102, 10 | LiquidityPoolNotFound = 103, 11 | TokenABiggerThanTokenB = 104, 12 | MinStakeInvalid = 105, 13 | MinRewardInvalid = 106, 14 | AdminNotSet = 107, 15 | OverflowingOps = 108, 16 | SameAdmin = 109, 17 | NoAdminChangeInPlace = 110, 18 | AdminChangeExpired = 111, 19 | TokenDecimalsInvalid = 112, 20 | } 21 | -------------------------------------------------------------------------------- /contracts/factory/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | mod contract; 3 | mod error; 4 | mod storage; 5 | mod utils; 6 | 7 | #[cfg(test)] 8 | mod tests; 9 | 10 | pub mod token_contract { 11 | // The import will code generate: 12 | // - A ContractClient type that can be used to invoke functions on the contract. 13 | // - Any types in the contract that were annotated with #[contracttype]. 14 | soroban_sdk::contractimport!( 15 | file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" 16 | ); 17 | } 18 | 19 | pub mod stake_contract { 20 | soroban_sdk::contractimport!( 21 | file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" 22 | ); 23 | } 24 | 25 | pub trait ConvertVec { 26 | fn convert_vec(&self) -> soroban_sdk::Vec; 27 | } 28 | 29 | impl ConvertVec for soroban_sdk::Vec { 30 | fn convert_vec(&self) -> soroban_sdk::Vec { 31 | let env = self.env(); // Get the environment 32 | let mut result = soroban_sdk::Vec::new(env); 33 | 34 | for stake in self.iter() { 35 | result.push_back(storage::Stake { 36 | stake: stake.stake, 37 | stake_timestamp: stake.stake_timestamp, 38 | }); 39 | } 40 | 41 | result 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contracts/factory/src/storage.rs: -------------------------------------------------------------------------------- 1 | use phoenix::ttl::{ 2 | INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL, PERSISTENT_RENEWAL_THRESHOLD, 3 | PERSISTENT_TARGET_TTL, 4 | }; 5 | use soroban_sdk::{ 6 | contracttype, log, panic_with_error, symbol_short, Address, BytesN, ConversionError, Env, 7 | Symbol, TryFromVal, Val, Vec, 8 | }; 9 | 10 | use crate::error::ContractError; 11 | 12 | pub const ADMIN: Symbol = symbol_short!("ADMIN"); 13 | pub const FACTORY_KEY: Symbol = symbol_short!("FACTORY"); 14 | pub(crate) const PENDING_ADMIN: Symbol = symbol_short!("p_admin"); 15 | const STABLE_WASM_HASH: Symbol = symbol_short!("stabwasm"); 16 | 17 | #[derive(Clone, Copy)] 18 | #[repr(u32)] 19 | pub enum DataKey { 20 | Config = 1, 21 | LpVec = 2, 22 | Initialized = 3, // TODO: deprecated, remove in next upgrade 23 | } 24 | 25 | #[derive(Clone)] 26 | #[contracttype] 27 | pub struct PairTupleKey { 28 | pub(crate) token_a: Address, 29 | pub(crate) token_b: Address, 30 | } 31 | 32 | impl TryFromVal for Val { 33 | type Error = ConversionError; 34 | 35 | fn try_from_val(_env: &Env, v: &DataKey) -> Result { 36 | Ok((*v as u32).into()) 37 | } 38 | } 39 | 40 | #[contracttype] 41 | #[derive(Clone, Debug, PartialEq, Eq)] 42 | pub struct Config { 43 | pub admin: Address, 44 | pub multihop_address: Address, 45 | pub lp_wasm_hash: BytesN<32>, 46 | pub stake_wasm_hash: BytesN<32>, 47 | pub token_wasm_hash: BytesN<32>, 48 | pub whitelisted_accounts: Vec
, 49 | pub lp_token_decimals: u32, 50 | } 51 | 52 | pub fn save_stable_wasm_hash(env: &Env, hash: BytesN<32>) { 53 | env.storage().persistent().set(&STABLE_WASM_HASH, &hash); 54 | env.storage().persistent().extend_ttl( 55 | &STABLE_WASM_HASH, 56 | PERSISTENT_RENEWAL_THRESHOLD, 57 | PERSISTENT_TARGET_TTL, 58 | ); 59 | } 60 | 61 | pub fn get_stable_wasm_hash(env: &Env) -> BytesN<32> { 62 | let hash = env 63 | .storage() 64 | .persistent() 65 | .get(&STABLE_WASM_HASH) 66 | .expect("Stable wasm hash not set"); 67 | 68 | env.storage().persistent().extend_ttl( 69 | &STABLE_WASM_HASH, 70 | PERSISTENT_RENEWAL_THRESHOLD, 71 | PERSISTENT_TARGET_TTL, 72 | ); 73 | 74 | hash 75 | } 76 | 77 | #[contracttype] 78 | #[derive(Clone, Debug, PartialEq, Eq)] 79 | pub struct UserPortfolio { 80 | pub lp_portfolio: Vec, 81 | pub stake_portfolio: Vec, 82 | } 83 | 84 | #[contracttype] 85 | #[derive(Clone, Debug, PartialEq, Eq)] 86 | pub struct LpPortfolio { 87 | pub assets: (Asset, Asset), 88 | } 89 | 90 | #[contracttype] 91 | #[derive(Clone, Debug, PartialEq, Eq)] 92 | pub struct StakePortfolio { 93 | pub staking_contract: Address, 94 | pub stakes: Vec, 95 | } 96 | 97 | #[contracttype] 98 | #[derive(Clone, Debug, PartialEq, Eq)] 99 | pub struct Asset { 100 | /// Address of the asset 101 | pub address: Address, 102 | /// The total amount of those tokens in the pool 103 | pub amount: i128, 104 | } 105 | 106 | /// This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool. 107 | #[contracttype] 108 | #[derive(Clone, Debug, PartialEq, Eq)] 109 | pub struct PoolResponse { 110 | /// The asset A in the pool together with asset amounts 111 | pub asset_a: Asset, 112 | /// The asset B in the pool together with asset amounts 113 | pub asset_b: Asset, 114 | /// The total amount of LP tokens currently issued 115 | pub asset_lp_share: Asset, 116 | /// The address of the Stake contract for the liquidity pool 117 | pub stake_address: Address, 118 | } 119 | 120 | #[contracttype] 121 | #[derive(Clone, Debug, PartialEq, Eq)] 122 | pub struct LiquidityPoolInfo { 123 | pub pool_address: Address, 124 | pub pool_response: PoolResponse, 125 | pub total_fee_bps: i64, 126 | } 127 | 128 | #[contracttype] 129 | #[derive(Clone, Debug, Eq, PartialEq)] 130 | pub struct StakedResponse { 131 | pub stakes: Vec, 132 | pub total_stake: i128, 133 | } 134 | 135 | #[contracttype] 136 | #[derive(Clone, Debug, Eq, PartialEq)] 137 | pub struct Stake { 138 | /// The amount of staked tokens 139 | pub stake: i128, 140 | /// The timestamp when the stake was made 141 | pub stake_timestamp: u64, 142 | } 143 | 144 | pub fn save_config(env: &Env, config: Config) { 145 | env.storage().persistent().set(&DataKey::Config, &config); 146 | env.storage().persistent().extend_ttl( 147 | &DataKey::Config, 148 | PERSISTENT_RENEWAL_THRESHOLD, 149 | PERSISTENT_TARGET_TTL, 150 | ); 151 | } 152 | 153 | pub fn get_config(env: &Env) -> Config { 154 | let config = env 155 | .storage() 156 | .persistent() 157 | .get(&DataKey::Config) 158 | .expect("Config not set"); 159 | 160 | env.storage().persistent().extend_ttl( 161 | &DataKey::Config, 162 | PERSISTENT_RENEWAL_THRESHOLD, 163 | PERSISTENT_TARGET_TTL, 164 | ); 165 | 166 | config 167 | } 168 | 169 | pub fn _save_admin(env: &Env, admin_addr: Address) { 170 | env.storage().instance().set(&ADMIN, &admin_addr); 171 | 172 | env.storage() 173 | .instance() 174 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 175 | } 176 | 177 | pub fn _get_admin(env: &Env) -> Address { 178 | let admin_addr = env.storage().instance().get(&ADMIN).unwrap_or_else(|| { 179 | log!(env, "Factory: Admin not set"); 180 | panic_with_error!(&env, ContractError::AdminNotSet) 181 | }); 182 | 183 | env.storage() 184 | .instance() 185 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 186 | 187 | admin_addr 188 | } 189 | 190 | pub fn get_lp_vec(env: &Env) -> Vec
{ 191 | let lp_vec = env 192 | .storage() 193 | .persistent() 194 | .get(&DataKey::LpVec) 195 | .expect("Factory: get_lp_vec: Liquidity Pool vector not found"); 196 | 197 | env.storage().persistent().extend_ttl( 198 | &DataKey::LpVec, 199 | PERSISTENT_RENEWAL_THRESHOLD, 200 | PERSISTENT_TARGET_TTL, 201 | ); 202 | 203 | lp_vec 204 | } 205 | 206 | pub fn save_lp_vec(env: &Env, lp_info: Vec
) { 207 | env.storage().persistent().set(&DataKey::LpVec, &lp_info); 208 | env.storage().persistent().extend_ttl( 209 | &DataKey::LpVec, 210 | PERSISTENT_RENEWAL_THRESHOLD, 211 | PERSISTENT_TARGET_TTL, 212 | ); 213 | } 214 | 215 | pub fn save_lp_vec_with_tuple_as_key( 216 | env: &Env, 217 | tuple_pool: (&Address, &Address), 218 | lp_address: &Address, 219 | ) { 220 | env.storage().persistent().set( 221 | &PairTupleKey { 222 | token_a: tuple_pool.0.clone(), 223 | token_b: tuple_pool.1.clone(), 224 | }, 225 | &lp_address, 226 | ); 227 | 228 | env.storage().persistent().extend_ttl( 229 | &PairTupleKey { 230 | token_a: tuple_pool.0.clone(), 231 | token_b: tuple_pool.1.clone(), 232 | }, 233 | PERSISTENT_RENEWAL_THRESHOLD, 234 | PERSISTENT_TARGET_TTL, 235 | ); 236 | } 237 | -------------------------------------------------------------------------------- /contracts/factory/src/tests.rs: -------------------------------------------------------------------------------- 1 | mod admin_change; 2 | mod config; 3 | mod queries; 4 | mod setup; 5 | -------------------------------------------------------------------------------- /contracts/factory/src/tests/admin_change.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{ 2 | testutils::{Address as _, Ledger}, 3 | Address, Env, 4 | }; 5 | 6 | use crate::{error::ContractError, storage::PENDING_ADMIN, tests::setup::deploy_factory_contract}; 7 | use phoenix::utils::AdminChange; 8 | 9 | #[test] 10 | fn propose_admin() { 11 | let env = Env::default(); 12 | env.mock_all_auths(); 13 | 14 | let admin = Address::generate(&env); 15 | let new_admin = Address::generate(&env); 16 | 17 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 18 | 19 | let result = factory.propose_admin(&new_admin, &None); 20 | assert_eq!(result, new_admin.clone()); 21 | 22 | let pending_admin: AdminChange = env.as_contract(&factory.address, || { 23 | env.storage().instance().get(&PENDING_ADMIN).unwrap() 24 | }); 25 | 26 | assert_eq!(pending_admin.new_admin, new_admin); 27 | assert_eq!(pending_admin.time_limit, None); 28 | } 29 | 30 | #[test] 31 | fn replace_admin_fails_when_new_admin_is_same_as_current() { 32 | let env = Env::default(); 33 | env.mock_all_auths(); 34 | 35 | let admin = Address::generate(&env); 36 | 37 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 38 | 39 | assert_eq!( 40 | factory.try_propose_admin(&admin, &None), 41 | Err(Ok(ContractError::SameAdmin)) 42 | ); 43 | } 44 | 45 | #[test] 46 | fn accept_admin_successfully() { 47 | let env = Env::default(); 48 | env.mock_all_auths(); 49 | 50 | let admin = Address::generate(&env); 51 | let new_admin = Address::generate(&env); 52 | 53 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 54 | 55 | factory.propose_admin(&new_admin, &None); 56 | 57 | let result = factory.accept_admin(); 58 | assert_eq!(result, new_admin.clone()); 59 | 60 | let config = factory.get_config(); 61 | assert_eq!(config.admin, new_admin); 62 | 63 | let pending_admin: Option = env.as_contract(&factory.address, || { 64 | env.storage().instance().get(&PENDING_ADMIN) 65 | }); 66 | assert!(pending_admin.is_none()); 67 | } 68 | 69 | #[test] 70 | fn accept_admin_fails_when_no_pending_admin() { 71 | let env = Env::default(); 72 | env.mock_all_auths(); 73 | 74 | let admin = Address::generate(&env); 75 | 76 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 77 | 78 | assert_eq!( 79 | factory.try_accept_admin(), 80 | Err(Ok(ContractError::NoAdminChangeInPlace)) 81 | ) 82 | } 83 | 84 | #[test] 85 | fn accept_admin_fails_when_time_limit_expired() { 86 | let env = Env::default(); 87 | env.mock_all_auths(); 88 | 89 | let admin = Address::generate(&env); 90 | let new_admin = Address::generate(&env); 91 | 92 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 93 | 94 | let time_limit = 1000u64; 95 | factory.propose_admin(&new_admin, &Some(time_limit)); 96 | env.ledger().set_timestamp(time_limit + 100); 97 | 98 | assert_eq!( 99 | factory.try_accept_admin(), 100 | Err(Ok(ContractError::AdminChangeExpired)) 101 | ) 102 | } 103 | 104 | #[test] 105 | fn accept_admin_successfully_with_time_limit() { 106 | let env = Env::default(); 107 | env.mock_all_auths(); 108 | 109 | let admin = Address::generate(&env); 110 | let new_admin = Address::generate(&env); 111 | 112 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 113 | 114 | let time_limit = 1_500; 115 | factory.propose_admin(&new_admin, &Some(time_limit)); 116 | 117 | env.ledger().set_timestamp(1_000u64); 118 | 119 | let result = factory.accept_admin(); 120 | assert_eq!(result, new_admin.clone()); 121 | 122 | let config = factory.get_config(); 123 | assert_eq!(config.admin, new_admin); 124 | 125 | let pending_admin: Option = env.as_contract(&factory.address, || { 126 | env.storage().instance().get(&PENDING_ADMIN) 127 | }); 128 | assert!(pending_admin.is_none()); 129 | } 130 | 131 | #[test] 132 | fn accept_admin_successfully_on_time_limit() { 133 | let env = Env::default(); 134 | env.mock_all_auths(); 135 | 136 | let admin = Address::generate(&env); 137 | let new_admin = Address::generate(&env); 138 | 139 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 140 | 141 | let time_limit = 1_500; 142 | factory.propose_admin(&new_admin, &Some(time_limit)); 143 | 144 | env.ledger().set_timestamp(time_limit); 145 | 146 | let result = factory.accept_admin(); 147 | assert_eq!(result, new_admin.clone()); 148 | 149 | let config = factory.get_config(); 150 | assert_eq!(config.admin, new_admin); 151 | 152 | let pending_admin: Option = env.as_contract(&factory.address, || { 153 | env.storage().instance().get(&PENDING_ADMIN) 154 | }); 155 | assert!(pending_admin.is_none()); 156 | } 157 | 158 | #[test] 159 | fn propose_admin_then_revoke() { 160 | let env = Env::default(); 161 | env.mock_all_auths(); 162 | 163 | let admin = Address::generate(&env); 164 | let new_admin = Address::generate(&env); 165 | 166 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 167 | 168 | factory.propose_admin(&new_admin, &None); 169 | factory.revoke_admin_change(); 170 | 171 | let pending_admin: Option = env.as_contract(&factory.address, || { 172 | env.storage().instance().get(&PENDING_ADMIN) 173 | }); 174 | 175 | assert!(pending_admin.is_none()); 176 | } 177 | 178 | #[test] 179 | fn revoke_admin_should_fail_when_no_admin_change_in_place() { 180 | let env = Env::default(); 181 | env.mock_all_auths(); 182 | 183 | let admin = Address::generate(&env); 184 | 185 | let factory = deploy_factory_contract(&env, Some(admin.clone())); 186 | 187 | assert_eq!( 188 | factory.try_revoke_admin_change(), 189 | Err(Ok(ContractError::NoAdminChangeInPlace)) 190 | ); 191 | } 192 | -------------------------------------------------------------------------------- /contracts/factory/src/utils.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{xdr::ToXdr, Address, Bytes, BytesN, Env}; 2 | 3 | pub fn deploy_and_initialize_multihop_contract( 4 | env: Env, 5 | admin: Address, 6 | multihop_wasm_hash: BytesN<32>, 7 | ) -> Address { 8 | let mut salt = Bytes::new(&env); 9 | salt.append(&admin.clone().to_xdr(&env)); 10 | let salt = env.crypto().sha256(&salt); 11 | 12 | env.deployer() 13 | .with_current_contract(salt) 14 | .deploy_v2(multihop_wasm_hash, (admin, env.current_contract_address())) 15 | } 16 | -------------------------------------------------------------------------------- /contracts/multihop/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phoenix-multihop" 3 | version = { workspace = true } 4 | authors = ["Jakub "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | testutils = ["soroban-sdk/testutils"] 14 | upgrade = [] 15 | 16 | [lints] 17 | workspace = true 18 | 19 | [dependencies] 20 | soroban-sdk = { workspace = true } 21 | phoenix = { workspace = true } 22 | 23 | [dev-dependencies] 24 | soroban-sdk = { workspace = true, features = ["testutils"] } 25 | -------------------------------------------------------------------------------- /contracts/multihop/Makefile: -------------------------------------------------------------------------------- 1 | ifeq (,$(BUILD_FLAGS)) 2 | DEPS = ../factory ../pool 3 | endif 4 | 5 | default: all 6 | 7 | all: lint build test 8 | 9 | test: 10 | $(MAKE) -C ../factory build || break; 11 | $(MAKE) -C ../pool build || break; 12 | cargo test 13 | 14 | build: 15 | @for dir in $(DEPS) ; do \ 16 | $(MAKE) -C $$dir build || break; \ 17 | done 18 | cargo build --target wasm32-unknown-unknown --release 19 | 20 | lint: fmt clippy 21 | 22 | fmt: 23 | cargo fmt --all 24 | 25 | clippy: build 26 | cargo clippy --all-targets -- -D warnings 27 | 28 | clean: 29 | cargo clean 30 | -------------------------------------------------------------------------------- /contracts/multihop/README.md: -------------------------------------------------------------------------------- 1 | # Dex Multihop 2 | 3 | ## Main functionality 4 | The main purpose of the multihop contract is to provide the ability of the users to swap tokens between multiple liquidity pools. 5 | 6 | 7 | 8 | ## Messages: 9 | `initialize` 10 | 11 | Params: 12 | 13 | - `admin`: `Address` of the contract administrator to be 14 | - `factory`: `Address` of the factory contract to be deployed initially 15 | 16 | Return type: 17 | void 18 | 19 | Description: 20 | Used for the initialization of the multihop contract - this sets the multihop contract as initialized, stores the admin and factory address in the Config struct 21 | 22 |
23 | 24 | `swap` 25 | 26 | Params: 27 | 28 | - `recipient`: `Address` of the contract that will receive the amount swapped. 29 | - `referral`: `Option
` of the referral, that will get a referral commission bonus for the swap. 30 | - `operations`: `Vec` that holds both the addresses of the asked and offer assets. 31 | - `max_belief_price`: `Option` value for the maximum believe price that will be used for the swaps. 32 | - `max_spread_bps`: `Option` maximum permitted difference between the asked and offered price in BPS. 33 | - `amount`: `i128` value representing the amount offered for swap 34 | 35 | Return type: 36 | void 37 | 38 | Description: 39 | Takes a list of `Swap` operations between the different pools and iterates over them, swapping the tokens in question by calling the pool contract. 40 | 41 |
42 | 43 | `simulate_swap` 44 | Params: 45 | 46 | - `operations`: `Vec`holding the addresses of the asked and offer assets 47 | - `amount`: `i128` value representing the amount that should be swapped 48 | 49 | Return type: 50 | `SimulateSwapResponse` containing the details of the swap 51 | 52 | Description: 53 | Dry runs a swap operation. This is useful when we want to display some additional information such as pool commission fee, slippage tolerance and expected returned values from the swap in question. 54 | 55 |
56 | 57 | `simulate_reverse_swap` 58 | 59 | Params: 60 | 61 | - `operations`: `Vec` holding the addresses of the asked and offer assets 62 | - `amount`: `i128` value representing the amount that should be swapped 63 | 64 | Return type: 65 | `SimulateReverseSwapResponse` containing the details of the same swap but in reverse 66 | 67 | Description: 68 | Dry runs a swap operation but in reverse. This is useful when we want to display some additional information such as pool commission fee, slippage tolerance and expected returned values from the reversed swap in question. 69 | 70 |
71 | 72 | `get_admin` 73 | Params: 74 | 75 | * None 76 | 77 | Return type: 78 | `Address` of the admin for the current Multihop contract. 79 | 80 | Description: 81 | Queries for the admin address of the current multihop contract. 82 | -------------------------------------------------------------------------------- /contracts/multihop/src/error.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::contracterror; 2 | 3 | #[contracterror] 4 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 5 | #[repr(u32)] 6 | pub enum ContractError { 7 | AlreadyInitialized = 200, 8 | OperationsEmpty = 201, 9 | IncorrectAssetSwap = 202, 10 | AdminNotSet = 203, 11 | SameAdmin = 204, 12 | NoAdminChangeInPlace = 205, 13 | AdminChangeExpired = 206, 14 | } 15 | -------------------------------------------------------------------------------- /contracts/multihop/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | mod contract; 3 | mod error; 4 | mod storage; 5 | mod utils; 6 | 7 | pub mod xyk_pool { 8 | // The import will code generate: 9 | // - A ContractClient type that can be used to invoke functions on the contract. 10 | // - Any types in the contract that were annotated with #[contracttype]. 11 | soroban_sdk::contractimport!( 12 | file = "../../target/wasm32-unknown-unknown/release/phoenix_pool.wasm" 13 | ); 14 | } 15 | 16 | pub mod stable_pool { 17 | soroban_sdk::contractimport!( 18 | file = "../../target/wasm32-unknown-unknown/release/phoenix_pool_stable.wasm" 19 | ); 20 | } 21 | 22 | pub mod factory_contract { 23 | soroban_sdk::contractimport!( 24 | file = "../../target/wasm32-unknown-unknown/release/phoenix_factory.wasm" 25 | ); 26 | } 27 | 28 | pub mod token_contract { 29 | soroban_sdk::contractimport!( 30 | file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" 31 | ); 32 | } 33 | 34 | #[cfg(test)] 35 | mod tests; 36 | -------------------------------------------------------------------------------- /contracts/multihop/src/storage.rs: -------------------------------------------------------------------------------- 1 | use phoenix::ttl::{INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL}; 2 | use soroban_sdk::{ 3 | contracttype, log, panic_with_error, symbol_short, Address, Env, String, Symbol, Vec, 4 | }; 5 | 6 | use crate::error::ContractError; 7 | 8 | pub const ADMIN: Symbol = symbol_short!("ADMIN"); 9 | pub const MULTIHOP_KEY: Symbol = symbol_short!("MULTIHOP"); 10 | pub(crate) const PENDING_ADMIN: Symbol = symbol_short!("p_admin"); 11 | 12 | #[contracttype] 13 | #[derive(Clone, Debug, Eq, PartialEq)] 14 | pub struct Swap { 15 | pub ask_asset: Address, 16 | pub offer_asset: Address, 17 | pub ask_asset_min_amount: Option, 18 | } 19 | 20 | #[derive(Clone)] 21 | #[contracttype] 22 | pub struct Pair { 23 | pub token_a: Address, 24 | pub token_b: Address, 25 | } 26 | 27 | #[derive(Clone)] 28 | #[contracttype] 29 | pub enum DataKey { 30 | PairKey(Pair), 31 | FactoryKey, 32 | Admin, 33 | Initialized, // TODO: deprecated, remove in next upgrade 34 | } 35 | 36 | #[contracttype] 37 | #[derive(Clone, Debug, PartialEq, Eq)] 38 | pub struct Asset { 39 | /// Address of the asset 40 | pub address: Address, 41 | /// The total amount of those tokens in the pool 42 | pub amount: i128, 43 | } 44 | 45 | #[contracttype] 46 | #[derive(Clone, Debug, PartialEq, Eq)] 47 | pub struct SimulateSwapResponse { 48 | pub ask_amount: i128, 49 | /// tuple of ask_asset denom and commission amount for the swap 50 | pub commission_amounts: Vec<(String, i128)>, 51 | pub spread_amount: Vec, 52 | } 53 | 54 | #[contracttype] 55 | #[derive(Clone, Debug, PartialEq, Eq)] 56 | pub struct SimulateReverseSwapResponse { 57 | pub offer_amount: i128, 58 | /// tuple of offer_asset denom and commission amount for the swap 59 | pub commission_amounts: Vec<(String, i128)>, 60 | pub spread_amount: Vec, 61 | } 62 | 63 | /// This struct is used to return a query result with the total amount of LP tokens and assets in a specific pool. 64 | #[contracttype] 65 | #[derive(Clone, Debug, PartialEq, Eq)] 66 | pub struct PoolResponse { 67 | /// The asset A in the pool together with asset amounts 68 | pub asset_a: Asset, 69 | /// The asset B in the pool together with asset amounts 70 | pub asset_b: Asset, 71 | /// The total amount of LP tokens currently issued 72 | pub asset_lp_share: Asset, 73 | } 74 | 75 | #[cfg(not(tarpaulin_include))] 76 | pub fn save_factory(env: &Env, factory: Address) { 77 | env.storage().instance().set(&DataKey::FactoryKey, &factory); 78 | env.storage() 79 | .instance() 80 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 81 | } 82 | 83 | #[cfg(not(tarpaulin_include))] 84 | pub fn get_factory(env: &Env) -> Address { 85 | let address = env 86 | .storage() 87 | .instance() 88 | .get(&DataKey::FactoryKey) 89 | .expect("No address found."); 90 | 91 | env.storage() 92 | .instance() 93 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 94 | 95 | address 96 | } 97 | 98 | #[cfg(not(tarpaulin_include))] 99 | pub fn save_admin_old(env: &Env, admin: &Address) { 100 | env.storage().instance().set(&DataKey::Admin, admin); 101 | env.storage() 102 | .instance() 103 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 104 | } 105 | 106 | #[cfg(not(tarpaulin_include))] 107 | pub fn _save_admin(env: &Env, admin: &Address) { 108 | env.storage().instance().set(&ADMIN, admin); 109 | env.storage() 110 | .instance() 111 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 112 | } 113 | 114 | #[cfg(not(tarpaulin_include))] 115 | pub fn get_admin_old(env: &Env) -> Address { 116 | let address = env 117 | .storage() 118 | .instance() 119 | .get(&DataKey::Admin) 120 | .unwrap_or_else(|| { 121 | log!(env, "Admin not set"); 122 | panic_with_error!(&env, ContractError::AdminNotSet) 123 | }); 124 | 125 | env.storage() 126 | .instance() 127 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 128 | 129 | address 130 | } 131 | 132 | #[cfg(not(tarpaulin_include))] 133 | pub fn _get_admin(env: &Env) -> Address { 134 | let admin = env.storage().instance().get(&ADMIN).unwrap_or_else(|| { 135 | log!(env, "Multihop: Admin not set"); 136 | panic_with_error!(&env, ContractError::AdminNotSet) 137 | }); 138 | 139 | env.storage() 140 | .instance() 141 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 142 | 143 | admin 144 | } 145 | -------------------------------------------------------------------------------- /contracts/multihop/src/tests.rs: -------------------------------------------------------------------------------- 1 | mod admin_change; 2 | mod query; 3 | mod setup; 4 | mod swap; 5 | -------------------------------------------------------------------------------- /contracts/multihop/src/tests/admin_change.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{ 2 | testutils::{Address as _, Ledger}, 3 | Address, Env, 4 | }; 5 | 6 | use crate::{error::ContractError, storage::PENDING_ADMIN, tests::setup::deploy_multihop_contract}; 7 | use phoenix::utils::AdminChange; 8 | 9 | #[test] 10 | fn propose_admin() { 11 | let env = Env::default(); 12 | env.mock_all_auths(); 13 | 14 | let admin = Address::generate(&env); 15 | let new_admin = Address::generate(&env); 16 | 17 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 18 | 19 | let result = multihop.propose_admin(&new_admin, &None); 20 | assert_eq!(result, new_admin.clone()); 21 | 22 | let pending_admin: AdminChange = env.as_contract(&multihop.address, || { 23 | env.storage().instance().get(&PENDING_ADMIN).unwrap() 24 | }); 25 | 26 | assert_eq!(pending_admin.new_admin, new_admin); 27 | assert_eq!(pending_admin.time_limit, None); 28 | } 29 | 30 | #[test] 31 | fn replace_admin_fails_when_new_admin_is_same_as_current() { 32 | let env = Env::default(); 33 | env.mock_all_auths(); 34 | 35 | let admin = Address::generate(&env); 36 | 37 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 38 | 39 | assert_eq!( 40 | multihop.try_propose_admin(&admin, &None), 41 | Err(Ok(ContractError::SameAdmin)) 42 | ); 43 | } 44 | 45 | #[test] 46 | fn accept_admin_successfully() { 47 | let env = Env::default(); 48 | env.mock_all_auths(); 49 | 50 | let admin = Address::generate(&env); 51 | let new_admin = Address::generate(&env); 52 | 53 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 54 | 55 | multihop.propose_admin(&new_admin, &None); 56 | 57 | let result = multihop.accept_admin(); 58 | assert_eq!(result, new_admin.clone()); 59 | 60 | let pending_admin: Option = env.as_contract(&multihop.address, || { 61 | env.storage().instance().get(&PENDING_ADMIN) 62 | }); 63 | assert!(pending_admin.is_none()); 64 | } 65 | 66 | #[test] 67 | fn accept_admin_fails_when_no_pending_admin() { 68 | let env = Env::default(); 69 | env.mock_all_auths(); 70 | 71 | let admin = Address::generate(&env); 72 | 73 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 74 | 75 | assert_eq!( 76 | multihop.try_accept_admin(), 77 | Err(Ok(ContractError::NoAdminChangeInPlace)) 78 | ) 79 | } 80 | 81 | #[test] 82 | fn accept_admin_fails_when_time_limit_expired() { 83 | let env = Env::default(); 84 | env.mock_all_auths(); 85 | 86 | let admin = Address::generate(&env); 87 | let new_admin = Address::generate(&env); 88 | 89 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 90 | 91 | let time_limit = 1000u64; 92 | multihop.propose_admin(&new_admin, &Some(time_limit)); 93 | env.ledger().set_timestamp(time_limit + 100); 94 | 95 | assert_eq!( 96 | multihop.try_accept_admin(), 97 | Err(Ok(ContractError::AdminChangeExpired)) 98 | ) 99 | } 100 | 101 | #[test] 102 | fn accept_admin_successfully_with_time_limit() { 103 | let env = Env::default(); 104 | env.mock_all_auths(); 105 | 106 | let admin = Address::generate(&env); 107 | let new_admin = Address::generate(&env); 108 | 109 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 110 | 111 | let time_limit = 1_500; 112 | multihop.propose_admin(&new_admin, &Some(time_limit)); 113 | 114 | env.ledger().set_timestamp(1_000u64); 115 | 116 | let result = multihop.accept_admin(); 117 | assert_eq!(result, new_admin.clone()); 118 | 119 | let pending_admin: Option = env.as_contract(&multihop.address, || { 120 | env.storage().instance().get(&PENDING_ADMIN) 121 | }); 122 | assert!(pending_admin.is_none()); 123 | } 124 | 125 | #[test] 126 | fn accept_admin_successfully_on_time_limit() { 127 | let env = Env::default(); 128 | env.mock_all_auths(); 129 | 130 | let admin = Address::generate(&env); 131 | let new_admin = Address::generate(&env); 132 | 133 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 134 | 135 | let time_limit = 1_500; 136 | multihop.propose_admin(&new_admin, &Some(time_limit)); 137 | 138 | env.ledger().set_timestamp(time_limit); 139 | 140 | let result = multihop.accept_admin(); 141 | assert_eq!(result, new_admin.clone()); 142 | 143 | let pending_admin: Option = env.as_contract(&multihop.address, || { 144 | env.storage().instance().get(&PENDING_ADMIN) 145 | }); 146 | assert!(pending_admin.is_none()); 147 | } 148 | 149 | #[test] 150 | fn propose_admin_then_revoke() { 151 | let env = Env::default(); 152 | env.mock_all_auths(); 153 | 154 | let admin = Address::generate(&env); 155 | let new_admin = Address::generate(&env); 156 | 157 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 158 | 159 | multihop.propose_admin(&new_admin, &None); 160 | multihop.revoke_admin_change(); 161 | 162 | let pending_admin: Option = env.as_contract(&multihop.address, || { 163 | env.storage().instance().get(&PENDING_ADMIN) 164 | }); 165 | 166 | assert!(pending_admin.is_none()); 167 | } 168 | 169 | #[test] 170 | fn revoke_admin_should_fail_when_no_admin_change_in_place() { 171 | let env = Env::default(); 172 | env.mock_all_auths(); 173 | 174 | let admin = Address::generate(&env); 175 | 176 | let multihop = deploy_multihop_contract(&env, admin.clone(), &Address::generate(&env)); 177 | 178 | assert_eq!( 179 | multihop.try_revoke_admin_change(), 180 | Err(Ok(ContractError::NoAdminChangeInPlace)) 181 | ); 182 | } 183 | -------------------------------------------------------------------------------- /contracts/multihop/src/utils.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{log, panic_with_error, Env, Vec}; 2 | 3 | use crate::{error::ContractError, storage::Swap}; 4 | 5 | pub fn verify_swap(env: &Env, operations: &Vec) { 6 | for (current, next) in operations.iter().zip(operations.iter().skip(1)) { 7 | if current.ask_asset != next.offer_asset { 8 | log!(&env, "Multihop: Verify Swap: Provided bad swap order"); 9 | panic_with_error!(&env, ContractError::IncorrectAssetSwap); 10 | } 11 | } 12 | } 13 | 14 | pub fn verify_reverse_swap(env: &Env, operations: &Vec) { 15 | for (current, next) in operations.iter().zip(operations.iter().skip(1)) { 16 | if current.offer_asset != next.ask_asset { 17 | log!( 18 | &env, 19 | "Multihop: Verify Reverse Swap: Provided bad swap order" 20 | ); 21 | panic_with_error!(&env, ContractError::IncorrectAssetSwap); 22 | } 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use super::*; 29 | use crate::{storage::Swap, utils::verify_swap}; 30 | 31 | use soroban_sdk::{testutils::Address as _, vec, Address, Env}; 32 | 33 | #[test] 34 | fn verify_operations_in_swap_should_work() { 35 | let env = Env::default(); 36 | 37 | let token1 = Address::generate(&env); 38 | let token2 = Address::generate(&env); 39 | let token3 = Address::generate(&env); 40 | let token4 = Address::generate(&env); 41 | 42 | let swap1 = Swap { 43 | offer_asset: token1.clone(), 44 | ask_asset: token2.clone(), 45 | ask_asset_min_amount: None::, 46 | }; 47 | let swap2 = Swap { 48 | offer_asset: token2.clone(), 49 | ask_asset: token3.clone(), 50 | ask_asset_min_amount: None::, 51 | }; 52 | let swap3 = Swap { 53 | offer_asset: token3.clone(), 54 | ask_asset: token4.clone(), 55 | ask_asset_min_amount: None::, 56 | }; 57 | 58 | let operations = vec![&env, swap1, swap2, swap3]; 59 | 60 | verify_swap(&env, &operations); 61 | } 62 | 63 | #[test] 64 | fn verify_operations_in_reverse_swap_should_work() { 65 | let env = Env::default(); 66 | 67 | let token1 = Address::generate(&env); 68 | let token2 = Address::generate(&env); 69 | let token3 = Address::generate(&env); 70 | let token4 = Address::generate(&env); 71 | 72 | let swap1 = Swap { 73 | offer_asset: token3.clone(), 74 | ask_asset: token4.clone(), 75 | ask_asset_min_amount: None::, 76 | }; 77 | let swap2 = Swap { 78 | offer_asset: token2.clone(), 79 | ask_asset: token3.clone(), 80 | ask_asset_min_amount: None::, 81 | }; 82 | let swap3 = Swap { 83 | offer_asset: token1.clone(), 84 | ask_asset: token2.clone(), 85 | ask_asset_min_amount: None::, 86 | }; 87 | 88 | let operations = vec![&env, swap1, swap2, swap3]; 89 | 90 | verify_reverse_swap(&env, &operations); 91 | } 92 | 93 | #[test] 94 | #[should_panic(expected = "Multihop: Verify Swap: Provided bad swap order")] 95 | fn verify_operations_should_fail_when_bad_order_provided() { 96 | let env = Env::default(); 97 | 98 | let token1 = Address::generate(&env); 99 | let token2 = Address::generate(&env); 100 | let token3 = Address::generate(&env); 101 | let token4 = Address::generate(&env); 102 | 103 | let swap1 = Swap { 104 | offer_asset: token1.clone(), 105 | ask_asset: token2.clone(), 106 | ask_asset_min_amount: None::, 107 | }; 108 | let swap2 = Swap { 109 | offer_asset: token3.clone(), 110 | ask_asset: token4.clone(), 111 | ask_asset_min_amount: None::, 112 | }; 113 | 114 | let operations = vec![&env, swap1, swap2]; 115 | 116 | verify_swap(&env, &operations); 117 | } 118 | 119 | #[test] 120 | #[should_panic(expected = "Multihop: Verify Reverse Swap: Provided bad swap order")] 121 | fn verify_operations_reverse_swap_should_fail_when_bad_order_provided() { 122 | let env = Env::default(); 123 | 124 | let token1 = Address::generate(&env); 125 | let token2 = Address::generate(&env); 126 | let token3 = Address::generate(&env); 127 | let token4 = Address::generate(&env); 128 | 129 | let swap1 = Swap { 130 | offer_asset: token1.clone(), 131 | ask_asset: token2.clone(), 132 | ask_asset_min_amount: None::, 133 | }; 134 | let swap2 = Swap { 135 | offer_asset: token3.clone(), 136 | ask_asset: token4.clone(), 137 | ask_asset_min_amount: None::, 138 | }; 139 | 140 | let operations = vec![&env, swap1, swap2]; 141 | 142 | verify_reverse_swap(&env, &operations); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /contracts/pool/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phoenix-pool" 3 | version = { workspace = true } 4 | authors = ["Jakub "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | testutils = ["soroban-sdk/testutils"] 14 | upgrade = [] 15 | 16 | [lints] 17 | workspace = true 18 | 19 | [dependencies] 20 | soroban-decimal = { workspace = true } 21 | phoenix = { workspace = true } 22 | num-integer = { workspace = true } 23 | soroban-sdk = { workspace = true } 24 | 25 | [dev-dependencies] 26 | soroban-sdk = { workspace = true, features = ["testutils"] } 27 | pretty_assertions = { workspace = true } 28 | test-case = "3.3.1" 29 | -------------------------------------------------------------------------------- /contracts/pool/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | all: lint build test 4 | 5 | test: build # because of token dependency 6 | cargo test 7 | 8 | build: 9 | $(MAKE) -C ../token build || break; 10 | $(MAKE) -C ../stake build || break; 11 | cargo build --target wasm32-unknown-unknown --release 12 | 13 | lint: fmt clippy 14 | 15 | fmt: 16 | cargo fmt --all 17 | 18 | clippy: build 19 | cargo clippy --all-targets -- -D warnings -A clippy::too_many_arguments 20 | 21 | clean: 22 | cargo clean 23 | -------------------------------------------------------------------------------- /contracts/pool/src/error.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::contracterror; 2 | 3 | #[contracterror] 4 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 5 | #[repr(u32)] 6 | pub enum ContractError { 7 | SpreadExceedsLimit = 300, 8 | 9 | ProvideLiquiditySlippageToleranceTooHigh = 301, 10 | ProvideLiquidityAtLeastOneTokenMustBeBiggerThenZero = 302, 11 | 12 | WithdrawLiquidityMinimumAmountOfAOrBIsNotSatisfied = 303, 13 | SplitDepositBothPoolsAndDepositMustBePositive = 304, 14 | ValidateFeeBpsTotalFeesCantBeGreaterThan100 = 305, 15 | 16 | GetDepositAmountsMinABiggerThenDesiredA = 306, 17 | GetDepositAmountsMinBBiggerThenDesiredB = 307, 18 | GetDepositAmountsAmountABiggerThenDesiredA = 308, 19 | GetDepositAmountsAmountALessThenMinA = 309, 20 | GetDepositAmountsAmountBBiggerThenDesiredB = 310, 21 | GetDepositAmountsAmountBLessThenMinB = 311, 22 | TotalSharesEqualZero = 312, 23 | DesiredAmountsBelowOrEqualZero = 313, 24 | MinAmountsBelowZero = 314, 25 | AssetNotInPool = 315, 26 | AlreadyInitialized = 316, 27 | TokenABiggerThanTokenB = 317, 28 | InvalidBps = 318, 29 | SlippageInvalid = 319, 30 | 31 | SwapMinReceivedBiggerThanReturn = 320, 32 | TransactionAfterTimestampDeadline = 321, 33 | CannotConvertU256ToI128 = 322, 34 | UserDeclinesPoolFee = 323, 35 | SwapFeeBpsOverLimit = 324, 36 | NotEnoughSharesToBeMinted = 325, 37 | NotEnoughLiquidityProvided = 326, 38 | AdminNotSet = 327, 39 | ContractMathError = 328, 40 | NegativeInputProvided = 329, 41 | SameAdmin = 330, 42 | NoAdminChangeInPlace = 331, 43 | AdminChangeExpired = 332, 44 | } 45 | -------------------------------------------------------------------------------- /contracts/pool/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | mod contract; 3 | mod error; 4 | mod storage; 5 | 6 | pub mod token_contract { 7 | // The import will code generate: 8 | // - A ContractClient type that can be used to invoke functions on the contract. 9 | // - Any types in the contract that were annotated with #[contracttype]. 10 | soroban_sdk::contractimport!( 11 | file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" 12 | ); 13 | } 14 | 15 | pub mod stake_contract { 16 | soroban_sdk::contractimport!( 17 | file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" 18 | ); 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests; 23 | -------------------------------------------------------------------------------- /contracts/pool/src/tests.rs: -------------------------------------------------------------------------------- 1 | mod admin_change; 2 | mod config; 3 | mod liquidity; 4 | mod setup; 5 | mod stake_deployment; 6 | mod swap; 7 | -------------------------------------------------------------------------------- /contracts/pool/src/tests/stake_deployment.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | use soroban_sdk::{testutils::Address as _, Address, Env}; 3 | 4 | use super::setup::{deploy_liquidity_pool_contract, deploy_token_contract}; 5 | use crate::{ 6 | stake_contract, 7 | storage::{Config, PairType}, 8 | }; 9 | 10 | #[test] 11 | fn confirm_stake_contract_deployment() { 12 | let env = Env::default(); 13 | env.mock_all_auths(); 14 | env.cost_estimate().budget().reset_unlimited(); 15 | 16 | let mut admin1 = Address::generate(&env); 17 | let mut admin2 = Address::generate(&env); 18 | 19 | let mut token1 = deploy_token_contract(&env, &admin1); 20 | let mut token2 = deploy_token_contract(&env, &admin2); 21 | if token2.address < token1.address { 22 | std::mem::swap(&mut token1, &mut token2); 23 | std::mem::swap(&mut admin1, &mut admin2); 24 | } 25 | let user1 = Address::generate(&env); 26 | let stake_manager = Address::generate(&env); 27 | let stake_owner = Address::generate(&env); 28 | 29 | let swap_fees = 0i64; 30 | let pool = deploy_liquidity_pool_contract( 31 | &env, 32 | Some(admin1.clone()), 33 | (&token1.address, &token2.address), 34 | swap_fees, 35 | user1.clone(), 36 | 500, 37 | 200, 38 | stake_manager.clone(), 39 | stake_owner.clone(), 40 | ); 41 | 42 | let share_token_address = pool.query_share_token_address(); 43 | let stake_token_address = pool.query_stake_contract_address(); 44 | 45 | assert_eq!( 46 | pool.query_config(), 47 | Config { 48 | token_a: token1.address.clone(), 49 | token_b: token2.address.clone(), 50 | share_token: share_token_address.clone(), 51 | stake_contract: stake_token_address.clone(), 52 | pool_type: PairType::Xyk, 53 | total_fee_bps: 0, 54 | fee_recipient: user1, 55 | max_allowed_slippage_bps: 500, 56 | max_allowed_spread_bps: 200, 57 | max_referral_bps: 5_000, 58 | } 59 | ); 60 | 61 | let stake_client = stake_contract::Client::new(&env, &stake_token_address); 62 | assert_eq!( 63 | stake_client.query_config(), 64 | stake_contract::ConfigResponse { 65 | config: stake_contract::Config { 66 | lp_token: share_token_address, 67 | min_bond: 10, 68 | min_reward: 5, 69 | manager: stake_manager, 70 | owner: stake_owner, 71 | max_complexity: 10, 72 | } 73 | } 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /contracts/pool_stable/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phoenix-pool-stable" 3 | version = { workspace = true } 4 | authors = ["Jakub "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | testutils = ["soroban-sdk/testutils"] 14 | 15 | [lints] 16 | workspace = true 17 | 18 | [dependencies] 19 | soroban-decimal = { workspace = true } 20 | phoenix = { workspace = true } 21 | num-integer = { workspace = true } 22 | soroban-sdk = { workspace = true } 23 | 24 | [dev-dependencies] 25 | soroban-sdk = { workspace = true, features = ["testutils"] } 26 | -------------------------------------------------------------------------------- /contracts/pool_stable/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | all: lint build test 4 | 5 | test: build # because of token dependency 6 | cargo test 7 | 8 | build: 9 | $(MAKE) -C ../stake build || break; 10 | $(MAKE) -C ../token build || break; 11 | cargo build --target wasm32-unknown-unknown --release 12 | 13 | lint: fmt clippy 14 | 15 | fmt: 16 | cargo fmt --all 17 | 18 | clippy: build 19 | cargo clippy --all-targets -- -D warnings -A clippy::too_many_arguments 20 | 21 | clean: 22 | cargo clean 23 | -------------------------------------------------------------------------------- /contracts/pool_stable/README.md: -------------------------------------------------------------------------------- 1 | # Dex Stable Pool 2 | 3 | ## Main functionality 4 | This contract is being used for managing stable coins within the Phoenix DEX. It offers liquidity provision, trading assets and pool management functionalities. 5 | 6 | ## Messages: 7 | `initialize` 8 | 9 | Params: 10 | - `admin`: `Address` of the contract administrator to be. 11 | - `share_token_decimals`: `u32` value for the number of decimals to be used for the given contract. 12 | - `swap_fee_bps`: `i64` value for the comission fee for the network in the given stable liquidity pool. 13 | - `fee_recipient`: `Address` that will receive the aforementioned fee. 14 | - `max_allowed_slippage_bps`: `i64` value for the maximum allowed slippage for a swap, set in BPS. 15 | - `max_allowed_spread_bps`: `i64` value for the maximum allowed difference between the price at the current moment and the price on which the users agree to sell. Measured in BPS. 16 | - `token_init_info`: `TokenInitInfo` struct containing information for the initialization of one of the two tokens in the pool. 17 | - `stake_contract_info`: `StakeInitInfo` struct containing information for the initialization of the stake contract for the given stable liquidity pool. 18 | 19 | Return type: 20 | void 21 | 22 | Description: 23 | Used for the initialization of the stable liquidity pool contract - this sets the admin in Config, initializes both token contracts, that will be in the pool and also initializes the staking contract needed for providing liquidity. 24 | 25 |
26 | 27 | `provide_liquidity` 28 | 29 | Params: 30 | - `depositor`: `Address` of the ledger calling the current method and providing liqudity for the pool 31 | - `desired_a`: Optional `i128` value for amount of the first asset that the depositor wants to provide in the pool. 32 | - `min_a`: Optional `i128` value for minimum amount of the first asset that the depositor wants to provide in the pool. 33 | - `desired_b`: Optional `i128` value for amount of the second asset that the depositor wants to provide in the pool. 34 | - `min_b`: Optional `i128` value for minimum amount of the second asset that the depositor wants to provide in the pool. 35 | - `custom_slippage_bps`: Optional `i64` value for amount measured in BPS for the slippage tolerance. 36 | 37 | Return type: 38 | void 39 | 40 | Description: 41 | Allows the users to deposit optional pairs of tokens in the pool and receive awards in return. The awards are calculated based on the amount of assets deposited in the pool. 42 | 43 |
44 | 45 | `swap` 46 | 47 | Params: 48 | - `sender`: `Address` of the user that requests the swap. 49 | - `offer_asset`: `Address` for the asset the user wants to swap. 50 | - `offer_amount`: `i128` amount that the user wants to swap. 51 | - `belief_price`: Optional `i64` value that represents that users belived/expected price per token. 52 | - `max_spread_bps`: Optional `i64` value representing maximum allowed spread/slippage for the swap. 53 | 54 | Return type: 55 | i128 56 | 57 | Description: 58 | Exchanges one asset for another in the pool. 59 | 60 |
61 | 62 | `withdraw_liquidity` 63 | 64 | Params: 65 | - `recipient`: `Address` that will receive the withdrawn liquidity. 66 | - `share_amount`: `i128` amount of shares that the user will remove from the stable liquidity pool. 67 | - `min_a`: `i128` amount of the first token. 68 | - `min_b`: `i128` amount of the second token. 69 | 70 | Return type: 71 | (i128, i128) tuple of the amount of the first and second token to be sent back to the user. 72 | 73 | Description: 74 | Allows for users to withdraw their liquidity out of a pool, forcing them to burn their share tokens in the given pool, before they can get the assets back. 75 | 76 |
77 | 78 | `update_config` 79 | 80 | Params: 81 | - `sender`: `Address` of sender that wants to update the `Config` 82 | - `new_admin`: Optional `Address` of the new admin for the stable liquidity pool 83 | - `total_fee_bps`: Optional `i64` value for the total fees (in bps) charged by the pool 84 | - `fee_recipient`: Optional `Address` for the recipient of the swap commission fee 85 | - `max_allowed_slippage_bps`: Optional `i64` value the maximum allowed slippage for a swap, set in BPS. 86 | - `max_allowed_spread_bps`: Optional `i64` value for maximum allowed difference between the price at the current moment and the price on which the users agree to sell. Measured in BPS. 87 | 88 | Return type: 89 | void 90 | 91 | Description: 92 | Updates the stable liquidity pool `Config` information with new one. 93 | 94 |
95 | 96 | `upgrade` 97 | 98 | Params: 99 | - `new_wasm_hash`: `WASM hash` of the new stable liquidity pool contract 100 | 101 | Return type: 102 | void 103 | 104 | Description: 105 | Migration entrypoint 106 | 107 |
108 | 109 | ## Queries: 110 | `query_config` 111 | 112 | Params: 113 | None 114 | 115 | Return type: 116 | `Config` struct. 117 | 118 | Description: 119 | Queries the contract `Config` 120 | 121 |
122 | 123 | `query_share_token_address` 124 | 125 | Params: 126 | None 127 | 128 | Return type: 129 | `Address` of the pool's share token. 130 | 131 | Description: 132 | Returns the address for the pool share token. 133 | 134 |
135 | 136 | `query_stake_contract_address` 137 | 138 | Params: 139 | None 140 | 141 | Return type: 142 | `Address` of the pool's stake contract. 143 | 144 | Description: 145 | Returns the address for the pool stake contract. 146 | 147 |
148 | 149 | `query_pool_info` 150 | 151 | Params: 152 | None 153 | 154 | Return type: 155 | `PoolResponse` struct represented by two token assets and share token. 156 | 157 | Description: 158 | Returns the total amount of LP tokens and assets in a specific pool. 159 | 160 |
161 | 162 | `query_pool_info_for_factory` 163 | 164 | Params: 165 | None 166 | 167 | Return type: 168 | `LiquidityPoolInfo` struct representing information relevant for the stable liquidity pool. 169 | 170 | Description: 171 | Returns all the required information for a liquidity pool that is called by the factory contract. 172 |
173 | 174 | 175 | `simulate_swap` 176 | 177 | Params: 178 | - `offer_asset`: `Address` of the token that the user wants to sell. 179 | - `sell_amount`: `i128` value for the total amount that the user wants to sell. 180 | 181 | Return type: 182 | `SimulateSwapResponse` struct represented by `ask_amount: i128`, `commission_amount: i128`, `spread_amount: i128` and `total_return: i128`. 183 | 184 | Description: 185 | Simulate swap transaction. 186 |
187 | 188 | `simulate_reverse_swap` 189 | 190 | Params: 191 | - `ask_asset`: `Address` of the token that the user wants to buy. 192 | - `ask_amount`: `i128` value for the total amount that the user wants to buy. 193 | 194 | Return type: 195 | `SimulateReverseSwapResponse` struct represented by `offer_amount: i128`, `commission_amount: i128` and `spread_amount: i128`. 196 | 197 | Description: 198 | Simulate reverse swap transaction. 199 | -------------------------------------------------------------------------------- /contracts/pool_stable/src/error.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::contracterror; 2 | 3 | #[contracterror] 4 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 5 | #[repr(u32)] 6 | pub enum ContractError { 7 | SpreadExceedsLimit = 400, 8 | ProvideLiquiditySlippageToleranceTooHigh = 401, 9 | WithdrawLiquidityMinimumAmountOfAOrBIsNotSatisfied = 402, 10 | ValidateFeeBpsTotalFeesCantBeGreaterThan100 = 403, 11 | TotalSharesEqualZero = 404, 12 | AssetNotInPool = 405, 13 | AlreadyInitialized = 406, 14 | TokenABiggerThanTokenB = 407, 15 | InvalidBps = 408, 16 | LowLiquidity = 409, 17 | Unauthorized = 410, 18 | IncorrectAssetSwap = 411, 19 | NewtonMethodFailed = 412, 20 | CalcYErr = 413, 21 | SwapMinReceivedBiggerThanReturn = 414, 22 | ProvideLiquidityBothTokensMustBeMoreThanZero = 415, 23 | DivisionByZero = 416, 24 | InvalidAMP = 417, 25 | TransactionAfterTimestampDeadline = 418, 26 | SlippageToleranceExceeded = 419, 27 | IssuedSharesLessThanUserRequested = 420, 28 | SwapFeeBpsOverLimit = 421, 29 | UserDeclinesPoolFee = 422, 30 | AdminNotSet = 423, 31 | ContractMathError = 424, 32 | NegativeInputProvided = 425, 33 | SameAdmin = 426, 34 | NoAdminChangeInPlace = 427, 35 | AdminChangeExpired = 428, 36 | } 37 | -------------------------------------------------------------------------------- /contracts/pool_stable/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | mod contract; 3 | mod error; 4 | mod math; 5 | mod storage; 6 | 7 | pub mod token_contract { 8 | // The import will code generate: 9 | // - A ContractClient type that can be used to invoke functions on the contract. 10 | // - Any types in the contract that were annotated with #[contracttype]. 11 | soroban_sdk::contractimport!( 12 | file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" 13 | ); 14 | } 15 | 16 | pub mod stake_contract { 17 | soroban_sdk::contractimport!( 18 | file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" 19 | ); 20 | } 21 | 22 | const DECIMAL_PRECISION: u32 = 18; 23 | 24 | #[cfg(test)] 25 | mod tests; 26 | -------------------------------------------------------------------------------- /contracts/pool_stable/src/tests.rs: -------------------------------------------------------------------------------- 1 | mod admin_change; 2 | mod config; 3 | mod liquidity; 4 | mod queries; 5 | mod setup; 6 | mod stake_deployment; 7 | mod swap; 8 | -------------------------------------------------------------------------------- /contracts/pool_stable/src/tests/setup.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String}; 2 | 3 | use crate::{ 4 | contract::{StableLiquidityPool, StableLiquidityPoolClient}, 5 | token_contract, 6 | }; 7 | 8 | use phoenix::utils::{LiquidityPoolInitInfo, StakeInitInfo, TokenInitInfo}; 9 | 10 | pub fn deploy_token_contract<'a>(env: &Env, admin: &Address) -> token_contract::Client<'a> { 11 | token_contract::Client::new( 12 | env, 13 | &env.register_stellar_asset_contract_v2(admin.clone()) 14 | .address(), 15 | ) 16 | } 17 | 18 | pub fn install_token_wasm(env: &Env) -> BytesN<32> { 19 | soroban_sdk::contractimport!( 20 | file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" 21 | ); 22 | env.deployer().upload_contract_wasm(WASM) 23 | } 24 | 25 | pub fn install_stable_pool_wasm(env: &Env) -> BytesN<32> { 26 | soroban_sdk::contractimport!( 27 | file = "../../target/wasm32-unknown-unknown/release/phoenix_pool_stable.wasm" 28 | ); 29 | env.deployer().upload_contract_wasm(WASM) 30 | } 31 | 32 | pub fn install_stake_wasm(env: &Env) -> BytesN<32> { 33 | soroban_sdk::contractimport!( 34 | file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" 35 | ); 36 | env.deployer().upload_contract_wasm(WASM) 37 | } 38 | 39 | pub mod old_stable_liquidity_pool { 40 | soroban_sdk::contractimport!(file = "../../.artifacts_sdk_update/old_phoenix_pool_stable.wasm"); 41 | } 42 | 43 | pub fn install_old_token_wasm(env: &Env) -> BytesN<32> { 44 | soroban_sdk::contractimport!( 45 | file = "../../.artifacts_sdk_update/old_soroban_token_contract.wasm" 46 | ); 47 | env.deployer().upload_contract_wasm(WASM) 48 | } 49 | 50 | pub fn install_old_stake_wasm(env: &Env) -> BytesN<32> { 51 | soroban_sdk::contractimport!(file = "../../.artifacts_sdk_update/old_phoenix_stake.wasm"); 52 | env.deployer().upload_contract_wasm(WASM) 53 | } 54 | 55 | pub fn deploy_stable_liquidity_pool_contract<'a>( 56 | env: &Env, 57 | admin: impl Into>, 58 | token_a_b: (&Address, &Address), 59 | swap_fees: i64, 60 | fee_recipient: impl Into>, 61 | max_allowed_slippage_bps: impl Into>, 62 | max_allowed_spread_bps: impl Into>, 63 | stake_manager: Address, 64 | factory: Address, 65 | init_amp: impl Into>, 66 | ) -> StableLiquidityPoolClient<'a> { 67 | let admin = admin.into().unwrap_or(Address::generate(env)); 68 | 69 | let fee_recipient = fee_recipient 70 | .into() 71 | .unwrap_or_else(|| Address::generate(env)); 72 | 73 | let token_init_info = TokenInitInfo { 74 | token_a: token_a_b.0.clone(), 75 | token_b: token_a_b.1.clone(), 76 | }; 77 | let stake_init_info = StakeInitInfo { 78 | min_bond: 10i128, 79 | min_reward: 5i128, 80 | manager: stake_manager, 81 | max_complexity: 10u32, 82 | }; 83 | 84 | let token_wasm_hash = install_token_wasm(env); 85 | let stake_wasm_hash = install_stake_wasm(env); 86 | 87 | let lp_init_info = LiquidityPoolInitInfo { 88 | admin, 89 | swap_fee_bps: swap_fees, 90 | fee_recipient, 91 | max_allowed_slippage_bps: max_allowed_slippage_bps.into().unwrap_or(5_000), 92 | default_slippage_bps: 2_500, 93 | max_allowed_spread_bps: max_allowed_spread_bps.into().unwrap_or(1_000), 94 | max_referral_bps: 5_000, 95 | token_init_info, 96 | stake_init_info, 97 | }; 98 | let pool = StableLiquidityPoolClient::new( 99 | env, 100 | &env.register( 101 | StableLiquidityPool, 102 | ( 103 | &stake_wasm_hash, 104 | &token_wasm_hash, 105 | lp_init_info, 106 | &factory, 107 | String::from_str(env, "LP_SHARE_TOKEN"), 108 | String::from_str(env, "PHOBTCLP"), 109 | &init_amp.into().unwrap_or(6u64), 110 | &1_000i64, 111 | ), 112 | ), 113 | ); 114 | 115 | pool 116 | } 117 | -------------------------------------------------------------------------------- /contracts/pool_stable/src/tests/stake_deployment.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | use phoenix::utils::{LiquidityPoolInitInfo, StakeInitInfo, TokenInitInfo}; 3 | use soroban_sdk::{testutils::Address as _, Address, Env, String}; 4 | 5 | use super::setup::{deploy_stable_liquidity_pool_contract, deploy_token_contract}; 6 | use crate::contract::{StableLiquidityPool, StableLiquidityPoolClient}; 7 | use crate::tests::setup::{install_stake_wasm, install_token_wasm}; 8 | use crate::{ 9 | stake_contract, 10 | storage::{Config, PairType}, 11 | }; 12 | 13 | #[test] 14 | fn confirm_stake_contract_deployment() { 15 | let env = Env::default(); 16 | env.mock_all_auths(); 17 | env.cost_estimate().budget().reset_unlimited(); 18 | 19 | let mut admin1 = Address::generate(&env); 20 | let mut admin2 = Address::generate(&env); 21 | 22 | let mut token1 = deploy_token_contract(&env, &admin1); 23 | let mut token2 = deploy_token_contract(&env, &admin2); 24 | if token2.address < token1.address { 25 | std::mem::swap(&mut token1, &mut token2); 26 | std::mem::swap(&mut admin1, &mut admin2); 27 | } 28 | let user1 = Address::generate(&env); 29 | let swap_fees = 0i64; 30 | let factory = Address::generate(&env); 31 | let stake_manager = Address::generate(&env); 32 | let pool = deploy_stable_liquidity_pool_contract( 33 | &env, 34 | Some(admin1.clone()), 35 | (&token1.address, &token2.address), 36 | swap_fees, 37 | user1.clone(), 38 | 500, 39 | 200, 40 | stake_manager.clone(), 41 | factory.clone(), 42 | None, 43 | ); 44 | 45 | let share_token_address = pool.query_share_token_address(); 46 | let stake_token_address = pool.query_stake_contract_address(); 47 | 48 | assert_eq!( 49 | pool.query_config(), 50 | Config { 51 | token_a: token1.address.clone(), 52 | token_b: token2.address.clone(), 53 | share_token: share_token_address.clone(), 54 | stake_contract: stake_token_address.clone(), 55 | pool_type: PairType::Stable, 56 | total_fee_bps: 0, 57 | fee_recipient: user1, 58 | max_allowed_slippage_bps: 500, 59 | default_slippage_bps: 2_500, 60 | max_allowed_spread_bps: 200, 61 | } 62 | ); 63 | 64 | let stake_client = stake_contract::Client::new(&env, &stake_token_address); 65 | assert_eq!( 66 | stake_client.query_config(), 67 | stake_contract::ConfigResponse { 68 | config: stake_contract::Config { 69 | lp_token: share_token_address, 70 | min_bond: 10, 71 | min_reward: 5, 72 | owner: factory, 73 | manager: stake_manager, 74 | max_complexity: 10, 75 | } 76 | } 77 | ); 78 | } 79 | 80 | #[test] 81 | #[should_panic( 82 | expected = "Pool Stable: Initialize: First token must be alphabetically smaller than second token" 83 | )] 84 | fn pool_stable_initialization_should_fail_with_token_a_bigger_than_token_b() { 85 | let env = Env::default(); 86 | env.mock_all_auths(); 87 | env.cost_estimate().budget().reset_unlimited(); 88 | 89 | let mut admin1 = Address::generate(&env); 90 | let mut admin2 = Address::generate(&env); 91 | let user = Address::generate(&env); 92 | 93 | let mut token1 = deploy_token_contract(&env, &admin1); 94 | let mut token2 = deploy_token_contract(&env, &admin2); 95 | if token2.address >= token1.address { 96 | std::mem::swap(&mut token2, &mut token1); 97 | std::mem::swap(&mut admin2, &mut admin1); 98 | } 99 | 100 | let token_wasm_hash = install_token_wasm(&env); 101 | let stake_wasm_hash = install_stake_wasm(&env); 102 | let fee_recipient = user; 103 | let max_allowed_slippage = 5_000i64; // 50% if not specified 104 | let max_allowed_spread = 500i64; // 5% if not specified 105 | let amp = 6u64; 106 | let stake_manager = Address::generate(&env); 107 | let factory = Address::generate(&env); 108 | 109 | let token_init_info = TokenInitInfo { 110 | token_a: token1.address.clone(), 111 | token_b: token2.address.clone(), 112 | }; 113 | let stake_init_info = StakeInitInfo { 114 | min_bond: 10i128, 115 | min_reward: 5i128, 116 | manager: stake_manager.clone(), 117 | max_complexity: 10, 118 | }; 119 | 120 | let lp_init_info = LiquidityPoolInitInfo { 121 | admin: admin1, 122 | swap_fee_bps: 0i64, 123 | fee_recipient, 124 | max_allowed_slippage_bps: max_allowed_slippage, 125 | default_slippage_bps: 2_500, 126 | max_allowed_spread_bps: max_allowed_spread, 127 | max_referral_bps: 500, 128 | token_init_info, 129 | stake_init_info, 130 | }; 131 | 132 | let _ = StableLiquidityPoolClient::new( 133 | &env, 134 | &env.register( 135 | StableLiquidityPool, 136 | ( 137 | &stake_wasm_hash, 138 | &token_wasm_hash, 139 | lp_init_info, 140 | &factory, 141 | String::from_str(&env, "LP_SHARE_TOKEN"), 142 | String::from_str(&env, "PHOBTCLP"), 143 | &, 144 | &150i64, 145 | ), 146 | ), 147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /contracts/stake/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phoenix-stake" 3 | version = { workspace = true } 4 | authors = ["Jakub "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | testutils = ["soroban-sdk/testutils"] 14 | 15 | [lints] 16 | workspace = true 17 | 18 | [dependencies] 19 | soroban-decimal = { workspace = true } 20 | curve = { workspace = true } 21 | phoenix = { workspace = true } 22 | soroban-sdk = { workspace = true } 23 | itoa = { version = "1.0", default-features = false } 24 | 25 | [dev-dependencies] 26 | soroban-sdk = { workspace = true, features = ["testutils"] } 27 | pretty_assertions = { workspace = true } 28 | -------------------------------------------------------------------------------- /contracts/stake/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | all: lint build test 4 | 5 | test: build 6 | cargo test 7 | 8 | build: 9 | $(MAKE) -C ../token build || break; 10 | cargo build --target wasm32-unknown-unknown --release 11 | 12 | lint: fmt clippy 13 | 14 | fmt: 15 | cargo fmt --all 16 | 17 | clippy: build 18 | cargo clippy --all-targets -- -D warnings 19 | 20 | clean: 21 | cargo clean 22 | -------------------------------------------------------------------------------- /contracts/stake/README.md: -------------------------------------------------------------------------------- 1 | # STAKING 2 | 3 | ## Main functionality 4 | Provides staking capabilities, reward distribution and reward management functionalities to the Phoenix DEX. 5 | 6 | ## Messages: 7 | `initialize` 8 | 9 | Params: 10 | - `admin`: `Address` of the administrator for the contract 11 | - `lp_token`: `Address` of the liquidity pool used with this stake contract 12 | - `min_bond`: `i128` value showing the minimum required bond 13 | - `min_reward`: `i128` the minimum amount of rewards the user can withdraw. 14 | 15 | Return type: 16 | void 17 | 18 | Description: 19 | Used to set up the staking contract with the initial parameters. 20 | 21 |
22 | 23 | `bond` 24 | 25 | Params: 26 | - `sender`: `Address` of the user that sends tokens to the stake contract. 27 | - `tokens`: `i128` value representing the number of tokens the user sends. 28 | 29 | Return type: 30 | void 31 | 32 | Description: 33 | Allows for users to stake/bond their lp tokens 34 | 35 |
36 | 37 | `unbond` 38 | 39 | Params: 40 | - `sender`: `Address` of the user that wants to unbond/unstake their tokens. 41 | - `stake_amount`: `i128` value representing the numbers of stake to be unbond. 42 | - `take_timestamp`: `u64`value used to calculate the correct stake to be removed 43 | 44 | Return type: 45 | void 46 | 47 | Description: 48 | Allows the user remove their staked tokens from the stake contract, with any rewards they may have earned, based on the amount of staked tokens and stake's timestamp. 49 | 50 |
51 | 52 | `create_distribution_flow` 53 | 54 | Params: 55 | - `sender`: `Address` of the user that creates the flow 56 | - `sender`: `Address` of the user that will be managing the flow 57 | - `asset`: `Address` of the asset that will be used in the distribution flow 58 | 59 | Return type: 60 | void 61 | 62 | Description: 63 | Creates a distribution flow for sending rewards, that are managed by a manager for a specific asset. 64 | 65 |
66 | 67 | `distribute_rewards` 68 | 69 | Params: 70 | None 71 | 72 | Return type: 73 | void 74 | 75 | Description: 76 | Sends the rewards to all the users that have stakes, on the basis of the current reward distribution rule set and total staked amount. 77 | 78 |
79 | 80 | `withdraw_rewards` 81 | 82 | Params: 83 | - `sender`: `Address` of the user that wants to withdraw their rewards 84 | 85 | Return type: 86 | void 87 | 88 | Description: 89 | Allows for users to withdraw their rewards from the stake contract. 90 | 91 |
92 | 93 | `fund_distribution` 94 | 95 | Params: 96 | - `sender`: `Address` of the user that calls this method. 97 | - `start_time`: `u64` value representing the time in which the funding has started. 98 | - `distribution_duration`: `u64` value representing the duration for the distribution in seconds 99 | - `token_address`: `Address` of the token that will be used for the reward distribution 100 | - `token_amount`: `i128` value representing how many tokens will be allocated for the distribution time 101 | 102 | 103 | Return type: 104 | void 105 | 106 | Description: 107 | Sends funds for a reward distribution. 108 | 109 |
110 | 111 | ## Queries: 112 | `query_config` 113 | 114 | Params: 115 | None 116 | 117 | Return type: 118 | `ConfigResponse` struct. 119 | 120 | Description: 121 | Queries the contract `Config` 122 | 123 |
124 | 125 | `query_admin` 126 | 127 | Params: 128 | None 129 | 130 | Return type: 131 | `Address` struct. 132 | 133 | Description: 134 | Returns the address of the admin for the given stake contract. 135 | 136 |
137 | 138 | `query_staked` 139 | 140 | Params: 141 | - `address`: `Address` of the stake contract we want to query 142 | 143 | Return type: 144 | `StakedResponse` struct. 145 | 146 | Description: 147 | Provides information about the stakes of a specific address. 148 | 149 |
150 | 151 | `query_total_staked` 152 | 153 | Params: 154 | None 155 | 156 | Return type: 157 | `i128` 158 | 159 | Description: 160 | Returns the total amount of tokens currently staked in the contract. 161 | 162 |
163 | 164 | `query_annualized_rewards` 165 | 166 | Params: 167 | None 168 | 169 | Return type: 170 | `AnnualizedRewardsResponse` struct 171 | 172 | Description: 173 | Provides an overview of the annualized rewards for each distributed asset. 174 | 175 |
176 | 177 | `query_withdrawable_rewards` 178 | 179 | Params: 180 | - `address`: `Address` whose rewards we are searching 181 | 182 | Return type: 183 | `WithdrawableRewardsResponse` struct 184 | 185 | Description: 186 | Queries the amount of rewards that a given address can withdraw. 187 | 188 |
189 | 190 | `query_distributed_rewards` 191 | 192 | Params: 193 | - `asset`: `Address` of the token for which we query 194 | 195 | Return type: 196 | `u128` 197 | 198 | Description: 199 | Reports the total amount of rewards distributed for a specific asset. 200 | 201 |
202 | 203 | `query_undistributed_rewards` 204 | 205 | Params: 206 | - `asset`: `Address` of the token for which we query 207 | 208 | Return type: 209 | `u128` 210 | 211 | Description: 212 | Queries the total amount of remaining rewards for a given asset. 213 | -------------------------------------------------------------------------------- /contracts/stake/src/distribution.rs: -------------------------------------------------------------------------------- 1 | use soroban_decimal::Decimal; 2 | use soroban_sdk::{contracttype, log, panic_with_error, Address, Env, Map}; 3 | 4 | use crate::{error::ContractError, storage::BondingInfo}; 5 | use phoenix::ttl::{PERSISTENT_RENEWAL_THRESHOLD, PERSISTENT_TARGET_TTL}; 6 | 7 | const SECONDS_PER_DAY: u64 = 24 * 60 * 60; 8 | 9 | #[derive(Clone)] 10 | #[contracttype] 11 | pub enum DistributionDataKey { 12 | RewardHistory(Address), 13 | TotalStakedHistory, 14 | } 15 | 16 | pub fn save_reward_history(e: &Env, reward_token: &Address, reward_history: Map) { 17 | e.storage().persistent().set( 18 | &DistributionDataKey::RewardHistory(reward_token.clone()), 19 | &reward_history, 20 | ); 21 | e.storage().persistent().extend_ttl( 22 | &DistributionDataKey::RewardHistory(reward_token.clone()), 23 | PERSISTENT_RENEWAL_THRESHOLD, 24 | PERSISTENT_TARGET_TTL, 25 | ); 26 | } 27 | 28 | pub fn get_reward_history(e: &Env, reward_token: &Address) -> Map { 29 | let reward_history = e 30 | .storage() 31 | .persistent() 32 | .get(&DistributionDataKey::RewardHistory(reward_token.clone())) 33 | .unwrap(); 34 | e.storage().persistent().extend_ttl( 35 | &DistributionDataKey::RewardHistory(reward_token.clone()), 36 | PERSISTENT_RENEWAL_THRESHOLD, 37 | PERSISTENT_TARGET_TTL, 38 | ); 39 | 40 | reward_history 41 | } 42 | 43 | pub fn save_total_staked_history(e: &Env, total_staked_history: Map) { 44 | e.storage().persistent().set( 45 | &DistributionDataKey::TotalStakedHistory, 46 | &total_staked_history, 47 | ); 48 | e.storage().persistent().extend_ttl( 49 | &DistributionDataKey::TotalStakedHistory, 50 | PERSISTENT_RENEWAL_THRESHOLD, 51 | PERSISTENT_TARGET_TTL, 52 | ); 53 | } 54 | 55 | pub fn get_total_staked_history(e: &Env) -> Map { 56 | let total_staked_history = e 57 | .storage() 58 | .persistent() 59 | .get(&DistributionDataKey::TotalStakedHistory) 60 | .unwrap(); 61 | e.storage().persistent().extend_ttl( 62 | &DistributionDataKey::TotalStakedHistory, 63 | PERSISTENT_RENEWAL_THRESHOLD, 64 | PERSISTENT_TARGET_TTL, 65 | ); 66 | 67 | total_staked_history 68 | } 69 | 70 | pub fn calculate_pending_rewards( 71 | env: &Env, 72 | reward_token: &Address, 73 | user_info: &BondingInfo, 74 | ) -> i128 { 75 | let current_timestamp = env.ledger().timestamp(); 76 | let last_reward_day = user_info.last_reward_time; 77 | 78 | // Load reward history and total staked history from storage 79 | let reward_history = get_reward_history(env, reward_token); 80 | let total_staked_history = get_total_staked_history(env); 81 | 82 | // Get the keys from the reward history map (which are the days) 83 | let reward_keys = reward_history.keys(); 84 | 85 | let mut pending_rewards: i128 = 0; 86 | 87 | // Find the closest timestamp after last_reward_day 88 | if let Some(first_relevant_day) = reward_keys.iter().find(|&day| day > last_reward_day) { 89 | for staking_reward_day in reward_keys 90 | .iter() 91 | .skip_while(|&day| day < first_relevant_day) 92 | .take_while(|&day| day <= current_timestamp) 93 | { 94 | if let (Some(daily_reward), Some(total_staked)) = ( 95 | reward_history.get(staking_reward_day), 96 | total_staked_history.get(staking_reward_day), 97 | ) { 98 | if total_staked > 0 { 99 | // Calculate multiplier based on the age of each stake 100 | for stake in user_info.stakes.iter() { 101 | // Calculate the user's share of the total staked amount at the time 102 | let user_share = (stake.stake as u128) 103 | .checked_mul(daily_reward) 104 | .and_then(|product| product.checked_div(total_staked)) 105 | .unwrap_or_else(|| { 106 | log!(&env, "Pool Stable: Math error in user share calculation"); 107 | panic_with_error!(&env, ContractError::ContractMathError); 108 | }); 109 | let stake_age_days = (staking_reward_day 110 | .saturating_sub(stake.stake_timestamp)) 111 | / SECONDS_PER_DAY; 112 | if stake_age_days == 0u64 { 113 | continue; 114 | } 115 | let multiplier = if stake_age_days >= 60 { 116 | Decimal::one() 117 | } else { 118 | Decimal::from_ratio(stake_age_days, 60) 119 | }; 120 | 121 | // Apply the multiplier and accumulate the rewards 122 | let adjusted_reward = user_share as i128 * multiplier; 123 | pending_rewards = pending_rewards 124 | .checked_add(adjusted_reward) 125 | .unwrap_or_else(|| { 126 | log!(&env, "Pool Stable: overflow occured"); 127 | panic_with_error!(&env, ContractError::ContractMathError); 128 | }); 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | pending_rewards 136 | } 137 | -------------------------------------------------------------------------------- /contracts/stake/src/error.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::contracterror; 2 | 3 | #[contracterror] 4 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 5 | #[repr(u32)] 6 | pub enum ContractError { 7 | AlreadyInitialized = 500, 8 | InvalidMinBond = 501, 9 | InvalidMinReward = 502, 10 | InvalidBond = 503, 11 | Unauthorized = 504, 12 | MinRewardNotEnough = 505, 13 | RewardsInvalid = 506, 14 | StakeNotFound = 509, 15 | InvalidTime = 510, 16 | DistributionExists = 511, 17 | InvalidRewardAmount = 512, 18 | InvalidMaxComplexity = 513, 19 | DistributionNotFound = 514, 20 | AdminNotSet = 515, 21 | ContractMathError = 516, 22 | RewardCurveDoesNotExist = 517, 23 | SameAdmin = 518, 24 | NoAdminChangeInPlace = 519, 25 | AdminChangeExpired = 520, 26 | } 27 | -------------------------------------------------------------------------------- /contracts/stake/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | mod contract; 3 | mod distribution; 4 | mod error; 5 | mod msg; 6 | mod storage; 7 | 8 | pub const TOKEN_PER_POWER: i32 = 1_000; 9 | 10 | pub mod token_contract { 11 | // The import will code generate: 12 | // - A ContractClient type that can be used to invoke functions on the contract. 13 | // - Any types in the contract that were annotated with #[contracttype]. 14 | soroban_sdk::contractimport!( 15 | file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" 16 | ); 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests; 21 | -------------------------------------------------------------------------------- /contracts/stake/src/msg.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contracttype, Address, String, Vec}; 2 | 3 | use crate::storage::{Config, Stake}; 4 | 5 | #[contracttype] 6 | #[derive(Clone, Debug, Eq, PartialEq)] 7 | pub struct ConfigResponse { 8 | pub config: Config, 9 | } 10 | 11 | #[contracttype] 12 | #[derive(Clone, Debug, Eq, PartialEq)] 13 | pub struct StakedResponse { 14 | pub stakes: Vec, 15 | pub total_stake: i128, 16 | pub last_reward_time: u64, 17 | } 18 | 19 | #[contracttype] 20 | #[derive(Debug, Clone, Eq, PartialEq)] 21 | pub struct AnnualizedReward { 22 | pub asset: Address, 23 | pub amount: String, 24 | } 25 | 26 | #[contracttype] 27 | #[derive(Debug, Clone, Eq, PartialEq)] 28 | pub struct AnnualizedRewardsResponse { 29 | pub rewards: Vec, 30 | } 31 | #[contracttype] 32 | #[derive(Debug, Clone, Eq, PartialEq)] 33 | pub struct WithdrawableReward { 34 | pub reward_address: Address, 35 | pub reward_amount: u128, 36 | } 37 | 38 | #[contracttype] 39 | #[derive(Debug, Clone, Eq, PartialEq)] 40 | pub struct WithdrawableRewardsResponse { 41 | /// Amount of rewards assigned for withdrawal from the given address. 42 | pub rewards: Vec, 43 | } 44 | -------------------------------------------------------------------------------- /contracts/stake/src/tests.rs: -------------------------------------------------------------------------------- 1 | mod bond; 2 | mod distribution; 3 | mod setup; 4 | -------------------------------------------------------------------------------- /contracts/stake/src/tests/admin_change.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use phoenix::utils::AdminChange; 4 | use pretty_assertions::assert_eq; 5 | use soroban_sdk::{ 6 | testutils::{Address as _, Ledger}, 7 | Address, Env, 8 | }; 9 | 10 | use crate::{error::ContractError, storage::PENDING_ADMIN, tests::setup::deploy_staking_contract}; 11 | 12 | #[test] 13 | fn propose_admin() { 14 | let env = Env::default(); 15 | env.mock_all_auths(); 16 | 17 | let admin = Address::generate(&env); 18 | let new_admin = Address::generate(&env); 19 | 20 | let staking = deploy_staking_contract( 21 | &env, 22 | admin.clone(), 23 | &Address::generate(&env), 24 | &Address::generate(&env), 25 | &Address::generate(&env), 26 | &7u32, 27 | ); 28 | 29 | let result = staking.propose_admin(&new_admin, &None); 30 | assert_eq!(result, new_admin.clone()); 31 | 32 | let pending_admin: AdminChange = env.as_contract(&staking.address, || { 33 | env.storage().instance().get(&PENDING_ADMIN).unwrap() 34 | }); 35 | 36 | assert_eq!(staking.query_admin(), admin); 37 | assert_eq!(pending_admin.new_admin, new_admin); 38 | assert_eq!(pending_admin.time_limit, None); 39 | } 40 | 41 | #[test] 42 | fn replace_admin_fails_when_new_admin_is_same_as_current() { 43 | let env = Env::default(); 44 | env.mock_all_auths(); 45 | 46 | let admin = Address::generate(&env); 47 | 48 | let staking = deploy_staking_contract( 49 | &env, 50 | admin.clone(), 51 | &Address::generate(&env), 52 | &Address::generate(&env), 53 | &Address::generate(&env), 54 | &7u32, 55 | ); 56 | 57 | assert_eq!( 58 | staking.try_propose_admin(&admin, &None), 59 | Err(Ok(ContractError::SameAdmin)) 60 | ); 61 | assert_eq!(staking.query_admin(), admin); 62 | } 63 | 64 | #[test] 65 | fn accept_admin_successfully() { 66 | let env = Env::default(); 67 | env.mock_all_auths(); 68 | 69 | let admin = Address::generate(&env); 70 | let new_admin = Address::generate(&env); 71 | 72 | let staking = deploy_staking_contract( 73 | &env, 74 | admin.clone(), 75 | &Address::generate(&env), 76 | &Address::generate(&env), 77 | &Address::generate(&env), 78 | &7u32, 79 | ); 80 | 81 | staking.propose_admin(&new_admin, &None); 82 | assert_eq!(staking.query_admin(), admin); 83 | 84 | let result = staking.accept_admin(); 85 | assert_eq!(result, new_admin.clone()); 86 | assert_eq!(staking.query_admin(), new_admin); 87 | 88 | let pending_admin: Option = env.as_contract(&staking.address, || { 89 | env.storage().instance().get(&PENDING_ADMIN) 90 | }); 91 | assert!(pending_admin.is_none()); 92 | } 93 | 94 | #[test] 95 | fn accept_admin_fails_when_no_pending_admin() { 96 | let env = Env::default(); 97 | env.mock_all_auths(); 98 | 99 | let admin = Address::generate(&env); 100 | 101 | let staking = deploy_staking_contract( 102 | &env, 103 | admin.clone(), 104 | &Address::generate(&env), 105 | &Address::generate(&env), 106 | &Address::generate(&env), 107 | &7u32, 108 | ); 109 | 110 | assert_eq!( 111 | staking.try_accept_admin(), 112 | Err(Ok(ContractError::NoAdminChangeInPlace)) 113 | ); 114 | 115 | assert_eq!(staking.query_admin(), admin); 116 | } 117 | 118 | #[test] 119 | fn accept_admin_fails_when_time_limit_expired() { 120 | let env = Env::default(); 121 | env.mock_all_auths(); 122 | 123 | let admin = Address::generate(&env); 124 | let new_admin = Address::generate(&env); 125 | 126 | let staking = deploy_staking_contract( 127 | &env, 128 | admin.clone(), 129 | &Address::generate(&env), 130 | &Address::generate(&env), 131 | &Address::generate(&env), 132 | &7u32, 133 | ); 134 | 135 | let time_limit = 1000u64; 136 | staking.propose_admin(&new_admin, &Some(time_limit)); 137 | env.ledger().set_timestamp(time_limit + 100); 138 | 139 | assert_eq!( 140 | staking.try_accept_admin(), 141 | Err(Ok(ContractError::AdminChangeExpired)) 142 | ); 143 | assert_eq!(staking.query_admin(), admin); 144 | } 145 | 146 | #[test] 147 | fn accept_admin_successfully_with_time_limit() { 148 | let env = Env::default(); 149 | env.mock_all_auths(); 150 | 151 | let admin = Address::generate(&env); 152 | let new_admin = Address::generate(&env); 153 | 154 | let staking = deploy_staking_contract( 155 | &env, 156 | admin.clone(), 157 | &Address::generate(&env), 158 | &Address::generate(&env), 159 | &Address::generate(&env), 160 | &7u32, 161 | ); 162 | 163 | let time_limit = 1_500; 164 | staking.propose_admin(&new_admin, &Some(time_limit)); 165 | assert_eq!(staking.query_admin(), admin); 166 | 167 | env.ledger().set_timestamp(1_000u64); 168 | 169 | let result = staking.accept_admin(); 170 | assert_eq!(result, new_admin); 171 | assert_eq!(staking.query_admin(), new_admin); 172 | 173 | let pending_admin: Option = env.as_contract(&staking.address, || { 174 | env.storage().instance().get(&PENDING_ADMIN) 175 | }); 176 | assert!(pending_admin.is_none()); 177 | } 178 | 179 | #[test] 180 | fn accept_admin_successfully_on_time_limit() { 181 | let env = Env::default(); 182 | env.mock_all_auths(); 183 | 184 | let admin = Address::generate(&env); 185 | let new_admin = Address::generate(&env); 186 | 187 | let staking = deploy_staking_contract( 188 | &env, 189 | admin.clone(), 190 | &Address::generate(&env), 191 | &Address::generate(&env), 192 | &Address::generate(&env), 193 | &7u32, 194 | ); 195 | 196 | let time_limit = 1_500; 197 | staking.propose_admin(&new_admin, &Some(time_limit)); 198 | assert_eq!(staking.query_admin(), admin); 199 | 200 | env.ledger().set_timestamp(time_limit); 201 | 202 | let result = staking.accept_admin(); 203 | assert_eq!(result, new_admin); 204 | assert_eq!(staking.query_admin(), new_admin); 205 | 206 | let pending_admin: Option = env.as_contract(&staking.address, || { 207 | env.storage().instance().get(&PENDING_ADMIN) 208 | }); 209 | assert!(pending_admin.is_none()); 210 | } 211 | 212 | #[test] 213 | fn propose_admin_then_revoke() { 214 | let env = Env::default(); 215 | env.mock_all_auths(); 216 | 217 | let admin = Address::generate(&env); 218 | let new_admin = Address::generate(&env); 219 | 220 | let staking = deploy_staking_contract( 221 | &env, 222 | admin.clone(), 223 | &Address::generate(&env), 224 | &Address::generate(&env), 225 | &Address::generate(&env), 226 | &7u32, 227 | ); 228 | 229 | staking.propose_admin(&new_admin, &None); 230 | staking.revoke_admin_change(); 231 | 232 | let pending_admin: Option = env.as_contract(&staking.address, || { 233 | env.storage().instance().get(&PENDING_ADMIN) 234 | }); 235 | 236 | assert!(pending_admin.is_none()); 237 | } 238 | 239 | #[test] 240 | fn revoke_admin_should_fail_when_no_admin_change_in_place() { 241 | let env = Env::default(); 242 | env.mock_all_auths(); 243 | 244 | let admin = Address::generate(&env); 245 | 246 | let staking = deploy_staking_contract( 247 | &env, 248 | admin.clone(), 249 | &Address::generate(&env), 250 | &Address::generate(&env), 251 | &Address::generate(&env), 252 | &7u32, 253 | ); 254 | 255 | assert_eq!( 256 | staking.try_revoke_admin_change(), 257 | Err(Ok(ContractError::NoAdminChangeInPlace)) 258 | ); 259 | } 260 | -------------------------------------------------------------------------------- /contracts/stake/src/tests/setup.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | contract::{Staking, StakingClient}, 3 | token_contract, 4 | }; 5 | use soroban_sdk::{testutils::Address as _, Address, BytesN, Env}; 6 | 7 | pub const ONE_WEEK: u64 = 604800; 8 | pub const ONE_DAY: u64 = 86400; 9 | pub const SIXTY_DAYS: u64 = 60 * ONE_DAY; 10 | 11 | pub fn deploy_token_contract<'a>(env: &Env, admin: &Address) -> token_contract::Client<'a> { 12 | token_contract::Client::new( 13 | env, 14 | &env.register_stellar_asset_contract_v2(admin.clone()) 15 | .address(), 16 | ) 17 | } 18 | 19 | pub mod latest_stake { 20 | soroban_sdk::contractimport!( 21 | file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" 22 | ); 23 | } 24 | 25 | #[allow(dead_code)] 26 | fn install_stake_latest_wasm(env: &Env) -> BytesN<32> { 27 | env.deployer().upload_contract_wasm(latest_stake::WASM) 28 | } 29 | 30 | const MIN_BOND: i128 = 1000; 31 | const MIN_REWARD: i128 = 1000; 32 | 33 | pub fn deploy_staking_contract<'a>( 34 | env: &Env, 35 | admin: impl Into>, 36 | lp_token: &Address, 37 | manager: &Address, 38 | owner: &Address, 39 | max_complexity: &u32, 40 | ) -> StakingClient<'a> { 41 | let admin = admin.into().unwrap_or(Address::generate(env)); 42 | let staking = StakingClient::new( 43 | env, 44 | &env.register( 45 | Staking, 46 | ( 47 | &admin, 48 | lp_token, 49 | &MIN_BOND, 50 | &MIN_REWARD, 51 | manager, 52 | owner, 53 | max_complexity, 54 | ), 55 | ), 56 | ); 57 | 58 | staking 59 | } 60 | -------------------------------------------------------------------------------- /contracts/token/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "soroban-token-contract" 3 | description = "Soroban standard token contract" 4 | version = "0.0.6" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | soroban-sdk = { workspace = true } 12 | soroban-token-sdk = { workspace = true } 13 | 14 | [dev-dependencies] 15 | soroban-sdk = { workspace = true, features = ["testutils"] } 16 | -------------------------------------------------------------------------------- /contracts/token/Makefile: -------------------------------------------------------------------------------- 1 | default: build 2 | 3 | all: test 4 | 5 | test: build 6 | cargo test 7 | 8 | build: 9 | cargo build --target wasm32-unknown-unknown --release 10 | 11 | fmt: 12 | cargo fmt --all 13 | 14 | clippy: build 15 | cargo clippy --tests -- -D warnings 16 | 17 | clean: 18 | cargo clean 19 | -------------------------------------------------------------------------------- /contracts/token/README.md: -------------------------------------------------------------------------------- 1 | # TOKEN 2 | 3 | ```Follows the example of [Soroban token](https://github.com/stellar/soroban-examples/tree/main/token)``` 4 | -------------------------------------------------------------------------------- /contracts/token/src/admin.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{Address, Env}; 2 | 3 | use crate::storage_types::DataKey; 4 | 5 | pub fn read_administrator(e: &Env) -> Address { 6 | let key = DataKey::Admin; 7 | e.storage().instance().get(&key).unwrap() 8 | } 9 | 10 | pub fn write_administrator(e: &Env, id: &Address) { 11 | let key = DataKey::Admin; 12 | e.storage().instance().set(&key, id); 13 | } 14 | -------------------------------------------------------------------------------- /contracts/token/src/allowance.rs: -------------------------------------------------------------------------------- 1 | use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; 2 | use soroban_sdk::{Address, Env}; 3 | 4 | pub fn read_allowance(e: &Env, from: Address, spender: Address) -> AllowanceValue { 5 | let key = DataKey::Allowance(AllowanceDataKey { from, spender }); 6 | if let Some(allowance) = e.storage().temporary().get::<_, AllowanceValue>(&key) { 7 | if allowance.expiration_ledger < e.ledger().sequence() { 8 | AllowanceValue { 9 | amount: 0, 10 | expiration_ledger: allowance.expiration_ledger, 11 | } 12 | } else { 13 | allowance 14 | } 15 | } else { 16 | AllowanceValue { 17 | amount: 0, 18 | expiration_ledger: 0, 19 | } 20 | } 21 | } 22 | 23 | pub fn write_allowance( 24 | e: &Env, 25 | from: Address, 26 | spender: Address, 27 | amount: i128, 28 | expiration_ledger: u32, 29 | ) { 30 | let allowance = AllowanceValue { 31 | amount, 32 | expiration_ledger, 33 | }; 34 | 35 | if amount > 0 && expiration_ledger < e.ledger().sequence() { 36 | panic!("expiration_ledger is less than ledger seq when amount > 0") 37 | } 38 | 39 | let key = DataKey::Allowance(AllowanceDataKey { from, spender }); 40 | e.storage().temporary().set(&key.clone(), &allowance); 41 | 42 | if amount > 0 { 43 | let live_for = expiration_ledger 44 | .checked_sub(e.ledger().sequence()) 45 | .unwrap(); 46 | 47 | e.storage().temporary().extend_ttl(&key, live_for, live_for) 48 | } 49 | } 50 | 51 | pub fn spend_allowance(e: &Env, from: Address, spender: Address, amount: i128) { 52 | let allowance = read_allowance(e, from.clone(), spender.clone()); 53 | if allowance.amount < amount { 54 | panic!("insufficient allowance"); 55 | } 56 | if amount > 0 { 57 | write_allowance( 58 | e, 59 | from, 60 | spender, 61 | allowance.amount - amount, 62 | allowance.expiration_ledger, 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /contracts/token/src/balance.rs: -------------------------------------------------------------------------------- 1 | use crate::storage_types::{DataKey, BALANCE_BUMP_AMOUNT, BALANCE_LIFETIME_THRESHOLD}; 2 | use soroban_sdk::{Address, Env}; 3 | 4 | pub fn read_balance(e: &Env, addr: Address) -> i128 { 5 | let key = DataKey::Balance(addr); 6 | if let Some(balance) = e.storage().persistent().get::(&key) { 7 | e.storage() 8 | .persistent() 9 | .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); 10 | balance 11 | } else { 12 | 0 13 | } 14 | } 15 | 16 | fn write_balance(e: &Env, addr: Address, amount: i128) { 17 | let key = DataKey::Balance(addr); 18 | e.storage().persistent().set(&key, &amount); 19 | e.storage() 20 | .persistent() 21 | .extend_ttl(&key, BALANCE_LIFETIME_THRESHOLD, BALANCE_BUMP_AMOUNT); 22 | } 23 | 24 | pub fn receive_balance(e: &Env, addr: Address, amount: i128) { 25 | let balance = read_balance(e, addr.clone()); 26 | write_balance(e, addr, balance + amount); 27 | } 28 | 29 | pub fn spend_balance(e: &Env, addr: Address, amount: i128) { 30 | let balance = read_balance(e, addr.clone()); 31 | if balance < amount { 32 | panic!("insufficient balance"); 33 | } 34 | write_balance(e, addr, balance - amount); 35 | } 36 | -------------------------------------------------------------------------------- /contracts/token/src/contract.rs: -------------------------------------------------------------------------------- 1 | //! This contract demonstrates a sample implementation of the Soroban token 2 | //! interface. 3 | use crate::admin::{read_administrator, write_administrator}; 4 | use crate::allowance::{read_allowance, spend_allowance, write_allowance}; 5 | use crate::balance::{read_balance, receive_balance, spend_balance}; 6 | use crate::metadata::{read_decimal, read_name, read_symbol, write_metadata}; 7 | #[cfg(test)] 8 | use crate::storage_types::{AllowanceDataKey, AllowanceValue, DataKey}; 9 | use crate::storage_types::{INSTANCE_BUMP_AMOUNT, INSTANCE_LIFETIME_THRESHOLD}; 10 | use soroban_sdk::token::{self, Interface as _}; 11 | use soroban_sdk::{contract, contractimpl, Address, Env, String}; 12 | use soroban_token_sdk::metadata::TokenMetadata; 13 | use soroban_token_sdk::TokenUtils; 14 | 15 | fn check_nonnegative_amount(amount: i128) { 16 | if amount < 0 { 17 | panic!("negative amount is not allowed: {}", amount) 18 | } 19 | } 20 | 21 | #[contract] 22 | pub struct Token; 23 | 24 | #[contractimpl] 25 | impl Token { 26 | pub fn __constructor(e: Env, admin: Address, decimal: u32, name: String, symbol: String) { 27 | if decimal > 18 { 28 | panic!("Decimal must not be greater than 18"); 29 | } 30 | write_administrator(&e, &admin); 31 | write_metadata( 32 | &e, 33 | TokenMetadata { 34 | decimal, 35 | name, 36 | symbol, 37 | }, 38 | ) 39 | } 40 | 41 | pub fn mint(e: Env, to: Address, amount: i128) { 42 | check_nonnegative_amount(amount); 43 | let admin = read_administrator(&e); 44 | admin.require_auth(); 45 | 46 | e.storage() 47 | .instance() 48 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 49 | 50 | receive_balance(&e, to.clone(), amount); 51 | TokenUtils::new(&e).events().mint(admin, to, amount); 52 | } 53 | 54 | pub fn set_admin(e: Env, new_admin: Address) { 55 | let admin = read_administrator(&e); 56 | admin.require_auth(); 57 | 58 | e.storage() 59 | .instance() 60 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 61 | 62 | write_administrator(&e, &new_admin); 63 | TokenUtils::new(&e).events().set_admin(admin, new_admin); 64 | } 65 | 66 | #[cfg(test)] 67 | pub fn get_allowance(e: Env, from: Address, spender: Address) -> Option { 68 | let key = DataKey::Allowance(AllowanceDataKey { from, spender }); 69 | e.storage().temporary().get::<_, AllowanceValue>(&key) 70 | } 71 | } 72 | 73 | #[contractimpl] 74 | impl token::Interface for Token { 75 | fn allowance(e: Env, from: Address, spender: Address) -> i128 { 76 | e.storage() 77 | .instance() 78 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 79 | read_allowance(&e, from, spender).amount 80 | } 81 | 82 | fn approve(e: Env, from: Address, spender: Address, amount: i128, expiration_ledger: u32) { 83 | from.require_auth(); 84 | 85 | check_nonnegative_amount(amount); 86 | 87 | e.storage() 88 | .instance() 89 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 90 | 91 | write_allowance(&e, from.clone(), spender.clone(), amount, expiration_ledger); 92 | TokenUtils::new(&e) 93 | .events() 94 | .approve(from, spender, amount, expiration_ledger); 95 | } 96 | 97 | fn balance(e: Env, id: Address) -> i128 { 98 | e.storage() 99 | .instance() 100 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 101 | read_balance(&e, id) 102 | } 103 | 104 | fn transfer(e: Env, from: Address, to: Address, amount: i128) { 105 | from.require_auth(); 106 | 107 | check_nonnegative_amount(amount); 108 | 109 | e.storage() 110 | .instance() 111 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 112 | 113 | spend_balance(&e, from.clone(), amount); 114 | receive_balance(&e, to.clone(), amount); 115 | TokenUtils::new(&e).events().transfer(from, to, amount); 116 | } 117 | 118 | fn transfer_from(e: Env, spender: Address, from: Address, to: Address, amount: i128) { 119 | spender.require_auth(); 120 | 121 | check_nonnegative_amount(amount); 122 | 123 | e.storage() 124 | .instance() 125 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 126 | 127 | spend_allowance(&e, from.clone(), spender, amount); 128 | spend_balance(&e, from.clone(), amount); 129 | receive_balance(&e, to.clone(), amount); 130 | TokenUtils::new(&e).events().transfer(from, to, amount) 131 | } 132 | 133 | fn burn(e: Env, from: Address, amount: i128) { 134 | from.require_auth(); 135 | 136 | check_nonnegative_amount(amount); 137 | 138 | e.storage() 139 | .instance() 140 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 141 | 142 | spend_balance(&e, from.clone(), amount); 143 | TokenUtils::new(&e).events().burn(from, amount); 144 | } 145 | 146 | fn burn_from(e: Env, spender: Address, from: Address, amount: i128) { 147 | spender.require_auth(); 148 | 149 | check_nonnegative_amount(amount); 150 | 151 | e.storage() 152 | .instance() 153 | .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT); 154 | 155 | spend_allowance(&e, from.clone(), spender, amount); 156 | spend_balance(&e, from.clone(), amount); 157 | TokenUtils::new(&e).events().burn(from, amount) 158 | } 159 | 160 | fn decimals(e: Env) -> u32 { 161 | read_decimal(&e) 162 | } 163 | 164 | fn name(e: Env) -> String { 165 | read_name(&e) 166 | } 167 | 168 | fn symbol(e: Env) -> String { 169 | read_symbol(&e) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /contracts/token/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | mod admin; 4 | mod allowance; 5 | mod balance; 6 | mod contract; 7 | mod metadata; 8 | mod storage_types; 9 | mod test; 10 | 11 | pub use crate::contract::TokenClient; 12 | -------------------------------------------------------------------------------- /contracts/token/src/metadata.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{Env, String}; 2 | use soroban_token_sdk::{metadata::TokenMetadata, TokenUtils}; 3 | 4 | pub fn read_decimal(e: &Env) -> u32 { 5 | let util = TokenUtils::new(e); 6 | util.metadata().get_metadata().decimal 7 | } 8 | 9 | pub fn read_name(e: &Env) -> String { 10 | let util = TokenUtils::new(e); 11 | util.metadata().get_metadata().name 12 | } 13 | 14 | pub fn read_symbol(e: &Env) -> String { 15 | let util = TokenUtils::new(e); 16 | util.metadata().get_metadata().symbol 17 | } 18 | 19 | pub fn write_metadata(e: &Env, metadata: TokenMetadata) { 20 | let util = TokenUtils::new(e); 21 | util.metadata().set_metadata(&metadata); 22 | } 23 | -------------------------------------------------------------------------------- /contracts/token/src/storage_types.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{contracttype, Address}; 2 | 3 | pub(crate) const DAY_IN_LEDGERS: u32 = 17280; 4 | pub(crate) const INSTANCE_BUMP_AMOUNT: u32 = 7 * DAY_IN_LEDGERS; 5 | pub(crate) const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; 6 | 7 | pub(crate) const BALANCE_BUMP_AMOUNT: u32 = 30 * DAY_IN_LEDGERS; 8 | pub(crate) const BALANCE_LIFETIME_THRESHOLD: u32 = BALANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; 9 | 10 | #[derive(Clone)] 11 | #[contracttype] 12 | pub struct AllowanceDataKey { 13 | pub from: Address, 14 | pub spender: Address, 15 | } 16 | 17 | #[contracttype] 18 | pub struct AllowanceValue { 19 | pub amount: i128, 20 | pub expiration_ledger: u32, 21 | } 22 | 23 | #[derive(Clone)] 24 | #[contracttype] 25 | pub enum DataKey { 26 | Allowance(AllowanceDataKey), 27 | Balance(Address), 28 | State(Address), 29 | Admin, 30 | } 31 | -------------------------------------------------------------------------------- /contracts/token/src/test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | extern crate std; 3 | 4 | use crate::{contract::Token, TokenClient}; 5 | use soroban_sdk::{ 6 | symbol_short, 7 | testutils::{Address as _, AuthorizedFunction, AuthorizedInvocation}, 8 | Address, Env, FromVal, IntoVal, String, Symbol, 9 | }; 10 | 11 | fn create_token<'a>(e: &Env, admin: &Address) -> TokenClient<'a> { 12 | let token_contract = e.register( 13 | Token, 14 | ( 15 | admin, 16 | 7_u32, 17 | String::from_val(e, &"name"), 18 | String::from_val(e, &"symbol"), 19 | ), 20 | ); 21 | TokenClient::new(e, &token_contract) 22 | } 23 | 24 | #[test] 25 | fn test() { 26 | let e = Env::default(); 27 | e.mock_all_auths(); 28 | 29 | let admin1 = Address::generate(&e); 30 | let admin2 = Address::generate(&e); 31 | let user1 = Address::generate(&e); 32 | let user2 = Address::generate(&e); 33 | let user3 = Address::generate(&e); 34 | let token = create_token(&e, &admin1); 35 | 36 | token.mint(&user1, &1000); 37 | assert_eq!( 38 | e.auths(), 39 | std::vec![( 40 | admin1.clone(), 41 | AuthorizedInvocation { 42 | function: AuthorizedFunction::Contract(( 43 | token.address.clone(), 44 | symbol_short!("mint"), 45 | (&user1, 1000_i128).into_val(&e), 46 | )), 47 | sub_invocations: std::vec![] 48 | } 49 | )] 50 | ); 51 | assert_eq!(token.balance(&user1), 1000); 52 | 53 | token.approve(&user2, &user3, &500, &200); 54 | assert_eq!( 55 | e.auths(), 56 | std::vec![( 57 | user2.clone(), 58 | AuthorizedInvocation { 59 | function: AuthorizedFunction::Contract(( 60 | token.address.clone(), 61 | symbol_short!("approve"), 62 | (&user2, &user3, 500_i128, 200_u32).into_val(&e), 63 | )), 64 | sub_invocations: std::vec![] 65 | } 66 | )] 67 | ); 68 | assert_eq!(token.allowance(&user2, &user3), 500); 69 | 70 | token.transfer(&user1, &user2, &600); 71 | assert_eq!( 72 | e.auths(), 73 | std::vec![( 74 | user1.clone(), 75 | AuthorizedInvocation { 76 | function: AuthorizedFunction::Contract(( 77 | token.address.clone(), 78 | symbol_short!("transfer"), 79 | (&user1, &user2, 600_i128).into_val(&e), 80 | )), 81 | sub_invocations: std::vec![] 82 | } 83 | )] 84 | ); 85 | assert_eq!(token.balance(&user1), 400); 86 | assert_eq!(token.balance(&user2), 600); 87 | 88 | token.transfer_from(&user3, &user2, &user1, &400); 89 | assert_eq!( 90 | e.auths(), 91 | std::vec![( 92 | user3.clone(), 93 | AuthorizedInvocation { 94 | function: AuthorizedFunction::Contract(( 95 | token.address.clone(), 96 | Symbol::new(&e, "transfer_from"), 97 | (&user3, &user2, &user1, 400_i128).into_val(&e), 98 | )), 99 | sub_invocations: std::vec![] 100 | } 101 | )] 102 | ); 103 | assert_eq!(token.balance(&user1), 800); 104 | assert_eq!(token.balance(&user2), 200); 105 | 106 | token.transfer(&user1, &user3, &300); 107 | assert_eq!(token.balance(&user1), 500); 108 | assert_eq!(token.balance(&user3), 300); 109 | 110 | token.set_admin(&admin2); 111 | assert_eq!( 112 | e.auths(), 113 | std::vec![( 114 | admin1.clone(), 115 | AuthorizedInvocation { 116 | function: AuthorizedFunction::Contract(( 117 | token.address.clone(), 118 | symbol_short!("set_admin"), 119 | (&admin2,).into_val(&e), 120 | )), 121 | sub_invocations: std::vec![] 122 | } 123 | )] 124 | ); 125 | 126 | // Increase to 500 127 | token.approve(&user2, &user3, &500, &200); 128 | assert_eq!(token.allowance(&user2, &user3), 500); 129 | token.approve(&user2, &user3, &0, &200); 130 | assert_eq!( 131 | e.auths(), 132 | std::vec![( 133 | user2.clone(), 134 | AuthorizedInvocation { 135 | function: AuthorizedFunction::Contract(( 136 | token.address.clone(), 137 | symbol_short!("approve"), 138 | (&user2, &user3, 0_i128, 200_u32).into_val(&e), 139 | )), 140 | sub_invocations: std::vec![] 141 | } 142 | )] 143 | ); 144 | assert_eq!(token.allowance(&user2, &user3), 0); 145 | } 146 | 147 | #[test] 148 | fn test_burn() { 149 | let e = Env::default(); 150 | e.mock_all_auths(); 151 | 152 | let admin = Address::generate(&e); 153 | let user1 = Address::generate(&e); 154 | let user2 = Address::generate(&e); 155 | let token = create_token(&e, &admin); 156 | 157 | token.mint(&user1, &1000); 158 | assert_eq!(token.balance(&user1), 1000); 159 | 160 | token.approve(&user1, &user2, &500, &200); 161 | assert_eq!(token.allowance(&user1, &user2), 500); 162 | 163 | token.burn_from(&user2, &user1, &500); 164 | assert_eq!( 165 | e.auths(), 166 | std::vec![( 167 | user2.clone(), 168 | AuthorizedInvocation { 169 | function: AuthorizedFunction::Contract(( 170 | token.address.clone(), 171 | symbol_short!("burn_from"), 172 | (&user2, &user1, 500_i128).into_val(&e), 173 | )), 174 | sub_invocations: std::vec![] 175 | } 176 | )] 177 | ); 178 | 179 | assert_eq!(token.allowance(&user1, &user2), 0); 180 | assert_eq!(token.balance(&user1), 500); 181 | assert_eq!(token.balance(&user2), 0); 182 | 183 | token.burn(&user1, &500); 184 | assert_eq!( 185 | e.auths(), 186 | std::vec![( 187 | user1.clone(), 188 | AuthorizedInvocation { 189 | function: AuthorizedFunction::Contract(( 190 | token.address.clone(), 191 | symbol_short!("burn"), 192 | (&user1, 500_i128).into_val(&e), 193 | )), 194 | sub_invocations: std::vec![] 195 | } 196 | )] 197 | ); 198 | 199 | assert_eq!(token.balance(&user1), 0); 200 | assert_eq!(token.balance(&user2), 0); 201 | } 202 | 203 | #[test] 204 | #[should_panic(expected = "insufficient balance")] 205 | fn transfer_insufficient_balance() { 206 | let e = Env::default(); 207 | e.mock_all_auths(); 208 | 209 | let admin = Address::generate(&e); 210 | let user1 = Address::generate(&e); 211 | let user2 = Address::generate(&e); 212 | let token = create_token(&e, &admin); 213 | 214 | token.mint(&user1, &1000); 215 | assert_eq!(token.balance(&user1), 1000); 216 | 217 | token.transfer(&user1, &user2, &1001); 218 | } 219 | 220 | #[test] 221 | #[should_panic(expected = "insufficient allowance")] 222 | fn transfer_from_insufficient_allowance() { 223 | let e = Env::default(); 224 | e.mock_all_auths(); 225 | 226 | let admin = Address::generate(&e); 227 | let user1 = Address::generate(&e); 228 | let user2 = Address::generate(&e); 229 | let user3 = Address::generate(&e); 230 | let token = create_token(&e, &admin); 231 | 232 | token.mint(&user1, &1000); 233 | assert_eq!(token.balance(&user1), 1000); 234 | 235 | token.approve(&user1, &user3, &100, &200); 236 | assert_eq!(token.allowance(&user1, &user3), 100); 237 | 238 | token.transfer_from(&user3, &user1, &user2, &101); 239 | } 240 | 241 | #[test] 242 | #[should_panic(expected = "Decimal must not be greater than 18")] 243 | fn decimal_is_over_eighteen() { 244 | let e = Env::default(); 245 | let admin = Address::generate(&e); 246 | let _ = TokenClient::new( 247 | &e, 248 | &e.register( 249 | Token, 250 | ( 251 | admin, 252 | 19_u32, 253 | String::from_val(&e, &"name"), 254 | String::from_val(&e, &"symbol"), 255 | ), 256 | ), 257 | ); 258 | } 259 | 260 | #[test] 261 | fn test_zero_allowance() { 262 | // Here we test that transfer_from with a 0 amount does not create an empty allowance 263 | let e = Env::default(); 264 | e.mock_all_auths(); 265 | 266 | let admin = Address::generate(&e); 267 | let spender = Address::generate(&e); 268 | let from = Address::generate(&e); 269 | let token = create_token(&e, &admin); 270 | 271 | token.transfer_from(&spender, &from, &spender, &0); 272 | assert!(token.get_allowance(&from, &spender).is_none()); 273 | } 274 | -------------------------------------------------------------------------------- /contracts/trader/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phoenix-trader" 3 | version = { workspace = true } 4 | authors = ["Jakub ", "Kaloyan "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [features] 13 | testutils = ["soroban-sdk/testutils"] 14 | 15 | [lints] 16 | workspace = true 17 | 18 | [dependencies] 19 | soroban-sdk = { workspace = true } 20 | soroban-decimal = { workspace = true } 21 | phoenix = { workspace = true } 22 | 23 | [dev-dependencies] 24 | soroban-sdk = { workspace = true, features = ["testutils"] } 25 | test-case = "3.3.1" 26 | -------------------------------------------------------------------------------- /contracts/trader/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | all: lint build test 4 | 5 | test: build 6 | cargo test 7 | 8 | build: 9 | $(MAKE) -C ../token build || break; 10 | cargo build --target wasm32-unknown-unknown --release 11 | 12 | lint: fmt clippy 13 | 14 | fmt: 15 | cargo fmt --all 16 | 17 | clippy: build 18 | cargo clippy --all-targets -- -D warnings -A clippy::too_many_arguments 19 | 20 | clean: 21 | cargo clean 22 | -------------------------------------------------------------------------------- /contracts/trader/README.md: -------------------------------------------------------------------------------- 1 | This is the designated trader contract, which is responsible for accumulating fees from each trading pair and converting them into the $PHO token. The contract is initialized with an admin, who is responsible for adding trade routes and configuring trading pairs. 2 | 3 | **Messages** 4 | 5 | `initialize(env: Env, admin: Address, contract_name: String, pair_addresses: (Address, Address), output_token: Address, max_spread: Option)` 6 | 7 | Initializes the contract with the given admin and configuration. 8 | 9 | `trade_token(env: Env, token_address: Address, liquidity_pool: Address, amount: Option)` 10 | 11 | Performs a trade using the provided liquidity pool's contract. Only available tokens are traded. 12 | 13 | `transfer(env: Env, recipient: Address, amount: u64, token_address: Option
)` 14 | 15 | Transfers tokens between addresses. Admins can withdraw PHO tokens to a given recipient. 16 | 17 | **Queries** 18 | 19 | `query_balances(env: Env)` 20 | 21 | Returns the balances of all supported tokens. 22 | 23 | `query_trading_pairs(env: Env)` 24 | 25 | Returns the list of trading pairs configured for this contract. 26 | 27 | `query_admin_info(env: Env)` 28 | 29 | Returns information about the admin of this contract, including their address and permissions. 30 | 31 | `query token_info(env: Env)` 32 | 33 | Returns information about the output token, including its address and balance. 34 | 35 | `query_output_token_info(env: Env)` 36 | Return `OutputTokenInfo` struct representing info about the output token. -------------------------------------------------------------------------------- /contracts/trader/src/error.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::contracterror; 2 | 3 | #[contracterror] 4 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 5 | #[repr(u32)] 6 | pub enum ContractError { 7 | AdminNotFound = 601, 8 | ContractIdNotFound = 602, 9 | PairNotFound = 603, 10 | OutputTokenNotFound = 604, 11 | MaxSpreadNotFound = 605, 12 | Unauthorized = 606, 13 | SwapTokenNotInPair = 607, 14 | InvalidMaxSpreadBps = 608, 15 | InitValueNotFound = 609, 16 | AlreadyInitialized = 610, 17 | AdminNotSet = 611, 18 | SameAdmin = 612, 19 | NoAdminChangeInPlace = 613, 20 | AdminChangeExpired = 614, 21 | OutputTokenInPair = 615, 22 | } 23 | -------------------------------------------------------------------------------- /contracts/trader/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | mod contract; 3 | mod error; 4 | mod storage; 5 | #[cfg(test)] 6 | mod tests; 7 | 8 | pub mod token_contract { 9 | soroban_sdk::contractimport!( 10 | file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" 11 | ); 12 | } 13 | 14 | pub mod lp_contract { 15 | soroban_sdk::contractimport!( 16 | file = "../../target/wasm32-unknown-unknown/release/phoenix_pool.wasm" 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /contracts/trader/src/storage.rs: -------------------------------------------------------------------------------- 1 | use phoenix::ttl::{ 2 | INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL, PERSISTENT_RENEWAL_THRESHOLD, 3 | PERSISTENT_TARGET_TTL, 4 | }; 5 | use soroban_sdk::{ 6 | contracttype, log, panic_with_error, symbol_short, Address, ConversionError, Env, String, 7 | Symbol, TryFromVal, Val, 8 | }; 9 | 10 | use crate::error::ContractError; 11 | 12 | pub const ADMIN: Symbol = symbol_short!("ADMIN"); 13 | pub const TRADER_KEY: Symbol = symbol_short!("TRADER"); 14 | pub(crate) const PENDING_ADMIN: Symbol = symbol_short!("p_admin"); 15 | 16 | #[derive(Clone, Copy)] 17 | #[repr(u32)] 18 | pub enum DataKey { 19 | Admin, 20 | ContractId, 21 | Pair, 22 | Token, 23 | MaxSpread, 24 | IsInitialized, //TODO: deprecated, remove in future upgrade 25 | } 26 | 27 | #[contracttype] 28 | #[derive(Clone, Debug, PartialEq, Eq)] 29 | pub struct Asset { 30 | /// Denom 31 | pub symbol: String, 32 | /// The total amount of those tokens in the pool 33 | pub amount: i128, 34 | } 35 | 36 | #[contracttype] 37 | #[derive(Clone, Debug, Eq, PartialEq)] 38 | pub struct BalanceInfo { 39 | pub output_token: Asset, 40 | pub token_a: Asset, 41 | pub token_b: Asset, 42 | } 43 | 44 | #[contracttype] 45 | #[derive(Clone, Debug, Eq, PartialEq)] 46 | pub struct OutputTokenInfo { 47 | pub address: Address, 48 | pub name: String, 49 | pub symbol: String, 50 | pub decimal: u32, 51 | } 52 | 53 | impl TryFromVal for Val { 54 | type Error = ConversionError; 55 | 56 | fn try_from_val(_env: &Env, v: &DataKey) -> Result { 57 | Ok((*v as u32).into()) 58 | } 59 | } 60 | 61 | pub fn save_admin_old(env: &Env, address: &Address) { 62 | env.storage().persistent().set(&DataKey::Admin, address); 63 | env.storage().persistent().extend_ttl( 64 | &DataKey::Admin, 65 | PERSISTENT_RENEWAL_THRESHOLD, 66 | PERSISTENT_TARGET_TTL, 67 | ); 68 | } 69 | 70 | #[cfg(not(tarpaulin_include))] 71 | pub fn _save_admin(env: &Env, address: &Address) { 72 | env.storage().instance().set(&ADMIN, address); 73 | env.storage() 74 | .instance() 75 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 76 | } 77 | 78 | pub fn get_admin_old(env: &Env) -> Address { 79 | let admin = env 80 | .storage() 81 | .persistent() 82 | .get(&DataKey::Admin) 83 | .unwrap_or_else(|| { 84 | log!(&env, "Admin not set"); 85 | panic_with_error!(&env, ContractError::AdminNotFound) 86 | }); 87 | env.storage().persistent().extend_ttl( 88 | &DataKey::Admin, 89 | PERSISTENT_RENEWAL_THRESHOLD, 90 | PERSISTENT_TARGET_TTL, 91 | ); 92 | 93 | admin 94 | } 95 | 96 | #[cfg(not(tarpaulin_include))] 97 | pub fn _get_admin(env: &Env) -> Address { 98 | env.storage() 99 | .instance() 100 | .extend_ttl(INSTANCE_RENEWAL_THRESHOLD, INSTANCE_TARGET_TTL); 101 | 102 | env.storage().instance().get(&ADMIN).unwrap_or_else(|| { 103 | log!(env, "Trader: Admin not set"); 104 | panic_with_error!(&env, ContractError::AdminNotSet) 105 | }) 106 | } 107 | 108 | pub fn save_name(env: &Env, contract_id: &String) { 109 | env.storage() 110 | .persistent() 111 | .set(&DataKey::ContractId, contract_id); 112 | env.storage().persistent().extend_ttl( 113 | &DataKey::ContractId, 114 | PERSISTENT_RENEWAL_THRESHOLD, 115 | PERSISTENT_TARGET_TTL, 116 | ); 117 | } 118 | 119 | pub fn get_name(env: &Env) -> String { 120 | let name = env 121 | .storage() 122 | .persistent() 123 | .get(&DataKey::ContractId) 124 | .unwrap_or_else(|| { 125 | log!(&env, "Contract ID not set"); 126 | panic_with_error!(&env, ContractError::ContractIdNotFound) 127 | }); 128 | env.storage().persistent().extend_ttl( 129 | &DataKey::ContractId, 130 | PERSISTENT_RENEWAL_THRESHOLD, 131 | PERSISTENT_TARGET_TTL, 132 | ); 133 | 134 | name 135 | } 136 | 137 | pub fn save_pair(env: &Env, pair: &(Address, Address)) { 138 | env.storage().persistent().set(&DataKey::Pair, pair); 139 | env.storage().persistent().extend_ttl( 140 | &DataKey::Pair, 141 | PERSISTENT_RENEWAL_THRESHOLD, 142 | PERSISTENT_TARGET_TTL, 143 | ); 144 | } 145 | 146 | pub fn get_pair(env: &Env) -> (Address, Address) { 147 | let pair = env 148 | .storage() 149 | .persistent() 150 | .get(&DataKey::Pair) 151 | .unwrap_or_else(|| { 152 | log!(&env, "Pair not set"); 153 | panic_with_error!(env, ContractError::PairNotFound) 154 | }); 155 | env.storage().persistent().extend_ttl( 156 | &DataKey::Pair, 157 | PERSISTENT_RENEWAL_THRESHOLD, 158 | PERSISTENT_TARGET_TTL, 159 | ); 160 | 161 | pair 162 | } 163 | 164 | pub fn save_output_token(env: &Env, token: &Address) { 165 | env.storage().persistent().set(&DataKey::Token, token); 166 | env.storage().persistent().extend_ttl( 167 | &DataKey::Token, 168 | PERSISTENT_RENEWAL_THRESHOLD, 169 | PERSISTENT_TARGET_TTL, 170 | ); 171 | } 172 | 173 | pub fn get_output_token(env: &Env) -> Address { 174 | let token_addr = env 175 | .storage() 176 | .persistent() 177 | .get(&DataKey::Token) 178 | .unwrap_or_else(|| { 179 | log!(&env, "Token not set"); 180 | panic_with_error!(env, ContractError::OutputTokenNotFound) 181 | }); 182 | env.storage().persistent().extend_ttl( 183 | &DataKey::Token, 184 | PERSISTENT_RENEWAL_THRESHOLD, 185 | PERSISTENT_TARGET_TTL, 186 | ); 187 | 188 | token_addr 189 | } 190 | -------------------------------------------------------------------------------- /contracts/trader/src/tests.rs: -------------------------------------------------------------------------------- 1 | mod admin_change; 2 | mod msgs; 3 | mod setup; 4 | -------------------------------------------------------------------------------- /contracts/trader/src/tests/admin_change.rs: -------------------------------------------------------------------------------- 1 | extern crate std; 2 | 3 | use phoenix::utils::AdminChange; 4 | use soroban_sdk::{ 5 | testutils::{Address as _, Ledger}, 6 | Address, Env, String, 7 | }; 8 | 9 | use crate::{ 10 | contract::{Trader, TraderClient}, 11 | error::ContractError, 12 | storage::PENDING_ADMIN, 13 | }; 14 | 15 | #[test] 16 | fn propose_admin() { 17 | let env = Env::default(); 18 | env.mock_all_auths(); 19 | 20 | let admin = Address::generate(&env); 21 | let new_admin = Address::generate(&env); 22 | 23 | let trader = TraderClient::new( 24 | &env, 25 | &env.register( 26 | Trader, 27 | ( 28 | &admin, 29 | String::from_str(&env, "Trader"), 30 | &(Address::generate(&env), Address::generate(&env)), 31 | &Address::generate(&env), 32 | ), 33 | ), 34 | ); 35 | 36 | let result = trader.propose_admin(&new_admin, &None); 37 | assert_eq!(result, new_admin.clone()); 38 | 39 | let pending_admin: AdminChange = env.as_contract(&trader.address, || { 40 | env.storage().instance().get(&PENDING_ADMIN).unwrap() 41 | }); 42 | 43 | assert_eq!(trader.query_admin_address(), admin); 44 | assert_eq!(pending_admin.new_admin, new_admin); 45 | assert_eq!(pending_admin.time_limit, None); 46 | } 47 | 48 | #[test] 49 | fn replace_admin_fails_when_new_admin_is_same_as_current() { 50 | let env = Env::default(); 51 | env.mock_all_auths(); 52 | 53 | let admin = Address::generate(&env); 54 | 55 | let trader = TraderClient::new( 56 | &env, 57 | &env.register( 58 | Trader, 59 | ( 60 | &admin, 61 | String::from_str(&env, "Trader"), 62 | &(Address::generate(&env), Address::generate(&env)), 63 | &Address::generate(&env), 64 | ), 65 | ), 66 | ); 67 | 68 | assert_eq!( 69 | trader.try_propose_admin(&admin, &None), 70 | Err(Ok(ContractError::SameAdmin)) 71 | ); 72 | assert_eq!(trader.query_admin_address(), admin); 73 | } 74 | 75 | #[test] 76 | fn accept_admin_successfully() { 77 | let env = Env::default(); 78 | env.mock_all_auths(); 79 | 80 | let admin = Address::generate(&env); 81 | let new_admin = Address::generate(&env); 82 | 83 | let trader = TraderClient::new( 84 | &env, 85 | &env.register( 86 | Trader, 87 | ( 88 | &admin, 89 | String::from_str(&env, "Trader"), 90 | &(Address::generate(&env), Address::generate(&env)), 91 | &Address::generate(&env), 92 | ), 93 | ), 94 | ); 95 | 96 | trader.propose_admin(&new_admin, &None); 97 | assert_eq!(trader.query_admin_address(), admin); 98 | 99 | let result = trader.accept_admin(); 100 | assert_eq!(result, new_admin.clone()); 101 | assert_eq!(trader.query_admin_address(), new_admin); 102 | 103 | let pending_admin: Option = env.as_contract(&trader.address, || { 104 | env.storage().instance().get(&PENDING_ADMIN) 105 | }); 106 | assert!(pending_admin.is_none()); 107 | } 108 | 109 | #[test] 110 | fn accept_admin_fails_when_no_pending_admin() { 111 | let env = Env::default(); 112 | env.mock_all_auths(); 113 | 114 | let admin = Address::generate(&env); 115 | 116 | let trader = TraderClient::new( 117 | &env, 118 | &env.register( 119 | Trader, 120 | ( 121 | &admin, 122 | String::from_str(&env, "Trader"), 123 | &(Address::generate(&env), Address::generate(&env)), 124 | &Address::generate(&env), 125 | ), 126 | ), 127 | ); 128 | 129 | assert_eq!( 130 | trader.try_accept_admin(), 131 | Err(Ok(ContractError::NoAdminChangeInPlace)) 132 | ); 133 | 134 | assert_eq!(trader.query_admin_address(), admin); 135 | } 136 | 137 | #[test] 138 | fn accept_admin_fails_when_time_limit_expired() { 139 | let env = Env::default(); 140 | env.mock_all_auths(); 141 | 142 | let admin = Address::generate(&env); 143 | let new_admin = Address::generate(&env); 144 | 145 | let trader = TraderClient::new( 146 | &env, 147 | &env.register( 148 | Trader, 149 | ( 150 | &admin, 151 | String::from_str(&env, "Trader"), 152 | &(Address::generate(&env), Address::generate(&env)), 153 | &Address::generate(&env), 154 | ), 155 | ), 156 | ); 157 | 158 | let time_limit = 1000u64; 159 | trader.propose_admin(&new_admin, &Some(time_limit)); 160 | env.ledger().set_timestamp(time_limit + 100); 161 | 162 | assert_eq!( 163 | trader.try_accept_admin(), 164 | Err(Ok(ContractError::AdminChangeExpired)) 165 | ); 166 | assert_eq!(trader.query_admin_address(), admin); 167 | } 168 | 169 | #[test] 170 | fn accept_admin_successfully_with_time_limit() { 171 | let env = Env::default(); 172 | env.mock_all_auths(); 173 | 174 | let admin = Address::generate(&env); 175 | let new_admin = Address::generate(&env); 176 | 177 | let trader = TraderClient::new( 178 | &env, 179 | &env.register( 180 | Trader, 181 | ( 182 | &admin, 183 | String::from_str(&env, "Trader"), 184 | &(Address::generate(&env), Address::generate(&env)), 185 | &Address::generate(&env), 186 | ), 187 | ), 188 | ); 189 | 190 | let time_limit = 1_500; 191 | trader.propose_admin(&new_admin, &Some(time_limit)); 192 | assert_eq!(trader.query_admin_address(), admin); 193 | 194 | env.ledger().set_timestamp(1_000u64); 195 | 196 | let result = trader.accept_admin(); 197 | assert_eq!(result, new_admin); 198 | assert_eq!(trader.query_admin_address(), new_admin); 199 | 200 | let pending_admin: Option = env.as_contract(&trader.address, || { 201 | env.storage().instance().get(&PENDING_ADMIN) 202 | }); 203 | assert!(pending_admin.is_none()); 204 | } 205 | 206 | #[test] 207 | fn accept_admin_successfully_on_time_limit() { 208 | let env = Env::default(); 209 | env.mock_all_auths(); 210 | 211 | let admin = Address::generate(&env); 212 | let new_admin = Address::generate(&env); 213 | 214 | let trader_client = TraderClient::new( 215 | &env, 216 | &env.register( 217 | Trader, 218 | ( 219 | &admin, 220 | String::from_str(&env, "Trader"), 221 | &(Address::generate(&env), Address::generate(&env)), 222 | &Address::generate(&env), 223 | ), 224 | ), 225 | ); 226 | 227 | let time_limit = 1_500; 228 | trader_client.propose_admin(&new_admin, &Some(time_limit)); 229 | assert_eq!(trader_client.query_admin_address(), admin); 230 | 231 | env.ledger().set_timestamp(time_limit); 232 | 233 | let result = trader_client.accept_admin(); 234 | assert_eq!(result, new_admin); 235 | assert_eq!(trader_client.query_admin_address(), new_admin); 236 | 237 | let pending_admin: Option = env.as_contract(&trader_client.address, || { 238 | env.storage().instance().get(&PENDING_ADMIN) 239 | }); 240 | assert!(pending_admin.is_none()); 241 | } 242 | 243 | #[test] 244 | fn propose_admin_then_revoke() { 245 | let env = Env::default(); 246 | env.mock_all_auths(); 247 | 248 | let admin = Address::generate(&env); 249 | let new_admin = Address::generate(&env); 250 | 251 | let trader_client = TraderClient::new( 252 | &env, 253 | &env.register( 254 | Trader, 255 | ( 256 | &admin, 257 | String::from_str(&env, "Trader"), 258 | &(Address::generate(&env), Address::generate(&env)), 259 | &Address::generate(&env), 260 | ), 261 | ), 262 | ); 263 | 264 | trader_client.propose_admin(&new_admin, &None); 265 | trader_client.revoke_admin_change(); 266 | 267 | let pending_admin: Option = env.as_contract(&trader_client.address, || { 268 | env.storage().instance().get(&PENDING_ADMIN) 269 | }); 270 | 271 | assert!(pending_admin.is_none()); 272 | } 273 | 274 | #[test] 275 | fn revoke_admin_should_fail_when_no_admin_change_in_place() { 276 | let env = Env::default(); 277 | env.mock_all_auths(); 278 | 279 | let admin = Address::generate(&env); 280 | 281 | let trader_client = TraderClient::new( 282 | &env, 283 | &env.register( 284 | Trader, 285 | ( 286 | &admin, 287 | String::from_str(&env, "Trader"), 288 | &(Address::generate(&env), Address::generate(&env)), 289 | &Address::generate(&env), 290 | ), 291 | ), 292 | ); 293 | 294 | assert_eq!( 295 | trader_client.try_revoke_admin_change(), 296 | Err(Ok(ContractError::NoAdminChangeInPlace)) 297 | ); 298 | } 299 | -------------------------------------------------------------------------------- /contracts/trader/src/tests/setup.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{ 2 | testutils::{arbitrary::std, Address as _}, 3 | Address, BytesN, Env, String, 4 | }; 5 | 6 | use crate::{ 7 | lp_contract::{self, LiquidityPoolInitInfo, StakeInitInfo, TokenInitInfo}, 8 | token_contract, 9 | }; 10 | 11 | const TOKEN_WASM: &[u8] = 12 | include_bytes!("../../../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm"); 13 | 14 | pub fn install_token_wasm(env: &Env) -> BytesN<32> { 15 | env.deployer().upload_contract_wasm(token_contract::WASM) 16 | } 17 | 18 | pub fn install_stake_wasm(env: &Env) -> BytesN<32> { 19 | soroban_sdk::contractimport!( 20 | file = "../../target/wasm32-unknown-unknown/release/phoenix_stake.wasm" 21 | ); 22 | env.deployer().upload_contract_wasm(WASM) 23 | } 24 | 25 | pub fn deploy_token_contract<'a>( 26 | env: &Env, 27 | admin: &Address, 28 | decimal: &u32, 29 | name: &String, 30 | symbol: &String, 31 | ) -> token_contract::Client<'a> { 32 | let token_addr = env.register(TOKEN_WASM, (admin, *decimal, name.clone(), symbol.clone())); 33 | let token_client = token_contract::Client::new(env, &token_addr); 34 | 35 | token_client 36 | } 37 | 38 | pub fn deploy_and_init_lp_client( 39 | env: &Env, 40 | admin: Address, 41 | token_a: Address, 42 | token_a_amount: i128, 43 | token_b: Address, 44 | token_b_amount: i128, 45 | swap_fee_bps: i64, 46 | ) -> lp_contract::Client { 47 | let stake_wasm_hash = install_stake_wasm(env); 48 | let token_wasm_hash = install_token_wasm(env); 49 | 50 | let token_init_info = TokenInitInfo { 51 | token_a: token_a.clone(), 52 | token_b: token_b.clone(), 53 | }; 54 | let stake_init_info = StakeInitInfo { 55 | min_bond: 10i128, 56 | min_reward: 5i128, 57 | manager: Address::generate(env), 58 | max_complexity: 10u32, 59 | }; 60 | 61 | let lp_init_info = LiquidityPoolInitInfo { 62 | admin: admin.clone(), 63 | fee_recipient: admin.clone(), 64 | max_allowed_slippage_bps: 5000, 65 | default_slippage_bps: 2_500, 66 | max_allowed_spread_bps: 5000, 67 | swap_fee_bps, 68 | max_referral_bps: 5_000, 69 | token_init_info, 70 | stake_init_info, 71 | }; 72 | 73 | let lp_client = lp_contract::Client::new( 74 | env, 75 | &env.register( 76 | lp_contract::WASM, 77 | ( 78 | &stake_wasm_hash, 79 | &token_wasm_hash, 80 | lp_init_info, 81 | &Address::generate(env), 82 | String::from_str(env, "staked Phoenix"), 83 | String::from_str(env, "sPHO"), 84 | &100i64, 85 | &1_000i64, 86 | ), 87 | ), 88 | ); 89 | 90 | lp_client.provide_liquidity( 91 | &admin.clone(), 92 | &Some(token_a_amount), 93 | &None::, 94 | &Some(token_b_amount), 95 | &None::, 96 | &None::, 97 | &None, 98 | &false, 99 | ); 100 | lp_client 101 | } 102 | -------------------------------------------------------------------------------- /contracts/vesting/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phoenix-vesting" 3 | version = { workspace = true } 4 | authors = ["Jakub ", "Kaloyan "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [features] 10 | # Enables minter feature on the vesting contract 11 | # if enabled, a specified address can mint/burn tokens 12 | minter = [] 13 | default = [] 14 | 15 | [lib] 16 | crate-type = ["cdylib"] 17 | 18 | [lints] 19 | workspace = true 20 | 21 | [dependencies] 22 | soroban-decimal = { workspace = true } 23 | curve = { workspace = true } 24 | phoenix = { workspace = true } 25 | soroban-sdk = { workspace = true } 26 | 27 | [dev-dependencies] 28 | curve = { workspace = true, features = ["testutils"] } 29 | soroban-sdk = { workspace = true, features = ["testutils"] } 30 | -------------------------------------------------------------------------------- /contracts/vesting/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | all: lint build test 4 | 5 | test: build 6 | cargo test --all-features 7 | 8 | build: 9 | $(MAKE) -C ../token build || break; 10 | cargo build --all-features --target wasm32-unknown-unknown --release 11 | 12 | lint: fmt clippy 13 | 14 | fmt: 15 | cargo fmt --all 16 | 17 | clippy: build 18 | cargo clippy --all-targets --all-features -- -D warnings 19 | 20 | clean: 21 | cargo clean 22 | -------------------------------------------------------------------------------- /contracts/vesting/README.md: -------------------------------------------------------------------------------- 1 | # Phoenix Protocol Vesting 2 | 3 | ## Main functionality 4 | This is the vesting contract for the Phoenix Protocol. It's purpose is to regulate the distribution of tokens/assets over 5 | a certain time period in accordance to some predifined **conditions**. 6 | 7 | ## Messages 8 | 9 | `initialize` 10 | 11 | Params: 12 | - `admin`: `Address` of the admin. 13 | - `vesting_token`: `VestingTokenInfo` Struct representing relevant informatio to the token that will be vested. 14 | - `vesting_balances`: `Vec` vector of structs that holds the address, balance and curve of the initial vesting balances. 15 | - `minter_info`: `Option` address and capacity (curve) for the minter. 16 | - `max_vesting_complexity`: `u32` maximum allowed complexity of the vesting curve. 17 | 18 | Return type: 19 | `Result<(), ContractError>` 20 | 21 | Description: 22 | Initializes the vesting contract with the given parameters. 23 | 24 |
25 | 26 | `transfer_token` 27 | 28 | Params: 29 | - `from`: `Address` of the sender. 30 | - `to`: `Address` of the receiver. 31 | - `amount`: `i128` amount of tokens to transfer. 32 | 33 | Return type: 34 | `Result<(), ContractError>` 35 | 36 | Description: 37 | Transfers the given amount of tokens from the sender to the receiver obeying the vesting rules. 38 | 39 |
40 | 41 | `burn` 42 | 43 | Params: 44 | - `sender`: `Address` of the sender. 45 | - `amount`: `i128` amount of tokens to burn. 46 | 47 | Return type: 48 | `Result<(), ContractError>` 49 | 50 | Description: 51 | Burns the given amount of tokens from the sender. 52 | 53 |
54 | 55 | `mint` 56 | 57 | Params: 58 | - `sender`: `Address` of the sender. 59 | - `to`: `Address` of the receiver. 60 | - `amount`: `i128` amount of tokens to mint. 61 | 62 | Return type: 63 | Void 64 | 65 | Description: 66 | Mints the given amount of tokens to the receiver. 67 | 68 |
69 | 70 | `update_minter` 71 | 72 | Params: 73 | - `sender`: `Address` of the sender. 74 | - `new_minter`: `Address` new minter address. 75 | 76 | Return type: 77 | Void 78 | 79 | Description: 80 | Updates the minter address. 81 | 82 |
83 | 84 | `update_minter_capacity` 85 | 86 | Params: 87 | - `sender`: `Address` of the sender. 88 | - `new_capacity`: `u128` new capacity of the minter. 89 | 90 | Return type: 91 | Void 92 | 93 | Description: 94 | Updates the minter capacity by completely replacing it. 95 | 96 |
97 | 98 | ## Queries 99 | 100 | `query_balance` 101 | 102 | Params: 103 | - `address`: `Address` of the account we query 104 | 105 | Return type: 106 | `i128` balance of the account. 107 | 108 | Description: 109 | Queries the balance of the given account. 110 | 111 |
112 | 113 | `query_distribution_info` 114 | 115 | Params: 116 | - `address`: `Address` of the account we query 117 | 118 | Return type: 119 | `Result` curve of the account. 120 | 121 | Description: 122 | Queries the distribution info (Curve) of the given account. 123 | 124 |
125 | 126 | `query_token_info` 127 | 128 | Params: 129 | None 130 | 131 | Return type: 132 | `VestingTokenInfo` struct representing the token information. 133 | 134 | Description: 135 | Queries the token information. 136 | 137 |
138 | 139 | `query_minter` 140 | 141 | Params: 142 | None 143 | 144 | Return type: 145 | `MinterInfo` struct representing the minter information. 146 | 147 | Description: 148 | Queries the minter information. 149 | 150 |
151 | 152 | `query_vesting_contract_balance` 153 | 154 | Params: 155 | None 156 | 157 | Return type: 158 | `i128` total supply of the vesting token. 159 | 160 | Description: 161 | Queries the total supply of the vesting token. 162 | 163 |
-------------------------------------------------------------------------------- /contracts/vesting/src/error.rs: -------------------------------------------------------------------------------- 1 | use curve::CurveError; 2 | use soroban_sdk::contracterror; 3 | 4 | #[contracterror] 5 | #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] 6 | #[repr(u32)] 7 | pub enum ContractError { 8 | VestingNotFoundForAddress = 700, 9 | AllowanceNotFoundForGivenPair = 701, 10 | MinterNotFound = 702, 11 | NoBalanceFoundForAddress = 703, 12 | NoConfigFound = 704, 13 | NoAdminFound = 705, 14 | MissingBalance = 706, 15 | VestingComplexityTooHigh = 707, 16 | TotalVestedOverCapacity = 708, 17 | InvalidTransferAmount = 709, 18 | CantMoveVestingTokens = 710, 19 | NotEnoughCapacity = 711, 20 | NotAuthorized = 712, 21 | NeverFullyVested = 713, 22 | VestsMoreThanSent = 714, 23 | InvalidBurnAmount = 715, 24 | InvalidMintAmount = 716, 25 | InvalidAllowanceAmount = 717, 26 | DuplicateInitialBalanceAddresses = 718, 27 | CurveError = 719, 28 | NoWhitelistFound = 720, 29 | NoTokenInfoFound = 721, 30 | NoVestingComplexityValueFound = 722, 31 | NoAddressesToAdd = 723, 32 | NoEnoughtTokensToStart = 724, 33 | NotEnoughBalance = 725, 34 | 35 | VestingBothPresent = 726, 36 | VestingNonePresent = 727, 37 | 38 | CurveConstant = 728, 39 | CurveSLNotDecreasing = 729, 40 | AlreadyInitialized = 730, 41 | AdminNotFound = 731, 42 | ContractMathError = 732, 43 | 44 | SameAdmin = 733, 45 | NoAdminChangeInPlace = 734, 46 | AdminChangeExpired = 735, 47 | SameTokenAddress = 745, 48 | InvalidMaxComplexity = 746, 49 | } 50 | 51 | impl From for ContractError { 52 | fn from(_: CurveError) -> Self { 53 | ContractError::CurveError 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod test { 59 | use super::*; 60 | 61 | #[test] 62 | fn test_from_curve_error() { 63 | let curve_error = CurveError::TooComplex; 64 | let contract_error = ContractError::from(curve_error); 65 | assert_eq!(contract_error, ContractError::CurveError); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contracts/vesting/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | mod contract; 3 | mod error; 4 | mod storage; 5 | mod utils; 6 | 7 | pub mod token_contract { 8 | // The import will code generate: 9 | // - A ContractClient type that can be used to invoke functions on the contract. 10 | // - Any types in the contract that were annotated with #[contracttype]. 11 | soroban_sdk::contractimport!( 12 | file = "../../target/wasm32-unknown-unknown/release/soroban_token_contract.wasm" 13 | ); 14 | } 15 | 16 | #[cfg(test)] 17 | mod tests; 18 | -------------------------------------------------------------------------------- /contracts/vesting/src/tests.rs: -------------------------------------------------------------------------------- 1 | mod admin_change; 2 | mod claim; 3 | mod instantiate; 4 | #[cfg(feature = "minter")] 5 | mod minter; 6 | mod setup; 7 | -------------------------------------------------------------------------------- /contracts/vesting/src/tests/instantiate.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{testutils::Address as _, vec, Address, Env, String}; 2 | 3 | use crate::{ 4 | contract::{Vesting, VestingClient}, 5 | storage::{MinterInfo, VestingInfoResponse, VestingSchedule, VestingTokenInfo}, 6 | tests::setup::deploy_token_contract, 7 | }; 8 | use curve::{Curve, SaturatingLinear}; 9 | 10 | use super::setup::{install_latest_vesting, old_vesting}; 11 | 12 | #[test] 13 | fn instantiate_contract_successfully() { 14 | let env = Env::default(); 15 | env.mock_all_auths(); 16 | 17 | let admin = Address::generate(&env); 18 | let vester1 = Address::generate(&env); 19 | let vester2 = Address::generate(&env); 20 | 21 | let token_client = deploy_token_contract(&env, &admin); 22 | 23 | let vesting_token = VestingTokenInfo { 24 | name: String::from_str(&env, "Phoenix"), 25 | symbol: String::from_str(&env, "PHO"), 26 | decimals: 6, 27 | address: token_client.address.clone(), 28 | }; 29 | let vesting_schedules = vec![ 30 | &env, 31 | VestingSchedule { 32 | recipient: vester1.clone(), 33 | curve: Curve::SaturatingLinear(SaturatingLinear { 34 | min_x: 15, 35 | min_y: 120, 36 | max_x: 60, 37 | max_y: 0, 38 | }), 39 | }, 40 | VestingSchedule { 41 | recipient: vester2, 42 | curve: Curve::SaturatingLinear(SaturatingLinear { 43 | min_x: 30, 44 | min_y: 240, 45 | max_x: 120, 46 | max_y: 0, 47 | }), 48 | }, 49 | ]; 50 | 51 | let vesting_client = VestingClient::new( 52 | &env, 53 | &env.register( 54 | Vesting, 55 | (&admin, vesting_token.clone(), &10u32, None::), 56 | ), 57 | ); 58 | 59 | token_client.mint(&admin, &480); 60 | vesting_client.create_vesting_schedules(&vesting_schedules); 61 | 62 | assert_eq!(vesting_client.query_token_info(), vesting_token); 63 | assert_eq!( 64 | vesting_client.query_all_vesting_info(&vester1), 65 | vec![ 66 | &env, 67 | VestingInfoResponse { 68 | recipient: vester1, 69 | balance: 120, 70 | schedule: Curve::SaturatingLinear(SaturatingLinear { 71 | min_x: 15, 72 | min_y: 120, 73 | max_x: 60, 74 | max_y: 0, 75 | }), 76 | index: 0, 77 | } 78 | ] 79 | ); 80 | 81 | let config = vesting_client.query_config(); 82 | assert!(!config.is_with_minter); 83 | 84 | let new_vesting_token = deploy_token_contract(&env, &Address::generate(&env)); 85 | 86 | vesting_client.update_vesting_token(&new_vesting_token.address); 87 | vesting_client.update_max_complexity(&10); 88 | } 89 | 90 | #[should_panic( 91 | expected = "Vesting: Create vesting account: At least one vesting schedule must be provided." 92 | )] 93 | #[test] 94 | fn instantiate_contract_without_any_vesting_balances_should_fail() { 95 | let env = Env::default(); 96 | env.mock_all_auths(); 97 | 98 | let admin = Address::generate(&env); 99 | let token_client = deploy_token_contract(&env, &admin); 100 | 101 | let vesting_token = VestingTokenInfo { 102 | name: String::from_str(&env, "Phoenix"), 103 | symbol: String::from_str(&env, "PHO"), 104 | decimals: 6, 105 | address: token_client.address.clone(), 106 | }; 107 | let vesting_schedules = vec![&env]; 108 | 109 | let vesting_client = VestingClient::new( 110 | &env, 111 | &env.register(Vesting, (&admin, vesting_token, &10u32, None::)), 112 | ); 113 | 114 | token_client.mint(&admin, &100); 115 | vesting_client.create_vesting_schedules(&vesting_schedules); 116 | } 117 | 118 | #[should_panic( 119 | expected = "Vesting: Create vesting account: Admin does not have enough tokens to start the vesting schedule" 120 | )] 121 | #[test] 122 | fn create_schedule_panics_when_admin_has_no_tokens_to_fund() { 123 | let env = Env::default(); 124 | env.mock_all_auths(); 125 | 126 | let admin = Address::generate(&env); 127 | let vester1 = Address::generate(&env); 128 | 129 | let token_client = deploy_token_contract(&env, &admin); 130 | 131 | let vesting_token = VestingTokenInfo { 132 | name: String::from_str(&env, "Phoenix"), 133 | symbol: String::from_str(&env, "PHO"), 134 | decimals: 6, 135 | address: token_client.address.clone(), 136 | }; 137 | let vesting_schedules = vec![ 138 | &env, 139 | VestingSchedule { 140 | recipient: vester1, 141 | curve: Curve::SaturatingLinear(SaturatingLinear { 142 | min_x: 15, 143 | min_y: 120, 144 | max_x: 60, 145 | max_y: 0, 146 | }), 147 | }, 148 | ]; 149 | 150 | let vesting_client = VestingClient::new( 151 | &env, 152 | &env.register(Vesting, (&admin, vesting_token, &10u32, None::)), 153 | ); 154 | 155 | vesting_client.create_vesting_schedules(&vesting_schedules); 156 | } 157 | 158 | #[test] 159 | fn test_update_vesting() { 160 | let env = Env::default(); 161 | env.mock_all_auths(); 162 | 163 | let admin = Address::generate(&env); 164 | 165 | let token_client = deploy_token_contract(&env, &admin); 166 | 167 | let vesting_token = old_vesting::VestingTokenInfo { 168 | name: String::from_str(&env, "Phoenix"), 169 | symbol: String::from_str(&env, "PHO"), 170 | decimals: 6, 171 | address: token_client.address.clone(), 172 | }; 173 | 174 | let vesting_addr = env.register(old_vesting::WASM, ()); 175 | let old_vesting = old_vesting::Client::new(&env, &vesting_addr); 176 | 177 | old_vesting.initialize(&admin, &vesting_token, &6); 178 | 179 | let new_wasm_hash = install_latest_vesting(&env); 180 | old_vesting.update(&new_wasm_hash); 181 | 182 | let latest_vesting = VestingClient::new(&env, &old_vesting.address); 183 | assert_eq!( 184 | latest_vesting.query_token_info().address, 185 | vesting_token.address 186 | ); 187 | } 188 | 189 | #[test] 190 | fn test_update_vesting_with_minter() { 191 | let env = Env::default(); 192 | env.mock_all_auths(); 193 | 194 | let admin = Address::generate(&env); 195 | 196 | let token_client = deploy_token_contract(&env, &admin); 197 | 198 | let vesting_token = old_vesting::VestingTokenInfo { 199 | name: String::from_str(&env, "Phoenix"), 200 | symbol: String::from_str(&env, "PHO"), 201 | decimals: 6, 202 | address: token_client.address.clone(), 203 | }; 204 | 205 | let vesting_addr = env.register(old_vesting::WASM, ()); 206 | let old_vesting = old_vesting::Client::new(&env, &vesting_addr); 207 | 208 | let minter_addr = Address::generate(&env); 209 | let minter_capacity = 6; 210 | let minter_info = old_vesting::MinterInfo { 211 | address: minter_addr.clone(), 212 | mint_capacity: minter_capacity, 213 | }; 214 | 215 | old_vesting.initialize_with_minter(&admin, &vesting_token, &6, &minter_info); 216 | 217 | let new_wasm_hash = install_latest_vesting(&env); 218 | old_vesting.update(&new_wasm_hash); 219 | 220 | let latest_vesting = VestingClient::new(&env, &old_vesting.address); 221 | assert_eq!(latest_vesting.query_minter().address, minter_addr); 222 | } 223 | -------------------------------------------------------------------------------- /contracts/vesting/src/tests/setup.rs: -------------------------------------------------------------------------------- 1 | use soroban_sdk::{ 2 | testutils::{Address as _, Ledger}, 3 | Address, BytesN, Env, 4 | }; 5 | 6 | use crate::{contract::VestingClient, storage::Config, token_contract}; 7 | 8 | pub mod old_vesting { 9 | soroban_sdk::contractimport!(file = "../../.wasm_binaries_mainnet/live_vesting.wasm"); 10 | } 11 | 12 | pub fn install_latest_vesting(env: &Env) -> BytesN<32> { 13 | soroban_sdk::contractimport!( 14 | file = "../../target/wasm32-unknown-unknown/release/phoenix_vesting.wasm" 15 | ); 16 | env.deployer().upload_contract_wasm(WASM) 17 | } 18 | 19 | pub fn deploy_token_contract<'a>(env: &Env, admin: &Address) -> token_contract::Client<'a> { 20 | token_contract::Client::new( 21 | env, 22 | &env.register_stellar_asset_contract_v2(admin.clone()) 23 | .address(), 24 | ) 25 | } 26 | 27 | #[test] 28 | #[allow(deprecated)] 29 | fn upgrade_vesting_contract() { 30 | use soroban_sdk::{vec, String}; 31 | 32 | use crate::tests::setup::deploy_token_contract; 33 | 34 | let env = Env::default(); 35 | env.mock_all_auths(); 36 | env.cost_estimate().budget().reset_unlimited(); 37 | let admin = Address::generate(&env); 38 | let user = Address::generate(&env); 39 | 40 | let token_client = deploy_token_contract(&env, &admin); 41 | token_client.mint(&user, &1_000); 42 | 43 | let env = Env::default(); 44 | env.mock_all_auths(); 45 | env.cost_estimate().budget().reset_unlimited(); 46 | 47 | let admin = Address::generate(&env); 48 | let new_admin = Address::generate(&env); 49 | let vester1 = Address::generate(&env); 50 | let token_client = deploy_token_contract(&env, &admin); 51 | 52 | token_client.mint(&admin, &320); 53 | 54 | let vesting_token_info = old_vesting::VestingTokenInfo { 55 | name: String::from_str(&env, "Phoenix"), 56 | symbol: String::from_str(&env, "PHO"), 57 | decimals: 6, 58 | address: token_client.address.clone(), 59 | }; 60 | 61 | let vesting_schedules = vec![ 62 | &env, 63 | old_vesting::VestingSchedule { 64 | recipient: vester1.clone(), 65 | curve: old_vesting::Curve::SaturatingLinear(old_vesting::SaturatingLinear { 66 | min_x: 0, 67 | min_y: 120, 68 | max_x: 60, 69 | max_y: 0, 70 | }), 71 | }, 72 | ]; 73 | 74 | let vesting_addr = env.register_contract_wasm(None, old_vesting::WASM); 75 | 76 | let old_vesting_client = old_vesting::Client::new(&env, &vesting_addr); 77 | 78 | old_vesting_client.initialize(&admin, &vesting_token_info, &10u32); 79 | 80 | old_vesting_client.create_vesting_schedules(&vesting_schedules); 81 | 82 | assert_eq!(token_client.balance(&old_vesting_client.address), 120); 83 | 84 | env.ledger().with_mut(|li| li.timestamp = 30); 85 | 86 | old_vesting_client.claim(&vester1, &0); 87 | assert_eq!(token_client.balance(&vester1), 60); 88 | assert_eq!(token_client.balance(&old_vesting_client.address), 60); 89 | 90 | let new_wasm_hash = install_latest_vesting(&env); 91 | old_vesting_client.update(&new_wasm_hash); 92 | 93 | let latest_vesting = VestingClient::new(&env, &vesting_addr); 94 | 95 | latest_vesting.migrate_config(&false); 96 | 97 | let actual_config = latest_vesting.query_config(); 98 | assert_eq!( 99 | actual_config, 100 | Config { 101 | is_with_minter: false 102 | } 103 | ); 104 | 105 | latest_vesting.propose_admin(&new_admin, &None); 106 | 107 | latest_vesting.accept_admin(); 108 | 109 | let actual_admin = latest_vesting.query_admin(); 110 | assert_eq!(actual_admin, new_admin); 111 | 112 | assert_eq!(token_client.balance(&vester1), 60); 113 | assert_eq!(token_client.balance(&old_vesting_client.address), 60); 114 | 115 | // fully vested 116 | env.ledger().with_mut(|li| li.timestamp = 60); 117 | 118 | latest_vesting.claim(&vester1, &0); 119 | 120 | assert_eq!(token_client.balance(&vester1), 120); 121 | assert_eq!(token_client.balance(&old_vesting_client.address), 0); 122 | } 123 | -------------------------------------------------------------------------------- /contracts/vesting/src/utils.rs: -------------------------------------------------------------------------------- 1 | use curve::Curve; 2 | use soroban_sdk::{log, panic_with_error, Address, Env, Vec}; 3 | 4 | use crate::{error::ContractError, storage::VestingSchedule}; 5 | 6 | pub fn check_duplications(env: &Env, accounts: Vec) { 7 | let mut addresses: Vec
= Vec::new(env); 8 | for account in accounts.iter() { 9 | if addresses.contains(&account.recipient) { 10 | log!(&env, "Vesting: Initialize: Duplicate addresses found"); 11 | panic_with_error!(env, ContractError::DuplicateInitialBalanceAddresses); 12 | } 13 | addresses.push_back(account.recipient.clone()); 14 | } 15 | } 16 | 17 | /// Asserts the vesting schedule decreases to 0 eventually 18 | /// returns the total vested amount 19 | pub fn validate_vesting_schedule(env: &Env, schedule: &Curve) -> Result { 20 | schedule.validate_monotonic_decreasing()?; 21 | match schedule { 22 | Curve::Constant(_) => { 23 | log!( 24 | &env, 25 | "Vesting: Constant curve is not valid for a vesting schedule" 26 | ); 27 | panic_with_error!(&env, ContractError::CurveConstant) 28 | } 29 | Curve::SaturatingLinear(sl) => { 30 | // Check range 31 | let (low, high) = (sl.max_y, sl.min_y); 32 | if low != 0 { 33 | log!( 34 | &env, 35 | "Vesting: Transfer Vesting: Cannot transfer when non-fully vested" 36 | ); 37 | panic_with_error!(&env, ContractError::NeverFullyVested) 38 | } else { 39 | Ok(high) // return the total amount to be transferred 40 | } 41 | } 42 | Curve::PiecewiseLinear(pl) => { 43 | // Check the last step value 44 | if pl.end_value().unwrap() != 0 { 45 | log!( 46 | &env, 47 | "Vesting: Transfer Vesting: Cannot transfer when non-fully vested" 48 | ); 49 | panic_with_error!(&env, ContractError::NeverFullyVested) 50 | } 51 | 52 | // Return the amount to be distributed (value of the first step) 53 | Ok(pl.first_value().unwrap()) 54 | } 55 | } 56 | } 57 | 58 | #[cfg(test)] 59 | mod test { 60 | use curve::{PiecewiseLinear, SaturatingLinear, Step}; 61 | use soroban_sdk::testutils::Address as _; 62 | use soroban_sdk::vec; 63 | 64 | use super::*; 65 | 66 | #[test] 67 | fn check_duplications_works() { 68 | let env = Env::default(); 69 | let address1 = Address::generate(&env); 70 | let address2 = Address::generate(&env); 71 | let address3 = Address::generate(&env); 72 | 73 | let accounts = vec![ 74 | &env, 75 | VestingSchedule { 76 | recipient: address1.clone(), 77 | curve: Curve::Constant(1), 78 | }, 79 | VestingSchedule { 80 | recipient: address2.clone(), 81 | curve: Curve::Constant(1), 82 | }, 83 | VestingSchedule { 84 | recipient: address3.clone(), 85 | curve: Curve::Constant(1), 86 | }, 87 | ]; 88 | 89 | // not panicking should be enough to pass the test 90 | check_duplications(&env, accounts); 91 | } 92 | 93 | #[test] 94 | #[should_panic(expected = "Vesting: Initialize: Duplicate addresses found")] 95 | fn check_duplications_should_panic() { 96 | let env = Env::default(); 97 | let duplicate_address = Address::generate(&env); 98 | let accounts = vec![ 99 | &env, 100 | VestingSchedule { 101 | recipient: duplicate_address.clone(), 102 | curve: Curve::Constant(1), 103 | }, 104 | VestingSchedule { 105 | recipient: Address::generate(&env), 106 | curve: Curve::Constant(1), 107 | }, 108 | VestingSchedule { 109 | recipient: duplicate_address, 110 | curve: Curve::Constant(1), 111 | }, 112 | ]; 113 | 114 | check_duplications(&env, accounts); 115 | } 116 | 117 | #[test] 118 | fn validate_saturating_linear_vesting() { 119 | let env = Env::default(); 120 | let curve = Curve::SaturatingLinear(SaturatingLinear { 121 | min_x: 15, 122 | min_y: 120, 123 | max_x: 60, 124 | max_y: 0, 125 | }); 126 | 127 | assert_eq!(validate_vesting_schedule(&env, &curve), Ok(120)); 128 | } 129 | 130 | #[test] 131 | fn validate_piecewise_linear_vesting() { 132 | let env = Env::default(); 133 | let curve = Curve::PiecewiseLinear(PiecewiseLinear { 134 | steps: vec![ 135 | &env, 136 | Step { 137 | time: 60, 138 | value: 150, 139 | }, 140 | Step { 141 | time: 120, 142 | value: 0, 143 | }, 144 | ], 145 | }); 146 | 147 | assert_eq!(validate_vesting_schedule(&env, &curve), Ok(150)); 148 | } 149 | 150 | #[test] 151 | #[should_panic(expected = "Vesting: Transfer Vesting: Cannot transfer when non-fully vested")] 152 | fn saturating_linear_schedule_fails_when_not_fully_vested() { 153 | let env = Env::default(); 154 | let curve = Curve::SaturatingLinear(SaturatingLinear { 155 | min_x: 15, 156 | min_y: 120, 157 | max_x: 60, 158 | max_y: 1, // leave 1 token at the end 159 | }); 160 | 161 | validate_vesting_schedule(&env, &curve).unwrap(); 162 | } 163 | 164 | #[test] 165 | #[should_panic(expected = "Vesting: Transfer Vesting: Cannot transfer when non-fully vested")] 166 | fn piecewise_linear_schedule_fails_when_not_fully_vested() { 167 | let env = Env::default(); 168 | let curve = Curve::PiecewiseLinear(PiecewiseLinear { 169 | steps: vec![ 170 | &env, 171 | Step { 172 | time: 60, 173 | value: 120, 174 | }, 175 | Step { 176 | time: 120, 177 | value: 10, 178 | }, 179 | ], 180 | }); 181 | 182 | validate_vesting_schedule(&env, &curve).unwrap(); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /docs/VAR_MoonBite_240103_OfficialR.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phoenix-Protocol-Group/phoenix-contracts/3af5ffafed41f1a5444f79ab1642cf9a7f0f59bc/docs/VAR_MoonBite_240103_OfficialR.pdf -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Phoenix DEX Smart Contracts Design Document 2 | 3 | This design document outlines the structure and primary functions of the Automated Market Maker (AMM) smart contracts. The AMM consists of the following contracts: `Pool`, `StablePool`, `StakingContract`, and `Factory`. Each contract serves a specific purpose within the Phoenix project. 4 | 5 | ```mermaid 6 | --- 7 | title: Phoenix DEX architecture 8 | --- 9 | graph TB 10 | A[Factory Contract] -->|Instantiates| B[Liquidity Pool XYK and Stable] 11 | B -->|Fees From Swaps| D[Fee Collector] 12 | B -->|Instantiates| E[LP Share Token] 13 | E -->|Used to Bond On| F[Staking Contract] 14 | G[External Rewards] -->|Pays Rewards On| F 15 | H[Routing Contract] -.->|Uses for Multiple Hop Swaps| B 16 | ``` 17 | 18 | ## Pool / Stable pool Contract 19 | 20 | The `Pool` contract represents a trading pool in the AMM. It allows users to swap between two different tokens. The primary functions of the `Pool` contract are as follows: 21 | 22 | 1. **Swap**: The `swap` function enables users to exchange one token for another within the trading pool. This function calculates the exchange rate based on the current token balances and reserves. 23 | 24 | 2. **Add Liquidity**: The `add_liquidity` function allows users to provide liquidity to the trading pool by depositing both tokens in proportion to the existing reserves. This function calculates the number of LP (Liquidity Provider) tokens to mint and assigns them to the liquidity provider. 25 | 26 | 3. **Remove Liquidity**: The `remove_liquidity` function enables liquidity providers to withdraw their deposited tokens from the trading pool. It burns the corresponding LP tokens and redistributes the proportional amounts of the tokens to the liquidity provider. 27 | 28 | ## Staking Contract 29 | 30 | The `StakingContract` allows users to stake their LP tokens from either the `Pool` or `StablePool` contracts to earn additional rewards. The primary functions of the `StakingContract` are as follows: 31 | 32 | 1. **Stake**: The `stake` function allows users to stake their LP tokens into the contract to start earning rewards. The contract keeps track of the staked LP tokens and the associated staker. Staking should happen automatically during providing liquidity. Each day user keeps the liqudity, rewards increase 0.5% up to 30% (60 days). 33 | 34 | 2. **Unstake**: The `unstake` function enables stakers to withdraw their staked LP tokens from the contract. It also distributes the earned rewards to the staker based on their contribution and the total reward pool. 35 | 36 | ```mermaid 37 | --- 38 | title: Providing liquidity 39 | --- 40 | sequenceDiagram 41 | participant U as User 42 | participant LP as Liquidity Pool Contract 43 | participant LT as LP Token Contract 44 | participant S as Staking Contract 45 | 46 | U->>LP: Provides liquidity (e.g., X and Y tokens) 47 | LP->>LT: Triggers minting of LP Tokens 48 | LT-->>U: Returns LP Tokens 49 | U->>S: Bonds LP Tokens 50 | S-->>U: Confirms bonding 51 | 52 | ``` 53 | 54 | ## Routing Contract 55 | 56 | The `Routing` contract serves as an intermediary that aids in executing complex trading routes across multiple liquidity pools. This contract significantly simplifies the token swapping process and enhances trading efficiency for users. 57 | 58 | ```mermaid 59 | --- 60 | title: Swapping using routing contract 61 | --- 62 | sequenceDiagram 63 | participant U as User 64 | participant R as Routing Contract 65 | participant XY as XYK Pool X/Y 66 | participant YZ as XYK Pool Y/Z 67 | 68 | U->>R: Sends swap request (X to Z) with routes 69 | R->>XY: Swaps X for Y 70 | XY-->>R: Returns Y 71 | R->>YZ: Swaps Y for Z 72 | YZ-->>R: Returns Z 73 | R-->>U: Returns Z 74 | ``` 75 | 76 | ## Factory Contract 77 | 78 | The `Factory` contract serves as the main contract responsible for deploying new instances of the `Pool` and `StablePool` contracts. Its primary functions are as follows: 79 | 80 | 1. **Create Pool**: The `create_pool` function allows the factory contract owner to create a new instance of the `Pool`/`StablePool` contract with the specified token pool. It deploys the new contract and emits an event with the contract address. 81 | 82 | 2. **DeregisterPool**: The `deregister_pool` function allows the factory contract owner to remove given pool from the collection of contracts. It disables swapping and providing liqudity on given pool. 83 | 84 | ```mermaid 85 | --- 86 | title: Liqudity Pool and Staking instantiation using Factory 87 | --- 88 | sequenceDiagram 89 | participant U as User 90 | participant F as Factory Contract 91 | participant LP as Liquidity Pool Contract 92 | participant LT as LP Token Contract 93 | participant S as Staking Contract 94 | 95 | U->>F: Calls CreatePool (X/Y or Stable) 96 | F->>LP: Instantiates Liquidity Pool 97 | LP->>LT: Instantiates LP Token 98 | LP->>S: Instantiates Staking Contract with LP Token address 99 | ``` 100 | 101 | By using these contracts together, users can trade tokens, provide liquidity to the AMM, stake LP tokens and earn rewards within the PHOENIX ecosystem. 102 | 103 | Please note that this design document provides a high-level overview of the contracts and their primary functions. The actual implementation may require additional functions, modifiers, and security considerations to ensure the contracts' robustness and reliability. 104 | 105 | ## Error Enums 106 | 107 | The `error.rs` mod in each smart contract show the `Error` enum's values for the different contracts. Due to Stellar's way of handling error messages when such occur, we are using number sets for the different contracts. That way the number ranges follow this structure: 108 | 109 | ``` 110 | Factory <-> 1-99 111 | Multihop <-> 100-199 112 | Xyk_Pool <-> 200-299 113 | Stable_Pool <-> 300-399 114 | Stake <-> 400-499 115 | ``` 116 | -------------------------------------------------------------------------------- /packages/curve/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "curve" 3 | version = { workspace = true } 4 | authors = ["Jakub "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | 9 | [features] 10 | testutils = ["soroban-sdk/testutils"] 11 | 12 | [dependencies] 13 | soroban-sdk = { workspace = true } 14 | 15 | [dev-dependencies] 16 | test-case = { workspace = true } 17 | -------------------------------------------------------------------------------- /packages/curve/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | all: lint build test 4 | 5 | test: 6 | cargo test 7 | 8 | build: 9 | cargo build --target wasm32-unknown-unknown --release 10 | 11 | lint: fmt clippy 12 | 13 | fmt: 14 | cargo fmt --all 15 | 16 | clippy: build 17 | cargo clippy --all-targets -- -D warnings 18 | 19 | clean: 20 | cargo clean 21 | -------------------------------------------------------------------------------- /packages/curve/README.md: -------------------------------------------------------------------------------- 1 | # Dex Curve 2 | 3 | ## Main functionality 4 | The purpose of this contract is to define different type of mathematical curves orientated for blockchain applications. Those curves can be used for rewards distributions, vesting schedules and any other functions that change with time. 5 | 6 | ## Messages 7 | 8 | `saturating_linear` 9 | Parameters**: 10 | - `min_x`: `u64` value representing starting time of the curve. 11 | - `min_y`: `u128` value at the starting time. 12 | - `max_x`: `u64` value representing time when the curve saturates. 13 | - `max_y`: `u128` value at the saturation time. 14 | 15 | Return Type: 16 | Curve 17 | 18 | Description: 19 | Ctor for Saturated curve. 20 | 21 |
22 | 23 | `constant` 24 | Parameter: 25 | - `y`: `u128` value representing the constant value of the curve. 26 | 27 | Return Type: 28 | Curve 29 | 30 | Description: 31 | Ctor for constant curve. 32 | 33 |
34 | 35 | `value` 36 | Parameter: 37 | - `x`: `u64` value representing the point at which to evaluate the curve. 38 | 39 | Return Type: 40 | u128 41 | 42 | Description: 43 | provides y = f(x) evaluation. 44 | 45 |
46 | 47 | `size` 48 | 49 | Parameters 50 | `None` 51 | 52 | Return Type: 53 | u32 54 | 55 | Description: 56 | Returns the number of steps in the curve. 57 | 58 |
59 | 60 | `validate` 61 | 62 | Parameters: 63 | `None` 64 | Return Type: 65 | Result<(), CurveError> 66 | 67 | Description: 68 | General sanity checks on input values to ensure this is valid. These checks should be included by the validate_monotonic_* functions 69 | 70 |
71 | 72 | `validate_monotonic_increasing` 73 | 74 | Parameters: 75 | `None` 76 | 77 | Return Type: 78 | Result<(), CurveError> 79 | 80 | Description: 81 | returns an error if there is ever x2 > x1 such that value(x2) < value(x1) 82 | 83 |
84 | 85 | `validate_monotonic_decreasing` 86 | 87 | Parameters: 88 | `None` 89 | 90 | Return Type: 91 | Result<(), CurveError> 92 | 93 | Description: 94 | Validates that the curve is monotonically decreasing. 95 | 96 |
97 | 98 | `validate_complexity` 99 | 100 | Parameter 101 | - `max`: `u32`, the maximum allowed size of the curve. 102 | 103 | Return Type: 104 | Result<(), CurveError> 105 | 106 | Description: 107 | returns an error if the size of the curve is more than the given max. 108 | 109 |
110 | 111 | `range` 112 | 113 | Parameters: 114 | None 115 | 116 | Return Type: 117 | (u128, u128) 118 | 119 | Description: 120 | return (min, max) that can ever be returned from value. These could potentially be u128::MIN and u128::MAX. 121 | 122 |
123 | 124 | `combine_const` 125 | 126 | Parameters: 127 | - `const_y`: `u128` value representing the y-value that will be combined with the curve. 128 | 129 | Return Type: 130 | Curve 131 | 132 | Description: 133 | combines a constant with a curve (shifting the curve up) 134 | 135 |
136 | 137 | `combine` 138 | 139 | Parameters: 140 | - `other`: `&Curve` value for another curve to combine with the current one. 141 | 142 | Return Type: 143 | Curve 144 | 145 | Description: 146 | returns a new curve that is the result of adding the given curve to this one. 147 | 148 |
149 | 150 | `end` 151 | Parameters 152 | None 153 | 154 | Return Type: 155 | `Option` 156 | 157 | Description 158 | Returns the end point as u64 value. 159 | -------------------------------------------------------------------------------- /packages/decimal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "soroban-decimal" 3 | version = { workspace = true } 4 | authors = ["Jakub "] 5 | repository = { workspace = true } 6 | edition = { workspace = true } 7 | license = { workspace = true } 8 | description = "A precise decimal arithmetic package for Soroban contracts" 9 | 10 | [dependencies] 11 | soroban-sdk = { workspace = true, features = ["alloc"] } 12 | 13 | [dev-dependencies] 14 | soroban-sdk = { workspace = true, features = ["testutils"] } 15 | -------------------------------------------------------------------------------- /packages/decimal/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | all: lint build test 4 | 5 | test: 6 | cargo test 7 | 8 | build: 9 | cargo build --target wasm32-unknown-unknown --release 10 | 11 | lint: fmt clippy 12 | 13 | fmt: 14 | cargo fmt --all 15 | 16 | clippy: build 17 | cargo clippy --all-targets -- -D warnings 18 | 19 | clean: 20 | cargo clean 21 | -------------------------------------------------------------------------------- /packages/decimal/README.md: -------------------------------------------------------------------------------- 1 | # Soroban Decimal 2 | This code is taken from the [cosmwasm-std crate](https://github.com/CosmWasm/cosmwasm.), which is licensed under the Apache License 2.0 3 | The contract provides a `Decimal` struct for arithmetic operations, suitable for blockchain De-Fi operations, where precision is of highest importance. It ensures that calculations are accurate up to 18 decimal places. 4 | 5 | # Decimal(i128) 6 | ## Methods 7 | 8 | - `new(value: i128) -> Self`: Creates a new Decimal. 9 | - `raw(value: i128) -> Self`: Returns the raw value from `i128`. 10 | - `one() -> Self`: Create a `1.0` Decimal. 11 | - `zero() -> Self`: Create a `0.0` Decimal. 12 | - `percent(x: i64) -> Self`: Convert `x%` into Decimal. 13 | - `permille(x: i64) -> Self`: Convert permille `(x/1000)` into Decimal. 14 | - `bps(x: i64) -> Self`: Convert basis points `(x/10000)` into Decimal. 15 | - `from_atomics(atomics: i128, decimal_places: i32) -> Self`: Creates a Decimal from atomic units and decimal places. 16 | - `inv(&self) -> Option`: Returns the multiplicative inverse `1/d` for decimal `d`. 17 | - `from_ratio(numerator: impl Into, denominator: impl Into) -> Self`: Returns the ratio (numerator / denominator) as a Decimal. 18 | - `abs(&self) -> Self`: Returns the absolute value of the Decimal. 19 | - `to_string(&self, env: &Env) -> String`: Converts the Decimal to a string. 20 | 21 | 22 | # Decimal256 23 | ## Methods 24 | 25 | - `new(value: u128) -> Self`: Creates a new Decimal. 26 | - `raw(value: u128) -> Self`: Returns the raw value from `u128`. 27 | - `one() -> Self`: Create a `1.0` Decimal. 28 | - `zero() -> Self`: Create a `0.0` Decimal. 29 | - `percent(x: u64) -> Self`: Convert `x%` into Decimal. 30 | - `permille(x: u64) -> Self`: Convert permille `(x/1000)` into Decimal. 31 | - `bps(x: u64) -> Self`: Convert basis points `(x/10000)` into Decimal. 32 | - `from_atomics(atomics: u128, decimal_places: i32) -> Self`: Creates a Decimal from atomic units and decimal places. 33 | - `inv(&self) -> Option`: Returns the multiplicative inverse `1/d` for decimal `d`. 34 | - `from_ratio(numerator: impl Into, denominator: impl Into) -> Self`: Returns the ratio (numerator / denominator) as a Decimal. 35 | - `abs(&self) -> Self`: Returns the absolute value of the Decimal. 36 | - `to_string(&self, env: &Env) -> String`: Converts the Decimal to a string. 37 | 38 | 39 | N.B.: `from_atomics(atomics: u128, decimal_places: i32) -> Self` currently supports maximum `38` as input for `decimcal_places` 40 | -------------------------------------------------------------------------------- /packages/decimal/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | mod decimal; 4 | mod decimal256; 5 | 6 | pub use decimal::Decimal; 7 | -------------------------------------------------------------------------------- /packages/phoenix/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phoenix" 3 | description = "A library used to share tools, utilities and code between our contracts" 4 | version = { workspace = true } 5 | authors = ["Jakub "] 6 | repository = { workspace = true } 7 | edition = { workspace = true } 8 | license = { workspace = true } 9 | 10 | [features] 11 | testutils = ["soroban-sdk/testutils"] 12 | 13 | [dependencies] 14 | soroban-sdk = { workspace = true } 15 | soroban-decimal = { workspace = true } 16 | 17 | [dev-dependencies] 18 | soroban-sdk = { workspace = true, features = ["testutils"] } 19 | test-case = { workspace = true } 20 | -------------------------------------------------------------------------------- /packages/phoenix/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | 3 | all: lint build test 4 | 5 | test: 6 | cargo test 7 | 8 | build: 9 | cargo build --target wasm32-unknown-unknown --release 10 | 11 | lint: fmt clippy 12 | 13 | fmt: 14 | cargo fmt --all 15 | 16 | clippy: build 17 | cargo clippy --all-targets -- -D warnings 18 | 19 | clean: 20 | cargo clean 21 | -------------------------------------------------------------------------------- /packages/phoenix/README.md: -------------------------------------------------------------------------------- 1 | # PHOENIX 2 | 3 | Helper library that contains different functionalities, utilities and error types to be shared 4 | between the contracts within the Phoenix DEX. 5 | -------------------------------------------------------------------------------- /packages/phoenix/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | pub mod ttl; 4 | pub mod utils; 5 | -------------------------------------------------------------------------------- /packages/phoenix/src/ttl.rs: -------------------------------------------------------------------------------- 1 | // Constants for storage bump amounts 2 | pub const DAY_IN_LEDGERS: u32 = 17280; 3 | 4 | // target TTL for the contract instance and its code. 5 | // When a TTL extension is triggered the instance's TTL is reset to this value (7 days of ledger units). 6 | pub const INSTANCE_TARGET_TTL: u32 = 7 * DAY_IN_LEDGERS; 7 | // if the current instance TTL falls below this threshold (i.e., less than 6 days of ledger units), the TTL extension mechanism will refresh it to INSTANCE_TARGET_TTL. 8 | pub const INSTANCE_RENEWAL_THRESHOLD: u32 = INSTANCE_TARGET_TTL - DAY_IN_LEDGERS; 9 | 10 | // when TTL extension (if the current TTL is below its renewal threshold), the persistent TTL is set to this value (30 days of ledger units). 11 | pub const PERSISTENT_TARGET_TTL: u32 = 30 * DAY_IN_LEDGERS; 12 | // if the current persistent TTL drops below this threshold (i.e., less than 29 days of ledger units), the TTL extension will bump it back to PERSISTENT_TARGET_TTL. 13 | pub const PERSISTENT_RENEWAL_THRESHOLD: u32 = PERSISTENT_TARGET_TTL - DAY_IN_LEDGERS; 14 | -------------------------------------------------------------------------------- /packages/phoenix/src/utils.rs: -------------------------------------------------------------------------------- 1 | use soroban_decimal::Decimal; 2 | use soroban_sdk::{contracttype, Address}; 3 | 4 | // Validate if int value is bigger then 0 5 | #[macro_export] 6 | macro_rules! validate_int_parameters { 7 | ($($arg:expr),*) => { 8 | { 9 | $( 10 | let value: Option = Into::>::into($arg); 11 | if let Some(val) = value { 12 | if val <= 0 { 13 | panic!("value cannot be less than or equal zero") 14 | } 15 | } 16 | )* 17 | } 18 | }; 19 | } 20 | 21 | // Validate all bps to be between the range 0..10_000 22 | #[macro_export] 23 | macro_rules! validate_bps { 24 | ($($value:expr),+) => { 25 | const MIN_BPS: i64 = 0; 26 | const MAX_BPS: i64 = 10_000; 27 | $( 28 | // if $value < MIN_BPS || $value > MAX_BPS { 29 | // panic!("The value {} is out of range. Must be between {} and {} bps.", $value, MIN_BPS, MAX_BPS); 30 | // } 31 | assert!((MIN_BPS..=MAX_BPS).contains(&$value), "The value {} is out of range. Must be between {} and {} bps.", $value, MIN_BPS, MAX_BPS); 32 | )+ 33 | } 34 | } 35 | 36 | pub fn is_approx_ratio(a: Decimal, b: Decimal, tolerance: Decimal) -> bool { 37 | let diff = (a - b).abs(); 38 | diff <= tolerance 39 | } 40 | 41 | pub fn convert_i128_to_u128(input: i128) -> u128 { 42 | if input < 0 { 43 | panic!("Cannot convert i128 to u128"); 44 | } else { 45 | input as u128 46 | } 47 | } 48 | 49 | pub fn convert_u128_to_i128(input: u128) -> i128 { 50 | if input > i128::MAX as u128 { 51 | panic!("Cannot convert u128 to i128"); 52 | } else { 53 | input as i128 54 | } 55 | } 56 | 57 | #[contracttype] 58 | #[derive(Clone, Debug, Eq, PartialEq)] 59 | pub struct TokenInitInfo { 60 | pub token_a: Address, 61 | pub token_b: Address, 62 | } 63 | 64 | #[contracttype] 65 | #[derive(Clone, Debug, Eq, PartialEq)] 66 | pub struct StakeInitInfo { 67 | pub min_bond: i128, 68 | pub min_reward: i128, 69 | pub manager: Address, 70 | pub max_complexity: u32, 71 | } 72 | 73 | #[contracttype] 74 | #[derive(Clone, Debug, Eq, PartialEq)] 75 | pub struct LiquidityPoolInitInfo { 76 | pub admin: Address, 77 | pub swap_fee_bps: i64, 78 | pub fee_recipient: Address, 79 | pub max_allowed_slippage_bps: i64, 80 | pub default_slippage_bps: i64, 81 | pub max_allowed_spread_bps: i64, 82 | pub max_referral_bps: i64, 83 | pub token_init_info: TokenInitInfo, 84 | pub stake_init_info: StakeInitInfo, 85 | } 86 | 87 | #[derive(Clone)] 88 | #[contracttype] 89 | pub struct AdminChange { 90 | pub new_admin: Address, 91 | pub time_limit: Option, 92 | } 93 | 94 | #[derive(Clone)] 95 | #[contracttype] 96 | pub struct AutoUnstakeInfo { 97 | pub stake_amount: i128, 98 | pub stake_timestamp: u64, 99 | } 100 | 101 | #[contracttype] 102 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 103 | #[repr(u32)] 104 | pub enum PoolType { 105 | Xyk = 0, 106 | Stable = 1, 107 | } 108 | 109 | #[cfg(test)] 110 | mod tests { 111 | use super::*; 112 | 113 | #[test] 114 | fn test_validate_int_parameters() { 115 | // The macro should not panic for valid parameters. 116 | validate_int_parameters!(1, 2, 3); 117 | validate_int_parameters!(1, 1, 1); 118 | validate_int_parameters!(1i128, 2i128, 3i128, Some(4i128), None::); 119 | validate_int_parameters!(None::, None::); 120 | validate_int_parameters!(Some(1i128), None::); 121 | } 122 | 123 | #[test] 124 | #[should_panic] 125 | fn should_panic_when_value_less_than_zero() { 126 | validate_int_parameters!(1, -2, 3); 127 | } 128 | 129 | #[test] 130 | #[should_panic] 131 | fn should_panic_when_first_value_equal_zero() { 132 | validate_int_parameters!(0, 1, 3); 133 | } 134 | 135 | #[test] 136 | #[should_panic] 137 | fn should_panic_when_last_value_equal_zero() { 138 | validate_int_parameters!(1, 1, 0); 139 | } 140 | 141 | #[test] 142 | #[should_panic] 143 | fn should_panic_when_some_equals_zero() { 144 | validate_int_parameters!(Some(0i128), None::); 145 | } 146 | 147 | #[test] 148 | #[should_panic] 149 | fn should_panic_when_some_less_than_zero() { 150 | validate_int_parameters!(Some(-1i128), None::); 151 | } 152 | 153 | #[test] 154 | fn test_assert_approx_ratio_close_values() { 155 | let a = Decimal::from_ratio(100, 101); 156 | let b = Decimal::from_ratio(100, 100); 157 | let tolerance = Decimal::percent(3); 158 | assert!(is_approx_ratio(a, b, tolerance)); 159 | } 160 | 161 | #[test] 162 | fn test_assert_approx_ratio_equal_values() { 163 | let a = Decimal::from_ratio(100, 100); 164 | let b = Decimal::from_ratio(100, 100); 165 | let tolerance = Decimal::percent(3); 166 | assert!(is_approx_ratio(a, b, tolerance)); 167 | } 168 | 169 | #[test] 170 | fn test_assert_approx_ratio_outside_tolerance() { 171 | let a = Decimal::from_ratio(100, 104); 172 | let b = Decimal::from_ratio(100, 100); 173 | let tolerance = Decimal::percent(3); 174 | assert!(!is_approx_ratio(a, b, tolerance)); 175 | } 176 | 177 | #[test] 178 | #[should_panic(expected = "The value -1 is out of range. Must be between 0 and 10000 bps.")] 179 | fn validate_bps_below_min() { 180 | validate_bps!(-1, 300, 5_000, 8_534); 181 | } 182 | 183 | #[test] 184 | #[should_panic(expected = "The value 10001 is out of range. Must be between 0 and 10000 bps.")] 185 | fn validate_bps_above_max() { 186 | validate_bps!(100, 10_001, 31_3134, 348); 187 | } 188 | 189 | #[test] 190 | fn bps_valid_range() { 191 | validate_bps!(0, 5_000, 7_500, 10_000); 192 | } 193 | 194 | #[test] 195 | fn should_successfully_convert_u128_to_i128() { 196 | let val = 10u128; 197 | let result: i128 = convert_u128_to_i128(val); 198 | assert_eq!(result, 10i128); 199 | } 200 | 201 | #[test] 202 | #[should_panic(expected = "Cannot convert u128 to i128")] 203 | fn should_panic_when_value_bigger_than_i128() { 204 | let val = i128::MAX as u128 + 1u128; 205 | convert_u128_to_i128(val); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.81" -------------------------------------------------------------------------------- /scripts/extend-queries.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set +x 4 | 5 | # validate the input arguments 6 | if [ -z "$2" ]; then 7 | echo "Error: Source account and Send is required as an argument." >&2 8 | echo "Usage: $0 [output_file]" >&2 9 | exit 1 10 | fi 11 | 12 | # vars 13 | SOURCE_ACCOUNT=$1 14 | SEND_TX=$2 15 | OUTPUT_FILE=${3:-/dev/null} # Redirect output to a file or /dev/null by default 16 | FACTORY_ID="CB4SVAWJA6TSRNOJZ7W2AWFW46D5VR4ZMFZKDIKXEINZCZEGZCJZCKMI" 17 | RPC_URL="https://mainnet.sorobanrpc.com" 18 | NETWORK_PASSPHRASE="Public Global Stellar Network ; September 2015" 19 | 20 | # helper to invoke the stellar network 21 | invoke_contract() { 22 | local CONTRACT_ID=$1 23 | local FUNCTION_NAME=$2 24 | local ARGS=${3:-} 25 | 26 | echo "Calling function: $FUNCTION_NAME on contract: $CONTRACT_ID with args: $ARGS" >&2 27 | 28 | # capture raw output and errors 29 | RAW_OUTPUT=$(stellar contract invoke \ 30 | --id $CONTRACT_ID \ 31 | --source-account $SOURCE_ACCOUNT \ 32 | --rpc-url $RPC_URL \ 33 | --network-passphrase "$NETWORK_PASSPHRASE" \ 34 | --send $SEND_TX \ 35 | -- \ 36 | $FUNCTION_NAME $ARGS 2>&1) 37 | 38 | # check for archived entry error 39 | if echo "$RAW_OUTPUT" | grep -q "EntryArchived"; then 40 | echo "Error: EntryArchived for function: $FUNCTION_NAME on contract: $CONTRACT_ID" >&2 41 | return 1 42 | fi 43 | 44 | # parse JSON output with jq 45 | echo "$RAW_OUTPUT" | jq 46 | } 47 | 48 | # redirect all output to the specified file or /dev/null 49 | exec >"$OUTPUT_FILE" 2>&1 50 | 51 | # all pools of factory 52 | POOLS=$(invoke_contract $FACTORY_ID query_pools | jq -r '.[]') 53 | 54 | # query the pools and get the details 55 | echo "Iterating over pool addresses" >&2 56 | for POOL in $POOLS; do 57 | invoke_contract $FACTORY_ID query_pool_details "--pool_address $POOL" 58 | done 59 | 60 | # same like before, just another storage key 61 | ALL_POOLS_DETAILS=$(invoke_contract $FACTORY_ID query_all_pools_details) 62 | 63 | # arrays for staking and lp_share token 64 | STAKE_ADDRESSES=() 65 | LP_SHARE_ADDRESSES=() 66 | 67 | # fill in the arrays 68 | echo "Extracting stake addresses and LP share addresses" >&2 69 | while read -r POOL_DETAIL; do 70 | STAKE_ADDRESS=$(echo "$POOL_DETAIL" | jq -r '.pool_response.stake_address') 71 | LP_SHARE_ADDRESS=$(echo "$POOL_DETAIL" | jq -r '.pool_response.asset_lp_share.address') 72 | 73 | STAKE_ADDRESSES+=("$STAKE_ADDRESS") 74 | LP_SHARE_ADDRESSES+=("$LP_SHARE_ADDRESS") 75 | done < <(echo "$ALL_POOLS_DETAILS" | jq -c '.[]') 76 | 77 | echo "DONE WITH FACTORY QUERIES" >&2 78 | 79 | echo "STARTING WITH QUERIES IN POOL" >&2 80 | 81 | # call the queries in pool contract 82 | for POOL in $POOLS; do 83 | invoke_contract $POOL query_config 84 | invoke_contract $POOL query_share_token_address 85 | invoke_contract $POOL query_stake_contract_address 86 | invoke_contract $POOL query_pool_info 87 | invoke_contract $POOL query_pool_info_for_factory 88 | done 89 | 90 | echo "DONE WITH QUERIES IN POOL CONTRACTS" >&2 91 | 92 | echo "STARTING WITH STAKE CONTRACT QUERIES" >&2 93 | 94 | # call the queries in stake contract 95 | for STAKE in "${STAKE_ADDRESSES[@]}"; do 96 | echo "Querying stake contract: $STAKE" >&2 97 | invoke_contract $STAKE query_config || continue 98 | invoke_contract $STAKE query_admin || continue 99 | invoke_contract $STAKE query_total_staked || continue 100 | # invoke_contract $STAKE query_annualized_rewards TODO - not present in the current version 101 | done 102 | 103 | echo "DONE WITH STAKE CONTRACT QUERIES" >&2 104 | 105 | echo "STARTING WITH LP SHARE QUERIES" >&2 106 | 107 | # call the queries in token contract 108 | for LP_SHARE in "${LP_SHARE_ADDRESSES[@]}"; do 109 | echo "Querying LP share name: $LP_SHARE" >&2 110 | invoke_contract $LP_SHARE name || continue 111 | done 112 | 113 | echo "DONE WITH ALL QUERIES" >&2 114 | -------------------------------------------------------------------------------- /scripts/upgrade_mainnet.sh: -------------------------------------------------------------------------------- 1 | # Ensure the script exits on any errors 2 | set -e 3 | 4 | # Check if the argument is provided 5 | if [ -z "$1" ]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | IDENTITY_STRING=$1 11 | ADMIN_ADDRESS=$(stellar keys address $IDENTITY_STRING) 12 | NETWORK="mainnet" 13 | 14 | FACTORY_ADDRESS="CB4SVAWJA6TSRNOJZ7W2AWFW46D5VR4ZMFZKDIKXEINZCZEGZCJZCKMI" 15 | MULTIHOP_ADDRESS="CCLZRD4E72T7JCZCN3P7KNPYNXFYKQCL64ECLX7WP5GNVYPYJGU2IO2G" 16 | VESTING_ADDRESS="CDEGWCGEMNFZT3UUQD7B4TTPDHXZLGEDB6WIP4PWNTXOR5EZD34HJ64O" 17 | 18 | PHO_USDC_POOL_ADDRESS="CD5XNKK3B6BEF2N7ULNHHGAMOKZ7P6456BFNIHRF4WNTEDKBRWAE7IAA" 19 | XLM_PHO_POOL_ADDRESS="CBCZGGNOEUZG4CAAE7TGTQQHETZMKUT4OIPFHHPKEUX46U4KXBBZ3GLH" 20 | XLM_USDC_POOL_ADDRESS="CBHCRSVX3ZZ7EGTSYMKPEFGZNWRVCSESQR3UABET4MIW52N4EVU6BIZX" 21 | XLM_EURC_POOL_ADDRESS="CBISULYO5ZGS32WTNCBMEFCNKNSLFXCQ4Z3XHVDP4X4FLPSEALGSY3PS" 22 | USDC_VEUR_POOL_ADDRESS="CDQLKNH3725BUP4HPKQKMM7OO62FDVXVTO7RCYPID527MZHJG2F3QBJW" 23 | USDC_VCHF_POOL_ADDRESS="CBW5G5SO5SDYUGQVU7RMZ2KJ34POM3AMODOBIV2RQYG4KJDUUBVC3P2T" 24 | XLM_USDX_POOL_ADDRESS="CDMXKSLG5GITGFYERUW2MRYOBUQCMRT2QE5Y4PU3QZ53EBFWUXAXUTBC" 25 | EURX_USDC_POOL_ADDRESS="CC6MJZN3HFOJKXN42ANTSCLRFOMHLFXHWPNAX64DQNUEBDMUYMPHASAV" 26 | XLM_EURX_POOL_ADDRESS="CB5QUVK5GS3IU23TMFZQ3P5J24YBBZP5PHUQAEJ2SP5K55PFTJRUQG2L" 27 | XLM_GBPX_POOL_ADDRESS="CCKOC2LJTPDBKDHTL3M5UO7HFZ2WFIHSOKCELMKQP3TLCIVUBKOQL4HB" 28 | GBPX_USDC_POOL_ADDRESS="CCUCE5H5CKW3S7JBESGCES6ZGDMWLNRY3HOFET3OH33MXZWKXNJTKSM3" 29 | 30 | 31 | pools=( 32 | PHO_USDC_POOL_ADDRESS 33 | XLM_PHO_POOL_ADDRESS 34 | XLM_USDC_POOL_ADDRESS 35 | XLM_EURC_POOL_ADDRESS 36 | USDC_VEUR_POOL_ADDRESS 37 | USDC_VCHF_POOL_ADDRESS 38 | XLM_USDX_POOL_ADDRESS 39 | EURX_USDC_POOL_ADDRESS 40 | XLM_EURX_POOL_ADDRESS 41 | XLM_GBPX_POOL_ADDRESS 42 | GBPX_USDC_POOL_ADDRESS 43 | ) 44 | 45 | 46 | PHO_USDC_STAKE_ADDRESS="CDOXQONPND365K6MHR3QBSVVTC3MKR44ORK6TI2GQXUXGGAS5SNDAYRI" 47 | XLM_PHO_STAKE_ADDRESS="CBRGNWGAC25CPLMOAMR7WBPOF5QTFA5RYXQH4DEJ4K65G2QFLTLMW7RO" 48 | XLM_USDC_STAKE_ADDRESS="CAF3UJ45ZQJP6USFUIMVMGOUETUTXEC35R2247VJYIVQBGKTKBZKNBJ3" 49 | XLM_EURC_STAKE_ADDRESS="CDEQYRWFU3IHPRR6H6VOQRUU3JFS6DTUYUL4YAQSD3ALB5IPBTEOZUFM" 50 | USDC_VEUR_STAKE_ADDRESS="CCP653KENMYCAYQ3PHJDT6PITMG4XYKVWV3OEDDCOAOS6Z4GOMXGYH3Z" 51 | USDC_VCHF_STAKE_ADDRESS="CCIWIW6ESCCCFMEI5QOSUHDKTMBEMRJ22F7GPYNRKM2UI2FH6WYUKOUU" 52 | XLM_USDX_STAKE_ADDRESS="CBULEXIMZ5C4CSUPZ4E5LXATWDZNS6MDM2A57DAUD5GXSUG4IWKLOSOC" 53 | EURX_USDC_STAKE_ADDRESS="CD2YKNPX3JPTGDANJRPEJS42MPQLEVUVVRZKJYLLUSPJKQJA7LUANBO4" 54 | XLM_EURX_STAKE_ADDRESS="CDBMVFP7KJXW3YEFSLOU5GYUQHHJJI7QPZJPCSPDK6HHBCBZAMCHS2QY" 55 | XLM_GBPX_STAKE_ADDRESS="CDH6JILIADIC5SKE6OZJAYV3GM62RTR4O54OMVNP4ZOK4HH4J2JWJPVW" 56 | GBPX_USDC_STAKE_ADDRESS="CBDCTYZSZIOWCK5IGCQZNFUOJ53KMPYG2MG7GMVGE3A2LEYCFTDYYZ3S" 57 | 58 | stakes=( 59 | PHO_USDC_STAKE_ADDRESS 60 | XLM_PHO_STAKE_ADDRESS 61 | XLM_USDC_STAKE_ADDRESS 62 | XLM_EURC_STAKE_ADDRESS 63 | USDC_VEUR_STAKE_ADDRESS 64 | USDC_VCHF_STAKE_ADDRESS 65 | XLM_USDX_STAKE_ADDRESS 66 | EURX_USDC_STAKE_ADDRESS 67 | XLM_EURX_STAKE_ADDRESS 68 | XLM_GBPX_STAKE_ADDRES 69 | GBPX_USDC_STAKE_ADDRESS 70 | ) 71 | 72 | upgrade_stellar_contract() { 73 | local contract_id="$1" 74 | local account="$2" 75 | local new_wasm_hash="$3" 76 | 77 | if [[ -z "$contract_id" || -z "$account" || -z "$new_wasm_hash" ]]; then 78 | echo "Error: Missing required parameters (contract_id, account, new_wasm_hash)." 79 | return 1 80 | fi 81 | 82 | echo "Processing contract id: $contract_id" 83 | 84 | echo "Building..." 85 | built=$(stellar contract invoke \ 86 | --id "$contract_id" \ 87 | --source-account "$account" \ 88 | --rpc-url https://mainnet.sorobanrpc.com \ 89 | --network-passphrase "Public Global Stellar Network ; September 2015" \ 90 | -- \ 91 | update \ 92 | --new_wasm_hash "$new_wasm_hash" \ 93 | --build-only) || { echo "Error: Build failed."; return 1; } 94 | 95 | echo "Simulate..." 96 | simulated=$(stellar tx simulate \ 97 | --source-account "$account" \ 98 | --rpc-url https://mainnet.sorobanrpc.com \ 99 | --network-passphrase "Public Global Stellar Network ; September 2015" \ 100 | "$built") || { echo "Error: Simulation failed."; return 1; } 101 | 102 | echo "Sign..." 103 | signed=$(stellar tx sign \ 104 | --rpc-url https://mainnet.sorobanrpc.com \ 105 | --network-passphrase "Public Global Stellar Network ; September 2015" \ 106 | --sign-with-key "$account" \ 107 | "$simulated") || { echo "Error: Signing failed."; return 1; } 108 | 109 | echo "Send!" 110 | stellar tx send --quiet \ 111 | --rpc-url https://mainnet.sorobanrpc.com \ 112 | --network-passphrase "Public Global Stellar Network ; September 2015" \ 113 | "$signed" || { echo "Error: Sending failed."; return 1; } 114 | 115 | echo "Transaction sent successfully for contract: $contract_id" 116 | } 117 | 118 | 119 | echo "Build and optimize the contracts..."; 120 | 121 | make build > /dev/null 122 | cd target/wasm32-unknown-unknown/release 123 | 124 | echo "Contracts compiled." 125 | echo "Optimize contracts..." 126 | 127 | soroban contract optimize --wasm phoenix_factory.wasm 128 | soroban contract optimize --wasm phoenix_pool.wasm 129 | soroban contract optimize --wasm phoenix_stake.wasm 130 | soroban contract optimize --wasm phoenix_multihop.wasm 131 | soroban contract optimize --wasm phoenix_vesting.wasm 132 | 133 | echo "Contracts optimized." 134 | 135 | echo "Uploading latest factory wasm..." 136 | NEW_FACTORY_WASM_HASH = $(stellar contract upload \ 137 | --wasm ../target/wasm32-unknown-unknown/release/phoenix_factory.optimized.wasm \ 138 | --source $IDENTITY_STRING \ 139 | --network $NETWORK) 140 | 141 | echo "Uploading latest multihop wasm..." 142 | NEW_MULTIHOP_WASM_HASH = $(stellar contract upload \ 143 | --wasm ../target/wasm32-unknown-unknown/release/phoenix_multihop.optimized.wasm \ 144 | --source $IDENTITY_STRING \ 145 | --network $NETWORK) 146 | 147 | echo "Uploading latest pool wasm..." 148 | NEW_POOL_WASM_HASH = $(stellar contract upload \ 149 | --wasm ../target/wasm32-unknown-unknown/release/phoenix_pool.optimized.wasm \ 150 | --source $IDENTITY_STRING \ 151 | --network $NETWORK) 152 | 153 | echo "Uploading latest stake wasm..." 154 | NEW_STAKE_WASM_HASH = $(stellar contract upload \ 155 | --wasm ../target/wasm32-unknown-unknown/release/phoenix_stake.optimized.wasm \ 156 | --source $IDENTITY_STRING \ 157 | --network $NETWORK) 158 | 159 | echo "Uploading latest vesting wasm..." 160 | NEW_VESTING_WASM_HASH = $(stellar contract upload \ 161 | --wasm ../target/wasm32-unknown-unknown/release/phoenix_vesting.optimized.wasm \ 162 | --source $IDENTITY_STRING \ 163 | --network $NETWORK) 164 | 165 | echo "Updating factory contract..." 166 | upgrade_stellar_contract $FACTORY_ADDRESS $ACCOUNT $NEW_FACTORY_WASM_HASH 167 | echo "Updated factory contract..." 168 | 169 | echo "Updating multihop contract..." 170 | upgrade_stellar_contract $MULTIHOP_ADDRESS $ACCOUNT $NEW_MULTIHOP_WASM_HASH 171 | echo "Updated multihop contract..." 172 | 173 | echo "Updating pools..." 174 | for pool in "${pools[@]}"; do 175 | echo "Will update $pool" 176 | pool_id="${!pool}" 177 | upgrade_stellar_contract $pool_id $ACCOUNT $NEW_POOL_WASM_HASH 178 | echo "Done updating $pool" 179 | done 180 | echo "Updated all pools" 181 | 182 | 183 | echo "Updating stake contracts..." 184 | for stake in "${stakes[@]}"; do 185 | echo "Will update $stake" 186 | stake_id="${!stake}" 187 | upgrade_stellar_contract $stake_id $ACCOUNT $NEW_STAKE_WASM_HASH 188 | echo "Done updating $stake" 189 | done 190 | echo "Updated all staking contracts" 191 | 192 | echo "Updating vesting contract..." 193 | upgrade_stellar_contract $VESTING_ADDRESS $ACCOUNT $NEW_VESTING_WASM_HASH 194 | echo "Updated vesting contract..." 195 | -------------------------------------------------------------------------------- /scripts/vesting_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Check if both arguments are provided: identity string and second wallet address 5 | if [ -z "$1" ] || [ -z "$2" ]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | IDENTITY_STRING=$1 11 | SECOND_IDENTITY_STRING=$2 12 | 13 | NETWORK="testnet" 14 | 15 | cd target/wasm32-unknown-unknown/release 16 | 17 | echo "Optimize contracts..." 18 | 19 | soroban contract optimize --wasm soroban_token_contract.wasm 20 | soroban contract optimize --wasm phoenix_vesting.wasm 21 | 22 | echo "Optimized contracts..." 23 | 24 | ADMIN_ADDRESS=$(soroban keys address $IDENTITY_STRING) 25 | SECOND_WALLET=$(soroban keys address $SECOND_IDENTITY_STRING) 26 | 27 | echo "Admin address: $ADMIN_ADDRESS" 28 | echo "Second wallet (vesting recipient): $SECOND_WALLET" 29 | 30 | echo "Deploying Vesting contract..." 31 | VESTING_ADDR=$(soroban contract deploy \ 32 | --wasm phoenix_vesting.optimized.wasm \ 33 | --source $IDENTITY_STRING \ 34 | --network $NETWORK) 35 | echo "Vesting contract deployed at: $VESTING_ADDR" 36 | 37 | echo "Deploying Vesting Token ..." 38 | VESTING_TOKEN_ADDR=$(soroban contract deploy \ 39 | --wasm soroban_token_contract.optimized.wasm \ 40 | --source $IDENTITY_STRING \ 41 | --network $NETWORK \ 42 | -- \ 43 | --admin $ADMIN_ADDRESS \ 44 | --decimal 7 \ 45 | --name VESTING \ 46 | --symbol VEST 47 | ) 48 | 49 | echo "Vesting Token deployed at: $VESTING_TOKEN_ADDR" 50 | echo "Minting additional vesting tokens to admin..." 51 | soroban contract invoke \ 52 | --id $VESTING_TOKEN_ADDR \ 53 | --source $IDENTITY_STRING \ 54 | --network $NETWORK \ 55 | -- \ 56 | mint --to $ADMIN_ADDRESS --amount 50000000000000 # 5_000_000 tokens 57 | 58 | echo "Initializing Vesting contract..." 59 | VESTING_TOKEN_JSON=$(cat <