├── .gitignore ├── .yarnrc.yml ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── client ├── Cargo.toml └── src │ ├── instructions │ ├── amm_instructions.rs │ ├── events_instructions_parse.rs │ ├── mod.rs │ ├── rpc.rs │ ├── token_instructions.rs │ └── utils.rs │ └── main.rs ├── client_config.ini ├── package.json ├── programs └── cp-swap │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ ├── curve │ ├── calculator.rs │ ├── constant_product.rs │ ├── fees.rs │ └── mod.rs │ ├── error.rs │ ├── instructions │ ├── admin │ │ ├── collect_fund_fee.rs │ │ ├── collect_protocol_fee.rs │ │ ├── create_config.rs │ │ ├── mod.rs │ │ ├── update_config.rs │ │ └── update_pool_status.rs │ ├── deposit.rs │ ├── initialize.rs │ ├── mod.rs │ ├── swap_base_input.rs │ ├── swap_base_output.rs │ └── withdraw.rs │ ├── lib.rs │ ├── states │ ├── config.rs │ ├── events.rs │ ├── mod.rs │ ├── oracle.rs │ └── pool.rs │ └── utils │ ├── account_load.rs │ ├── math.rs │ ├── mod.rs │ └── token.rs ├── tests ├── deposit.test.ts ├── initialize.test.ts ├── swap.test.ts ├── utils │ ├── fee.ts │ ├── index.ts │ ├── instruction.ts │ ├── pda.ts │ ├── util.ts │ └── web3.ts └── withdraw.test.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | test-ledger 8 | .vscode 9 | .yarn 10 | Makefile 11 | 12 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | anchor_version = "0.31.0" 3 | solana_version = "2.1.0" 4 | 5 | [workspace] 6 | members = ["programs/cp-swap"] 7 | 8 | [features] 9 | seeds = false 10 | skip-lint = false 11 | 12 | [programs.Localnet] 13 | raydium_cp_swap = "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" 14 | 15 | 16 | [registry] 17 | url = "https://github.com/raydium-io/raydium-cp-swap" 18 | 19 | [provider] 20 | cluster = "Localnet" 21 | wallet = "~/.config/solana/id.json" 22 | 23 | [scripts] 24 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.test.ts" 25 | 26 | [test] 27 | startup_wait = 10000 28 | 29 | [test.validator] 30 | url = "https://api.mainnet-beta.solana.com" 31 | 32 | [[test.validator.clone]] 33 | address = "DNXgeM9EiiaAbaWvwjHj9fQQLAX5ZsfHyvmYUNRAdNC8" # pool fee receiver 34 | 35 | [[test.validator.clone]] 36 | address = "D4FPEruKEHrG5TenZ2mpDGEfu1iUvTiqBxvpU8HLBvC2" # index 0 AMM Config account 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["programs/*", "client"] 4 | 5 | [profile.release] 6 | overflow-checks = true 7 | lto = "fat" 8 | codegen-units = 1 9 | [profile.release.build-override] 10 | opt-level = 3 11 | incremental = false 12 | codegen-units = 1 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raydium-cp-swap 2 | 3 | A revamped constant product AMM program optimized for straightforward pool deployment along with additional features and integrations: 4 | - No Openbook market ID is required for pool creation 5 | - Token22 is supported 6 | - Built-in price oracle 7 | - Optimized in Anchor 8 | 9 | The program has been audited by [MadShield](https://www.madshield.xyz/). The report can be found [here](https://github.com/raydium-io/raydium-docs/tree/master/audit/MadShield%20Q1%202024). 10 | 11 | The program assets are in-scope for Raydium’s [Immunefi bug bounty program](https://immunefi.com/bug-bounty/raydium/). 12 | 13 | ## Environment Setup 14 | 15 | 1. Install `Rust` 16 | 17 | ```shell 18 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 19 | rustup default 1.81.0 20 | ``` 21 | 22 | 2. Install `Solana ` 23 | 24 | ```shell 25 | sh -c "$(curl -sSfL https://release.anza.xyz/v2.1.0/install)" 26 | ``` 27 | 28 | then run `solana-keygen new` to create a keypair at the default location. 29 | 30 | 3. install `Anchor` 31 | 32 | ```shell 33 | # Installing using Anchor version manager (avm) 34 | cargo install --git https://github.com/coral-xyz/anchor avm --locked --force 35 | # Install anchor 36 | avm install 0.31.0 37 | ``` 38 | 39 | ## Quickstart 40 | 41 | Clone the repository and test the program. 42 | 43 | ```shell 44 | 45 | git clone https://github.com/raydium-io/raydium-cp-swap 46 | cd raydium-cp-swap && yarn && anchor test 47 | ``` 48 | 49 | ## License 50 | 51 | Raydium constant product swap is licensed under the Apache License, Version 2.0. 52 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Raydium CP-Swap (CPMM) Bug Bounty Program 2 | 3 | Raydium's full bug bounty program with ImmuneFi can be found at: https://immunefi.com/bounty/raydium/ 4 | 5 | ## Rewards by Threat Level 6 | 7 | Rewards are distributed according to the impact of the vulnerability based on the Immunefi Vulnerability Severity Classification System V2.3. This is a simplified 5-level scale, focusing on the impact of the vulnerability reported. 8 | 9 | ### Smart Contracts 10 | 11 | | Severity | Bounty | 12 | | -------- | ------------------------- | 13 | | Critical | USD 50,000 to USD 505,000 | 14 | | High | USD 40,000 | 15 | | Medium | USD 5,000 | 16 | 17 | All bug reports must include a Proof of Concept (PoC) demonstrating how the vulnerability can be exploited to impact an asset-in-scope to be eligible for a reward. Critical and High severity bug reports should also include a suggestion for a fix. Explanations and statements are not accepted as PoC and code is required. 18 | 19 | Rewards for critical smart contract bug reports will be further capped at 10% of direct funds at risk if the bug discovered is exploited. However, there is a minimum reward of USD 50,000. 20 | 21 | Bugs in `raydium-sdk` and other code outside of the smart contract will be assessed on a case-by-case basis. 22 | 23 | ## Report Submission 24 | 25 | Please email security@reactorlabs.io with a detailed description of the attack vector. For high- and critical-severity reports, please include a proof of concept. We will reach back out within 24 hours with additional questions or next steps on the bug bounty. 26 | 27 | ## Payout Information 28 | 29 | Payouts are handled by the Raydium team directly and are denominated in USD. Payouts can be done in RAY, SOL, or USDC. 30 | 31 | ## Out of Scope & Rules 32 | 33 | The following vulnerabilities are excluded from the rewards for this bug bounty program: 34 | 35 | - Attacks that the reporter has already exploited themselves, leading to damage 36 | - Attacks requiring access to leaked keys/credentials 37 | - Attacks requiring access to privileged addresses (governance, strategist) 38 | - Incorrect data supplied by third party oracles (not excluding oracle manipulation/flash loan attacks) 39 | - Basic economic governance attacks (e.g. 51% attack) 40 | - Lack of liquidity 41 | - Best practice critiques 42 | - Sybil attacks 43 | - Centralization risks 44 | - Any UI bugs 45 | - Bugs in the core Solana runtime (please submit these to [Solana's bug bounty program](https://github.com/solana-labs/solana/security/policy)) 46 | - Vulnerabilities that require a validator to execute them 47 | - Vulnerabilities requiring access to privileged keys/credentials 48 | - MEV vectors the team is already aware of 49 | 50 | ## AMM Assets in Scope 51 | 52 | | Target | Type | 53 | | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | 54 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/lib.rs | Smart Contract - lib | 55 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/error.rs | Smart Contract - error | 56 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/admin/collect_fund_fee.rs | Smart Contract - collect_fund_fee | 57 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/admin/collect_protocol_fee.rs | Smart Contract - collect_protocol_fee | 58 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/admin/create_config.rs | Smart Contract - create_config | 59 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/admin/mod.rs | Smart Contract - admin mod | 60 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/admin/update_config.rs | Smart Contract - update_config | 61 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/admin/update_pool_status.rs | Smart Contract - update_pool_status | 62 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/deposit.rs | Smart Contract - deposit | 63 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/initialize.rs | Smart Contract - initialize | 64 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/mod.rs | Smart Contract - instructions mod | 65 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/swap_base_input.rs | Smart Contract - swap_base_input | 66 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/swap_base_output.rs | Smart Contract - swap_base_output | 67 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/instructions/withdraw.rs | Smart Contract - withdraw | 68 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/states/config.rs | Smart Contract - config | 69 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/states/events.rs | Smart Contract - events | 70 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/states/mod.rs | Smart Contract - states mod | 71 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/states/pool.rs | Smart Contract - pool | 72 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/utils/math.rs | Smart Contract - math | 73 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/utils/mod.rs | Smart Contract - utils mod | 74 | | https://github.com/raydium-io/raydium-cp-swap/blob/master/programs/cp-swap/src/utils/token.rs | Smart Contract - utils token | 75 | 76 | ## Additional Information 77 | 78 | A public testnet of Raydium's CPMM can be found at A public testnet of Raydium’s AMM can be found at https://explorer.solana.com/address/CPMDWBwJDtYax9qW7AyRuVC19Cc4L4Vcy4n2BHAbHkCW?cluster=devnet 79 | 80 | A public testnet of OpenBook's Central Limit Order Book can be found at https://explorer.solana.com/address/EoTcMgcDRTJVZDMZWBoU6rhYHZfkNTVEAfz3uUJRcYGj 81 | 82 | If a Critical Impact can be caused to any other asset managed by Raydium that isn't on this table but for which the impact is in the Impacts in Scope section below, you are encouraged to submit it for consideration by the project. This only applies to Critical impacts. 83 | -------------------------------------------------------------------------------- /client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "client" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | anchor-client = { version = "0.31.0" } 8 | anchor-lang = { version = "0.31.0" } 9 | raydium-cp-swap = { path = "../programs/cp-swap", features = [ 10 | "no-entrypoint", 11 | "client", 12 | ] } 13 | solana-sdk = "=2.1.0" 14 | solana-client = "=2.1.0" 15 | solana-account-decoder = "=2.1.0" 16 | solana-transaction-status = "=2.1.0" 17 | spl-token = { version = "7.0.0", features = ["no-entrypoint"] } 18 | spl-token-client = "0.14.0" 19 | spl-memo = "6.0.0" 20 | spl-associated-token-account = { version = "6.0.0", features = [ 21 | "no-entrypoint", 22 | ] } 23 | spl-token-2022 = { version = "7.0.0", features = ["no-entrypoint"] } 24 | clap = { version = "4.1.8", features = ["derive"] } 25 | anyhow = "1.0.32" 26 | rand = "0.9.0" 27 | hex = "0.4.3" 28 | configparser = "3.0.0" 29 | serde_json = { version = "1.0.78" } 30 | serde = { version = "1.0", features = ["derive"] } 31 | arrayref = "0.3.7" 32 | bs58 = { version = "0.5.0" } 33 | bincode = { version = "1.3.3" } 34 | regex = "1" 35 | colorful = "0.3.2" 36 | base64 = "0.21.0" 37 | -------------------------------------------------------------------------------- /client/src/instructions/amm_instructions.rs: -------------------------------------------------------------------------------- 1 | use anchor_client::{Client, Cluster}; 2 | use anyhow::Result; 3 | use solana_sdk::{instruction::Instruction, pubkey::Pubkey, system_program, sysvar}; 4 | 5 | use raydium_cp_swap::accounts as raydium_cp_accounts; 6 | use raydium_cp_swap::instruction as raydium_cp_instructions; 7 | use raydium_cp_swap::{ 8 | states::{AMM_CONFIG_SEED, OBSERVATION_SEED, POOL_LP_MINT_SEED, POOL_SEED, POOL_VAULT_SEED}, 9 | AUTH_SEED, 10 | }; 11 | use std::rc::Rc; 12 | 13 | use super::super::{read_keypair_file, ClientConfig}; 14 | 15 | pub fn initialize_pool_instr( 16 | config: &ClientConfig, 17 | token_0_mint: Pubkey, 18 | token_1_mint: Pubkey, 19 | token_0_program: Pubkey, 20 | token_1_program: Pubkey, 21 | user_token_0_account: Pubkey, 22 | user_token_1_account: Pubkey, 23 | create_pool_fee: Pubkey, 24 | init_amount_0: u64, 25 | init_amount_1: u64, 26 | open_time: u64, 27 | ) -> Result> { 28 | let payer = read_keypair_file(&config.payer_path)?; 29 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 30 | // Client. 31 | let client = Client::new(url, Rc::new(payer)); 32 | let program = client.program(config.raydium_cp_program)?; 33 | 34 | let amm_config_index = 0u16; 35 | let (amm_config_key, __bump) = Pubkey::find_program_address( 36 | &[AMM_CONFIG_SEED.as_bytes(), &amm_config_index.to_be_bytes()], 37 | &program.id(), 38 | ); 39 | 40 | let (pool_account_key, __bump) = Pubkey::find_program_address( 41 | &[ 42 | POOL_SEED.as_bytes(), 43 | amm_config_key.to_bytes().as_ref(), 44 | token_0_mint.to_bytes().as_ref(), 45 | token_1_mint.to_bytes().as_ref(), 46 | ], 47 | &program.id(), 48 | ); 49 | let (authority, __bump) = Pubkey::find_program_address(&[AUTH_SEED.as_bytes()], &program.id()); 50 | let (token_0_vault, __bump) = Pubkey::find_program_address( 51 | &[ 52 | POOL_VAULT_SEED.as_bytes(), 53 | pool_account_key.to_bytes().as_ref(), 54 | token_0_mint.to_bytes().as_ref(), 55 | ], 56 | &program.id(), 57 | ); 58 | let (token_1_vault, __bump) = Pubkey::find_program_address( 59 | &[ 60 | POOL_VAULT_SEED.as_bytes(), 61 | pool_account_key.to_bytes().as_ref(), 62 | token_1_mint.to_bytes().as_ref(), 63 | ], 64 | &program.id(), 65 | ); 66 | let (lp_mint_key, __bump) = Pubkey::find_program_address( 67 | &[ 68 | POOL_LP_MINT_SEED.as_bytes(), 69 | pool_account_key.to_bytes().as_ref(), 70 | ], 71 | &program.id(), 72 | ); 73 | let (observation_key, __bump) = Pubkey::find_program_address( 74 | &[ 75 | OBSERVATION_SEED.as_bytes(), 76 | pool_account_key.to_bytes().as_ref(), 77 | ], 78 | &program.id(), 79 | ); 80 | 81 | let instructions = program 82 | .request() 83 | .accounts(raydium_cp_accounts::Initialize { 84 | creator: program.payer(), 85 | amm_config: amm_config_key, 86 | authority, 87 | pool_state: pool_account_key, 88 | token_0_mint, 89 | token_1_mint, 90 | lp_mint: lp_mint_key, 91 | creator_token_0: user_token_0_account, 92 | creator_token_1: user_token_1_account, 93 | creator_lp_token: spl_associated_token_account::get_associated_token_address( 94 | &program.payer(), 95 | &lp_mint_key, 96 | ), 97 | token_0_vault, 98 | token_1_vault, 99 | create_pool_fee, 100 | observation_state: observation_key, 101 | token_program: spl_token::id(), 102 | token_0_program, 103 | token_1_program, 104 | associated_token_program: spl_associated_token_account::id(), 105 | system_program: system_program::id(), 106 | rent: sysvar::rent::id(), 107 | }) 108 | .args(raydium_cp_instructions::Initialize { 109 | init_amount_0, 110 | init_amount_1, 111 | open_time, 112 | }) 113 | .instructions()?; 114 | Ok(instructions) 115 | } 116 | 117 | pub fn deposit_instr( 118 | config: &ClientConfig, 119 | pool_id: Pubkey, 120 | token_0_mint: Pubkey, 121 | token_1_mint: Pubkey, 122 | token_lp_mint: Pubkey, 123 | token_0_vault: Pubkey, 124 | token_1_vault: Pubkey, 125 | user_token_0_account: Pubkey, 126 | user_token_1_account: Pubkey, 127 | user_token_lp_account: Pubkey, 128 | lp_token_amount: u64, 129 | maximum_token_0_amount: u64, 130 | maximum_token_1_amount: u64, 131 | ) -> Result> { 132 | let payer = read_keypair_file(&config.payer_path)?; 133 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 134 | // Client. 135 | let client = Client::new(url, Rc::new(payer)); 136 | let program = client.program(config.raydium_cp_program)?; 137 | 138 | let (authority, __bump) = Pubkey::find_program_address(&[AUTH_SEED.as_bytes()], &program.id()); 139 | 140 | let instructions = program 141 | .request() 142 | .accounts(raydium_cp_accounts::Deposit { 143 | owner: program.payer(), 144 | authority, 145 | pool_state: pool_id, 146 | owner_lp_token: user_token_lp_account, 147 | token_0_account: user_token_0_account, 148 | token_1_account: user_token_1_account, 149 | token_0_vault, 150 | token_1_vault, 151 | token_program: spl_token::id(), 152 | token_program_2022: spl_token_2022::id(), 153 | vault_0_mint: token_0_mint, 154 | vault_1_mint: token_1_mint, 155 | lp_mint: token_lp_mint, 156 | }) 157 | .args(raydium_cp_instructions::Deposit { 158 | lp_token_amount, 159 | maximum_token_0_amount, 160 | maximum_token_1_amount, 161 | }) 162 | .instructions()?; 163 | Ok(instructions) 164 | } 165 | 166 | pub fn withdraw_instr( 167 | config: &ClientConfig, 168 | pool_id: Pubkey, 169 | token_0_mint: Pubkey, 170 | token_1_mint: Pubkey, 171 | token_lp_mint: Pubkey, 172 | token_0_vault: Pubkey, 173 | token_1_vault: Pubkey, 174 | user_token_0_account: Pubkey, 175 | user_token_1_account: Pubkey, 176 | user_token_lp_account: Pubkey, 177 | lp_token_amount: u64, 178 | minimum_token_0_amount: u64, 179 | minimum_token_1_amount: u64, 180 | ) -> Result> { 181 | let payer = read_keypair_file(&config.payer_path)?; 182 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 183 | // Client. 184 | let client = Client::new(url, Rc::new(payer)); 185 | let program = client.program(config.raydium_cp_program)?; 186 | 187 | let (authority, __bump) = Pubkey::find_program_address(&[AUTH_SEED.as_bytes()], &program.id()); 188 | 189 | let instructions = program 190 | .request() 191 | .accounts(raydium_cp_accounts::Withdraw { 192 | owner: program.payer(), 193 | authority, 194 | pool_state: pool_id, 195 | owner_lp_token: user_token_lp_account, 196 | token_0_account: user_token_0_account, 197 | token_1_account: user_token_1_account, 198 | token_0_vault, 199 | token_1_vault, 200 | token_program: spl_token::id(), 201 | token_program_2022: spl_token_2022::id(), 202 | vault_0_mint: token_0_mint, 203 | vault_1_mint: token_1_mint, 204 | lp_mint: token_lp_mint, 205 | memo_program: spl_memo::id(), 206 | }) 207 | .args(raydium_cp_instructions::Withdraw { 208 | lp_token_amount, 209 | minimum_token_0_amount, 210 | minimum_token_1_amount, 211 | }) 212 | .instructions()?; 213 | Ok(instructions) 214 | } 215 | 216 | pub fn swap_base_input_instr( 217 | config: &ClientConfig, 218 | pool_id: Pubkey, 219 | amm_config: Pubkey, 220 | observation_account: Pubkey, 221 | input_token_account: Pubkey, 222 | output_token_account: Pubkey, 223 | input_vault: Pubkey, 224 | output_vault: Pubkey, 225 | input_token_mint: Pubkey, 226 | output_token_mint: Pubkey, 227 | input_token_program: Pubkey, 228 | output_token_program: Pubkey, 229 | amount_in: u64, 230 | minimum_amount_out: u64, 231 | ) -> Result> { 232 | let payer = read_keypair_file(&config.payer_path)?; 233 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 234 | // Client. 235 | let client = Client::new(url, Rc::new(payer)); 236 | let program = client.program(config.raydium_cp_program)?; 237 | 238 | let (authority, __bump) = Pubkey::find_program_address(&[AUTH_SEED.as_bytes()], &program.id()); 239 | 240 | let instructions = program 241 | .request() 242 | .accounts(raydium_cp_accounts::Swap { 243 | payer: program.payer(), 244 | authority, 245 | amm_config, 246 | pool_state: pool_id, 247 | input_token_account, 248 | output_token_account, 249 | input_vault, 250 | output_vault, 251 | input_token_program, 252 | output_token_program, 253 | input_token_mint, 254 | output_token_mint, 255 | observation_state: observation_account, 256 | }) 257 | .args(raydium_cp_instructions::SwapBaseInput { 258 | amount_in, 259 | minimum_amount_out, 260 | }) 261 | .instructions()?; 262 | Ok(instructions) 263 | } 264 | 265 | pub fn swap_base_output_instr( 266 | config: &ClientConfig, 267 | pool_id: Pubkey, 268 | amm_config: Pubkey, 269 | observation_account: Pubkey, 270 | input_token_account: Pubkey, 271 | output_token_account: Pubkey, 272 | input_vault: Pubkey, 273 | output_vault: Pubkey, 274 | input_token_mint: Pubkey, 275 | output_token_mint: Pubkey, 276 | input_token_program: Pubkey, 277 | output_token_program: Pubkey, 278 | max_amount_in: u64, 279 | amount_out: u64, 280 | ) -> Result> { 281 | let payer = read_keypair_file(&config.payer_path)?; 282 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 283 | // Client. 284 | let client = Client::new(url, Rc::new(payer)); 285 | let program = client.program(config.raydium_cp_program)?; 286 | 287 | let (authority, __bump) = Pubkey::find_program_address(&[AUTH_SEED.as_bytes()], &program.id()); 288 | 289 | let instructions = program 290 | .request() 291 | .accounts(raydium_cp_accounts::Swap { 292 | payer: program.payer(), 293 | authority, 294 | amm_config, 295 | pool_state: pool_id, 296 | input_token_account, 297 | output_token_account, 298 | input_vault, 299 | output_vault, 300 | input_token_program, 301 | output_token_program, 302 | input_token_mint, 303 | output_token_mint, 304 | observation_state: observation_account, 305 | }) 306 | .args(raydium_cp_instructions::SwapBaseOutput { 307 | max_amount_in, 308 | amount_out, 309 | }) 310 | .instructions()?; 311 | Ok(instructions) 312 | } 313 | -------------------------------------------------------------------------------- /client/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod amm_instructions; 2 | pub mod events_instructions_parse; 3 | pub mod rpc; 4 | pub mod token_instructions; 5 | pub mod utils; 6 | -------------------------------------------------------------------------------- /client/src/instructions/rpc.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use solana_client::{ 3 | rpc_client::RpcClient, 4 | rpc_config::RpcSendTransactionConfig, 5 | rpc_request::RpcRequest, 6 | rpc_response::{RpcResult, RpcSimulateTransactionResult}, 7 | }; 8 | use solana_sdk::{ 9 | account::Account, commitment_config::CommitmentConfig, program_pack::Pack as TokenPack, 10 | pubkey::Pubkey, signature::Signature, transaction::Transaction, 11 | }; 12 | use std::convert::Into; 13 | 14 | pub fn simulate_transaction( 15 | client: &RpcClient, 16 | transaction: &Transaction, 17 | sig_verify: bool, 18 | cfg: CommitmentConfig, 19 | ) -> RpcResult { 20 | let serialized_encoded = bs58::encode(bincode::serialize(transaction).unwrap()).into_string(); 21 | client.send( 22 | RpcRequest::SimulateTransaction, 23 | serde_json::json!([serialized_encoded, { 24 | "sigVerify": sig_verify, "commitment": cfg.commitment 25 | }]), 26 | ) 27 | } 28 | 29 | pub fn send_txn(client: &RpcClient, txn: &Transaction, wait_confirm: bool) -> Result { 30 | Ok(client.send_and_confirm_transaction_with_spinner_and_config( 31 | txn, 32 | if wait_confirm { 33 | CommitmentConfig::confirmed() 34 | } else { 35 | CommitmentConfig::processed() 36 | }, 37 | RpcSendTransactionConfig { 38 | skip_preflight: true, 39 | ..RpcSendTransactionConfig::default() 40 | }, 41 | )?) 42 | } 43 | 44 | pub fn get_token_account(client: &RpcClient, addr: &Pubkey) -> Result { 45 | let account = client 46 | .get_account_with_commitment(addr, CommitmentConfig::processed())? 47 | .value 48 | .map_or(Err(anyhow!("Account not found")), Ok)?; 49 | T::unpack_from_slice(&account.data).map_err(Into::into) 50 | } 51 | 52 | pub fn get_multiple_accounts( 53 | client: &RpcClient, 54 | pubkeys: &[Pubkey], 55 | ) -> Result>> { 56 | Ok(client.get_multiple_accounts(pubkeys)?) 57 | } 58 | -------------------------------------------------------------------------------- /client/src/instructions/token_instructions.rs: -------------------------------------------------------------------------------- 1 | use super::super::{read_keypair_file, ClientConfig}; 2 | use anchor_client::{Client, Cluster}; 3 | use anyhow::Result; 4 | use solana_client::rpc_client::RpcClient; 5 | use solana_sdk::{ 6 | account::WritableAccount, 7 | instruction::Instruction, 8 | program_pack::Pack, 9 | pubkey::Pubkey, 10 | signature::{Keypair, Signer}, 11 | system_instruction, 12 | }; 13 | use spl_token_2022::{ 14 | extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensionsMut}, 15 | state::{Account, Mint}, 16 | }; 17 | use spl_token_client::token::ExtensionInitializationParams; 18 | use std::{rc::Rc, str::FromStr}; 19 | 20 | pub fn create_and_init_mint_instr( 21 | config: &ClientConfig, 22 | token_program: Pubkey, 23 | mint_key: &Pubkey, 24 | mint_authority: &Pubkey, 25 | freeze_authority: Option<&Pubkey>, 26 | extension_init_params: Vec, 27 | decimals: u8, 28 | ) -> Result> { 29 | let payer = read_keypair_file(&config.payer_path)?; 30 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 31 | // Client. 32 | let client = Client::new(url, Rc::new(payer)); 33 | let program = if token_program == spl_token::id() { 34 | client.program(spl_token::id())? 35 | } else { 36 | client.program(spl_token_2022::id())? 37 | }; 38 | let extension_types = extension_init_params 39 | .iter() 40 | .map(|e| e.extension()) 41 | .collect::>(); 42 | let space = ExtensionType::try_calculate_account_len::(&extension_types)?; 43 | 44 | let mut instructions = vec![system_instruction::create_account( 45 | &program.payer(), 46 | mint_key, 47 | program 48 | .rpc() 49 | .get_minimum_balance_for_rent_exemption(space)?, 50 | space as u64, 51 | &program.id(), 52 | )]; 53 | for params in extension_init_params { 54 | instructions.push(params.instruction(&token_program, &mint_key)?); 55 | } 56 | instructions.push(spl_token_2022::instruction::initialize_mint( 57 | &program.id(), 58 | mint_key, 59 | mint_authority, 60 | freeze_authority, 61 | decimals, 62 | )?); 63 | Ok(instructions) 64 | } 65 | 66 | pub fn create_account_rent_exmpt_instr( 67 | config: &ClientConfig, 68 | new_account_key: &Pubkey, 69 | owner: Pubkey, 70 | data_size: usize, 71 | ) -> Result> { 72 | let payer = read_keypair_file(&config.payer_path)?; 73 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 74 | // Client. 75 | let client = Client::new(url, Rc::new(payer)); 76 | let program = client.program(owner)?; 77 | let instructions = program 78 | .request() 79 | .instruction(system_instruction::create_account( 80 | &program.payer(), 81 | &new_account_key, 82 | program 83 | .rpc() 84 | .get_minimum_balance_for_rent_exemption(data_size)?, 85 | data_size as u64, 86 | &program.id(), 87 | )) 88 | .instructions()?; 89 | Ok(instructions) 90 | } 91 | 92 | pub fn create_ata_token_account_instr( 93 | config: &ClientConfig, 94 | token_program: Pubkey, 95 | mint: &Pubkey, 96 | owner: &Pubkey, 97 | ) -> Result> { 98 | let payer = read_keypair_file(&config.payer_path)?; 99 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 100 | // Client. 101 | let client = Client::new(url, Rc::new(payer)); 102 | let program = client.program(token_program)?; 103 | let instructions = program 104 | .request() 105 | .instruction( 106 | spl_associated_token_account::instruction::create_associated_token_account_idempotent( 107 | &program.payer(), 108 | owner, 109 | mint, 110 | &token_program, 111 | ), 112 | ) 113 | .instructions()?; 114 | Ok(instructions) 115 | } 116 | 117 | pub fn create_and_init_auxiliary_token( 118 | config: &ClientConfig, 119 | new_account_key: &Pubkey, 120 | mint: &Pubkey, 121 | owner: &Pubkey, 122 | ) -> Result> { 123 | let payer = read_keypair_file(&config.payer_path)?; 124 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 125 | let mint_account = &mut RpcClient::new(config.http_url.to_string()).get_account(&mint)?; 126 | // Client. 127 | let client = Client::new(url, Rc::new(payer)); 128 | let (program, space) = if mint_account.owner == spl_token::id() { 129 | ( 130 | client.program(spl_token::id())?, 131 | spl_token::state::Account::LEN, 132 | ) 133 | } else { 134 | let mut extensions = vec![]; 135 | extensions.push(ExtensionType::ImmutableOwner); 136 | let mint_state = StateWithExtensionsMut::::unpack(mint_account.data_as_mut_slice())?; 137 | let mint_extension_types = mint_state.get_extension_types()?; 138 | let mut required_extensions = 139 | ExtensionType::get_required_init_account_extensions(&mint_extension_types); 140 | for extension_type in extensions.into_iter() { 141 | if !required_extensions.contains(&extension_type) { 142 | required_extensions.push(extension_type); 143 | } 144 | } 145 | let space = ExtensionType::try_calculate_account_len::(&required_extensions)?; 146 | 147 | (client.program(spl_token_2022::id())?, space) 148 | }; 149 | 150 | let instructions = program 151 | .request() 152 | .instruction(system_instruction::create_account( 153 | &program.payer(), 154 | &mint, 155 | program 156 | .rpc() 157 | .get_minimum_balance_for_rent_exemption(space)?, 158 | space as u64, 159 | &program.id(), 160 | )) 161 | .instruction(spl_token_2022::instruction::initialize_immutable_owner( 162 | &program.id(), 163 | new_account_key, 164 | )?) 165 | .instruction(spl_token_2022::instruction::initialize_account( 166 | &program.id(), 167 | new_account_key, 168 | mint, 169 | owner, 170 | )?) 171 | .instructions()?; 172 | Ok(instructions) 173 | } 174 | 175 | pub fn close_token_account( 176 | config: &ClientConfig, 177 | close_account: &Pubkey, 178 | destination: &Pubkey, 179 | owner: &Keypair, 180 | ) -> Result> { 181 | let payer = read_keypair_file(&config.payer_path)?; 182 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 183 | // Client. 184 | let client = Client::new(url, Rc::new(payer)); 185 | let program = client.program(spl_token::id())?; 186 | let instructions = program 187 | .request() 188 | .instruction(spl_token::instruction::close_account( 189 | &program.id(), 190 | close_account, 191 | destination, 192 | &owner.pubkey(), 193 | &[], 194 | )?) 195 | .signer(owner) 196 | .instructions()?; 197 | Ok(instructions) 198 | } 199 | 200 | pub fn spl_token_transfer_instr( 201 | config: &ClientConfig, 202 | from: &Pubkey, 203 | to: &Pubkey, 204 | amount: u64, 205 | from_authority: &Keypair, 206 | ) -> Result> { 207 | let payer = read_keypair_file(&config.payer_path)?; 208 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 209 | // Client. 210 | let client = Client::new(url, Rc::new(payer)); 211 | let program = client.program(spl_token::id())?; 212 | let instructions = program 213 | .request() 214 | .instruction(spl_token::instruction::transfer( 215 | &program.id(), 216 | from, 217 | to, 218 | &from_authority.pubkey(), 219 | &[], 220 | amount, 221 | )?) 222 | .signer(from_authority) 223 | .instructions()?; 224 | Ok(instructions) 225 | } 226 | 227 | pub fn spl_token_mint_to_instr( 228 | config: &ClientConfig, 229 | token_program: Pubkey, 230 | mint: &Pubkey, 231 | to: &Pubkey, 232 | amount: u64, 233 | mint_authority: &Keypair, 234 | ) -> Result> { 235 | let payer = read_keypair_file(&config.payer_path)?; 236 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 237 | // Client. 238 | let client = Client::new(url, Rc::new(payer)); 239 | let program = if token_program == spl_token::id() { 240 | client.program(spl_token::id())? 241 | } else { 242 | client.program(spl_token_2022::id())? 243 | }; 244 | let instructions = program 245 | .request() 246 | .instruction(spl_token_2022::instruction::mint_to( 247 | &program.id(), 248 | mint, 249 | to, 250 | &mint_authority.pubkey(), 251 | &[], 252 | amount, 253 | )?) 254 | .signer(mint_authority) 255 | .instructions()?; 256 | Ok(instructions) 257 | } 258 | 259 | pub fn wrap_sol_instr(config: &ClientConfig, amount: u64) -> Result> { 260 | let payer = read_keypair_file(&config.payer_path)?; 261 | let wallet_key = payer.pubkey(); 262 | let url = Cluster::Custom(config.http_url.clone(), config.ws_url.clone()); 263 | let wsol_mint = Pubkey::from_str("So11111111111111111111111111111111111111112")?; 264 | let wsol_ata_account = 265 | spl_associated_token_account::get_associated_token_address(&wallet_key, &wsol_mint); 266 | // Client. 267 | let client = Client::new(url, Rc::new(payer)); 268 | let program = client.program(spl_token::id())?; 269 | 270 | let instructions = program 271 | .request() 272 | .instruction( 273 | spl_associated_token_account::instruction::create_associated_token_account_idempotent( 274 | &program.payer(), 275 | &wallet_key, 276 | &wsol_mint, 277 | &program.id(), 278 | ), 279 | ) 280 | .instruction(system_instruction::transfer( 281 | &wallet_key, 282 | &wsol_ata_account, 283 | amount, 284 | )) 285 | .instruction(spl_token::instruction::sync_native( 286 | &program.id(), 287 | &wsol_ata_account, 288 | )?) 289 | .instructions()?; 290 | Ok(instructions) 291 | } 292 | -------------------------------------------------------------------------------- /client/src/instructions/utils.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::AccountDeserialize; 2 | use anyhow::Result; 3 | use solana_client::rpc_client::RpcClient; 4 | use solana_sdk::{account::Account, pubkey::Pubkey}; 5 | use spl_token_2022::{ 6 | extension::{ 7 | transfer_fee::{TransferFeeConfig, MAX_FEE_BASIS_POINTS}, 8 | BaseState, BaseStateWithExtensions, StateWithExtensionsMut, 9 | }, 10 | state::Mint, 11 | }; 12 | use std::ops::Mul; 13 | 14 | pub fn deserialize_anchor_account(account: &Account) -> Result { 15 | let mut data: &[u8] = &account.data; 16 | T::try_deserialize(&mut data).map_err(Into::into) 17 | } 18 | 19 | #[derive(Debug)] 20 | pub struct TransferFeeInfo { 21 | pub mint: Pubkey, 22 | pub owner: Pubkey, 23 | pub transfer_fee: u64, 24 | } 25 | 26 | pub fn amount_with_slippage(amount: u64, slippage: f64, round_up: bool) -> u64 { 27 | if round_up { 28 | (amount as f64).mul(1_f64 + slippage).ceil() as u64 29 | } else { 30 | (amount as f64).mul(1_f64 - slippage).floor() as u64 31 | } 32 | } 33 | 34 | pub fn get_pool_mints_inverse_fee( 35 | rpc_client: &RpcClient, 36 | token_mint_0: Pubkey, 37 | token_mint_1: Pubkey, 38 | post_fee_amount_0: u64, 39 | post_fee_amount_1: u64, 40 | ) -> (TransferFeeInfo, TransferFeeInfo) { 41 | let load_accounts = vec![token_mint_0, token_mint_1]; 42 | let rsps = rpc_client.get_multiple_accounts(&load_accounts).unwrap(); 43 | let epoch = rpc_client.get_epoch_info().unwrap().epoch; 44 | let mut mint0_account = rsps[0].clone().ok_or("load mint0 rps error!").unwrap(); 45 | let mut mint1_account = rsps[1].clone().ok_or("load mint0 rps error!").unwrap(); 46 | let mint0_state = StateWithExtensionsMut::::unpack(&mut mint0_account.data).unwrap(); 47 | let mint1_state = StateWithExtensionsMut::::unpack(&mut mint1_account.data).unwrap(); 48 | ( 49 | TransferFeeInfo { 50 | mint: token_mint_0, 51 | owner: mint0_account.owner, 52 | transfer_fee: get_transfer_inverse_fee(&mint0_state, post_fee_amount_0, epoch), 53 | }, 54 | TransferFeeInfo { 55 | mint: token_mint_1, 56 | owner: mint1_account.owner, 57 | transfer_fee: get_transfer_inverse_fee(&mint1_state, post_fee_amount_1, epoch), 58 | }, 59 | ) 60 | } 61 | 62 | pub fn get_pool_mints_transfer_fee( 63 | rpc_client: &RpcClient, 64 | token_mint_0: Pubkey, 65 | token_mint_1: Pubkey, 66 | pre_fee_amount_0: u64, 67 | pre_fee_amount_1: u64, 68 | ) -> (TransferFeeInfo, TransferFeeInfo) { 69 | let load_accounts = vec![token_mint_0, token_mint_1]; 70 | let rsps = rpc_client.get_multiple_accounts(&load_accounts).unwrap(); 71 | let epoch = rpc_client.get_epoch_info().unwrap().epoch; 72 | let mut mint0_account = rsps[0].clone().ok_or("load mint0 rps error!").unwrap(); 73 | let mut mint1_account = rsps[1].clone().ok_or("load mint0 rps error!").unwrap(); 74 | let mint0_state = StateWithExtensionsMut::::unpack(&mut mint0_account.data).unwrap(); 75 | let mint1_state = StateWithExtensionsMut::::unpack(&mut mint1_account.data).unwrap(); 76 | ( 77 | TransferFeeInfo { 78 | mint: token_mint_0, 79 | owner: mint0_account.owner, 80 | transfer_fee: get_transfer_fee(&mint0_state, pre_fee_amount_0, epoch), 81 | }, 82 | TransferFeeInfo { 83 | mint: token_mint_1, 84 | owner: mint1_account.owner, 85 | transfer_fee: get_transfer_fee(&mint1_state, pre_fee_amount_1, epoch), 86 | }, 87 | ) 88 | } 89 | 90 | /// Calculate the fee for output amount 91 | pub fn get_transfer_inverse_fee<'data, S: BaseState>( 92 | account_state: &StateWithExtensionsMut<'data, S>, 93 | epoch: u64, 94 | post_fee_amount: u64, 95 | ) -> u64 { 96 | let fee = if let Ok(transfer_fee_config) = account_state.get_extension::() { 97 | let transfer_fee = transfer_fee_config.get_epoch_fee(epoch); 98 | if u16::from(transfer_fee.transfer_fee_basis_points) == MAX_FEE_BASIS_POINTS { 99 | u64::from(transfer_fee.maximum_fee) 100 | } else { 101 | transfer_fee_config 102 | .calculate_inverse_epoch_fee(epoch, post_fee_amount) 103 | .unwrap() 104 | } 105 | } else { 106 | 0 107 | }; 108 | fee 109 | } 110 | 111 | /// Calculate the fee for input amount 112 | pub fn get_transfer_fee<'data, S: BaseState>( 113 | account_state: &StateWithExtensionsMut<'data, S>, 114 | epoch: u64, 115 | pre_fee_amount: u64, 116 | ) -> u64 { 117 | let fee = if let Ok(transfer_fee_config) = account_state.get_extension::() { 118 | transfer_fee_config 119 | .calculate_epoch_fee(epoch, pre_fee_amount) 120 | .unwrap() 121 | } else { 122 | 0 123 | }; 124 | fee 125 | } 126 | -------------------------------------------------------------------------------- /client_config.ini: -------------------------------------------------------------------------------- 1 | [Global] 2 | http_url = https://api.devnet.solana.com 3 | ws_url = wss://api.devnet.solana.com/ 4 | payer_path = id.json 5 | admin_path = adMCyoCgfkg7bQiJ9aBJ59H3BXLY3r5LNLfPpQfMzBe.json 6 | raydium_cp_program = CPMDWBwJDtYax9qW7AyRuVC19Cc4L4Vcy4n2BHAbHkCW 7 | slippage = 0.01 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 4 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" 5 | }, 6 | "dependencies": { 7 | "@coral-xyz/anchor": "0.31.0", 8 | "@solana/spl-token": "^0.4.8", 9 | "@solana/web3.js": "^1.95.3" 10 | }, 11 | "devDependencies": { 12 | "@types/bn.js": "^5.1.0", 13 | "@types/chai": "^4.3.0", 14 | "@types/mocha": "^9.0.0", 15 | "chai": "^4.3.4", 16 | "mocha": "^9.0.3", 17 | "prettier": "^2.6.2", 18 | "ts-mocha": "^10.0.0", 19 | "typescript": "^4.3.5" 20 | } 21 | } -------------------------------------------------------------------------------- /programs/cp-swap/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "raydium-cp-swap" 3 | version = "0.2.0" 4 | description = "Raydium constant product AMM, supports Token2022 and without Openbook" 5 | edition = "2021" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "raydium_cp_swap" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-log-ix-name = [] 15 | cpi = ["no-entrypoint"] 16 | default = [] 17 | enable-log = [] 18 | devnet = [] 19 | client = [] 20 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 21 | 22 | [dependencies] 23 | anchor-lang = { version = "0.31.0", features = ["init-if-needed"] } 24 | anchor-spl = { version = "0.31.0", features = ["memo", "metadata"] } 25 | spl-math = { version = "0.3", features = ["no-entrypoint"] } 26 | uint = "0.10.0" 27 | solana-security-txt = "1.1.1" 28 | bytemuck = { version = "1.4.0", features = ["derive", "min_const_generics"] } 29 | arrayref = { version = "0.3.6" } 30 | 31 | [dev-dependencies] 32 | quickcheck = "1.0.3" 33 | proptest = "1.0" 34 | rand = "0.9.0" 35 | 36 | [profile.release] 37 | lto = "fat" 38 | codegen-units = 1 39 | panic = "abort" 40 | overflow-checks = true 41 | [profile.release.build-override] 42 | opt-level = 3 43 | incremental = false 44 | codegen-units = 1 45 | -------------------------------------------------------------------------------- /programs/cp-swap/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/cp-swap/src/curve/constant_product.rs: -------------------------------------------------------------------------------- 1 | //! The Uniswap invariantConstantProductCurve:: 2 | 3 | use crate::{ 4 | curve::calculator::{RoundDirection, TradingTokenResult}, 5 | utils::CheckedCeilDiv, 6 | }; 7 | 8 | /// ConstantProductCurve struct implementing CurveCalculator 9 | #[derive(Clone, Debug, Default, PartialEq)] 10 | pub struct ConstantProductCurve; 11 | 12 | impl ConstantProductCurve { 13 | /// Constant product swap ensures x * y = constant 14 | /// The constant product swap calculation, factored out of its class for reuse. 15 | /// 16 | /// This is guaranteed to work for all values such that: 17 | /// - 1 <= swap_source_amount * swap_destination_amount <= u128::MAX 18 | /// - 1 <= source_amount <= u64::MAX 19 | pub fn swap_base_input_without_fees( 20 | source_amount: u128, 21 | swap_source_amount: u128, 22 | swap_destination_amount: u128, 23 | ) -> u128 { 24 | // (x + delta_x) * (y - delta_y) = x * y 25 | // delta_y = (delta_x * y) / (x + delta_x) 26 | let numerator = source_amount.checked_mul(swap_destination_amount).unwrap(); 27 | let denominator = swap_source_amount.checked_add(source_amount).unwrap(); 28 | let destinsation_amount_swapped = numerator.checked_div(denominator).unwrap(); 29 | destinsation_amount_swapped 30 | } 31 | 32 | pub fn swap_base_output_without_fees( 33 | destinsation_amount: u128, 34 | swap_source_amount: u128, 35 | swap_destination_amount: u128, 36 | ) -> u128 { 37 | // (x + delta_x) * (y - delta_y) = x * y 38 | // delta_x = (x * delta_y) / (y - delta_y) 39 | let numerator = swap_source_amount.checked_mul(destinsation_amount).unwrap(); 40 | let denominator = swap_destination_amount 41 | .checked_sub(destinsation_amount) 42 | .unwrap(); 43 | let (source_amount_swapped, _) = numerator.checked_ceil_div(denominator).unwrap(); 44 | source_amount_swapped 45 | } 46 | 47 | /// Get the amount of trading tokens for the given amount of pool tokens, 48 | /// provided the total trading tokens and supply of pool tokens. 49 | /// 50 | /// The constant product implementation is a simple ratio calculation for how 51 | /// many trading tokens correspond to a certain number of pool tokens 52 | pub fn lp_tokens_to_trading_tokens( 53 | lp_token_amount: u128, 54 | lp_token_supply: u128, 55 | swap_token_0_amount: u128, 56 | swap_token_1_amount: u128, 57 | round_direction: RoundDirection, 58 | ) -> Option { 59 | let mut token_0_amount = lp_token_amount 60 | .checked_mul(swap_token_0_amount)? 61 | .checked_div(lp_token_supply)?; 62 | let mut token_1_amount = lp_token_amount 63 | .checked_mul(swap_token_1_amount)? 64 | .checked_div(lp_token_supply)?; 65 | let (token_0_amount, token_1_amount) = match round_direction { 66 | RoundDirection::Floor => (token_0_amount, token_1_amount), 67 | RoundDirection::Ceiling => { 68 | let token_0_remainder = lp_token_amount 69 | .checked_mul(swap_token_0_amount)? 70 | .checked_rem(lp_token_supply)?; 71 | // Also check for 0 token A and B amount to avoid taking too much 72 | // for tiny amounts of pool tokens. For example, if someone asks 73 | // for 1 pool token, which is worth 0.01 token A, we avoid the 74 | // ceiling of taking 1 token A and instead return 0, for it to be 75 | // rejected later in processing. 76 | if token_0_remainder > 0 && token_0_amount > 0 { 77 | token_0_amount += 1; 78 | } 79 | let token_1_remainder = lp_token_amount 80 | .checked_mul(swap_token_1_amount)? 81 | .checked_rem(lp_token_supply)?; 82 | if token_1_remainder > 0 && token_1_amount > 0 { 83 | token_1_amount += 1; 84 | } 85 | (token_0_amount, token_1_amount) 86 | } 87 | }; 88 | Some(TradingTokenResult { 89 | token_0_amount, 90 | token_1_amount, 91 | }) 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use { 98 | super::*, 99 | crate::curve::calculator::{ 100 | test::{ 101 | check_curve_value_from_swap, check_pool_value_from_deposit, 102 | check_pool_value_from_withdraw, total_and_intermediate, 103 | }, 104 | RoundDirection, TradeDirection, 105 | }, 106 | proptest::prelude::*, 107 | }; 108 | 109 | fn check_pool_token_rate( 110 | token_a: u128, 111 | token_b: u128, 112 | deposit: u128, 113 | supply: u128, 114 | expected_a: u128, 115 | expected_b: u128, 116 | ) { 117 | let results = ConstantProductCurve::lp_tokens_to_trading_tokens( 118 | deposit, 119 | supply, 120 | token_a, 121 | token_b, 122 | RoundDirection::Ceiling, 123 | ) 124 | .unwrap(); 125 | assert_eq!(results.token_0_amount, expected_a); 126 | assert_eq!(results.token_1_amount, expected_b); 127 | } 128 | 129 | #[test] 130 | fn trading_token_conversion() { 131 | check_pool_token_rate(2, 49, 5, 10, 1, 25); 132 | check_pool_token_rate(100, 202, 5, 101, 5, 10); 133 | check_pool_token_rate(5, 501, 2, 10, 1, 101); 134 | } 135 | 136 | #[test] 137 | fn fail_trading_token_conversion() { 138 | let results = ConstantProductCurve::lp_tokens_to_trading_tokens( 139 | 5, 140 | 10, 141 | u128::MAX, 142 | 0, 143 | RoundDirection::Floor, 144 | ); 145 | assert!(results.is_none()); 146 | let results = ConstantProductCurve::lp_tokens_to_trading_tokens( 147 | 5, 148 | 10, 149 | 0, 150 | u128::MAX, 151 | RoundDirection::Floor, 152 | ); 153 | assert!(results.is_none()); 154 | } 155 | 156 | fn test_truncation( 157 | source_amount: u128, 158 | swap_source_amount: u128, 159 | swap_destination_amount: u128, 160 | expected_source_amount_swapped: u128, 161 | expected_destination_amount_swapped: u128, 162 | ) { 163 | let invariant = swap_source_amount * swap_destination_amount; 164 | let destination_amount_swapped = ConstantProductCurve::swap_base_input_without_fees( 165 | source_amount, 166 | swap_source_amount, 167 | swap_destination_amount, 168 | ); 169 | assert_eq!(source_amount, expected_source_amount_swapped); 170 | assert_eq!( 171 | destination_amount_swapped, 172 | expected_destination_amount_swapped 173 | ); 174 | let new_invariant = (swap_source_amount + source_amount) 175 | * (swap_destination_amount - destination_amount_swapped); 176 | assert!(new_invariant >= invariant); 177 | } 178 | 179 | #[test] 180 | fn constant_product_swap_rounding() { 181 | let tests: &[(u128, u128, u128, u128, u128)] = &[ 182 | // spot: 10 * 70b / ~4m = 174,999.99 183 | (10, 4_000_000, 70_000_000_000, 10, 174_999), 184 | // spot: 20 * 1 / 3.000 = 6.6667 (source can be 18 to get 6 dest.) 185 | (20, 30_000 - 20, 10_000, 20, 6), 186 | // spot: 19 * 1 / 2.999 = 6.3334 (source can be 18 to get 6 dest.) 187 | (19, 30_000 - 20, 10_000, 19, 6), 188 | // spot: 18 * 1 / 2.999 = 6.0001 189 | (18, 30_000 - 20, 10_000, 18, 6), 190 | // spot: 10 * 3 / 2.0010 = 14.99 191 | (10, 20_000, 30_000, 10, 14), 192 | // spot: 10 * 3 / 2.0001 = 14.999 193 | (10, 20_000 - 9, 30_000, 10, 14), 194 | // spot: 10 * 3 / 2.0000 = 15 195 | (10, 20_000 - 10, 30_000, 10, 15), 196 | // spot: 100 * 3 / 6.001 = 49.99 (source can be 99 to get 49 dest.) 197 | (100, 60_000, 30_000, 100, 49), 198 | // spot: 99 * 3 / 6.001 = 49.49 199 | (99, 60_000, 30_000, 99, 49), 200 | // spot: 98 * 3 / 6.001 = 48.99 (source can be 97 to get 48 dest.) 201 | (98, 60_000, 30_000, 98, 48), 202 | ]; 203 | for ( 204 | source_amount, 205 | swap_source_amount, 206 | swap_destination_amount, 207 | expected_source_amount, 208 | expected_destination_amount, 209 | ) in tests.iter() 210 | { 211 | test_truncation( 212 | *source_amount, 213 | *swap_source_amount, 214 | *swap_destination_amount, 215 | *expected_source_amount, 216 | *expected_destination_amount, 217 | ); 218 | } 219 | } 220 | 221 | proptest! { 222 | #[test] 223 | fn curve_value_does_not_decrease_from_swap( 224 | source_token_amount in 1..u64::MAX, 225 | swap_source_amount in 1..u64::MAX, 226 | swap_destination_amount in 1..u64::MAX, 227 | ) { 228 | check_curve_value_from_swap( 229 | source_token_amount as u128, 230 | swap_source_amount as u128, 231 | swap_destination_amount as u128, 232 | TradeDirection::ZeroForOne 233 | ); 234 | } 235 | } 236 | 237 | proptest! { 238 | #[test] 239 | fn curve_value_does_not_decrease_from_deposit( 240 | pool_token_amount in 1..u64::MAX, 241 | pool_token_supply in 1..u64::MAX, 242 | swap_token_a_amount in 1..u64::MAX, 243 | swap_token_b_amount in 1..u64::MAX, 244 | ) { 245 | let pool_token_amount = pool_token_amount as u128; 246 | let pool_token_supply = pool_token_supply as u128; 247 | let swap_token_a_amount = swap_token_a_amount as u128; 248 | let swap_token_b_amount = swap_token_b_amount as u128; 249 | // Make sure we will get at least one trading token out for each 250 | // side, otherwise the calculation fails 251 | prop_assume!(pool_token_amount * swap_token_a_amount / pool_token_supply >= 1); 252 | prop_assume!(pool_token_amount * swap_token_b_amount / pool_token_supply >= 1); 253 | check_pool_value_from_deposit( 254 | pool_token_amount, 255 | pool_token_supply, 256 | swap_token_a_amount, 257 | swap_token_b_amount, 258 | ); 259 | } 260 | } 261 | 262 | proptest! { 263 | #[test] 264 | fn curve_value_does_not_decrease_from_withdraw( 265 | (pool_token_supply, pool_token_amount) in total_and_intermediate(u64::MAX), 266 | swap_token_a_amount in 1..u64::MAX, 267 | swap_token_b_amount in 1..u64::MAX, 268 | ) { 269 | let pool_token_amount = pool_token_amount as u128; 270 | let pool_token_supply = pool_token_supply as u128; 271 | let swap_token_a_amount = swap_token_a_amount as u128; 272 | let swap_token_b_amount = swap_token_b_amount as u128; 273 | // Make sure we will get at least one trading token out for each 274 | // side, otherwise the calculation fails 275 | prop_assume!(pool_token_amount * swap_token_a_amount / pool_token_supply >= 1); 276 | prop_assume!(pool_token_amount * swap_token_b_amount / pool_token_supply >= 1); 277 | check_pool_value_from_withdraw( 278 | pool_token_amount, 279 | pool_token_supply, 280 | swap_token_a_amount, 281 | swap_token_b_amount, 282 | ); 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /programs/cp-swap/src/curve/fees.rs: -------------------------------------------------------------------------------- 1 | //! All fee information, to be used for validation currently 2 | 3 | pub const FEE_RATE_DENOMINATOR_VALUE: u64 = 1_000_000; 4 | 5 | pub struct Fees {} 6 | 7 | fn ceil_div(token_amount: u128, fee_numerator: u128, fee_denominator: u128) -> Option { 8 | token_amount 9 | .checked_mul(u128::from(fee_numerator)) 10 | .unwrap() 11 | .checked_add(fee_denominator)? 12 | .checked_sub(1)? 13 | .checked_div(fee_denominator) 14 | } 15 | 16 | /// Helper function for calculating swap fee 17 | pub fn floor_div(token_amount: u128, fee_numerator: u128, fee_denominator: u128) -> Option { 18 | Some( 19 | token_amount 20 | .checked_mul(fee_numerator)? 21 | .checked_div(fee_denominator)?, 22 | ) 23 | } 24 | 25 | impl Fees { 26 | /// Calculate the trading fee in trading tokens 27 | pub fn trading_fee(amount: u128, trade_fee_rate: u64) -> Option { 28 | ceil_div( 29 | amount, 30 | u128::from(trade_fee_rate), 31 | u128::from(FEE_RATE_DENOMINATOR_VALUE), 32 | ) 33 | } 34 | 35 | /// Calculate the owner protocol fee in trading tokens 36 | pub fn protocol_fee(amount: u128, protocol_fee_rate: u64) -> Option { 37 | floor_div( 38 | amount, 39 | u128::from(protocol_fee_rate), 40 | u128::from(FEE_RATE_DENOMINATOR_VALUE), 41 | ) 42 | } 43 | 44 | /// Calculate the owner fund fee in trading tokens 45 | pub fn fund_fee(amount: u128, fund_fee_rate: u64) -> Option { 46 | floor_div( 47 | amount, 48 | u128::from(fund_fee_rate), 49 | u128::from(FEE_RATE_DENOMINATOR_VALUE), 50 | ) 51 | } 52 | 53 | pub fn calculate_pre_fee_amount(post_fee_amount: u128, trade_fee_rate: u64) -> Option { 54 | if trade_fee_rate == 0 { 55 | Some(post_fee_amount) 56 | } else { 57 | let numerator = post_fee_amount.checked_mul(u128::from(FEE_RATE_DENOMINATOR_VALUE))?; 58 | let denominator = 59 | u128::from(FEE_RATE_DENOMINATOR_VALUE).checked_sub(u128::from(trade_fee_rate))?; 60 | 61 | numerator 62 | .checked_add(denominator)? 63 | .checked_sub(1)? 64 | .checked_div(denominator) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /programs/cp-swap/src/curve/mod.rs: -------------------------------------------------------------------------------- 1 | //! Curve invariant implementations 2 | 3 | pub mod calculator; 4 | pub mod constant_product; 5 | pub mod fees; 6 | 7 | pub use calculator::*; 8 | pub use constant_product::*; 9 | pub use fees::*; 10 | -------------------------------------------------------------------------------- /programs/cp-swap/src/error.rs: -------------------------------------------------------------------------------- 1 | /// Errors that may be returned by the TokenSwap program. 2 | use anchor_lang::prelude::*; 3 | 4 | #[error_code] 5 | pub enum ErrorCode { 6 | #[msg("Not approved")] 7 | NotApproved, 8 | /// The owner of the input isn't set to the program address generated by the 9 | /// program. 10 | #[msg("Input account owner is not the program address")] 11 | InvalidOwner, 12 | /// The input token account is empty. 13 | #[msg("Input token account empty")] 14 | EmptySupply, 15 | /// The input token is invalid for swap. 16 | #[msg("InvalidInput")] 17 | InvalidInput, 18 | /// Address of the provided pool token mint is incorrect 19 | #[msg("Address of the provided lp token mint is incorrect")] 20 | IncorrectLpMint, 21 | /// Exceeds desired slippage limit 22 | #[msg("Exceeds desired slippage limit")] 23 | ExceededSlippage, 24 | /// Given pool token amount results in zero trading tokens 25 | #[msg("Given pool token amount results in zero trading tokens")] 26 | ZeroTradingTokens, 27 | #[msg("Not support token_2022 mint extension")] 28 | NotSupportMint, 29 | #[msg("invaild vault")] 30 | InvalidVault, 31 | #[msg("Init lp amount is too less(Because 100 amount lp will be locked)")] 32 | InitLpAmountTooLess, 33 | #[msg("TransferFee calculate not match")] 34 | TransferFeeCalculateNotMatch, 35 | } 36 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/admin/collect_fund_fee.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ErrorCode; 2 | use crate::states::*; 3 | use crate::utils::token::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::token::Token; 6 | use anchor_spl::token_interface::Mint; 7 | use anchor_spl::token_interface::Token2022; 8 | use anchor_spl::token_interface::TokenAccount; 9 | #[derive(Accounts)] 10 | pub struct CollectFundFee<'info> { 11 | /// Only admin or fund_owner can collect fee now 12 | #[account(constraint = (owner.key() == amm_config.fund_owner || owner.key() == crate::admin::ID) @ ErrorCode::InvalidOwner)] 13 | pub owner: Signer<'info>, 14 | 15 | /// CHECK: pool vault and lp mint authority 16 | #[account( 17 | seeds = [ 18 | crate::AUTH_SEED.as_bytes(), 19 | ], 20 | bump, 21 | )] 22 | pub authority: UncheckedAccount<'info>, 23 | 24 | /// Pool state stores accumulated protocol fee amount 25 | #[account(mut)] 26 | pub pool_state: AccountLoader<'info, PoolState>, 27 | 28 | /// Amm config account stores fund_owner 29 | #[account(address = pool_state.load()?.amm_config)] 30 | pub amm_config: Account<'info, AmmConfig>, 31 | 32 | /// The address that holds pool tokens for token_0 33 | #[account( 34 | mut, 35 | constraint = token_0_vault.key() == pool_state.load()?.token_0_vault 36 | )] 37 | pub token_0_vault: Box>, 38 | 39 | /// The address that holds pool tokens for token_1 40 | #[account( 41 | mut, 42 | constraint = token_1_vault.key() == pool_state.load()?.token_1_vault 43 | )] 44 | pub token_1_vault: Box>, 45 | 46 | /// The mint of token_0 vault 47 | #[account( 48 | address = token_0_vault.mint 49 | )] 50 | pub vault_0_mint: Box>, 51 | 52 | /// The mint of token_1 vault 53 | #[account( 54 | address = token_1_vault.mint 55 | )] 56 | pub vault_1_mint: Box>, 57 | 58 | /// The address that receives the collected token_0 fund fees 59 | #[account(mut)] 60 | pub recipient_token_0_account: Box>, 61 | 62 | /// The address that receives the collected token_1 fund fees 63 | #[account(mut)] 64 | pub recipient_token_1_account: Box>, 65 | 66 | /// The SPL program to perform token transfers 67 | pub token_program: Program<'info, Token>, 68 | 69 | /// The SPL program 2022 to perform token transfers 70 | pub token_program_2022: Program<'info, Token2022>, 71 | } 72 | 73 | pub fn collect_fund_fee( 74 | ctx: Context, 75 | amount_0_requested: u64, 76 | amount_1_requested: u64, 77 | ) -> Result<()> { 78 | let amount_0: u64; 79 | let amount_1: u64; 80 | let auth_bump: u8; 81 | { 82 | let mut pool_state = ctx.accounts.pool_state.load_mut()?; 83 | amount_0 = amount_0_requested.min(pool_state.fund_fees_token_0); 84 | amount_1 = amount_1_requested.min(pool_state.fund_fees_token_1); 85 | 86 | pool_state.fund_fees_token_0 = pool_state.fund_fees_token_0.checked_sub(amount_0).unwrap(); 87 | pool_state.fund_fees_token_1 = pool_state.fund_fees_token_1.checked_sub(amount_1).unwrap(); 88 | auth_bump = pool_state.auth_bump; 89 | pool_state.recent_epoch = Clock::get()?.epoch; 90 | } 91 | transfer_from_pool_vault_to_user( 92 | ctx.accounts.authority.to_account_info(), 93 | ctx.accounts.token_0_vault.to_account_info(), 94 | ctx.accounts.recipient_token_0_account.to_account_info(), 95 | ctx.accounts.vault_0_mint.to_account_info(), 96 | if ctx.accounts.vault_0_mint.to_account_info().owner == ctx.accounts.token_program.key { 97 | ctx.accounts.token_program.to_account_info() 98 | } else { 99 | ctx.accounts.token_program_2022.to_account_info() 100 | }, 101 | amount_0, 102 | ctx.accounts.vault_0_mint.decimals, 103 | &[&[crate::AUTH_SEED.as_bytes(), &[auth_bump]]], 104 | )?; 105 | 106 | transfer_from_pool_vault_to_user( 107 | ctx.accounts.authority.to_account_info(), 108 | ctx.accounts.token_1_vault.to_account_info(), 109 | ctx.accounts.recipient_token_1_account.to_account_info(), 110 | ctx.accounts.vault_1_mint.to_account_info(), 111 | if ctx.accounts.vault_1_mint.to_account_info().owner == ctx.accounts.token_program.key { 112 | ctx.accounts.token_program.to_account_info() 113 | } else { 114 | ctx.accounts.token_program_2022.to_account_info() 115 | }, 116 | amount_1, 117 | ctx.accounts.vault_1_mint.decimals, 118 | &[&[crate::AUTH_SEED.as_bytes(), &[auth_bump]]], 119 | )?; 120 | 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/admin/collect_protocol_fee.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ErrorCode; 2 | use crate::states::*; 3 | use crate::utils::*; 4 | use anchor_lang::prelude::*; 5 | use anchor_spl::token::Token; 6 | use anchor_spl::token_interface::Mint; 7 | use anchor_spl::token_interface::Token2022; 8 | use anchor_spl::token_interface::TokenAccount; 9 | 10 | #[derive(Accounts)] 11 | pub struct CollectProtocolFee<'info> { 12 | /// Only admin or owner can collect fee now 13 | #[account(constraint = (owner.key() == amm_config.protocol_owner || owner.key() == crate::admin::ID) @ ErrorCode::InvalidOwner)] 14 | pub owner: Signer<'info>, 15 | 16 | /// CHECK: pool vault and lp mint authority 17 | #[account( 18 | seeds = [ 19 | crate::AUTH_SEED.as_bytes(), 20 | ], 21 | bump, 22 | )] 23 | pub authority: UncheckedAccount<'info>, 24 | 25 | /// Pool state stores accumulated protocol fee amount 26 | #[account(mut)] 27 | pub pool_state: AccountLoader<'info, PoolState>, 28 | 29 | /// Amm config account stores owner 30 | #[account(address = pool_state.load()?.amm_config)] 31 | pub amm_config: Account<'info, AmmConfig>, 32 | 33 | /// The address that holds pool tokens for token_0 34 | #[account( 35 | mut, 36 | constraint = token_0_vault.key() == pool_state.load()?.token_0_vault 37 | )] 38 | pub token_0_vault: Box>, 39 | 40 | /// The address that holds pool tokens for token_1 41 | #[account( 42 | mut, 43 | constraint = token_1_vault.key() == pool_state.load()?.token_1_vault 44 | )] 45 | pub token_1_vault: Box>, 46 | 47 | /// The mint of token_0 vault 48 | #[account( 49 | address = token_0_vault.mint 50 | )] 51 | pub vault_0_mint: Box>, 52 | 53 | /// The mint of token_1 vault 54 | #[account( 55 | address = token_1_vault.mint 56 | )] 57 | pub vault_1_mint: Box>, 58 | 59 | /// The address that receives the collected token_0 protocol fees 60 | #[account(mut)] 61 | pub recipient_token_0_account: Box>, 62 | 63 | /// The address that receives the collected token_1 protocol fees 64 | #[account(mut)] 65 | pub recipient_token_1_account: Box>, 66 | 67 | /// The SPL program to perform token transfers 68 | pub token_program: Program<'info, Token>, 69 | 70 | /// The SPL program 2022 to perform token transfers 71 | pub token_program_2022: Program<'info, Token2022>, 72 | } 73 | 74 | pub fn collect_protocol_fee( 75 | ctx: Context, 76 | amount_0_requested: u64, 77 | amount_1_requested: u64, 78 | ) -> Result<()> { 79 | let amount_0: u64; 80 | let amount_1: u64; 81 | let auth_bump: u8; 82 | { 83 | let mut pool_state = ctx.accounts.pool_state.load_mut()?; 84 | 85 | amount_0 = amount_0_requested.min(pool_state.protocol_fees_token_0); 86 | amount_1 = amount_1_requested.min(pool_state.protocol_fees_token_1); 87 | 88 | pool_state.protocol_fees_token_0 = pool_state 89 | .protocol_fees_token_0 90 | .checked_sub(amount_0) 91 | .unwrap(); 92 | pool_state.protocol_fees_token_1 = pool_state 93 | .protocol_fees_token_1 94 | .checked_sub(amount_1) 95 | .unwrap(); 96 | 97 | auth_bump = pool_state.auth_bump; 98 | pool_state.recent_epoch = Clock::get()?.epoch; 99 | } 100 | transfer_from_pool_vault_to_user( 101 | ctx.accounts.authority.to_account_info(), 102 | ctx.accounts.token_0_vault.to_account_info(), 103 | ctx.accounts.recipient_token_0_account.to_account_info(), 104 | ctx.accounts.vault_0_mint.to_account_info(), 105 | if ctx.accounts.vault_0_mint.to_account_info().owner == ctx.accounts.token_program.key { 106 | ctx.accounts.token_program.to_account_info() 107 | } else { 108 | ctx.accounts.token_program_2022.to_account_info() 109 | }, 110 | amount_0, 111 | ctx.accounts.vault_0_mint.decimals, 112 | &[&[crate::AUTH_SEED.as_bytes(), &[auth_bump]]], 113 | )?; 114 | 115 | transfer_from_pool_vault_to_user( 116 | ctx.accounts.authority.to_account_info(), 117 | ctx.accounts.token_1_vault.to_account_info(), 118 | ctx.accounts.recipient_token_1_account.to_account_info(), 119 | ctx.accounts.vault_1_mint.to_account_info(), 120 | if ctx.accounts.vault_1_mint.to_account_info().owner == ctx.accounts.token_program.key { 121 | ctx.accounts.token_program.to_account_info() 122 | } else { 123 | ctx.accounts.token_program_2022.to_account_info() 124 | }, 125 | amount_1, 126 | ctx.accounts.vault_1_mint.decimals, 127 | &[&[crate::AUTH_SEED.as_bytes(), &[auth_bump]]], 128 | )?; 129 | 130 | Ok(()) 131 | } 132 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/admin/create_config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ErrorCode; 2 | use crate::states::*; 3 | use anchor_lang::prelude::*; 4 | use std::ops::DerefMut; 5 | 6 | #[derive(Accounts)] 7 | #[instruction(index: u16)] 8 | pub struct CreateAmmConfig<'info> { 9 | /// Address to be set as protocol owner. 10 | #[account( 11 | mut, 12 | address = crate::admin::ID @ ErrorCode::InvalidOwner 13 | )] 14 | pub owner: Signer<'info>, 15 | 16 | /// Initialize config state account to store protocol owner address and fee rates. 17 | #[account( 18 | init, 19 | seeds = [ 20 | AMM_CONFIG_SEED.as_bytes(), 21 | &index.to_be_bytes() 22 | ], 23 | bump, 24 | payer = owner, 25 | space = AmmConfig::LEN 26 | )] 27 | pub amm_config: Account<'info, AmmConfig>, 28 | 29 | pub system_program: Program<'info, System>, 30 | } 31 | 32 | pub fn create_amm_config( 33 | ctx: Context, 34 | index: u16, 35 | trade_fee_rate: u64, 36 | protocol_fee_rate: u64, 37 | fund_fee_rate: u64, 38 | create_pool_fee: u64, 39 | ) -> Result<()> { 40 | let amm_config = ctx.accounts.amm_config.deref_mut(); 41 | amm_config.protocol_owner = ctx.accounts.owner.key(); 42 | amm_config.bump = ctx.bumps.amm_config; 43 | amm_config.disable_create_pool = false; 44 | amm_config.index = index; 45 | amm_config.trade_fee_rate = trade_fee_rate; 46 | amm_config.protocol_fee_rate = protocol_fee_rate; 47 | amm_config.fund_fee_rate = fund_fee_rate; 48 | amm_config.create_pool_fee = create_pool_fee; 49 | amm_config.fund_owner = ctx.accounts.owner.key(); 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/admin/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod create_config; 2 | pub use create_config::*; 3 | 4 | pub mod update_config; 5 | pub use update_config::*; 6 | 7 | pub mod update_pool_status; 8 | pub use update_pool_status::*; 9 | 10 | pub mod collect_protocol_fee; 11 | pub use collect_protocol_fee::*; 12 | 13 | pub mod collect_fund_fee; 14 | pub use collect_fund_fee::*; 15 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/admin/update_config.rs: -------------------------------------------------------------------------------- 1 | use crate::curve::fees::FEE_RATE_DENOMINATOR_VALUE; 2 | use crate::error::ErrorCode; 3 | use crate::states::*; 4 | use anchor_lang::prelude::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct UpdateAmmConfig<'info> { 8 | /// The amm config owner or admin 9 | #[account(address = crate::admin::ID @ ErrorCode::InvalidOwner)] 10 | pub owner: Signer<'info>, 11 | 12 | /// Amm config account to be changed 13 | #[account(mut)] 14 | pub amm_config: Account<'info, AmmConfig>, 15 | } 16 | 17 | pub fn update_amm_config(ctx: Context, param: u8, value: u64) -> Result<()> { 18 | let amm_config = &mut ctx.accounts.amm_config; 19 | let match_param = Some(param); 20 | match match_param { 21 | Some(0) => update_trade_fee_rate(amm_config, value), 22 | Some(1) => update_protocol_fee_rate(amm_config, value), 23 | Some(2) => update_fund_fee_rate(amm_config, value), 24 | Some(3) => { 25 | let new_procotol_owner = *ctx.remaining_accounts.iter().next().unwrap().key; 26 | set_new_protocol_owner(amm_config, new_procotol_owner)?; 27 | } 28 | Some(4) => { 29 | let new_fund_owner = *ctx.remaining_accounts.iter().next().unwrap().key; 30 | set_new_fund_owner(amm_config, new_fund_owner)?; 31 | } 32 | Some(5) => amm_config.create_pool_fee = value, 33 | Some(6) => amm_config.disable_create_pool = if value == 0 { false } else { true }, 34 | _ => return err!(ErrorCode::InvalidInput), 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | fn update_protocol_fee_rate(amm_config: &mut Account, protocol_fee_rate: u64) { 41 | assert!(protocol_fee_rate <= FEE_RATE_DENOMINATOR_VALUE); 42 | assert!(protocol_fee_rate + amm_config.fund_fee_rate <= FEE_RATE_DENOMINATOR_VALUE); 43 | amm_config.protocol_fee_rate = protocol_fee_rate; 44 | } 45 | 46 | fn update_trade_fee_rate(amm_config: &mut Account, trade_fee_rate: u64) { 47 | assert!(trade_fee_rate < FEE_RATE_DENOMINATOR_VALUE); 48 | amm_config.trade_fee_rate = trade_fee_rate; 49 | } 50 | 51 | fn update_fund_fee_rate(amm_config: &mut Account, fund_fee_rate: u64) { 52 | assert!(fund_fee_rate <= FEE_RATE_DENOMINATOR_VALUE); 53 | assert!(fund_fee_rate + amm_config.protocol_fee_rate <= FEE_RATE_DENOMINATOR_VALUE); 54 | amm_config.fund_fee_rate = fund_fee_rate; 55 | } 56 | 57 | fn set_new_protocol_owner(amm_config: &mut Account, new_owner: Pubkey) -> Result<()> { 58 | require_keys_neq!(new_owner, Pubkey::default()); 59 | #[cfg(feature = "enable-log")] 60 | msg!( 61 | "amm_config, old_protocol_owner:{}, new_owner:{}", 62 | amm_config.protocol_owner.to_string(), 63 | new_owner.key().to_string() 64 | ); 65 | amm_config.protocol_owner = new_owner; 66 | Ok(()) 67 | } 68 | 69 | fn set_new_fund_owner(amm_config: &mut Account, new_fund_owner: Pubkey) -> Result<()> { 70 | require_keys_neq!(new_fund_owner, Pubkey::default()); 71 | #[cfg(feature = "enable-log")] 72 | msg!( 73 | "amm_config, old_fund_owner:{}, new_fund_owner:{}", 74 | amm_config.fund_owner.to_string(), 75 | new_fund_owner.key().to_string() 76 | ); 77 | amm_config.fund_owner = new_fund_owner; 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/admin/update_pool_status.rs: -------------------------------------------------------------------------------- 1 | use crate::states::*; 2 | use anchor_lang::prelude::*; 3 | 4 | #[derive(Accounts)] 5 | pub struct UpdatePoolStatus<'info> { 6 | #[account( 7 | address = crate::admin::ID 8 | )] 9 | pub authority: Signer<'info>, 10 | 11 | #[account(mut)] 12 | pub pool_state: AccountLoader<'info, PoolState>, 13 | } 14 | 15 | pub fn update_pool_status(ctx: Context, status: u8) -> Result<()> { 16 | require_gte!(255, status); 17 | let mut pool_state = ctx.accounts.pool_state.load_mut()?; 18 | pool_state.set_status(status); 19 | pool_state.recent_epoch = Clock::get()?.epoch; 20 | Ok(()) 21 | } 22 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/deposit.rs: -------------------------------------------------------------------------------- 1 | use crate::curve::CurveCalculator; 2 | use crate::curve::RoundDirection; 3 | use crate::error::ErrorCode; 4 | use crate::states::*; 5 | use crate::utils::token::*; 6 | use anchor_lang::prelude::*; 7 | use anchor_spl::token::Token; 8 | use anchor_spl::token_interface::{Mint, Token2022, TokenAccount}; 9 | 10 | #[derive(Accounts)] 11 | pub struct Deposit<'info> { 12 | /// Pays to mint the position 13 | pub owner: Signer<'info>, 14 | 15 | /// CHECK: pool vault and lp mint authority 16 | #[account( 17 | seeds = [ 18 | crate::AUTH_SEED.as_bytes(), 19 | ], 20 | bump, 21 | )] 22 | pub authority: UncheckedAccount<'info>, 23 | 24 | #[account(mut)] 25 | pub pool_state: AccountLoader<'info, PoolState>, 26 | 27 | /// Owner lp token account 28 | #[account(mut, token::authority = owner)] 29 | pub owner_lp_token: Box>, 30 | 31 | /// The payer's token account for token_0 32 | #[account( 33 | mut, 34 | token::mint = token_0_vault.mint, 35 | token::authority = owner 36 | )] 37 | pub token_0_account: Box>, 38 | 39 | /// The payer's token account for token_1 40 | #[account( 41 | mut, 42 | token::mint = token_1_vault.mint, 43 | token::authority = owner 44 | )] 45 | pub token_1_account: Box>, 46 | 47 | /// The address that holds pool tokens for token_0 48 | #[account( 49 | mut, 50 | constraint = token_0_vault.key() == pool_state.load()?.token_0_vault 51 | )] 52 | pub token_0_vault: Box>, 53 | 54 | /// The address that holds pool tokens for token_1 55 | #[account( 56 | mut, 57 | constraint = token_1_vault.key() == pool_state.load()?.token_1_vault 58 | )] 59 | pub token_1_vault: Box>, 60 | 61 | /// token Program 62 | pub token_program: Program<'info, Token>, 63 | 64 | /// Token program 2022 65 | pub token_program_2022: Program<'info, Token2022>, 66 | 67 | /// The mint of token_0 vault 68 | #[account( 69 | address = token_0_vault.mint 70 | )] 71 | pub vault_0_mint: Box>, 72 | 73 | /// The mint of token_1 vault 74 | #[account( 75 | address = token_1_vault.mint 76 | )] 77 | pub vault_1_mint: Box>, 78 | 79 | /// Lp token mint 80 | #[account( 81 | mut, 82 | address = pool_state.load()?.lp_mint @ ErrorCode::IncorrectLpMint) 83 | ] 84 | pub lp_mint: Box>, 85 | } 86 | 87 | pub fn deposit( 88 | ctx: Context, 89 | lp_token_amount: u64, 90 | maximum_token_0_amount: u64, 91 | maximum_token_1_amount: u64, 92 | ) -> Result<()> { 93 | require_gt!(lp_token_amount, 0); 94 | let pool_id = ctx.accounts.pool_state.key(); 95 | let pool_state = &mut ctx.accounts.pool_state.load_mut()?; 96 | if !pool_state.get_status_by_bit(PoolStatusBitIndex::Deposit) { 97 | return err!(ErrorCode::NotApproved); 98 | } 99 | let (total_token_0_amount, total_token_1_amount) = pool_state.vault_amount_without_fee( 100 | ctx.accounts.token_0_vault.amount, 101 | ctx.accounts.token_1_vault.amount, 102 | ); 103 | let results = CurveCalculator::lp_tokens_to_trading_tokens( 104 | u128::from(lp_token_amount), 105 | u128::from(pool_state.lp_supply), 106 | u128::from(total_token_0_amount), 107 | u128::from(total_token_1_amount), 108 | RoundDirection::Ceiling, 109 | ) 110 | .ok_or(ErrorCode::ZeroTradingTokens)?; 111 | if results.token_0_amount == 0 || results.token_1_amount == 0 { 112 | return err!(ErrorCode::ZeroTradingTokens); 113 | } 114 | let token_0_amount = u64::try_from(results.token_0_amount).unwrap(); 115 | let (transfer_token_0_amount, transfer_token_0_fee) = { 116 | let transfer_fee = 117 | get_transfer_inverse_fee(&ctx.accounts.vault_0_mint.to_account_info(), token_0_amount)?; 118 | ( 119 | token_0_amount.checked_add(transfer_fee).unwrap(), 120 | transfer_fee, 121 | ) 122 | }; 123 | 124 | let token_1_amount = u64::try_from(results.token_1_amount).unwrap(); 125 | let (transfer_token_1_amount, transfer_token_1_fee) = { 126 | let transfer_fee = 127 | get_transfer_inverse_fee(&ctx.accounts.vault_1_mint.to_account_info(), token_1_amount)?; 128 | ( 129 | token_1_amount.checked_add(transfer_fee).unwrap(), 130 | transfer_fee, 131 | ) 132 | }; 133 | 134 | #[cfg(feature = "enable-log")] 135 | msg!( 136 | "results.token_0_amount;{}, results.token_1_amount:{},transfer_token_0_amount:{},transfer_token_0_fee:{}, 137 | transfer_token_1_amount:{},transfer_token_1_fee:{}", 138 | results.token_0_amount, 139 | results.token_1_amount, 140 | transfer_token_0_amount, 141 | transfer_token_0_fee, 142 | transfer_token_1_amount, 143 | transfer_token_1_fee 144 | ); 145 | 146 | emit!(LpChangeEvent { 147 | pool_id, 148 | lp_amount_before: pool_state.lp_supply, 149 | token_0_vault_before: total_token_0_amount, 150 | token_1_vault_before: total_token_1_amount, 151 | token_0_amount, 152 | token_1_amount, 153 | token_0_transfer_fee: transfer_token_0_fee, 154 | token_1_transfer_fee: transfer_token_1_fee, 155 | change_type: 0 156 | }); 157 | 158 | if transfer_token_0_amount > maximum_token_0_amount 159 | || transfer_token_1_amount > maximum_token_1_amount 160 | { 161 | return Err(ErrorCode::ExceededSlippage.into()); 162 | } 163 | 164 | transfer_from_user_to_pool_vault( 165 | ctx.accounts.owner.to_account_info(), 166 | ctx.accounts.token_0_account.to_account_info(), 167 | ctx.accounts.token_0_vault.to_account_info(), 168 | ctx.accounts.vault_0_mint.to_account_info(), 169 | if ctx.accounts.vault_0_mint.to_account_info().owner == ctx.accounts.token_program.key { 170 | ctx.accounts.token_program.to_account_info() 171 | } else { 172 | ctx.accounts.token_program_2022.to_account_info() 173 | }, 174 | transfer_token_0_amount, 175 | ctx.accounts.vault_0_mint.decimals, 176 | )?; 177 | 178 | transfer_from_user_to_pool_vault( 179 | ctx.accounts.owner.to_account_info(), 180 | ctx.accounts.token_1_account.to_account_info(), 181 | ctx.accounts.token_1_vault.to_account_info(), 182 | ctx.accounts.vault_1_mint.to_account_info(), 183 | if ctx.accounts.vault_1_mint.to_account_info().owner == ctx.accounts.token_program.key { 184 | ctx.accounts.token_program.to_account_info() 185 | } else { 186 | ctx.accounts.token_program_2022.to_account_info() 187 | }, 188 | transfer_token_1_amount, 189 | ctx.accounts.vault_1_mint.decimals, 190 | )?; 191 | 192 | pool_state.lp_supply = pool_state.lp_supply.checked_add(lp_token_amount).unwrap(); 193 | 194 | token_mint_to( 195 | ctx.accounts.authority.to_account_info(), 196 | ctx.accounts.token_program.to_account_info(), 197 | ctx.accounts.lp_mint.to_account_info(), 198 | ctx.accounts.owner_lp_token.to_account_info(), 199 | lp_token_amount, 200 | &[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]], 201 | )?; 202 | pool_state.recent_epoch = Clock::get()?.epoch; 203 | 204 | Ok(()) 205 | } 206 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/initialize.rs: -------------------------------------------------------------------------------- 1 | use crate::curve::CurveCalculator; 2 | use crate::error::ErrorCode; 3 | use crate::states::*; 4 | use crate::utils::*; 5 | use anchor_lang::{ 6 | accounts::interface_account::InterfaceAccount, 7 | prelude::*, 8 | solana_program::{clock, program::invoke, system_instruction}, 9 | system_program, 10 | }; 11 | use anchor_spl::{ 12 | associated_token::AssociatedToken, 13 | token::spl_token, 14 | token::Token, 15 | token_2022::spl_token_2022, 16 | token_interface::{Mint, TokenAccount, TokenInterface}, 17 | }; 18 | use std::ops::Deref; 19 | 20 | #[derive(Accounts)] 21 | pub struct Initialize<'info> { 22 | /// Address paying to create the pool. Can be anyone 23 | #[account(mut)] 24 | pub creator: Signer<'info>, 25 | 26 | /// Which config the pool belongs to. 27 | pub amm_config: Box>, 28 | 29 | /// CHECK: 30 | /// pool vault and lp mint authority 31 | #[account( 32 | seeds = [ 33 | crate::AUTH_SEED.as_bytes(), 34 | ], 35 | bump, 36 | )] 37 | pub authority: UncheckedAccount<'info>, 38 | 39 | /// CHECK: Initialize an account to store the pool state 40 | /// PDA account: 41 | /// seeds = [ 42 | ///     POOL_SEED.as_bytes(), 43 | ///     amm_config.key().as_ref(), 44 | ///     token_0_mint.key().as_ref(), 45 | ///     token_1_mint.key().as_ref(), 46 | /// ], 47 | /// 48 | /// Or random account: must be signed by cli 49 | #[account(mut)] 50 | pub pool_state: UncheckedAccount<'info>, 51 | 52 | /// Token_0 mint, the key must smaller than token_1 mint. 53 | #[account( 54 | constraint = token_0_mint.key() < token_1_mint.key(), 55 | mint::token_program = token_0_program, 56 | )] 57 | pub token_0_mint: Box>, 58 | 59 | /// Token_1 mint, the key must grater then token_0 mint. 60 | #[account( 61 | mint::token_program = token_1_program, 62 | )] 63 | pub token_1_mint: Box>, 64 | 65 | /// pool lp mint 66 | #[account( 67 | init, 68 | seeds = [ 69 | POOL_LP_MINT_SEED.as_bytes(), 70 | pool_state.key().as_ref(), 71 | ], 72 | bump, 73 | mint::decimals = 9, 74 | mint::authority = authority, 75 | payer = creator, 76 | mint::token_program = token_program, 77 | )] 78 | pub lp_mint: Box>, 79 | 80 | /// payer token0 account 81 | #[account( 82 | mut, 83 | token::mint = token_0_mint, 84 | token::authority = creator, 85 | )] 86 | pub creator_token_0: Box>, 87 | 88 | /// creator token1 account 89 | #[account( 90 | mut, 91 | token::mint = token_1_mint, 92 | token::authority = creator, 93 | )] 94 | pub creator_token_1: Box>, 95 | 96 | /// creator lp token account 97 | #[account( 98 | init, 99 | associated_token::mint = lp_mint, 100 | associated_token::authority = creator, 101 | payer = creator, 102 | token::token_program = token_program, 103 | )] 104 | pub creator_lp_token: Box>, 105 | 106 | /// CHECK: Token_0 vault for the pool, created by contract 107 | #[account( 108 | mut, 109 | seeds = [ 110 | POOL_VAULT_SEED.as_bytes(), 111 | pool_state.key().as_ref(), 112 | token_0_mint.key().as_ref() 113 | ], 114 | bump, 115 | )] 116 | pub token_0_vault: UncheckedAccount<'info>, 117 | 118 | /// CHECK: Token_1 vault for the pool, created by contract 119 | #[account( 120 | mut, 121 | seeds = [ 122 | POOL_VAULT_SEED.as_bytes(), 123 | pool_state.key().as_ref(), 124 | token_1_mint.key().as_ref() 125 | ], 126 | bump, 127 | )] 128 | pub token_1_vault: UncheckedAccount<'info>, 129 | 130 | /// create pool fee account 131 | #[account( 132 | mut, 133 | address= crate::create_pool_fee_reveiver::ID, 134 | )] 135 | pub create_pool_fee: Box>, 136 | 137 | /// an account to store oracle observations 138 | #[account( 139 | init, 140 | seeds = [ 141 | OBSERVATION_SEED.as_bytes(), 142 | pool_state.key().as_ref(), 143 | ], 144 | bump, 145 | payer = creator, 146 | space = ObservationState::LEN 147 | )] 148 | pub observation_state: AccountLoader<'info, ObservationState>, 149 | 150 | /// Program to create mint account and mint tokens 151 | pub token_program: Program<'info, Token>, 152 | /// Spl token program or token program 2022 153 | pub token_0_program: Interface<'info, TokenInterface>, 154 | /// Spl token program or token program 2022 155 | pub token_1_program: Interface<'info, TokenInterface>, 156 | /// Program to create an ATA for receiving position NFT 157 | pub associated_token_program: Program<'info, AssociatedToken>, 158 | /// To create a new program account 159 | pub system_program: Program<'info, System>, 160 | /// Sysvar for program account 161 | pub rent: Sysvar<'info, Rent>, 162 | } 163 | 164 | pub fn initialize( 165 | ctx: Context, 166 | init_amount_0: u64, 167 | init_amount_1: u64, 168 | mut open_time: u64, 169 | ) -> Result<()> { 170 | if !(is_supported_mint(&ctx.accounts.token_0_mint).unwrap() 171 | && is_supported_mint(&ctx.accounts.token_1_mint).unwrap()) 172 | { 173 | return err!(ErrorCode::NotSupportMint); 174 | } 175 | 176 | if ctx.accounts.amm_config.disable_create_pool { 177 | return err!(ErrorCode::NotApproved); 178 | } 179 | let block_timestamp = clock::Clock::get()?.unix_timestamp as u64; 180 | if open_time <= block_timestamp { 181 | open_time = block_timestamp + 1; 182 | } 183 | // due to stack/heap limitations, we have to create redundant new accounts ourselves. 184 | create_token_account( 185 | &ctx.accounts.authority.to_account_info(), 186 | &ctx.accounts.creator.to_account_info(), 187 | &ctx.accounts.token_0_vault.to_account_info(), 188 | &ctx.accounts.token_0_mint.to_account_info(), 189 | &ctx.accounts.system_program.to_account_info(), 190 | &ctx.accounts.token_0_program.to_account_info(), 191 | &[ 192 | POOL_VAULT_SEED.as_bytes(), 193 | ctx.accounts.pool_state.key().as_ref(), 194 | ctx.accounts.token_0_mint.key().as_ref(), 195 | &[ctx.bumps.token_0_vault][..], 196 | ], 197 | )?; 198 | 199 | create_token_account( 200 | &ctx.accounts.authority.to_account_info(), 201 | &ctx.accounts.creator.to_account_info(), 202 | &ctx.accounts.token_1_vault.to_account_info(), 203 | &ctx.accounts.token_1_mint.to_account_info(), 204 | &ctx.accounts.system_program.to_account_info(), 205 | &ctx.accounts.token_1_program.to_account_info(), 206 | &[ 207 | POOL_VAULT_SEED.as_bytes(), 208 | ctx.accounts.pool_state.key().as_ref(), 209 | ctx.accounts.token_1_mint.key().as_ref(), 210 | &[ctx.bumps.token_1_vault][..], 211 | ], 212 | )?; 213 | 214 | let pool_state_loader = create_pool( 215 | &ctx.accounts.creator.to_account_info(), 216 | &ctx.accounts.pool_state.to_account_info(), 217 | &ctx.accounts.amm_config.to_account_info(), 218 | &ctx.accounts.token_0_mint.to_account_info(), 219 | &ctx.accounts.token_1_mint.to_account_info(), 220 | &ctx.accounts.system_program.to_account_info(), 221 | )?; 222 | let pool_state = &mut pool_state_loader.load_init()?; 223 | 224 | let mut observation_state = ctx.accounts.observation_state.load_init()?; 225 | observation_state.pool_id = ctx.accounts.pool_state.key(); 226 | 227 | transfer_from_user_to_pool_vault( 228 | ctx.accounts.creator.to_account_info(), 229 | ctx.accounts.creator_token_0.to_account_info(), 230 | ctx.accounts.token_0_vault.to_account_info(), 231 | ctx.accounts.token_0_mint.to_account_info(), 232 | ctx.accounts.token_0_program.to_account_info(), 233 | init_amount_0, 234 | ctx.accounts.token_0_mint.decimals, 235 | )?; 236 | 237 | transfer_from_user_to_pool_vault( 238 | ctx.accounts.creator.to_account_info(), 239 | ctx.accounts.creator_token_1.to_account_info(), 240 | ctx.accounts.token_1_vault.to_account_info(), 241 | ctx.accounts.token_1_mint.to_account_info(), 242 | ctx.accounts.token_1_program.to_account_info(), 243 | init_amount_1, 244 | ctx.accounts.token_1_mint.decimals, 245 | )?; 246 | 247 | let token_0_vault = 248 | spl_token_2022::extension::StateWithExtensions::::unpack( 249 | ctx.accounts 250 | .token_0_vault 251 | .to_account_info() 252 | .try_borrow_data()? 253 | .deref(), 254 | )? 255 | .base; 256 | let token_1_vault = 257 | spl_token_2022::extension::StateWithExtensions::::unpack( 258 | ctx.accounts 259 | .token_1_vault 260 | .to_account_info() 261 | .try_borrow_data()? 262 | .deref(), 263 | )? 264 | .base; 265 | 266 | CurveCalculator::validate_supply(token_0_vault.amount, token_1_vault.amount)?; 267 | 268 | let liquidity = U128::from(token_0_vault.amount) 269 | .checked_mul(token_1_vault.amount.into()) 270 | .unwrap() 271 | .integer_sqrt() 272 | .as_u64(); 273 | let lock_lp_amount = 100; 274 | msg!( 275 | "liquidity:{}, lock_lp_amount:{}, vault_0_amount:{},vault_1_amount:{}", 276 | liquidity, 277 | lock_lp_amount, 278 | token_0_vault.amount, 279 | token_1_vault.amount 280 | ); 281 | token::token_mint_to( 282 | ctx.accounts.authority.to_account_info(), 283 | ctx.accounts.token_program.to_account_info(), 284 | ctx.accounts.lp_mint.to_account_info(), 285 | ctx.accounts.creator_lp_token.to_account_info(), 286 | liquidity 287 | .checked_sub(lock_lp_amount) 288 | .ok_or(ErrorCode::InitLpAmountTooLess)?, 289 | &[&[crate::AUTH_SEED.as_bytes(), &[ctx.bumps.authority]]], 290 | )?; 291 | 292 | // Charge the fee to create a pool 293 | if ctx.accounts.amm_config.create_pool_fee != 0 { 294 | invoke( 295 | &system_instruction::transfer( 296 | ctx.accounts.creator.key, 297 | &ctx.accounts.create_pool_fee.key(), 298 | u64::from(ctx.accounts.amm_config.create_pool_fee), 299 | ), 300 | &[ 301 | ctx.accounts.creator.to_account_info(), 302 | ctx.accounts.create_pool_fee.to_account_info(), 303 | ctx.accounts.system_program.to_account_info(), 304 | ], 305 | )?; 306 | invoke( 307 | &spl_token::instruction::sync_native( 308 | ctx.accounts.token_program.key, 309 | &ctx.accounts.create_pool_fee.key(), 310 | )?, 311 | &[ 312 | ctx.accounts.token_program.to_account_info(), 313 | ctx.accounts.create_pool_fee.to_account_info(), 314 | ], 315 | )?; 316 | } 317 | 318 | pool_state.initialize( 319 | ctx.bumps.authority, 320 | liquidity, 321 | open_time, 322 | ctx.accounts.creator.key(), 323 | ctx.accounts.amm_config.key(), 324 | ctx.accounts.token_0_vault.key(), 325 | ctx.accounts.token_1_vault.key(), 326 | &ctx.accounts.token_0_mint, 327 | &ctx.accounts.token_1_mint, 328 | &ctx.accounts.lp_mint, 329 | ctx.accounts.observation_state.key(), 330 | ); 331 | 332 | Ok(()) 333 | } 334 | 335 | pub fn create_pool<'info>( 336 | payer: &AccountInfo<'info>, 337 | pool_account_info: &AccountInfo<'info>, 338 | amm_config: &AccountInfo<'info>, 339 | token_0_mint: &AccountInfo<'info>, 340 | token_1_mint: &AccountInfo<'info>, 341 | system_program: &AccountInfo<'info>, 342 | ) -> Result> { 343 | if pool_account_info.owner != &system_program::ID { 344 | return err!(ErrorCode::NotApproved); 345 | } 346 | 347 | let (expect_pda_address, bump) = Pubkey::find_program_address( 348 | &[ 349 | POOL_SEED.as_bytes(), 350 | amm_config.key().as_ref(), 351 | token_0_mint.key().as_ref(), 352 | token_1_mint.key().as_ref(), 353 | ], 354 | &crate::id(), 355 | ); 356 | 357 | if pool_account_info.key() != expect_pda_address { 358 | require_eq!(pool_account_info.is_signer, true); 359 | } 360 | 361 | token::create_or_allocate_account( 362 | &crate::id(), 363 | payer.to_account_info(), 364 | system_program.to_account_info(), 365 | pool_account_info.clone(), 366 | &[ 367 | POOL_SEED.as_bytes(), 368 | amm_config.key().as_ref(), 369 | token_0_mint.key().as_ref(), 370 | token_1_mint.key().as_ref(), 371 | &[bump], 372 | ], 373 | PoolState::LEN, 374 | )?; 375 | 376 | Ok(AccountLoad::::try_from_unchecked( 377 | &crate::id(), 378 | &pool_account_info, 379 | )?) 380 | } 381 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod deposit; 2 | pub mod initialize; 3 | pub mod swap_base_input; 4 | pub mod withdraw; 5 | 6 | pub use deposit::*; 7 | pub use initialize::*; 8 | pub use swap_base_input::*; 9 | pub use withdraw::*; 10 | 11 | pub mod admin; 12 | pub use admin::*; 13 | 14 | pub mod swap_base_output; 15 | pub use swap_base_output::*; 16 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/swap_base_input.rs: -------------------------------------------------------------------------------- 1 | use crate::curve::calculator::CurveCalculator; 2 | use crate::curve::TradeDirection; 3 | use crate::error::ErrorCode; 4 | use crate::states::*; 5 | use crate::utils::token::*; 6 | use anchor_lang::prelude::*; 7 | use anchor_lang::solana_program; 8 | use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; 9 | 10 | #[derive(Accounts)] 11 | pub struct Swap<'info> { 12 | /// The user performing the swap 13 | pub payer: Signer<'info>, 14 | 15 | /// CHECK: pool vault and lp mint authority 16 | #[account( 17 | seeds = [ 18 | crate::AUTH_SEED.as_bytes(), 19 | ], 20 | bump, 21 | )] 22 | pub authority: UncheckedAccount<'info>, 23 | 24 | /// The factory state to read protocol fees 25 | #[account(address = pool_state.load()?.amm_config)] 26 | pub amm_config: Box>, 27 | 28 | /// The program account of the pool in which the swap will be performed 29 | #[account(mut)] 30 | pub pool_state: AccountLoader<'info, PoolState>, 31 | 32 | /// The user token account for input token 33 | #[account(mut)] 34 | pub input_token_account: Box>, 35 | 36 | /// The user token account for output token 37 | #[account(mut)] 38 | pub output_token_account: Box>, 39 | 40 | /// The vault token account for input token 41 | #[account( 42 | mut, 43 | constraint = input_vault.key() == pool_state.load()?.token_0_vault || input_vault.key() == pool_state.load()?.token_1_vault 44 | )] 45 | pub input_vault: Box>, 46 | 47 | /// The vault token account for output token 48 | #[account( 49 | mut, 50 | constraint = output_vault.key() == pool_state.load()?.token_0_vault || output_vault.key() == pool_state.load()?.token_1_vault 51 | )] 52 | pub output_vault: Box>, 53 | 54 | /// SPL program for input token transfers 55 | pub input_token_program: Interface<'info, TokenInterface>, 56 | 57 | /// SPL program for output token transfers 58 | pub output_token_program: Interface<'info, TokenInterface>, 59 | 60 | /// The mint of input token 61 | #[account( 62 | address = input_vault.mint 63 | )] 64 | pub input_token_mint: Box>, 65 | 66 | /// The mint of output token 67 | #[account( 68 | address = output_vault.mint 69 | )] 70 | pub output_token_mint: Box>, 71 | /// The program account for the most recent oracle observation 72 | #[account(mut, address = pool_state.load()?.observation_key)] 73 | pub observation_state: AccountLoader<'info, ObservationState>, 74 | } 75 | 76 | pub fn swap_base_input(ctx: Context, amount_in: u64, minimum_amount_out: u64) -> Result<()> { 77 | let block_timestamp = solana_program::clock::Clock::get()?.unix_timestamp as u64; 78 | let pool_id = ctx.accounts.pool_state.key(); 79 | let pool_state = &mut ctx.accounts.pool_state.load_mut()?; 80 | if !pool_state.get_status_by_bit(PoolStatusBitIndex::Swap) 81 | || block_timestamp < pool_state.open_time 82 | { 83 | return err!(ErrorCode::NotApproved); 84 | } 85 | 86 | let transfer_fee = 87 | get_transfer_fee(&ctx.accounts.input_token_mint.to_account_info(), amount_in)?; 88 | // Take transfer fees into account for actual amount transferred in 89 | let actual_amount_in = amount_in.saturating_sub(transfer_fee); 90 | require_gt!(actual_amount_in, 0); 91 | 92 | // Calculate the trade amounts and the price before swap 93 | let ( 94 | trade_direction, 95 | total_input_token_amount, 96 | total_output_token_amount, 97 | token_0_price_x64, 98 | token_1_price_x64, 99 | ) = if ctx.accounts.input_vault.key() == pool_state.token_0_vault 100 | && ctx.accounts.output_vault.key() == pool_state.token_1_vault 101 | { 102 | let (total_input_token_amount, total_output_token_amount) = pool_state 103 | .vault_amount_without_fee( 104 | ctx.accounts.input_vault.amount, 105 | ctx.accounts.output_vault.amount, 106 | ); 107 | let (token_0_price_x64, token_1_price_x64) = pool_state.token_price_x32( 108 | ctx.accounts.input_vault.amount, 109 | ctx.accounts.output_vault.amount, 110 | ); 111 | 112 | ( 113 | TradeDirection::ZeroForOne, 114 | total_input_token_amount, 115 | total_output_token_amount, 116 | token_0_price_x64, 117 | token_1_price_x64, 118 | ) 119 | } else if ctx.accounts.input_vault.key() == pool_state.token_1_vault 120 | && ctx.accounts.output_vault.key() == pool_state.token_0_vault 121 | { 122 | let (total_output_token_amount, total_input_token_amount) = pool_state 123 | .vault_amount_without_fee( 124 | ctx.accounts.output_vault.amount, 125 | ctx.accounts.input_vault.amount, 126 | ); 127 | let (token_0_price_x64, token_1_price_x64) = pool_state.token_price_x32( 128 | ctx.accounts.output_vault.amount, 129 | ctx.accounts.input_vault.amount, 130 | ); 131 | 132 | ( 133 | TradeDirection::OneForZero, 134 | total_input_token_amount, 135 | total_output_token_amount, 136 | token_0_price_x64, 137 | token_1_price_x64, 138 | ) 139 | } else { 140 | return err!(ErrorCode::InvalidVault); 141 | }; 142 | let constant_before = u128::from(total_input_token_amount) 143 | .checked_mul(u128::from(total_output_token_amount)) 144 | .unwrap(); 145 | 146 | let result = CurveCalculator::swap_base_input( 147 | u128::from(actual_amount_in), 148 | u128::from(total_input_token_amount), 149 | u128::from(total_output_token_amount), 150 | ctx.accounts.amm_config.trade_fee_rate, 151 | ctx.accounts.amm_config.protocol_fee_rate, 152 | ctx.accounts.amm_config.fund_fee_rate, 153 | ) 154 | .ok_or(ErrorCode::ZeroTradingTokens)?; 155 | 156 | let constant_after = u128::from( 157 | result 158 | .new_swap_source_amount 159 | .checked_sub(result.trade_fee) 160 | .unwrap(), 161 | ) 162 | .checked_mul(u128::from(result.new_swap_destination_amount)) 163 | .unwrap(); 164 | #[cfg(feature = "enable-log")] 165 | msg!( 166 | "source_amount_swapped:{}, destination_amount_swapped:{}, trade_fee:{}, constant_before:{},constant_after:{}", 167 | result.source_amount_swapped, 168 | result.destination_amount_swapped, 169 | result.trade_fee, 170 | constant_before, 171 | constant_after 172 | ); 173 | require_eq!( 174 | u64::try_from(result.source_amount_swapped).unwrap(), 175 | actual_amount_in 176 | ); 177 | let (input_transfer_amount, input_transfer_fee) = (amount_in, transfer_fee); 178 | let (output_transfer_amount, output_transfer_fee) = { 179 | let amount_out = u64::try_from(result.destination_amount_swapped).unwrap(); 180 | let transfer_fee = get_transfer_fee( 181 | &ctx.accounts.output_token_mint.to_account_info(), 182 | amount_out, 183 | )?; 184 | let amount_received = amount_out.checked_sub(transfer_fee).unwrap(); 185 | require_gt!(amount_received, 0); 186 | require_gte!( 187 | amount_received, 188 | minimum_amount_out, 189 | ErrorCode::ExceededSlippage 190 | ); 191 | (amount_out, transfer_fee) 192 | }; 193 | 194 | let protocol_fee = u64::try_from(result.protocol_fee).unwrap(); 195 | let fund_fee = u64::try_from(result.fund_fee).unwrap(); 196 | 197 | match trade_direction { 198 | TradeDirection::ZeroForOne => { 199 | pool_state.protocol_fees_token_0 = pool_state 200 | .protocol_fees_token_0 201 | .checked_add(protocol_fee) 202 | .unwrap(); 203 | pool_state.fund_fees_token_0 = 204 | pool_state.fund_fees_token_0.checked_add(fund_fee).unwrap(); 205 | } 206 | TradeDirection::OneForZero => { 207 | pool_state.protocol_fees_token_1 = pool_state 208 | .protocol_fees_token_1 209 | .checked_add(protocol_fee) 210 | .unwrap(); 211 | pool_state.fund_fees_token_1 = 212 | pool_state.fund_fees_token_1.checked_add(fund_fee).unwrap(); 213 | } 214 | }; 215 | 216 | emit!(SwapEvent { 217 | pool_id, 218 | input_vault_before: total_input_token_amount, 219 | output_vault_before: total_output_token_amount, 220 | input_amount: u64::try_from(result.source_amount_swapped).unwrap(), 221 | output_amount: u64::try_from(result.destination_amount_swapped).unwrap(), 222 | input_transfer_fee, 223 | output_transfer_fee, 224 | base_input: true 225 | }); 226 | require_gte!(constant_after, constant_before); 227 | 228 | transfer_from_user_to_pool_vault( 229 | ctx.accounts.payer.to_account_info(), 230 | ctx.accounts.input_token_account.to_account_info(), 231 | ctx.accounts.input_vault.to_account_info(), 232 | ctx.accounts.input_token_mint.to_account_info(), 233 | ctx.accounts.input_token_program.to_account_info(), 234 | input_transfer_amount, 235 | ctx.accounts.input_token_mint.decimals, 236 | )?; 237 | 238 | transfer_from_pool_vault_to_user( 239 | ctx.accounts.authority.to_account_info(), 240 | ctx.accounts.output_vault.to_account_info(), 241 | ctx.accounts.output_token_account.to_account_info(), 242 | ctx.accounts.output_token_mint.to_account_info(), 243 | ctx.accounts.output_token_program.to_account_info(), 244 | output_transfer_amount, 245 | ctx.accounts.output_token_mint.decimals, 246 | &[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]], 247 | )?; 248 | 249 | // update the previous price to the observation 250 | ctx.accounts.observation_state.load_mut()?.update( 251 | oracle::block_timestamp(), 252 | token_0_price_x64, 253 | token_1_price_x64, 254 | ); 255 | pool_state.recent_epoch = Clock::get()?.epoch; 256 | 257 | Ok(()) 258 | } 259 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/swap_base_output.rs: -------------------------------------------------------------------------------- 1 | use super::swap_base_input::Swap; 2 | use crate::curve::{calculator::CurveCalculator, TradeDirection}; 3 | use crate::error::ErrorCode; 4 | use crate::states::*; 5 | use crate::utils::token::*; 6 | use anchor_lang::prelude::*; 7 | use anchor_lang::solana_program; 8 | 9 | pub fn swap_base_output( 10 | ctx: Context, 11 | max_amount_in: u64, 12 | amount_out_less_fee: u64, 13 | ) -> Result<()> { 14 | require_gt!(amount_out_less_fee, 0); 15 | let block_timestamp = solana_program::clock::Clock::get()?.unix_timestamp as u64; 16 | let pool_id = ctx.accounts.pool_state.key(); 17 | let pool_state = &mut ctx.accounts.pool_state.load_mut()?; 18 | if !pool_state.get_status_by_bit(PoolStatusBitIndex::Swap) 19 | || block_timestamp < pool_state.open_time 20 | { 21 | return err!(ErrorCode::NotApproved); 22 | } 23 | let out_transfer_fee = get_transfer_inverse_fee( 24 | &ctx.accounts.output_token_mint.to_account_info(), 25 | amount_out_less_fee, 26 | )?; 27 | let actual_amount_out = amount_out_less_fee.checked_add(out_transfer_fee).unwrap(); 28 | 29 | // Calculate the trade amounts and the price before swap 30 | let ( 31 | trade_direction, 32 | total_input_token_amount, 33 | total_output_token_amount, 34 | token_0_price_x64, 35 | token_1_price_x64, 36 | ) = if ctx.accounts.input_vault.key() == pool_state.token_0_vault 37 | && ctx.accounts.output_vault.key() == pool_state.token_1_vault 38 | { 39 | let (total_input_token_amount, total_output_token_amount) = pool_state 40 | .vault_amount_without_fee( 41 | ctx.accounts.input_vault.amount, 42 | ctx.accounts.output_vault.amount, 43 | ); 44 | let (token_0_price_x64, token_1_price_x64) = pool_state.token_price_x32( 45 | ctx.accounts.input_vault.amount, 46 | ctx.accounts.output_vault.amount, 47 | ); 48 | 49 | ( 50 | TradeDirection::ZeroForOne, 51 | total_input_token_amount, 52 | total_output_token_amount, 53 | token_0_price_x64, 54 | token_1_price_x64, 55 | ) 56 | } else if ctx.accounts.input_vault.key() == pool_state.token_1_vault 57 | && ctx.accounts.output_vault.key() == pool_state.token_0_vault 58 | { 59 | let (total_output_token_amount, total_input_token_amount) = pool_state 60 | .vault_amount_without_fee( 61 | ctx.accounts.output_vault.amount, 62 | ctx.accounts.input_vault.amount, 63 | ); 64 | let (token_0_price_x64, token_1_price_x64) = pool_state.token_price_x32( 65 | ctx.accounts.output_vault.amount, 66 | ctx.accounts.input_vault.amount, 67 | ); 68 | 69 | ( 70 | TradeDirection::OneForZero, 71 | total_input_token_amount, 72 | total_output_token_amount, 73 | token_0_price_x64, 74 | token_1_price_x64, 75 | ) 76 | } else { 77 | return err!(ErrorCode::InvalidVault); 78 | }; 79 | let constant_before = u128::from(total_input_token_amount) 80 | .checked_mul(u128::from(total_output_token_amount)) 81 | .unwrap(); 82 | 83 | let result = CurveCalculator::swap_base_output( 84 | u128::from(actual_amount_out), 85 | u128::from(total_input_token_amount), 86 | u128::from(total_output_token_amount), 87 | ctx.accounts.amm_config.trade_fee_rate, 88 | ctx.accounts.amm_config.protocol_fee_rate, 89 | ctx.accounts.amm_config.fund_fee_rate, 90 | ) 91 | .ok_or(ErrorCode::ZeroTradingTokens)?; 92 | 93 | let constant_after = u128::from( 94 | result 95 | .new_swap_source_amount 96 | .checked_sub(result.trade_fee) 97 | .unwrap(), 98 | ) 99 | .checked_mul(u128::from(result.new_swap_destination_amount)) 100 | .unwrap(); 101 | 102 | #[cfg(feature = "enable-log")] 103 | msg!( 104 | "source_amount_swapped:{}, destination_amount_swapped:{}, trade_fee:{}, constant_before:{},constant_after:{}", 105 | result.source_amount_swapped, 106 | result.destination_amount_swapped, 107 | result.trade_fee, 108 | constant_before, 109 | constant_after 110 | ); 111 | 112 | // Re-calculate the source amount swapped based on what the curve says 113 | let (input_transfer_amount, input_transfer_fee) = { 114 | let source_amount_swapped = u64::try_from(result.source_amount_swapped).unwrap(); 115 | require_gt!(source_amount_swapped, 0); 116 | let transfer_fee = get_transfer_inverse_fee( 117 | &ctx.accounts.input_token_mint.to_account_info(), 118 | source_amount_swapped, 119 | )?; 120 | let input_transfer_amount = source_amount_swapped.checked_add(transfer_fee).unwrap(); 121 | require_gte!( 122 | max_amount_in, 123 | input_transfer_amount, 124 | ErrorCode::ExceededSlippage 125 | ); 126 | (input_transfer_amount, transfer_fee) 127 | }; 128 | require_eq!( 129 | u64::try_from(result.destination_amount_swapped).unwrap(), 130 | actual_amount_out 131 | ); 132 | let (output_transfer_amount, output_transfer_fee) = (actual_amount_out, out_transfer_fee); 133 | 134 | let protocol_fee = u64::try_from(result.protocol_fee).unwrap(); 135 | let fund_fee = u64::try_from(result.fund_fee).unwrap(); 136 | 137 | match trade_direction { 138 | TradeDirection::ZeroForOne => { 139 | pool_state.protocol_fees_token_0 = pool_state 140 | .protocol_fees_token_0 141 | .checked_add(protocol_fee) 142 | .unwrap(); 143 | pool_state.fund_fees_token_0 = 144 | pool_state.fund_fees_token_0.checked_add(fund_fee).unwrap(); 145 | } 146 | TradeDirection::OneForZero => { 147 | pool_state.protocol_fees_token_1 = pool_state 148 | .protocol_fees_token_1 149 | .checked_add(protocol_fee) 150 | .unwrap(); 151 | pool_state.fund_fees_token_1 = 152 | pool_state.fund_fees_token_1.checked_add(fund_fee).unwrap(); 153 | } 154 | }; 155 | 156 | emit!(SwapEvent { 157 | pool_id, 158 | input_vault_before: total_input_token_amount, 159 | output_vault_before: total_output_token_amount, 160 | input_amount: u64::try_from(result.source_amount_swapped).unwrap(), 161 | output_amount: u64::try_from(result.destination_amount_swapped).unwrap(), 162 | input_transfer_fee, 163 | output_transfer_fee, 164 | base_input: false 165 | }); 166 | require_gte!(constant_after, constant_before); 167 | 168 | transfer_from_user_to_pool_vault( 169 | ctx.accounts.payer.to_account_info(), 170 | ctx.accounts.input_token_account.to_account_info(), 171 | ctx.accounts.input_vault.to_account_info(), 172 | ctx.accounts.input_token_mint.to_account_info(), 173 | ctx.accounts.input_token_program.to_account_info(), 174 | input_transfer_amount, 175 | ctx.accounts.input_token_mint.decimals, 176 | )?; 177 | 178 | transfer_from_pool_vault_to_user( 179 | ctx.accounts.authority.to_account_info(), 180 | ctx.accounts.output_vault.to_account_info(), 181 | ctx.accounts.output_token_account.to_account_info(), 182 | ctx.accounts.output_token_mint.to_account_info(), 183 | ctx.accounts.output_token_program.to_account_info(), 184 | output_transfer_amount, 185 | ctx.accounts.output_token_mint.decimals, 186 | &[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]], 187 | )?; 188 | 189 | // update the previous price to the observation 190 | ctx.accounts.observation_state.load_mut()?.update( 191 | oracle::block_timestamp(), 192 | token_0_price_x64, 193 | token_1_price_x64, 194 | ); 195 | pool_state.recent_epoch = Clock::get()?.epoch; 196 | 197 | Ok(()) 198 | } 199 | -------------------------------------------------------------------------------- /programs/cp-swap/src/instructions/withdraw.rs: -------------------------------------------------------------------------------- 1 | use crate::curve::CurveCalculator; 2 | use crate::curve::RoundDirection; 3 | use crate::error::ErrorCode; 4 | use crate::states::*; 5 | use crate::utils::token::*; 6 | use anchor_lang::prelude::*; 7 | use anchor_spl::{ 8 | memo::spl_memo, 9 | token::Token, 10 | token_interface::{Mint, Token2022, TokenAccount}, 11 | }; 12 | 13 | #[derive(Accounts)] 14 | pub struct Withdraw<'info> { 15 | /// Pays to mint the position 16 | pub owner: Signer<'info>, 17 | 18 | /// CHECK: pool vault and lp mint authority 19 | #[account( 20 | seeds = [ 21 | crate::AUTH_SEED.as_bytes(), 22 | ], 23 | bump, 24 | )] 25 | pub authority: UncheckedAccount<'info>, 26 | 27 | /// Pool state account 28 | #[account(mut)] 29 | pub pool_state: AccountLoader<'info, PoolState>, 30 | 31 | /// Owner lp token account 32 | #[account( 33 | mut, 34 | token::authority = owner 35 | )] 36 | pub owner_lp_token: Box>, 37 | 38 | /// The token account for receive token_0, 39 | #[account( 40 | mut, 41 | token::mint = token_0_vault.mint, 42 | )] 43 | pub token_0_account: Box>, 44 | 45 | /// The token account for receive token_1 46 | #[account( 47 | mut, 48 | token::mint = token_1_vault.mint, 49 | )] 50 | pub token_1_account: Box>, 51 | 52 | /// The address that holds pool tokens for token_0 53 | #[account( 54 | mut, 55 | constraint = token_0_vault.key() == pool_state.load()?.token_0_vault 56 | )] 57 | pub token_0_vault: Box>, 58 | 59 | /// The address that holds pool tokens for token_1 60 | #[account( 61 | mut, 62 | constraint = token_1_vault.key() == pool_state.load()?.token_1_vault 63 | )] 64 | pub token_1_vault: Box>, 65 | 66 | /// token Program 67 | pub token_program: Program<'info, Token>, 68 | 69 | /// Token program 2022 70 | pub token_program_2022: Program<'info, Token2022>, 71 | 72 | /// The mint of token_0 vault 73 | #[account( 74 | address = token_0_vault.mint 75 | )] 76 | pub vault_0_mint: Box>, 77 | 78 | /// The mint of token_1 vault 79 | #[account( 80 | address = token_1_vault.mint 81 | )] 82 | pub vault_1_mint: Box>, 83 | 84 | /// Pool lp token mint 85 | #[account( 86 | mut, 87 | address = pool_state.load()?.lp_mint @ ErrorCode::IncorrectLpMint) 88 | ] 89 | pub lp_mint: Box>, 90 | 91 | /// memo program 92 | /// CHECK: 93 | #[account( 94 | address = spl_memo::id() 95 | )] 96 | pub memo_program: UncheckedAccount<'info>, 97 | } 98 | 99 | pub fn withdraw( 100 | ctx: Context, 101 | lp_token_amount: u64, 102 | minimum_token_0_amount: u64, 103 | minimum_token_1_amount: u64, 104 | ) -> Result<()> { 105 | require_gt!(lp_token_amount, 0); 106 | require_gt!(ctx.accounts.lp_mint.supply, 0); 107 | let pool_id = ctx.accounts.pool_state.key(); 108 | let pool_state = &mut ctx.accounts.pool_state.load_mut()?; 109 | if !pool_state.get_status_by_bit(PoolStatusBitIndex::Withdraw) { 110 | return err!(ErrorCode::NotApproved); 111 | } 112 | let (total_token_0_amount, total_token_1_amount) = pool_state.vault_amount_without_fee( 113 | ctx.accounts.token_0_vault.amount, 114 | ctx.accounts.token_1_vault.amount, 115 | ); 116 | let results = CurveCalculator::lp_tokens_to_trading_tokens( 117 | u128::from(lp_token_amount), 118 | u128::from(pool_state.lp_supply), 119 | u128::from(total_token_0_amount), 120 | u128::from(total_token_1_amount), 121 | RoundDirection::Floor, 122 | ) 123 | .ok_or(ErrorCode::ZeroTradingTokens)?; 124 | if results.token_0_amount == 0 || results.token_1_amount == 0 { 125 | return err!(ErrorCode::ZeroTradingTokens); 126 | } 127 | let token_0_amount = u64::try_from(results.token_0_amount).unwrap(); 128 | let token_0_amount = std::cmp::min(total_token_0_amount, token_0_amount); 129 | let (receive_token_0_amount, token_0_transfer_fee) = { 130 | let transfer_fee = 131 | get_transfer_fee(&ctx.accounts.vault_0_mint.to_account_info(), token_0_amount)?; 132 | ( 133 | token_0_amount.checked_sub(transfer_fee).unwrap(), 134 | transfer_fee, 135 | ) 136 | }; 137 | 138 | let token_1_amount = u64::try_from(results.token_1_amount).unwrap(); 139 | let token_1_amount = std::cmp::min(total_token_1_amount, token_1_amount); 140 | let (receive_token_1_amount, token_1_transfer_fee) = { 141 | let transfer_fee = 142 | get_transfer_fee(&ctx.accounts.vault_1_mint.to_account_info(), token_1_amount)?; 143 | ( 144 | token_1_amount.checked_sub(transfer_fee).unwrap(), 145 | transfer_fee, 146 | ) 147 | }; 148 | 149 | #[cfg(feature = "enable-log")] 150 | msg!( 151 | "results.token_0_amount;{}, results.token_1_amount:{},receive_token_0_amount:{},token_0_transfer_fee:{}, 152 | receive_token_1_amount:{},token_1_transfer_fee:{}", 153 | results.token_0_amount, 154 | results.token_1_amount, 155 | receive_token_0_amount, 156 | token_0_transfer_fee, 157 | receive_token_1_amount, 158 | token_1_transfer_fee 159 | ); 160 | emit!(LpChangeEvent { 161 | pool_id, 162 | lp_amount_before: pool_state.lp_supply, 163 | token_0_vault_before: total_token_0_amount, 164 | token_1_vault_before: total_token_1_amount, 165 | token_0_amount: receive_token_0_amount, 166 | token_1_amount: receive_token_1_amount, 167 | token_0_transfer_fee, 168 | token_1_transfer_fee, 169 | change_type: 1 170 | }); 171 | 172 | if receive_token_0_amount < minimum_token_0_amount 173 | || receive_token_1_amount < minimum_token_1_amount 174 | { 175 | return Err(ErrorCode::ExceededSlippage.into()); 176 | } 177 | 178 | pool_state.lp_supply = pool_state.lp_supply.checked_sub(lp_token_amount).unwrap(); 179 | token_burn( 180 | ctx.accounts.owner.to_account_info(), 181 | ctx.accounts.token_program.to_account_info(), 182 | ctx.accounts.lp_mint.to_account_info(), 183 | ctx.accounts.owner_lp_token.to_account_info(), 184 | lp_token_amount, 185 | &[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]], 186 | )?; 187 | 188 | transfer_from_pool_vault_to_user( 189 | ctx.accounts.authority.to_account_info(), 190 | ctx.accounts.token_0_vault.to_account_info(), 191 | ctx.accounts.token_0_account.to_account_info(), 192 | ctx.accounts.vault_0_mint.to_account_info(), 193 | if ctx.accounts.vault_0_mint.to_account_info().owner == ctx.accounts.token_program.key { 194 | ctx.accounts.token_program.to_account_info() 195 | } else { 196 | ctx.accounts.token_program_2022.to_account_info() 197 | }, 198 | token_0_amount, 199 | ctx.accounts.vault_0_mint.decimals, 200 | &[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]], 201 | )?; 202 | 203 | transfer_from_pool_vault_to_user( 204 | ctx.accounts.authority.to_account_info(), 205 | ctx.accounts.token_1_vault.to_account_info(), 206 | ctx.accounts.token_1_account.to_account_info(), 207 | ctx.accounts.vault_1_mint.to_account_info(), 208 | if ctx.accounts.vault_1_mint.to_account_info().owner == ctx.accounts.token_program.key { 209 | ctx.accounts.token_program.to_account_info() 210 | } else { 211 | ctx.accounts.token_program_2022.to_account_info() 212 | }, 213 | token_1_amount, 214 | ctx.accounts.vault_1_mint.decimals, 215 | &[&[crate::AUTH_SEED.as_bytes(), &[pool_state.auth_bump]]], 216 | )?; 217 | pool_state.recent_epoch = Clock::get()?.epoch; 218 | 219 | Ok(()) 220 | } 221 | -------------------------------------------------------------------------------- /programs/cp-swap/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod curve; 2 | pub mod error; 3 | pub mod instructions; 4 | pub mod states; 5 | pub mod utils; 6 | 7 | use crate::curve::fees::FEE_RATE_DENOMINATOR_VALUE; 8 | use anchor_lang::prelude::*; 9 | use instructions::*; 10 | 11 | #[cfg(not(feature = "no-entrypoint"))] 12 | solana_security_txt::security_txt! { 13 | name: "raydium-cp-swap", 14 | project_url: "https://raydium.io", 15 | contacts: "link:https://immunefi.com/bounty/raydium", 16 | policy: "https://immunefi.com/bounty/raydium", 17 | source_code: "https://github.com/raydium-io/raydium-cp-swap", 18 | preferred_languages: "en", 19 | auditors: "https://github.com/raydium-io/raydium-docs/blob/master/audit/MadShield%20Q1%202024/raydium-cp-swap-v-1.0.0.pdf" 20 | } 21 | 22 | #[cfg(feature = "devnet")] 23 | declare_id!("CPMDWBwJDtYax9qW7AyRuVC19Cc4L4Vcy4n2BHAbHkCW"); 24 | #[cfg(not(feature = "devnet"))] 25 | declare_id!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"); 26 | 27 | pub mod admin { 28 | use super::{pubkey, Pubkey}; 29 | #[cfg(feature = "devnet")] 30 | pub const ID: Pubkey = pubkey!("adMCyoCgfkg7bQiJ9aBJ59H3BXLY3r5LNLfPpQfMzBe"); 31 | #[cfg(not(feature = "devnet"))] 32 | pub const ID: Pubkey = pubkey!("GThUX1Atko4tqhN2NaiTazWSeFWMuiUvfFnyJyUghFMJ"); 33 | } 34 | 35 | pub mod create_pool_fee_reveiver { 36 | use super::{pubkey, Pubkey}; 37 | #[cfg(feature = "devnet")] 38 | pub const ID: Pubkey = pubkey!("G11FKBRaAkHAKuLCgLM6K6NUc9rTjPAznRCjZifrTQe2"); 39 | #[cfg(not(feature = "devnet"))] 40 | pub const ID: Pubkey = pubkey!("DNXgeM9EiiaAbaWvwjHj9fQQLAX5ZsfHyvmYUNRAdNC8"); 41 | } 42 | 43 | pub const AUTH_SEED: &str = "vault_and_lp_mint_auth_seed"; 44 | 45 | #[program] 46 | pub mod raydium_cp_swap { 47 | use super::*; 48 | 49 | // The configuration of AMM protocol, include trade fee and protocol fee 50 | /// # Arguments 51 | /// 52 | /// * `ctx`- The accounts needed by instruction. 53 | /// * `index` - The index of amm config, there may be multiple config. 54 | /// * `trade_fee_rate` - Trade fee rate, can be changed. 55 | /// * `protocol_fee_rate` - The rate of protocol fee within trade fee. 56 | /// * `fund_fee_rate` - The rate of fund fee within trade fee. 57 | /// 58 | pub fn create_amm_config( 59 | ctx: Context, 60 | index: u16, 61 | trade_fee_rate: u64, 62 | protocol_fee_rate: u64, 63 | fund_fee_rate: u64, 64 | create_pool_fee: u64, 65 | ) -> Result<()> { 66 | assert!(trade_fee_rate < FEE_RATE_DENOMINATOR_VALUE); 67 | assert!(protocol_fee_rate <= FEE_RATE_DENOMINATOR_VALUE); 68 | assert!(fund_fee_rate <= FEE_RATE_DENOMINATOR_VALUE); 69 | assert!(fund_fee_rate + protocol_fee_rate <= FEE_RATE_DENOMINATOR_VALUE); 70 | instructions::create_amm_config( 71 | ctx, 72 | index, 73 | trade_fee_rate, 74 | protocol_fee_rate, 75 | fund_fee_rate, 76 | create_pool_fee, 77 | ) 78 | } 79 | 80 | /// Updates the owner of the amm config 81 | /// Must be called by the current owner or admin 82 | /// 83 | /// # Arguments 84 | /// 85 | /// * `ctx`- The context of accounts 86 | /// * `trade_fee_rate`- The new trade fee rate of amm config, be set when `param` is 0 87 | /// * `protocol_fee_rate`- The new protocol fee rate of amm config, be set when `param` is 1 88 | /// * `fund_fee_rate`- The new fund fee rate of amm config, be set when `param` is 2 89 | /// * `new_owner`- The config's new owner, be set when `param` is 3 90 | /// * `new_fund_owner`- The config's new fund owner, be set when `param` is 4 91 | /// * `param`- The value can be 0 | 1 | 2 | 3 | 4, otherwise will report a error 92 | /// 93 | pub fn update_amm_config(ctx: Context, param: u8, value: u64) -> Result<()> { 94 | instructions::update_amm_config(ctx, param, value) 95 | } 96 | 97 | /// Update pool status for given value 98 | /// 99 | /// # Arguments 100 | /// 101 | /// * `ctx`- The context of accounts 102 | /// * `status` - The value of status 103 | /// 104 | pub fn update_pool_status(ctx: Context, status: u8) -> Result<()> { 105 | instructions::update_pool_status(ctx, status) 106 | } 107 | 108 | /// Collect the protocol fee accrued to the pool 109 | /// 110 | /// # Arguments 111 | /// 112 | /// * `ctx` - The context of accounts 113 | /// * `amount_0_requested` - The maximum amount of token_0 to send, can be 0 to collect fees in only token_1 114 | /// * `amount_1_requested` - The maximum amount of token_1 to send, can be 0 to collect fees in only token_0 115 | /// 116 | pub fn collect_protocol_fee( 117 | ctx: Context, 118 | amount_0_requested: u64, 119 | amount_1_requested: u64, 120 | ) -> Result<()> { 121 | instructions::collect_protocol_fee(ctx, amount_0_requested, amount_1_requested) 122 | } 123 | 124 | /// Collect the fund fee accrued to the pool 125 | /// 126 | /// # Arguments 127 | /// 128 | /// * `ctx` - The context of accounts 129 | /// * `amount_0_requested` - The maximum amount of token_0 to send, can be 0 to collect fees in only token_1 130 | /// * `amount_1_requested` - The maximum amount of token_1 to send, can be 0 to collect fees in only token_0 131 | /// 132 | pub fn collect_fund_fee( 133 | ctx: Context, 134 | amount_0_requested: u64, 135 | amount_1_requested: u64, 136 | ) -> Result<()> { 137 | instructions::collect_fund_fee(ctx, amount_0_requested, amount_1_requested) 138 | } 139 | 140 | /// Creates a pool for the given token pair and the initial price 141 | /// 142 | /// # Arguments 143 | /// 144 | /// * `ctx`- The context of accounts 145 | /// * `init_amount_0` - the initial amount_0 to deposit 146 | /// * `init_amount_1` - the initial amount_1 to deposit 147 | /// * `open_time` - the timestamp allowed for swap 148 | /// 149 | pub fn initialize( 150 | ctx: Context, 151 | init_amount_0: u64, 152 | init_amount_1: u64, 153 | open_time: u64, 154 | ) -> Result<()> { 155 | instructions::initialize(ctx, init_amount_0, init_amount_1, open_time) 156 | } 157 | 158 | /// Deposit lp token to the pool 159 | /// 160 | /// # Arguments 161 | /// 162 | /// * `ctx`- The context of accounts 163 | /// * `lp_token_amount` - Pool token amount to transfer. token_a and token_b amount are set by the current exchange rate and size of the pool 164 | /// * `maximum_token_0_amount` - Maximum token 0 amount to deposit, prevents excessive slippage 165 | /// * `maximum_token_1_amount` - Maximum token 1 amount to deposit, prevents excessive slippage 166 | /// 167 | pub fn deposit( 168 | ctx: Context, 169 | lp_token_amount: u64, 170 | maximum_token_0_amount: u64, 171 | maximum_token_1_amount: u64, 172 | ) -> Result<()> { 173 | instructions::deposit( 174 | ctx, 175 | lp_token_amount, 176 | maximum_token_0_amount, 177 | maximum_token_1_amount, 178 | ) 179 | } 180 | 181 | /// Withdraw lp for token0 and token1 182 | /// 183 | /// # Arguments 184 | /// 185 | /// * `ctx`- The context of accounts 186 | /// * `lp_token_amount` - Amount of pool tokens to burn. User receives an output of token a and b based on the percentage of the pool tokens that are returned. 187 | /// * `minimum_token_0_amount` - Minimum amount of token 0 to receive, prevents excessive slippage 188 | /// * `minimum_token_1_amount` - Minimum amount of token 1 to receive, prevents excessive slippage 189 | /// 190 | pub fn withdraw( 191 | ctx: Context, 192 | lp_token_amount: u64, 193 | minimum_token_0_amount: u64, 194 | minimum_token_1_amount: u64, 195 | ) -> Result<()> { 196 | instructions::withdraw( 197 | ctx, 198 | lp_token_amount, 199 | minimum_token_0_amount, 200 | minimum_token_1_amount, 201 | ) 202 | } 203 | 204 | /// Swap the tokens in the pool base input amount 205 | /// 206 | /// # Arguments 207 | /// 208 | /// * `ctx`- The context of accounts 209 | /// * `amount_in` - input amount to transfer, output to DESTINATION is based on the exchange rate 210 | /// * `minimum_amount_out` - Minimum amount of output token, prevents excessive slippage 211 | /// 212 | pub fn swap_base_input( 213 | ctx: Context, 214 | amount_in: u64, 215 | minimum_amount_out: u64, 216 | ) -> Result<()> { 217 | instructions::swap_base_input(ctx, amount_in, minimum_amount_out) 218 | } 219 | 220 | /// Swap the tokens in the pool base output amount 221 | /// 222 | /// # Arguments 223 | /// 224 | /// * `ctx`- The context of accounts 225 | /// * `max_amount_in` - input amount prevents excessive slippage 226 | /// * `amount_out` - amount of output token 227 | /// 228 | pub fn swap_base_output(ctx: Context, max_amount_in: u64, amount_out: u64) -> Result<()> { 229 | instructions::swap_base_output(ctx, max_amount_in, amount_out) 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /programs/cp-swap/src/states/config.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub const AMM_CONFIG_SEED: &str = "amm_config"; 4 | 5 | /// Holds the current owner of the factory 6 | #[account] 7 | #[derive(Default, Debug)] 8 | pub struct AmmConfig { 9 | /// Bump to identify PDA 10 | pub bump: u8, 11 | /// Status to control if new pool can be create 12 | pub disable_create_pool: bool, 13 | /// Config index 14 | pub index: u16, 15 | /// The trade fee, denominated in hundredths of a bip (10^-6) 16 | pub trade_fee_rate: u64, 17 | /// The protocol fee 18 | pub protocol_fee_rate: u64, 19 | /// The fund fee, denominated in hundredths of a bip (10^-6) 20 | pub fund_fee_rate: u64, 21 | /// Fee for create a new pool 22 | pub create_pool_fee: u64, 23 | /// Address of the protocol fee owner 24 | pub protocol_owner: Pubkey, 25 | /// Address of the fund fee owner 26 | pub fund_owner: Pubkey, 27 | /// padding 28 | pub padding: [u64; 16], 29 | } 30 | 31 | impl AmmConfig { 32 | pub const LEN: usize = 8 + 1 + 1 + 2 + 4 * 8 + 32 * 2 + 8 * 16; 33 | } 34 | -------------------------------------------------------------------------------- /programs/cp-swap/src/states/events.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// Emitted when deposit and withdraw 4 | #[event] 5 | #[cfg_attr(feature = "client", derive(Debug))] 6 | pub struct LpChangeEvent { 7 | pub pool_id: Pubkey, 8 | pub lp_amount_before: u64, 9 | /// pool vault sub trade fees 10 | pub token_0_vault_before: u64, 11 | /// pool vault sub trade fees 12 | pub token_1_vault_before: u64, 13 | /// calculate result without transfer fee 14 | pub token_0_amount: u64, 15 | /// calculate result without transfer fee 16 | pub token_1_amount: u64, 17 | pub token_0_transfer_fee: u64, 18 | pub token_1_transfer_fee: u64, 19 | // 0: deposit, 1: withdraw 20 | pub change_type: u8, 21 | } 22 | 23 | /// Emitted when swap 24 | #[event] 25 | #[cfg_attr(feature = "client", derive(Debug))] 26 | pub struct SwapEvent { 27 | pub pool_id: Pubkey, 28 | /// pool vault sub trade fees 29 | pub input_vault_before: u64, 30 | /// pool vault sub trade fees 31 | pub output_vault_before: u64, 32 | /// calculate result without transfer fee 33 | pub input_amount: u64, 34 | /// calculate result without transfer fee 35 | pub output_amount: u64, 36 | pub input_transfer_fee: u64, 37 | pub output_transfer_fee: u64, 38 | pub base_input: bool, 39 | } 40 | -------------------------------------------------------------------------------- /programs/cp-swap/src/states/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub use config::*; 3 | 4 | pub mod pool; 5 | pub use pool::*; 6 | 7 | pub mod events; 8 | pub use events::*; 9 | 10 | pub mod oracle; 11 | pub use oracle::*; 12 | -------------------------------------------------------------------------------- /programs/cp-swap/src/states/oracle.rs: -------------------------------------------------------------------------------- 1 | /// Oracle provides price data useful for a wide variety of system designs 2 | /// 3 | use anchor_lang::prelude::*; 4 | #[cfg(test)] 5 | use std::time::{SystemTime, UNIX_EPOCH}; 6 | /// Seed to derive account address and signature 7 | pub const OBSERVATION_SEED: &str = "observation"; 8 | // Number of ObservationState element 9 | pub const OBSERVATION_NUM: usize = 100; 10 | pub const OBSERVATION_UPDATE_DURATION_DEFAULT: u64 = 15; 11 | 12 | /// The element of observations in ObservationState 13 | #[zero_copy(unsafe)] 14 | #[repr(C, packed)] 15 | #[derive(Default, Debug)] 16 | pub struct Observation { 17 | /// The block timestamp of the observation 18 | pub block_timestamp: u64, 19 | /// the cumulative of token0 price during the duration time, Q32.32, the remaining 64 bit for overflow 20 | pub cumulative_token_0_price_x32: u128, 21 | /// the cumulative of token1 price during the duration time, Q32.32, the remaining 64 bit for overflow 22 | pub cumulative_token_1_price_x32: u128, 23 | } 24 | impl Observation { 25 | pub const LEN: usize = 8 + 16 + 16; 26 | } 27 | 28 | #[account(zero_copy(unsafe))] 29 | #[repr(C, packed)] 30 | #[cfg_attr(feature = "client", derive(Debug))] 31 | pub struct ObservationState { 32 | /// Whether the ObservationState is initialized 33 | pub initialized: bool, 34 | /// the most-recently updated index of the observations array 35 | pub observation_index: u16, 36 | pub pool_id: Pubkey, 37 | /// observation array 38 | pub observations: [Observation; OBSERVATION_NUM], 39 | /// padding for feature update 40 | pub padding: [u64; 4], 41 | } 42 | 43 | impl Default for ObservationState { 44 | #[inline] 45 | fn default() -> ObservationState { 46 | ObservationState { 47 | initialized: false, 48 | observation_index: 0, 49 | pool_id: Pubkey::default(), 50 | observations: [Observation::default(); OBSERVATION_NUM], 51 | padding: [0u64; 4], 52 | } 53 | } 54 | } 55 | 56 | impl ObservationState { 57 | pub const LEN: usize = 8 + 1 + 2 + 32 + (Observation::LEN * OBSERVATION_NUM) + 8 * 4; 58 | 59 | // Writes an oracle observation to the account, returning the next observation_index. 60 | /// Writable at most once per second. Index represents the most recently written element. 61 | /// If the index is at the end of the allowable array length (100 - 1), the next index will turn to 0. 62 | /// 63 | /// # Arguments 64 | /// 65 | /// * `self` - The ObservationState account to write in 66 | /// * `block_timestamp` - The current timestamp of to update 67 | /// * `token_0_price_x32` - The token_0_price_x32 at the time of the new observation 68 | /// * `token_1_price_x32` - The token_1_price_x32 at the time of the new observation 69 | /// * `observation_index` - The last update index of element in the oracle array 70 | /// 71 | /// # Return 72 | /// * `next_observation_index` - The new index of element to update in the oracle array 73 | /// 74 | pub fn update( 75 | &mut self, 76 | block_timestamp: u64, 77 | token_0_price_x32: u128, 78 | token_1_price_x32: u128, 79 | ) { 80 | let observation_index = self.observation_index; 81 | if !self.initialized { 82 | // skip the pool init price 83 | self.initialized = true; 84 | self.observations[observation_index as usize].block_timestamp = block_timestamp; 85 | self.observations[observation_index as usize].cumulative_token_0_price_x32 = 0; 86 | self.observations[observation_index as usize].cumulative_token_1_price_x32 = 0; 87 | } else { 88 | let last_observation = self.observations[observation_index as usize]; 89 | let delta_time = block_timestamp.saturating_sub(last_observation.block_timestamp); 90 | if delta_time < OBSERVATION_UPDATE_DURATION_DEFAULT { 91 | return; 92 | } 93 | let delta_token_0_price_x32 = token_0_price_x32.checked_mul(delta_time.into()).unwrap(); 94 | let delta_token_1_price_x32 = token_1_price_x32.checked_mul(delta_time.into()).unwrap(); 95 | let next_observation_index = if observation_index as usize == OBSERVATION_NUM - 1 { 96 | 0 97 | } else { 98 | observation_index + 1 99 | }; 100 | self.observations[next_observation_index as usize].block_timestamp = block_timestamp; 101 | // cumulative_token_price_x32 only occupies the first 64 bits, and the remaining 64 bits are used to store overflow data 102 | self.observations[next_observation_index as usize].cumulative_token_0_price_x32 = 103 | last_observation 104 | .cumulative_token_0_price_x32 105 | .wrapping_add(delta_token_0_price_x32); 106 | self.observations[next_observation_index as usize].cumulative_token_1_price_x32 = 107 | last_observation 108 | .cumulative_token_1_price_x32 109 | .wrapping_add(delta_token_1_price_x32); 110 | self.observation_index = next_observation_index; 111 | } 112 | } 113 | } 114 | 115 | /// Returns the block timestamp truncated to 32 bits, i.e. mod 2**32 116 | /// 117 | pub fn block_timestamp() -> u64 { 118 | Clock::get().unwrap().unix_timestamp as u64 // truncation is desired 119 | } 120 | 121 | #[cfg(test)] 122 | pub fn block_timestamp_mock() -> u64 { 123 | SystemTime::now() 124 | .duration_since(UNIX_EPOCH) 125 | .unwrap() 126 | .as_secs() 127 | } 128 | 129 | #[cfg(test)] 130 | pub mod observation_test { 131 | use super::*; 132 | 133 | #[test] 134 | fn observation_state_size_test() { 135 | assert_eq!( 136 | std::mem::size_of::(), 137 | ObservationState::LEN - 8 138 | ) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /programs/cp-swap/src/states/pool.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_spl::token_interface::Mint; 3 | use std::ops::{BitAnd, BitOr, BitXor}; 4 | /// Seed to derive account address and signature 5 | pub const POOL_SEED: &str = "pool"; 6 | pub const POOL_LP_MINT_SEED: &str = "pool_lp_mint"; 7 | pub const POOL_VAULT_SEED: &str = "pool_vault"; 8 | 9 | pub const Q32: u128 = (u32::MAX as u128) + 1; // 2^32 10 | 11 | pub enum PoolStatusBitIndex { 12 | Deposit, 13 | Withdraw, 14 | Swap, 15 | } 16 | 17 | #[derive(PartialEq, Eq)] 18 | pub enum PoolStatusBitFlag { 19 | Enable, 20 | Disable, 21 | } 22 | 23 | #[account(zero_copy(unsafe))] 24 | #[repr(C, packed)] 25 | #[derive(Default, Debug)] 26 | pub struct PoolState { 27 | /// Which config the pool belongs 28 | pub amm_config: Pubkey, 29 | /// pool creator 30 | pub pool_creator: Pubkey, 31 | /// Token A 32 | pub token_0_vault: Pubkey, 33 | /// Token B 34 | pub token_1_vault: Pubkey, 35 | 36 | /// Pool tokens are issued when A or B tokens are deposited. 37 | /// Pool tokens can be withdrawn back to the original A or B token. 38 | pub lp_mint: Pubkey, 39 | /// Mint information for token A 40 | pub token_0_mint: Pubkey, 41 | /// Mint information for token B 42 | pub token_1_mint: Pubkey, 43 | 44 | /// token_0 program 45 | pub token_0_program: Pubkey, 46 | /// token_1 program 47 | pub token_1_program: Pubkey, 48 | 49 | /// observation account to store oracle data 50 | pub observation_key: Pubkey, 51 | 52 | pub auth_bump: u8, 53 | /// Bitwise representation of the state of the pool 54 | /// bit0, 1: disable deposit(value is 1), 0: normal 55 | /// bit1, 1: disable withdraw(value is 2), 0: normal 56 | /// bit2, 1: disable swap(value is 4), 0: normal 57 | pub status: u8, 58 | 59 | pub lp_mint_decimals: u8, 60 | /// mint0 and mint1 decimals 61 | pub mint_0_decimals: u8, 62 | pub mint_1_decimals: u8, 63 | 64 | /// True circulating supply without burns and lock ups 65 | pub lp_supply: u64, 66 | /// The amounts of token_0 and token_1 that are owed to the liquidity provider. 67 | pub protocol_fees_token_0: u64, 68 | pub protocol_fees_token_1: u64, 69 | 70 | pub fund_fees_token_0: u64, 71 | pub fund_fees_token_1: u64, 72 | 73 | /// The timestamp allowed for swap in the pool. 74 | pub open_time: u64, 75 | /// recent epoch 76 | pub recent_epoch: u64, 77 | /// padding for future updates 78 | pub padding: [u64; 31], 79 | } 80 | 81 | impl PoolState { 82 | pub const LEN: usize = 8 + 10 * 32 + 1 * 5 + 8 * 7 + 8 * 31; 83 | 84 | pub fn initialize( 85 | &mut self, 86 | auth_bump: u8, 87 | lp_supply: u64, 88 | open_time: u64, 89 | pool_creator: Pubkey, 90 | amm_config: Pubkey, 91 | token_0_vault: Pubkey, 92 | token_1_vault: Pubkey, 93 | token_0_mint: &InterfaceAccount, 94 | token_1_mint: &InterfaceAccount, 95 | lp_mint: &InterfaceAccount, 96 | observation_key: Pubkey, 97 | ) { 98 | self.amm_config = amm_config.key(); 99 | self.pool_creator = pool_creator.key(); 100 | self.token_0_vault = token_0_vault; 101 | self.token_1_vault = token_1_vault; 102 | self.lp_mint = lp_mint.key(); 103 | self.token_0_mint = token_0_mint.key(); 104 | self.token_1_mint = token_1_mint.key(); 105 | self.token_0_program = *token_0_mint.to_account_info().owner; 106 | self.token_1_program = *token_1_mint.to_account_info().owner; 107 | self.observation_key = observation_key; 108 | self.auth_bump = auth_bump; 109 | self.lp_mint_decimals = lp_mint.decimals; 110 | self.mint_0_decimals = token_0_mint.decimals; 111 | self.mint_1_decimals = token_1_mint.decimals; 112 | self.lp_supply = lp_supply; 113 | self.protocol_fees_token_0 = 0; 114 | self.protocol_fees_token_1 = 0; 115 | self.fund_fees_token_0 = 0; 116 | self.fund_fees_token_1 = 0; 117 | self.open_time = open_time; 118 | self.recent_epoch = Clock::get().unwrap().epoch; 119 | self.padding = [0u64; 31]; 120 | } 121 | 122 | pub fn set_status(&mut self, status: u8) { 123 | self.status = status 124 | } 125 | 126 | pub fn set_status_by_bit(&mut self, bit: PoolStatusBitIndex, flag: PoolStatusBitFlag) { 127 | let s = u8::from(1) << (bit as u8); 128 | if flag == PoolStatusBitFlag::Disable { 129 | self.status = self.status.bitor(s); 130 | } else { 131 | let m = u8::from(255).bitxor(s); 132 | self.status = self.status.bitand(m); 133 | } 134 | } 135 | 136 | /// Get status by bit, if it is `noraml` status, return true 137 | pub fn get_status_by_bit(&self, bit: PoolStatusBitIndex) -> bool { 138 | let status = u8::from(1) << (bit as u8); 139 | self.status.bitand(status) == 0 140 | } 141 | 142 | pub fn vault_amount_without_fee(&self, vault_0: u64, vault_1: u64) -> (u64, u64) { 143 | ( 144 | vault_0 145 | .checked_sub(self.protocol_fees_token_0 + self.fund_fees_token_0) 146 | .unwrap(), 147 | vault_1 148 | .checked_sub(self.protocol_fees_token_1 + self.fund_fees_token_1) 149 | .unwrap(), 150 | ) 151 | } 152 | 153 | pub fn token_price_x32(&self, vault_0: u64, vault_1: u64) -> (u128, u128) { 154 | let (token_0_amount, token_1_amount) = self.vault_amount_without_fee(vault_0, vault_1); 155 | ( 156 | token_1_amount as u128 * Q32 as u128 / token_0_amount as u128, 157 | token_0_amount as u128 * Q32 as u128 / token_1_amount as u128, 158 | ) 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | pub mod pool_test { 164 | use super::*; 165 | 166 | #[test] 167 | fn pool_state_size_test() { 168 | assert_eq!(std::mem::size_of::(), PoolState::LEN - 8) 169 | } 170 | 171 | mod pool_status_test { 172 | use super::*; 173 | 174 | #[test] 175 | fn get_set_status_by_bit() { 176 | let mut pool_state = PoolState::default(); 177 | pool_state.set_status(4); // 0000100 178 | assert_eq!( 179 | pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), 180 | false 181 | ); 182 | assert_eq!( 183 | pool_state.get_status_by_bit(PoolStatusBitIndex::Deposit), 184 | true 185 | ); 186 | assert_eq!( 187 | pool_state.get_status_by_bit(PoolStatusBitIndex::Withdraw), 188 | true 189 | ); 190 | 191 | // disable -> disable, nothing to change 192 | pool_state.set_status_by_bit(PoolStatusBitIndex::Swap, PoolStatusBitFlag::Disable); 193 | assert_eq!( 194 | pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), 195 | false 196 | ); 197 | 198 | // disable -> enable 199 | pool_state.set_status_by_bit(PoolStatusBitIndex::Swap, PoolStatusBitFlag::Enable); 200 | assert_eq!(pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), true); 201 | 202 | // enable -> enable, nothing to change 203 | pool_state.set_status_by_bit(PoolStatusBitIndex::Swap, PoolStatusBitFlag::Enable); 204 | assert_eq!(pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), true); 205 | // enable -> disable 206 | pool_state.set_status_by_bit(PoolStatusBitIndex::Swap, PoolStatusBitFlag::Disable); 207 | assert_eq!( 208 | pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), 209 | false 210 | ); 211 | 212 | pool_state.set_status(5); // 0000101 213 | assert_eq!( 214 | pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), 215 | false 216 | ); 217 | assert_eq!( 218 | pool_state.get_status_by_bit(PoolStatusBitIndex::Deposit), 219 | false 220 | ); 221 | assert_eq!( 222 | pool_state.get_status_by_bit(PoolStatusBitIndex::Withdraw), 223 | true 224 | ); 225 | 226 | pool_state.set_status(7); // 0000111 227 | assert_eq!( 228 | pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), 229 | false 230 | ); 231 | assert_eq!( 232 | pool_state.get_status_by_bit(PoolStatusBitIndex::Deposit), 233 | false 234 | ); 235 | assert_eq!( 236 | pool_state.get_status_by_bit(PoolStatusBitIndex::Withdraw), 237 | false 238 | ); 239 | 240 | pool_state.set_status(3); // 0000011 241 | assert_eq!(pool_state.get_status_by_bit(PoolStatusBitIndex::Swap), true); 242 | assert_eq!( 243 | pool_state.get_status_by_bit(PoolStatusBitIndex::Deposit), 244 | false 245 | ); 246 | assert_eq!( 247 | pool_state.get_status_by_bit(PoolStatusBitIndex::Withdraw), 248 | false 249 | ); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /programs/cp-swap/src/utils/account_load.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ 2 | error::{Error, ErrorCode}, 3 | solana_program::{account_info::AccountInfo, pubkey::Pubkey}, 4 | Key, Owner, Result, ToAccountInfos, ZeroCopy, 5 | }; 6 | use arrayref::array_ref; 7 | use std::cell::{Ref, RefMut}; 8 | use std::marker::PhantomData; 9 | use std::mem; 10 | use std::ops::DerefMut; 11 | 12 | #[derive(Clone)] 13 | pub struct AccountLoad<'info, T: ZeroCopy + Owner> { 14 | acc_info: AccountInfo<'info>, 15 | phantom: PhantomData<&'info T>, 16 | } 17 | 18 | impl<'info, T: ZeroCopy + Owner> AccountLoad<'info, T> { 19 | fn new(acc_info: AccountInfo<'info>) -> AccountLoad<'info, T> { 20 | Self { 21 | acc_info, 22 | phantom: PhantomData, 23 | } 24 | } 25 | 26 | /// Constructs a new `Loader` from a previously initialized account. 27 | #[inline(never)] 28 | pub fn try_from(acc_info: &AccountInfo<'info>) -> Result> { 29 | if acc_info.owner != &T::owner() { 30 | return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) 31 | .with_pubkeys((*acc_info.owner, T::owner()))); 32 | } 33 | let data: &[u8] = &acc_info.try_borrow_data()?; 34 | if data.len() < T::DISCRIMINATOR.len() { 35 | return Err(ErrorCode::AccountDiscriminatorNotFound.into()); 36 | } 37 | // Discriminator must match. 38 | let disc_bytes = array_ref![data, 0, 8]; 39 | if disc_bytes != &T::DISCRIMINATOR { 40 | return Err(ErrorCode::AccountDiscriminatorMismatch.into()); 41 | } 42 | 43 | Ok(AccountLoad::new(acc_info.clone())) 44 | } 45 | 46 | /// Constructs a new `Loader` from an uninitialized account. 47 | #[inline(never)] 48 | pub fn try_from_unchecked( 49 | _program_id: &Pubkey, 50 | acc_info: &AccountInfo<'info>, 51 | ) -> Result> { 52 | if acc_info.owner != &T::owner() { 53 | return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) 54 | .with_pubkeys((*acc_info.owner, T::owner()))); 55 | } 56 | Ok(AccountLoad::new(acc_info.clone())) 57 | } 58 | 59 | /// Returns a `RefMut` to the account data structure for reading or writing. 60 | /// Should only be called once, when the account is being initialized. 61 | pub fn load_init(&self) -> Result> { 62 | // AccountInfo api allows you to borrow mut even if the account isn't 63 | // writable, so add this check for a better dev experience. 64 | if !self.acc_info.is_writable { 65 | return Err(ErrorCode::AccountNotMutable.into()); 66 | } 67 | 68 | let mut data = self.acc_info.try_borrow_mut_data()?; 69 | 70 | // The discriminator should be zero, since we're initializing. 71 | let mut disc_bytes = [0u8; 8]; 72 | disc_bytes.copy_from_slice(&data[..8]); 73 | let discriminator = u64::from_le_bytes(disc_bytes); 74 | if discriminator != 0 { 75 | return Err(ErrorCode::AccountDiscriminatorAlreadySet.into()); 76 | } 77 | 78 | // write discriminator 79 | data[..8].copy_from_slice(&T::DISCRIMINATOR); 80 | 81 | Ok(RefMut::map(data, |data| { 82 | bytemuck::from_bytes_mut(&mut data.deref_mut()[8..mem::size_of::() + 8]) 83 | })) 84 | } 85 | 86 | /// Returns a `RefMut` to the account data structure for reading or writing directly. 87 | /// There is no need to convert AccountInfo to AccountLoad. 88 | /// So it is necessary to check the owner 89 | pub fn load_data_mut<'a>(acc_info: &'a AccountInfo) -> Result> { 90 | if acc_info.owner != &T::owner() { 91 | return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram) 92 | .with_pubkeys((*acc_info.owner, T::owner()))); 93 | } 94 | if !acc_info.is_writable { 95 | return Err(ErrorCode::AccountNotMutable.into()); 96 | } 97 | 98 | let data = acc_info.try_borrow_mut_data()?; 99 | if data.len() < T::DISCRIMINATOR.len() { 100 | return Err(ErrorCode::AccountDiscriminatorNotFound.into()); 101 | } 102 | 103 | let disc_bytes = array_ref![data, 0, 8]; 104 | if disc_bytes != &T::DISCRIMINATOR { 105 | return Err(ErrorCode::AccountDiscriminatorMismatch.into()); 106 | } 107 | 108 | Ok(RefMut::map(data, |data| { 109 | bytemuck::from_bytes_mut(&mut data.deref_mut()[8..mem::size_of::() + 8]) 110 | })) 111 | } 112 | 113 | /// Returns a Ref to the account data structure for reading. 114 | pub fn load(&self) -> Result> { 115 | let data = self.acc_info.try_borrow_data()?; 116 | if data.len() < T::DISCRIMINATOR.len() { 117 | return Err(ErrorCode::AccountDiscriminatorNotFound.into()); 118 | } 119 | 120 | let disc_bytes = array_ref![data, 0, 8]; 121 | if disc_bytes != &T::DISCRIMINATOR { 122 | return Err(ErrorCode::AccountDiscriminatorMismatch.into()); 123 | } 124 | 125 | Ok(Ref::map(data, |data| { 126 | bytemuck::from_bytes(&data[8..mem::size_of::() + 8]) 127 | })) 128 | } 129 | 130 | /// Returns a `RefMut` to the account data structure for reading or writing. 131 | pub fn load_mut(&self) -> Result> { 132 | // AccountInfo api allows you to borrow mut even if the account isn't 133 | // writable, so add this check for a better dev experience. 134 | if !self.acc_info.is_writable { 135 | return Err(ErrorCode::AccountNotMutable.into()); 136 | } 137 | 138 | let data = self.acc_info.try_borrow_mut_data()?; 139 | if data.len() < T::DISCRIMINATOR.len() { 140 | return Err(ErrorCode::AccountDiscriminatorNotFound.into()); 141 | } 142 | 143 | let disc_bytes = array_ref![data, 0, 8]; 144 | if disc_bytes != &T::DISCRIMINATOR { 145 | return Err(ErrorCode::AccountDiscriminatorMismatch.into()); 146 | } 147 | 148 | Ok(RefMut::map(data, |data| { 149 | bytemuck::from_bytes_mut(&mut data.deref_mut()[8..mem::size_of::() + 8]) 150 | })) 151 | } 152 | } 153 | 154 | impl<'info, T: ZeroCopy + Owner> AsRef> for AccountLoad<'info, T> { 155 | fn as_ref(&self) -> &AccountInfo<'info> { 156 | &self.acc_info 157 | } 158 | } 159 | impl<'info, T: ZeroCopy + Owner> ToAccountInfos<'info> for AccountLoad<'info, T> { 160 | fn to_account_infos(&self) -> Vec> { 161 | vec![self.acc_info.clone()] 162 | } 163 | } 164 | 165 | impl<'info, T: ZeroCopy + Owner> Key for AccountLoad<'info, T> { 166 | fn key(&self) -> Pubkey { 167 | *self.acc_info.key 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /programs/cp-swap/src/utils/math.rs: -------------------------------------------------------------------------------- 1 | ///! 128 and 256 bit numbers 2 | ///! U128 is more efficient that u128 3 | ///! https://github.com/solana-labs/solana/issues/19549 4 | use uint::construct_uint; 5 | construct_uint! { 6 | pub struct U128(2); 7 | } 8 | 9 | construct_uint! { 10 | pub struct U256(4); 11 | } 12 | 13 | pub trait CheckedCeilDiv: Sized { 14 | /// Perform ceiling division 15 | fn checked_ceil_div(&self, rhs: Self) -> Option<(Self, Self)>; 16 | } 17 | 18 | impl CheckedCeilDiv for u128 { 19 | fn checked_ceil_div(&self, mut rhs: Self) -> Option<(Self, Self)> { 20 | let mut quotient = self.checked_div(rhs)?; 21 | // Avoid dividing a small number by a big one and returning 1, and instead 22 | // fail. 23 | if quotient == 0 { 24 | // return None; 25 | if self.checked_mul(2 as u128)? >= rhs { 26 | return Some((1, 0)); 27 | } else { 28 | return Some((0, 0)); 29 | } 30 | } 31 | 32 | // Ceiling the destination amount if there's any remainder, which will 33 | // almost always be the case. 34 | let remainder = self.checked_rem(rhs)?; 35 | if remainder > 0 { 36 | quotient = quotient.checked_add(1)?; 37 | // calculate the minimum amount needed to get the dividend amount to 38 | // avoid truncating too much 39 | rhs = self.checked_div(quotient)?; 40 | let remainder = self.checked_rem(quotient)?; 41 | if remainder > 0 { 42 | rhs = rhs.checked_add(1)?; 43 | } 44 | } 45 | Some((quotient, rhs)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /programs/cp-swap/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod account_load; 2 | pub mod math; 3 | pub mod token; 4 | 5 | pub use account_load::*; 6 | pub use math::*; 7 | pub use token::*; 8 | -------------------------------------------------------------------------------- /programs/cp-swap/src/utils/token.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ErrorCode; 2 | use anchor_lang::{prelude::*, system_program}; 3 | use anchor_spl::{ 4 | token::{Token, TokenAccount}, 5 | token_2022::{ 6 | self, 7 | spl_token_2022::{ 8 | self, 9 | extension::{ 10 | transfer_fee::{TransferFeeConfig, MAX_FEE_BASIS_POINTS}, 11 | ExtensionType, StateWithExtensions, 12 | }, 13 | }, 14 | }, 15 | token_interface::{ 16 | initialize_account3, spl_token_2022::extension::BaseStateWithExtensions, 17 | InitializeAccount3, Mint, 18 | }, 19 | }; 20 | use std::collections::HashSet; 21 | 22 | const MINT_WHITELIST: [&'static str; 4] = [ 23 | "HVbpJAQGNpkgBaYBZQBR1t7yFdvaYVp2vCQQfKKEN4tM", 24 | "Crn4x1Y2HUKko7ox2EZMT6N2t2ZyH7eKtwkBGVnhEq1g", 25 | "FrBfWJ4qE5sCzKm3k3JaAtqZcXUh4LvJygDeketsrsH4", 26 | "2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo", 27 | ]; 28 | 29 | pub fn transfer_from_user_to_pool_vault<'a>( 30 | authority: AccountInfo<'a>, 31 | from: AccountInfo<'a>, 32 | to_vault: AccountInfo<'a>, 33 | mint: AccountInfo<'a>, 34 | token_program: AccountInfo<'a>, 35 | amount: u64, 36 | mint_decimals: u8, 37 | ) -> Result<()> { 38 | if amount == 0 { 39 | return Ok(()); 40 | } 41 | token_2022::transfer_checked( 42 | CpiContext::new( 43 | token_program.to_account_info(), 44 | token_2022::TransferChecked { 45 | from, 46 | to: to_vault, 47 | authority, 48 | mint, 49 | }, 50 | ), 51 | amount, 52 | mint_decimals, 53 | ) 54 | } 55 | 56 | pub fn transfer_from_pool_vault_to_user<'a>( 57 | authority: AccountInfo<'a>, 58 | from_vault: AccountInfo<'a>, 59 | to: AccountInfo<'a>, 60 | mint: AccountInfo<'a>, 61 | token_program: AccountInfo<'a>, 62 | amount: u64, 63 | mint_decimals: u8, 64 | signer_seeds: &[&[&[u8]]], 65 | ) -> Result<()> { 66 | if amount == 0 { 67 | return Ok(()); 68 | } 69 | token_2022::transfer_checked( 70 | CpiContext::new_with_signer( 71 | token_program.to_account_info(), 72 | token_2022::TransferChecked { 73 | from: from_vault, 74 | to, 75 | authority, 76 | mint, 77 | }, 78 | signer_seeds, 79 | ), 80 | amount, 81 | mint_decimals, 82 | ) 83 | } 84 | 85 | /// Issue a spl_token `MintTo` instruction. 86 | pub fn token_mint_to<'a>( 87 | authority: AccountInfo<'a>, 88 | token_program: AccountInfo<'a>, 89 | mint: AccountInfo<'a>, 90 | destination: AccountInfo<'a>, 91 | amount: u64, 92 | signer_seeds: &[&[&[u8]]], 93 | ) -> Result<()> { 94 | token_2022::mint_to( 95 | CpiContext::new_with_signer( 96 | token_program, 97 | token_2022::MintTo { 98 | to: destination, 99 | authority, 100 | mint, 101 | }, 102 | signer_seeds, 103 | ), 104 | amount, 105 | ) 106 | } 107 | 108 | pub fn token_burn<'a>( 109 | authority: AccountInfo<'a>, 110 | token_program: AccountInfo<'a>, 111 | mint: AccountInfo<'a>, 112 | from: AccountInfo<'a>, 113 | amount: u64, 114 | signer_seeds: &[&[&[u8]]], 115 | ) -> Result<()> { 116 | token_2022::burn( 117 | CpiContext::new_with_signer( 118 | token_program.to_account_info(), 119 | token_2022::Burn { 120 | from, 121 | authority, 122 | mint, 123 | }, 124 | signer_seeds, 125 | ), 126 | amount, 127 | ) 128 | } 129 | 130 | /// Calculate the fee for output amount 131 | pub fn get_transfer_inverse_fee(mint_info: &AccountInfo, post_fee_amount: u64) -> Result { 132 | if *mint_info.owner == Token::id() { 133 | return Ok(0); 134 | } 135 | if post_fee_amount == 0 { 136 | return err!(ErrorCode::InvalidInput); 137 | } 138 | let mint_data = mint_info.try_borrow_data()?; 139 | let mint = StateWithExtensions::::unpack(&mint_data)?; 140 | 141 | let fee = if let Ok(transfer_fee_config) = mint.get_extension::() { 142 | let epoch = Clock::get()?.epoch; 143 | 144 | let transfer_fee = transfer_fee_config.get_epoch_fee(epoch); 145 | if u16::from(transfer_fee.transfer_fee_basis_points) == MAX_FEE_BASIS_POINTS { 146 | u64::from(transfer_fee.maximum_fee) 147 | } else { 148 | let transfer_fee = transfer_fee_config 149 | .calculate_inverse_epoch_fee(epoch, post_fee_amount) 150 | .unwrap(); 151 | let transfer_fee_for_check = transfer_fee_config 152 | .calculate_epoch_fee(epoch, post_fee_amount.checked_add(transfer_fee).unwrap()) 153 | .unwrap(); 154 | if transfer_fee != transfer_fee_for_check { 155 | return err!(ErrorCode::TransferFeeCalculateNotMatch); 156 | } 157 | transfer_fee 158 | } 159 | } else { 160 | 0 161 | }; 162 | Ok(fee) 163 | } 164 | 165 | /// Calculate the fee for input amount 166 | pub fn get_transfer_fee(mint_info: &AccountInfo, pre_fee_amount: u64) -> Result { 167 | if *mint_info.owner == Token::id() { 168 | return Ok(0); 169 | } 170 | let mint_data = mint_info.try_borrow_data()?; 171 | let mint = StateWithExtensions::::unpack(&mint_data)?; 172 | 173 | let fee = if let Ok(transfer_fee_config) = mint.get_extension::() { 174 | transfer_fee_config 175 | .calculate_epoch_fee(Clock::get()?.epoch, pre_fee_amount) 176 | .unwrap() 177 | } else { 178 | 0 179 | }; 180 | Ok(fee) 181 | } 182 | 183 | pub fn is_supported_mint(mint_account: &InterfaceAccount) -> Result { 184 | let mint_info = mint_account.to_account_info(); 185 | if *mint_info.owner == Token::id() { 186 | return Ok(true); 187 | } 188 | let mint_whitelist: HashSet<&str> = MINT_WHITELIST.into_iter().collect(); 189 | if mint_whitelist.contains(mint_account.key().to_string().as_str()) { 190 | return Ok(true); 191 | } 192 | let mint_data = mint_info.try_borrow_data()?; 193 | let mint = StateWithExtensions::::unpack(&mint_data)?; 194 | let extensions = mint.get_extension_types()?; 195 | for e in extensions { 196 | if e != ExtensionType::TransferFeeConfig 197 | && e != ExtensionType::MetadataPointer 198 | && e != ExtensionType::TokenMetadata 199 | { 200 | return Ok(false); 201 | } 202 | } 203 | Ok(true) 204 | } 205 | 206 | pub fn create_token_account<'a>( 207 | authority: &AccountInfo<'a>, 208 | payer: &AccountInfo<'a>, 209 | token_account: &AccountInfo<'a>, 210 | mint_account: &AccountInfo<'a>, 211 | system_program: &AccountInfo<'a>, 212 | token_program: &AccountInfo<'a>, 213 | signer_seeds: &[&[u8]], 214 | ) -> Result<()> { 215 | let space = { 216 | let mint_info = mint_account.to_account_info(); 217 | if *mint_info.owner == token_2022::Token2022::id() { 218 | let mint_data = mint_info.try_borrow_data()?; 219 | let mint_state = 220 | StateWithExtensions::::unpack(&mint_data)?; 221 | let mint_extensions = mint_state.get_extension_types()?; 222 | let required_extensions = 223 | ExtensionType::get_required_init_account_extensions(&mint_extensions); 224 | ExtensionType::try_calculate_account_len::( 225 | &required_extensions, 226 | )? 227 | } else { 228 | TokenAccount::LEN 229 | } 230 | }; 231 | create_or_allocate_account( 232 | token_program.key, 233 | payer.to_account_info(), 234 | system_program.to_account_info(), 235 | token_account.to_account_info(), 236 | signer_seeds, 237 | space, 238 | )?; 239 | initialize_account3(CpiContext::new( 240 | token_program.to_account_info(), 241 | InitializeAccount3 { 242 | account: token_account.to_account_info(), 243 | mint: mint_account.to_account_info(), 244 | authority: authority.to_account_info(), 245 | }, 246 | )) 247 | } 248 | 249 | pub fn create_or_allocate_account<'a>( 250 | program_id: &Pubkey, 251 | payer: AccountInfo<'a>, 252 | system_program: AccountInfo<'a>, 253 | target_account: AccountInfo<'a>, 254 | siger_seed: &[&[u8]], 255 | space: usize, 256 | ) -> Result<()> { 257 | let rent = Rent::get()?; 258 | let current_lamports = target_account.lamports(); 259 | 260 | if current_lamports == 0 { 261 | let lamports = rent.minimum_balance(space); 262 | let cpi_accounts = system_program::CreateAccount { 263 | from: payer, 264 | to: target_account.clone(), 265 | }; 266 | let cpi_context = CpiContext::new(system_program.clone(), cpi_accounts); 267 | system_program::create_account( 268 | cpi_context.with_signer(&[siger_seed]), 269 | lamports, 270 | u64::try_from(space).unwrap(), 271 | program_id, 272 | )?; 273 | } else { 274 | let required_lamports = rent 275 | .minimum_balance(space) 276 | .max(1) 277 | .saturating_sub(current_lamports); 278 | if required_lamports > 0 { 279 | let cpi_accounts = system_program::Transfer { 280 | from: payer.to_account_info(), 281 | to: target_account.clone(), 282 | }; 283 | let cpi_context = CpiContext::new(system_program.clone(), cpi_accounts); 284 | system_program::transfer(cpi_context, required_lamports)?; 285 | } 286 | let cpi_accounts = system_program::Allocate { 287 | account_to_allocate: target_account.clone(), 288 | }; 289 | let cpi_context = CpiContext::new(system_program.clone(), cpi_accounts); 290 | system_program::allocate( 291 | cpi_context.with_signer(&[siger_seed]), 292 | u64::try_from(space).unwrap(), 293 | )?; 294 | 295 | let cpi_accounts = system_program::Assign { 296 | account_to_assign: target_account.clone(), 297 | }; 298 | let cpi_context = CpiContext::new(system_program.clone(), cpi_accounts); 299 | system_program::assign(cpi_context.with_signer(&[siger_seed]), program_id)?; 300 | } 301 | Ok(()) 302 | } 303 | -------------------------------------------------------------------------------- /tests/deposit.test.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program, BN } from "@coral-xyz/anchor"; 3 | import { RaydiumCpSwap } from "../target/types/raydium_cp_swap"; 4 | import { 5 | calculateFee, 6 | calculatePreFeeAmount, 7 | deposit, 8 | getUserAndPoolVaultAmount, 9 | setupDepositTest, 10 | } from "./utils"; 11 | import { assert } from "chai"; 12 | import { MAX_FEE_BASIS_POINTS, TOKEN_PROGRAM_ID } from "@solana/spl-token"; 13 | 14 | describe("deposit test", () => { 15 | anchor.setProvider(anchor.AnchorProvider.env()); 16 | const owner = anchor.Wallet.local().payer; 17 | 18 | const program = anchor.workspace.RaydiumCpSwap as Program; 19 | 20 | const confirmOptions = { 21 | skipPreflight: true, 22 | }; 23 | 24 | it("deposit test, add the same liquidity and check the correctness of the values with and without transfer fees", async () => { 25 | /// deposit without fee 26 | const { poolAddress, poolState } = await setupDepositTest( 27 | program, 28 | anchor.getProvider().connection, 29 | owner, 30 | { 31 | config_index: 0, 32 | tradeFeeRate: new BN(10), 33 | protocolFeeRate: new BN(1000), 34 | fundFeeRate: new BN(25000), 35 | create_fee: new BN(0), 36 | }, 37 | { transferFeeBasisPoints: 0, MaxFee: 0 } 38 | ); 39 | 40 | const { 41 | onwerToken0Account: ownerToken0AccountBefore, 42 | onwerToken1Account: ownerToken1AccountBefore, 43 | poolVault0TokenAccount: poolVault0TokenAccountBefore, 44 | poolVault1TokenAccount: poolVault1TokenAccountBefore, 45 | } = await getUserAndPoolVaultAmount( 46 | owner.publicKey, 47 | poolState.token0Mint, 48 | poolState.token0Program, 49 | poolState.token1Mint, 50 | poolState.token1Program, 51 | poolState.token0Vault, 52 | poolState.token1Vault 53 | ); 54 | 55 | const liquidity = new BN(10000000000); 56 | await deposit( 57 | program, 58 | owner, 59 | poolState.ammConfig, 60 | poolState.token0Mint, 61 | poolState.token0Program, 62 | poolState.token1Mint, 63 | poolState.token1Program, 64 | liquidity, 65 | new BN(10000000000), 66 | new BN(20000000000), 67 | confirmOptions 68 | ); 69 | const newPoolState = await program.account.poolState.fetch(poolAddress); 70 | assert(newPoolState.lpSupply.eq(liquidity.add(poolState.lpSupply))); 71 | 72 | const { 73 | onwerToken0Account: ownerToken0AccountAfter, 74 | onwerToken1Account: ownerToken1AccountAfter, 75 | poolVault0TokenAccount: poolVault0TokenAccountAfter, 76 | poolVault1TokenAccount: poolVault1TokenAccountAfter, 77 | } = await getUserAndPoolVaultAmount( 78 | owner.publicKey, 79 | poolState.token0Mint, 80 | poolState.token0Program, 81 | poolState.token1Mint, 82 | poolState.token1Program, 83 | poolState.token0Vault, 84 | poolState.token1Vault 85 | ); 86 | const input_token0_amount = 87 | ownerToken0AccountBefore.amount - ownerToken0AccountAfter.amount; 88 | const input_token1_amount = 89 | ownerToken1AccountBefore.amount - ownerToken1AccountAfter.amount; 90 | assert.equal( 91 | poolVault0TokenAccountAfter.amount - poolVault0TokenAccountBefore.amount, 92 | input_token0_amount 93 | ); 94 | assert.equal( 95 | poolVault1TokenAccountAfter.amount - poolVault1TokenAccountBefore.amount, 96 | input_token1_amount 97 | ); 98 | 99 | /// deposit with fee 100 | const transferFeeConfig = { 101 | transferFeeBasisPoints: 100, 102 | MaxFee: 50000000000, 103 | }; // %10 104 | 105 | // Ensure that the initialization state is the same with depsoit without fee 106 | const { poolAddress: poolAddress2, poolState: poolState2 } = 107 | await setupDepositTest( 108 | program, 109 | anchor.getProvider().connection, 110 | owner, 111 | { 112 | config_index: 0, 113 | tradeFeeRate: new BN(10), 114 | protocolFeeRate: new BN(1000), 115 | fundFeeRate: new BN(25000), 116 | create_fee: new BN(0), 117 | }, 118 | transferFeeConfig, 119 | confirmOptions, 120 | { 121 | initAmount0: new BN( 122 | calculatePreFeeAmount( 123 | transferFeeConfig, 124 | poolVault0TokenAccountBefore.amount, 125 | poolState.token0Program 126 | ).toString() 127 | ), 128 | initAmount1: new BN( 129 | calculatePreFeeAmount( 130 | transferFeeConfig, 131 | poolVault1TokenAccountBefore.amount, 132 | poolState.token1Program 133 | ).toString() 134 | ), 135 | }, 136 | { 137 | token0Program: poolState.token0Program, 138 | token1Program: poolState.token1Program, 139 | } 140 | ); 141 | const { 142 | onwerToken0Account: onwerToken0AccountBefore2, 143 | onwerToken1Account: onwerToken1AccountBefore2, 144 | poolVault0TokenAccount: poolVault0TokenAccountBefore2, 145 | poolVault1TokenAccount: poolVault1TokenAccountBefore2, 146 | } = await getUserAndPoolVaultAmount( 147 | owner.publicKey, 148 | poolState2.token0Mint, 149 | poolState2.token0Program, 150 | poolState2.token1Mint, 151 | poolState2.token1Program, 152 | poolState2.token0Vault, 153 | poolState2.token1Vault 154 | ); 155 | // check vault init state 156 | assert.equal( 157 | poolVault0TokenAccountBefore2.amount, 158 | poolVault0TokenAccountBefore.amount 159 | ); 160 | assert.equal( 161 | poolVault1TokenAccountBefore2.amount, 162 | poolVault1TokenAccountBefore.amount 163 | ); 164 | 165 | await deposit( 166 | program, 167 | owner, 168 | poolState2.ammConfig, 169 | poolState2.token0Mint, 170 | poolState2.token0Program, 171 | poolState2.token1Mint, 172 | poolState2.token1Program, 173 | liquidity, 174 | new BN(100000000000), 175 | new BN(200000000000), 176 | confirmOptions 177 | ); 178 | const newPoolState2 = await program.account.poolState.fetch(poolAddress2); 179 | assert(newPoolState2.lpSupply.eq(liquidity.add(poolState2.lpSupply))); 180 | 181 | const { 182 | onwerToken0Account: onwerToken0AccountAfter2, 183 | onwerToken1Account: onwerToken1AccountAfter2, 184 | poolVault0TokenAccount: poolVault0TokenAccountAfter2, 185 | poolVault1TokenAccount: poolVault1TokenAccountAfter2, 186 | } = await getUserAndPoolVaultAmount( 187 | owner.publicKey, 188 | poolState2.token0Mint, 189 | poolState2.token0Program, 190 | poolState2.token1Mint, 191 | poolState2.token1Program, 192 | poolState2.token0Vault, 193 | poolState2.token1Vault 194 | ); 195 | 196 | const input_token0_amount_with_fee = 197 | onwerToken0AccountBefore2.amount - onwerToken0AccountAfter2.amount; 198 | const input_token1_amount_with_fee = 199 | onwerToken1AccountBefore2.amount - onwerToken1AccountAfter2.amount; 200 | assert(input_token0_amount_with_fee >= input_token0_amount); 201 | assert(input_token1_amount_with_fee >= input_token1_amount); 202 | 203 | assert.equal( 204 | input_token0_amount_with_fee, 205 | calculateFee( 206 | transferFeeConfig, 207 | input_token0_amount_with_fee, 208 | poolState2.token0Program 209 | ) + input_token0_amount 210 | ); 211 | assert.equal( 212 | input_token1_amount_with_fee, 213 | calculateFee( 214 | transferFeeConfig, 215 | input_token1_amount_with_fee, 216 | poolState2.token1Program 217 | ) + input_token1_amount 218 | ); 219 | 220 | // Add the same liquidity, the amount increment of the pool vault will be the same as without fees. 221 | assert.equal( 222 | poolVault0TokenAccountAfter2.amount - 223 | poolVault0TokenAccountBefore2.amount, 224 | input_token0_amount 225 | ); 226 | assert.equal( 227 | poolVault1TokenAccountAfter2.amount - 228 | poolVault1TokenAccountBefore2.amount, 229 | input_token1_amount 230 | ); 231 | 232 | assert.equal( 233 | poolVault0TokenAccountAfter.amount, 234 | poolVault0TokenAccountAfter2.amount 235 | ); 236 | assert.equal( 237 | poolVault1TokenAccountAfter.amount, 238 | poolVault1TokenAccountAfter2.amount 239 | ); 240 | }); 241 | 242 | it("deposit test with 100% transferFeeConfig, reache maximum fee limit", async () => { 243 | const transferFeeConfig = { 244 | transferFeeBasisPoints: MAX_FEE_BASIS_POINTS, 245 | MaxFee: 5000000000, 246 | }; // %100 247 | 248 | const { poolAddress, poolState } = await setupDepositTest( 249 | program, 250 | anchor.getProvider().connection, 251 | owner, 252 | { 253 | config_index: 0, 254 | tradeFeeRate: new BN(10), 255 | protocolFeeRate: new BN(1000), 256 | fundFeeRate: new BN(25000), 257 | create_fee: new BN(0), 258 | }, 259 | transferFeeConfig 260 | ); 261 | 262 | const { 263 | onwerToken0Account: ownerToken0AccountBefore, 264 | onwerToken1Account: ownerToken1AccountBefore, 265 | poolVault0TokenAccount: poolVault0TokenAccountBefore, 266 | poolVault1TokenAccount: poolVault1TokenAccountBefore, 267 | } = await getUserAndPoolVaultAmount( 268 | owner.publicKey, 269 | poolState.token0Mint, 270 | poolState.token0Program, 271 | poolState.token1Mint, 272 | poolState.token1Program, 273 | poolState.token0Vault, 274 | poolState.token1Vault 275 | ); 276 | 277 | const liquidity = new BN(10000000000); 278 | await deposit( 279 | program, 280 | owner, 281 | poolState.ammConfig, 282 | poolState.token0Mint, 283 | poolState.token0Program, 284 | poolState.token1Mint, 285 | poolState.token1Program, 286 | liquidity, 287 | new BN(10000000000), 288 | new BN(20000000000), 289 | confirmOptions 290 | ); 291 | const newPoolState = await program.account.poolState.fetch(poolAddress); 292 | assert(newPoolState.lpSupply.eq(liquidity.add(poolState.lpSupply))); 293 | 294 | const { 295 | onwerToken0Account: ownerToken0AccountAfter, 296 | onwerToken1Account: ownerToken1AccountAfter, 297 | poolVault0TokenAccount: poolVault0TokenAccountAfter, 298 | poolVault1TokenAccount: poolVault1TokenAccountAfter, 299 | } = await getUserAndPoolVaultAmount( 300 | owner.publicKey, 301 | poolState.token0Mint, 302 | poolState.token0Program, 303 | poolState.token1Mint, 304 | poolState.token1Program, 305 | poolState.token0Vault, 306 | poolState.token1Vault 307 | ); 308 | const input_token0_amount = 309 | ownerToken0AccountBefore.amount - ownerToken0AccountAfter.amount; 310 | const input_token1_amount = 311 | ownerToken1AccountBefore.amount - ownerToken1AccountAfter.amount; 312 | 313 | if (poolState.token0Program.equals(TOKEN_PROGRAM_ID)) { 314 | assert.equal( 315 | poolVault0TokenAccountAfter.amount - 316 | poolVault0TokenAccountBefore.amount, 317 | input_token0_amount 318 | ); 319 | assert.equal( 320 | poolVault1TokenAccountAfter.amount - 321 | poolVault1TokenAccountBefore.amount, 322 | input_token1_amount - BigInt(transferFeeConfig.MaxFee) 323 | ); 324 | } else { 325 | assert.equal( 326 | poolVault0TokenAccountAfter.amount - 327 | poolVault0TokenAccountBefore.amount, 328 | input_token0_amount - BigInt(transferFeeConfig.MaxFee) 329 | ); 330 | assert.equal( 331 | poolVault1TokenAccountAfter.amount - 332 | poolVault1TokenAccountBefore.amount, 333 | input_token1_amount 334 | ); 335 | } 336 | }); 337 | }); 338 | -------------------------------------------------------------------------------- /tests/initialize.test.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program, BN } from "@coral-xyz/anchor"; 3 | import { RaydiumCpSwap } from "../target/types/raydium_cp_swap"; 4 | 5 | import { getAccount, TOKEN_PROGRAM_ID } from "@solana/spl-token"; 6 | import { setupInitializeTest, initialize, calculateFee } from "./utils"; 7 | import { assert } from "chai"; 8 | 9 | describe("initialize test", () => { 10 | anchor.setProvider(anchor.AnchorProvider.env()); 11 | const owner = anchor.Wallet.local().payer; 12 | console.log("owner: ", owner.publicKey.toString()); 13 | 14 | const program = anchor.workspace.RaydiumCpSwap as Program; 15 | 16 | const confirmOptions = { 17 | skipPreflight: true, 18 | }; 19 | 20 | it("create pool without fee", async () => { 21 | const { configAddress, token0, token0Program, token1, token1Program } = 22 | await setupInitializeTest( 23 | program, 24 | anchor.getProvider().connection, 25 | owner, 26 | { 27 | config_index: 0, 28 | tradeFeeRate: new BN(10), 29 | protocolFeeRate: new BN(1000), 30 | fundFeeRate: new BN(25000), 31 | create_fee: new BN(0), 32 | }, 33 | { transferFeeBasisPoints: 0, MaxFee: 0 }, 34 | confirmOptions 35 | ); 36 | 37 | const initAmount0 = new BN(10000000000); 38 | const initAmount1 = new BN(10000000000); 39 | const { poolAddress, poolState } = await initialize( 40 | program, 41 | owner, 42 | configAddress, 43 | token0, 44 | token0Program, 45 | token1, 46 | token1Program, 47 | confirmOptions, 48 | { initAmount0, initAmount1 } 49 | ); 50 | let vault0 = await getAccount( 51 | anchor.getProvider().connection, 52 | poolState.token0Vault, 53 | "processed", 54 | poolState.token0Program 55 | ); 56 | assert.equal(vault0.amount.toString(), initAmount0.toString()); 57 | 58 | let vault1 = await getAccount( 59 | anchor.getProvider().connection, 60 | poolState.token1Vault, 61 | "processed", 62 | poolState.token1Program 63 | ); 64 | assert.equal(vault1.amount.toString(), initAmount1.toString()); 65 | }); 66 | 67 | it("create pool with fee", async () => { 68 | const { configAddress, token0, token0Program, token1, token1Program } = 69 | await setupInitializeTest( 70 | program, 71 | anchor.getProvider().connection, 72 | owner, 73 | { 74 | config_index: 0, 75 | tradeFeeRate: new BN(10), 76 | protocolFeeRate: new BN(1000), 77 | fundFeeRate: new BN(25000), 78 | create_fee: new BN(100000000), 79 | }, 80 | { transferFeeBasisPoints: 0, MaxFee: 0 }, 81 | confirmOptions 82 | ); 83 | 84 | const initAmount0 = new BN(10000000000); 85 | const initAmount1 = new BN(10000000000); 86 | const { poolAddress, poolState } = await initialize( 87 | program, 88 | owner, 89 | configAddress, 90 | token0, 91 | token0Program, 92 | token1, 93 | token1Program, 94 | confirmOptions, 95 | { initAmount0, initAmount1 } 96 | ); 97 | let vault0 = await getAccount( 98 | anchor.getProvider().connection, 99 | poolState.token0Vault, 100 | "processed", 101 | poolState.token0Program 102 | ); 103 | assert.equal(vault0.amount.toString(), initAmount0.toString()); 104 | 105 | let vault1 = await getAccount( 106 | anchor.getProvider().connection, 107 | poolState.token1Vault, 108 | "processed", 109 | poolState.token1Program 110 | ); 111 | assert.equal(vault1.amount.toString(), initAmount1.toString()); 112 | }); 113 | 114 | it("create pool with token2022 mint has transfer fee", async () => { 115 | const transferFeeConfig = { transferFeeBasisPoints: 100, MaxFee: 50000000 }; // %10 116 | const { configAddress, token0, token0Program, token1, token1Program } = 117 | await setupInitializeTest( 118 | program, 119 | anchor.getProvider().connection, 120 | owner, 121 | { 122 | config_index: 0, 123 | tradeFeeRate: new BN(10), 124 | protocolFeeRate: new BN(1000), 125 | fundFeeRate: new BN(25000), 126 | create_fee: new BN(100000000), 127 | }, 128 | transferFeeConfig, 129 | confirmOptions 130 | ); 131 | 132 | const initAmount0 = new BN(10000000000); 133 | const initAmount1 = new BN(10000000000); 134 | const { poolAddress, poolState } = await initialize( 135 | program, 136 | owner, 137 | configAddress, 138 | token0, 139 | token0Program, 140 | token1, 141 | token1Program, 142 | confirmOptions, 143 | { initAmount0, initAmount1 } 144 | ); 145 | let vault0 = await getAccount( 146 | anchor.getProvider().connection, 147 | poolState.token0Vault, 148 | "processed", 149 | poolState.token0Program 150 | ); 151 | if (token0Program == TOKEN_PROGRAM_ID) { 152 | assert.equal(vault0.amount.toString(), initAmount0.toString()); 153 | } else { 154 | const total = 155 | vault0.amount + 156 | calculateFee( 157 | transferFeeConfig, 158 | BigInt(initAmount0.toString()), 159 | poolState.token0Program 160 | ); 161 | assert(new BN(total.toString()).gte(initAmount0)); 162 | } 163 | 164 | let vault1 = await getAccount( 165 | anchor.getProvider().connection, 166 | poolState.token1Vault, 167 | "processed", 168 | poolState.token1Program 169 | ); 170 | if (token1Program == TOKEN_PROGRAM_ID) { 171 | assert.equal(vault1.amount.toString(), initAmount1.toString()); 172 | } else { 173 | const total = 174 | vault1.amount + 175 | calculateFee( 176 | transferFeeConfig, 177 | BigInt(initAmount1.toString()), 178 | poolState.token1Program 179 | ); 180 | assert(new BN(total.toString()).gte(initAmount1)); 181 | } 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /tests/swap.test.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program, BN } from "@coral-xyz/anchor"; 3 | import { RaydiumCpSwap } from "../target/types/raydium_cp_swap"; 4 | import { setupSwapTest, swap_base_input, swap_base_output } from "./utils"; 5 | import { assert } from "chai"; 6 | import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; 7 | 8 | describe("swap test", () => { 9 | anchor.setProvider(anchor.AnchorProvider.env()); 10 | const owner = anchor.Wallet.local().payer; 11 | 12 | const program = anchor.workspace.RaydiumCpSwap as Program; 13 | 14 | const confirmOptions = { 15 | skipPreflight: true, 16 | }; 17 | 18 | it("swap base input without transfer fee", async () => { 19 | const { configAddress, poolAddress, poolState } = await setupSwapTest( 20 | program, 21 | anchor.getProvider().connection, 22 | owner, 23 | { 24 | config_index: 0, 25 | tradeFeeRate: new BN(10), 26 | protocolFeeRate: new BN(1000), 27 | fundFeeRate: new BN(25000), 28 | create_fee: new BN(0), 29 | }, 30 | { transferFeeBasisPoints: 0, MaxFee: 0 } 31 | ); 32 | const inputToken = poolState.token0Mint; 33 | const inputTokenProgram = poolState.token0Program; 34 | const inputTokenAccountAddr = getAssociatedTokenAddressSync( 35 | inputToken, 36 | owner.publicKey, 37 | false, 38 | inputTokenProgram 39 | ); 40 | const inputTokenAccountBefore = await getAccount( 41 | anchor.getProvider().connection, 42 | inputTokenAccountAddr, 43 | "processed", 44 | inputTokenProgram 45 | ); 46 | await sleep(1000); 47 | let amount_in = new BN(100000000); 48 | await swap_base_input( 49 | program, 50 | owner, 51 | configAddress, 52 | inputToken, 53 | inputTokenProgram, 54 | poolState.token1Mint, 55 | poolState.token1Program, 56 | amount_in, 57 | new BN(0) 58 | ); 59 | const inputTokenAccountAfter = await getAccount( 60 | anchor.getProvider().connection, 61 | inputTokenAccountAddr, 62 | "processed", 63 | inputTokenProgram 64 | ); 65 | assert.equal( 66 | inputTokenAccountBefore.amount - inputTokenAccountAfter.amount, 67 | BigInt(amount_in.toString()) 68 | ); 69 | }); 70 | 71 | it("swap base output without transfer fee", async () => { 72 | const { configAddress, poolAddress, poolState } = await setupSwapTest( 73 | program, 74 | anchor.getProvider().connection, 75 | owner, 76 | { 77 | config_index: 0, 78 | tradeFeeRate: new BN(10), 79 | protocolFeeRate: new BN(1000), 80 | fundFeeRate: new BN(25000), 81 | create_fee: new BN(0), 82 | }, 83 | { transferFeeBasisPoints: 0, MaxFee: 0 } 84 | ); 85 | const inputToken = poolState.token0Mint; 86 | const inputTokenProgram = poolState.token0Program; 87 | const inputTokenAccountAddr = getAssociatedTokenAddressSync( 88 | inputToken, 89 | owner.publicKey, 90 | false, 91 | inputTokenProgram 92 | ); 93 | const outputToken = poolState.token1Mint; 94 | const outputTokenProgram = poolState.token1Program; 95 | const outputTokenAccountAddr = getAssociatedTokenAddressSync( 96 | outputToken, 97 | owner.publicKey, 98 | false, 99 | outputTokenProgram 100 | ); 101 | const outputTokenAccountBefore = await getAccount( 102 | anchor.getProvider().connection, 103 | outputTokenAccountAddr, 104 | "processed", 105 | outputTokenProgram 106 | ); 107 | await sleep(1000); 108 | let amount_out = new BN(100000000); 109 | await swap_base_output( 110 | program, 111 | owner, 112 | configAddress, 113 | inputToken, 114 | inputTokenProgram, 115 | poolState.token1Mint, 116 | poolState.token1Program, 117 | amount_out, 118 | new BN(10000000000000), 119 | confirmOptions 120 | ); 121 | const outputTokenAccountAfter = await getAccount( 122 | anchor.getProvider().connection, 123 | outputTokenAccountAddr, 124 | "processed", 125 | outputTokenProgram 126 | ); 127 | assert.equal( 128 | outputTokenAccountAfter.amount - outputTokenAccountBefore.amount, 129 | BigInt(amount_out.toString()) 130 | ); 131 | }); 132 | 133 | it("swap base output with transfer fee", async () => { 134 | const transferFeeConfig = { transferFeeBasisPoints: 5, MaxFee: 5000 }; // %5 135 | const { configAddress, poolAddress, poolState } = await setupSwapTest( 136 | program, 137 | anchor.getProvider().connection, 138 | owner, 139 | { 140 | config_index: 0, 141 | tradeFeeRate: new BN(10), 142 | protocolFeeRate: new BN(1000), 143 | fundFeeRate: new BN(25000), 144 | create_fee: new BN(0), 145 | }, 146 | transferFeeConfig 147 | ); 148 | 149 | const inputToken = poolState.token0Mint; 150 | const inputTokenProgram = poolState.token0Program; 151 | const inputTokenAccountAddr = getAssociatedTokenAddressSync( 152 | inputToken, 153 | owner.publicKey, 154 | false, 155 | inputTokenProgram 156 | ); 157 | const outputToken = poolState.token1Mint; 158 | const outputTokenProgram = poolState.token1Program; 159 | const outputTokenAccountAddr = getAssociatedTokenAddressSync( 160 | outputToken, 161 | owner.publicKey, 162 | false, 163 | outputTokenProgram 164 | ); 165 | const outputTokenAccountBefore = await getAccount( 166 | anchor.getProvider().connection, 167 | outputTokenAccountAddr, 168 | "processed", 169 | outputTokenProgram 170 | ); 171 | await sleep(1000); 172 | let amount_out = new BN(100000000); 173 | await swap_base_output( 174 | program, 175 | owner, 176 | configAddress, 177 | inputToken, 178 | inputTokenProgram, 179 | poolState.token1Mint, 180 | poolState.token1Program, 181 | amount_out, 182 | new BN(10000000000000), 183 | confirmOptions 184 | ); 185 | const outputTokenAccountAfter = await getAccount( 186 | anchor.getProvider().connection, 187 | outputTokenAccountAddr, 188 | "processed", 189 | outputTokenProgram 190 | ); 191 | assert.equal( 192 | outputTokenAccountAfter.amount - outputTokenAccountBefore.amount, 193 | BigInt(amount_out.toString()) 194 | ); 195 | }); 196 | }); 197 | 198 | function sleep(ms: number): Promise { 199 | return new Promise((resolve) => setTimeout(resolve, ms)); 200 | } 201 | -------------------------------------------------------------------------------- /tests/utils/fee.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MAX_FEE_BASIS_POINTS, 3 | ONE_IN_BASIS_POINTS, 4 | TOKEN_PROGRAM_ID, 5 | } from "@solana/spl-token"; 6 | import { PublicKey } from "@solana/web3.js"; 7 | 8 | export function calculateFee( 9 | transferFeeConfig: { transferFeeBasisPoints: number; MaxFee: number }, 10 | preFeeAmount: bigint, 11 | tokenProgram: PublicKey 12 | ): bigint { 13 | if (tokenProgram.equals(TOKEN_PROGRAM_ID)) { 14 | return BigInt(0); 15 | } 16 | if (preFeeAmount === BigInt(0)) { 17 | return BigInt(0); 18 | } else { 19 | const numerator = 20 | preFeeAmount * BigInt(transferFeeConfig.transferFeeBasisPoints); 21 | const rawFee = 22 | (numerator + ONE_IN_BASIS_POINTS - BigInt(1)) / ONE_IN_BASIS_POINTS; 23 | const fee = 24 | rawFee > transferFeeConfig.MaxFee ? transferFeeConfig.MaxFee : rawFee; 25 | return BigInt(fee); 26 | } 27 | } 28 | 29 | export function calculatePreFeeAmount( 30 | transferFeeConfig: { transferFeeBasisPoints: number; MaxFee: number }, 31 | postFeeAmount: bigint, 32 | tokenProgram: PublicKey 33 | ) { 34 | if ( 35 | transferFeeConfig.transferFeeBasisPoints == 0 || 36 | tokenProgram.equals(TOKEN_PROGRAM_ID) 37 | ) { 38 | return postFeeAmount; 39 | } else { 40 | let numerator = postFeeAmount * BigInt(MAX_FEE_BASIS_POINTS); 41 | let denominator = 42 | MAX_FEE_BASIS_POINTS - transferFeeConfig.transferFeeBasisPoints; 43 | 44 | return (numerator + BigInt(denominator) - BigInt(1)) / BigInt(denominator); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./web3"; 2 | export * from "./pda"; 3 | export * from "./util"; 4 | export * from "./fee"; 5 | export * from "./instruction"; 6 | -------------------------------------------------------------------------------- /tests/utils/pda.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | export const AMM_CONFIG_SEED = Buffer.from( 4 | anchor.utils.bytes.utf8.encode("amm_config") 5 | ); 6 | export const POOL_SEED = Buffer.from(anchor.utils.bytes.utf8.encode("pool")); 7 | export const POOL_VAULT_SEED = Buffer.from( 8 | anchor.utils.bytes.utf8.encode("pool_vault") 9 | ); 10 | export const POOL_AUTH_SEED = Buffer.from( 11 | anchor.utils.bytes.utf8.encode("vault_and_lp_mint_auth_seed") 12 | ); 13 | export const POOL_LPMINT_SEED = Buffer.from( 14 | anchor.utils.bytes.utf8.encode("pool_lp_mint") 15 | ); 16 | export const TICK_ARRAY_SEED = Buffer.from( 17 | anchor.utils.bytes.utf8.encode("tick_array") 18 | ); 19 | 20 | export const OPERATION_SEED = Buffer.from( 21 | anchor.utils.bytes.utf8.encode("operation") 22 | ); 23 | 24 | export const ORACLE_SEED = Buffer.from( 25 | anchor.utils.bytes.utf8.encode("observation") 26 | ); 27 | 28 | export function u16ToBytes(num: number) { 29 | const arr = new ArrayBuffer(2); 30 | const view = new DataView(arr); 31 | view.setUint16(0, num, false); 32 | return new Uint8Array(arr); 33 | } 34 | 35 | export function i16ToBytes(num: number) { 36 | const arr = new ArrayBuffer(2); 37 | const view = new DataView(arr); 38 | view.setInt16(0, num, false); 39 | return new Uint8Array(arr); 40 | } 41 | 42 | export function u32ToBytes(num: number) { 43 | const arr = new ArrayBuffer(4); 44 | const view = new DataView(arr); 45 | view.setUint32(0, num, false); 46 | return new Uint8Array(arr); 47 | } 48 | 49 | export function i32ToBytes(num: number) { 50 | const arr = new ArrayBuffer(4); 51 | const view = new DataView(arr); 52 | view.setInt32(0, num, false); 53 | return new Uint8Array(arr); 54 | } 55 | 56 | export async function getAmmConfigAddress( 57 | index: number, 58 | programId: PublicKey 59 | ): Promise<[PublicKey, number]> { 60 | const [address, bump] = await PublicKey.findProgramAddress( 61 | [AMM_CONFIG_SEED, u16ToBytes(index)], 62 | programId 63 | ); 64 | return [address, bump]; 65 | } 66 | 67 | export async function getAuthAddress( 68 | programId: PublicKey 69 | ): Promise<[PublicKey, number]> { 70 | const [address, bump] = await PublicKey.findProgramAddress( 71 | [POOL_AUTH_SEED], 72 | programId 73 | ); 74 | return [address, bump]; 75 | } 76 | 77 | export async function getPoolAddress( 78 | ammConfig: PublicKey, 79 | tokenMint0: PublicKey, 80 | tokenMint1: PublicKey, 81 | programId: PublicKey 82 | ): Promise<[PublicKey, number]> { 83 | const [address, bump] = await PublicKey.findProgramAddress( 84 | [ 85 | POOL_SEED, 86 | ammConfig.toBuffer(), 87 | tokenMint0.toBuffer(), 88 | tokenMint1.toBuffer(), 89 | ], 90 | programId 91 | ); 92 | return [address, bump]; 93 | } 94 | 95 | export async function getPoolVaultAddress( 96 | pool: PublicKey, 97 | vaultTokenMint: PublicKey, 98 | programId: PublicKey 99 | ): Promise<[PublicKey, number]> { 100 | const [address, bump] = await PublicKey.findProgramAddress( 101 | [POOL_VAULT_SEED, pool.toBuffer(), vaultTokenMint.toBuffer()], 102 | programId 103 | ); 104 | return [address, bump]; 105 | } 106 | 107 | export async function getPoolLpMintAddress( 108 | pool: PublicKey, 109 | programId: PublicKey 110 | ): Promise<[PublicKey, number]> { 111 | const [address, bump] = await PublicKey.findProgramAddress( 112 | [POOL_LPMINT_SEED, pool.toBuffer()], 113 | programId 114 | ); 115 | return [address, bump]; 116 | } 117 | 118 | export async function getOrcleAccountAddress( 119 | pool: PublicKey, 120 | programId: PublicKey 121 | ): Promise<[PublicKey, number]> { 122 | const [address, bump] = await PublicKey.findProgramAddress( 123 | [ORACLE_SEED, pool.toBuffer()], 124 | programId 125 | ); 126 | return [address, bump]; 127 | } 128 | -------------------------------------------------------------------------------- /tests/utils/util.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { web3 } from "@coral-xyz/anchor"; 3 | import { 4 | Connection, 5 | PublicKey, 6 | Keypair, 7 | Signer, 8 | TransactionInstruction, 9 | SystemProgram, 10 | Transaction, 11 | sendAndConfirmTransaction, 12 | } from "@solana/web3.js"; 13 | import { 14 | createMint, 15 | TOKEN_PROGRAM_ID, 16 | getOrCreateAssociatedTokenAccount, 17 | mintTo, 18 | TOKEN_2022_PROGRAM_ID, 19 | getAssociatedTokenAddressSync, 20 | ExtensionType, 21 | getMintLen, 22 | createInitializeTransferFeeConfigInstruction, 23 | createInitializeMintInstruction, 24 | getAccount, 25 | } from "@solana/spl-token"; 26 | import { sendTransaction } from "./index"; 27 | 28 | // create a token mint and a token2022 mint with transferFeeConfig 29 | export async function createTokenMintAndAssociatedTokenAccount( 30 | connection: Connection, 31 | payer: Signer, 32 | mintAuthority: Signer, 33 | transferFeeConfig: { transferFeeBasisPoints: number; MaxFee: number } 34 | ) { 35 | let ixs: TransactionInstruction[] = []; 36 | ixs.push( 37 | web3.SystemProgram.transfer({ 38 | fromPubkey: payer.publicKey, 39 | toPubkey: mintAuthority.publicKey, 40 | lamports: web3.LAMPORTS_PER_SOL, 41 | }) 42 | ); 43 | await sendTransaction(connection, ixs, [payer]); 44 | 45 | interface Token { 46 | address: PublicKey; 47 | program: PublicKey; 48 | } 49 | 50 | let tokenArray: Token[] = []; 51 | let token0 = await createMint( 52 | connection, 53 | mintAuthority, 54 | mintAuthority.publicKey, 55 | null, 56 | 9 57 | ); 58 | tokenArray.push({ address: token0, program: TOKEN_PROGRAM_ID }); 59 | 60 | let token1 = await createMintWithTransferFee( 61 | connection, 62 | payer, 63 | mintAuthority, 64 | Keypair.generate(), 65 | transferFeeConfig 66 | ); 67 | 68 | tokenArray.push({ address: token1, program: TOKEN_2022_PROGRAM_ID }); 69 | 70 | tokenArray.sort(function (x, y) { 71 | const buffer1 = x.address.toBuffer(); 72 | const buffer2 = y.address.toBuffer(); 73 | 74 | for (let i = 0; i < buffer1.length && i < buffer2.length; i++) { 75 | if (buffer1[i] < buffer2[i]) { 76 | return -1; 77 | } 78 | if (buffer1[i] > buffer2[i]) { 79 | return 1; 80 | } 81 | } 82 | 83 | if (buffer1.length < buffer2.length) { 84 | return -1; 85 | } 86 | if (buffer1.length > buffer2.length) { 87 | return 1; 88 | } 89 | 90 | return 0; 91 | }); 92 | 93 | token0 = tokenArray[0].address; 94 | token1 = tokenArray[1].address; 95 | // console.log("Token 0", token0.toString()); 96 | // console.log("Token 1", token1.toString()); 97 | const token0Program = tokenArray[0].program; 98 | const token1Program = tokenArray[1].program; 99 | 100 | const ownerToken0Account = await getOrCreateAssociatedTokenAccount( 101 | connection, 102 | payer, 103 | token0, 104 | payer.publicKey, 105 | false, 106 | "processed", 107 | { skipPreflight: true }, 108 | token0Program 109 | ); 110 | 111 | await mintTo( 112 | connection, 113 | payer, 114 | token0, 115 | ownerToken0Account.address, 116 | mintAuthority, 117 | 100_000_000_000_000, 118 | [], 119 | { skipPreflight: true }, 120 | token0Program 121 | ); 122 | 123 | // console.log( 124 | // "ownerToken0Account key: ", 125 | // ownerToken0Account.address.toString() 126 | // ); 127 | 128 | const ownerToken1Account = await getOrCreateAssociatedTokenAccount( 129 | connection, 130 | payer, 131 | token1, 132 | payer.publicKey, 133 | false, 134 | "processed", 135 | { skipPreflight: true }, 136 | token1Program 137 | ); 138 | // console.log( 139 | // "ownerToken1Account key: ", 140 | // ownerToken1Account.address.toString() 141 | // ); 142 | await mintTo( 143 | connection, 144 | payer, 145 | token1, 146 | ownerToken1Account.address, 147 | mintAuthority, 148 | 100_000_000_000_000, 149 | [], 150 | { skipPreflight: true }, 151 | token1Program 152 | ); 153 | 154 | return [ 155 | { token0, token0Program }, 156 | { token1, token1Program }, 157 | ]; 158 | } 159 | 160 | async function createMintWithTransferFee( 161 | connection: Connection, 162 | payer: Signer, 163 | mintAuthority: Signer, 164 | mintKeypair = Keypair.generate(), 165 | transferFeeConfig: { transferFeeBasisPoints: number; MaxFee: number } 166 | ) { 167 | const transferFeeConfigAuthority = Keypair.generate(); 168 | const withdrawWithheldAuthority = Keypair.generate(); 169 | 170 | const extensions = [ExtensionType.TransferFeeConfig]; 171 | 172 | const mintLen = getMintLen(extensions); 173 | const decimals = 9; 174 | 175 | const mintLamports = await connection.getMinimumBalanceForRentExemption( 176 | mintLen 177 | ); 178 | const mintTransaction = new Transaction().add( 179 | SystemProgram.createAccount({ 180 | fromPubkey: payer.publicKey, 181 | newAccountPubkey: mintKeypair.publicKey, 182 | space: mintLen, 183 | lamports: mintLamports, 184 | programId: TOKEN_2022_PROGRAM_ID, 185 | }), 186 | createInitializeTransferFeeConfigInstruction( 187 | mintKeypair.publicKey, 188 | transferFeeConfigAuthority.publicKey, 189 | withdrawWithheldAuthority.publicKey, 190 | transferFeeConfig.transferFeeBasisPoints, 191 | BigInt(transferFeeConfig.MaxFee), 192 | TOKEN_2022_PROGRAM_ID 193 | ), 194 | createInitializeMintInstruction( 195 | mintKeypair.publicKey, 196 | decimals, 197 | mintAuthority.publicKey, 198 | null, 199 | TOKEN_2022_PROGRAM_ID 200 | ) 201 | ); 202 | await sendAndConfirmTransaction( 203 | connection, 204 | mintTransaction, 205 | [payer, mintKeypair], 206 | undefined 207 | ); 208 | 209 | return mintKeypair.publicKey; 210 | } 211 | 212 | export async function getUserAndPoolVaultAmount( 213 | owner: PublicKey, 214 | token0Mint: PublicKey, 215 | token0Program: PublicKey, 216 | token1Mint: PublicKey, 217 | token1Program: PublicKey, 218 | poolToken0Vault: PublicKey, 219 | poolToken1Vault: PublicKey 220 | ) { 221 | const onwerToken0AccountAddr = getAssociatedTokenAddressSync( 222 | token0Mint, 223 | owner, 224 | false, 225 | token0Program 226 | ); 227 | 228 | const onwerToken1AccountAddr = getAssociatedTokenAddressSync( 229 | token1Mint, 230 | owner, 231 | false, 232 | token1Program 233 | ); 234 | 235 | const onwerToken0Account = await getAccount( 236 | anchor.getProvider().connection, 237 | onwerToken0AccountAddr, 238 | "processed", 239 | token0Program 240 | ); 241 | 242 | const onwerToken1Account = await getAccount( 243 | anchor.getProvider().connection, 244 | onwerToken1AccountAddr, 245 | "processed", 246 | token1Program 247 | ); 248 | 249 | const poolVault0TokenAccount = await getAccount( 250 | anchor.getProvider().connection, 251 | poolToken0Vault, 252 | "processed", 253 | token0Program 254 | ); 255 | 256 | const poolVault1TokenAccount = await getAccount( 257 | anchor.getProvider().connection, 258 | poolToken1Vault, 259 | "processed", 260 | token1Program 261 | ); 262 | return { 263 | onwerToken0Account, 264 | onwerToken1Account, 265 | poolVault0TokenAccount, 266 | poolVault1TokenAccount, 267 | }; 268 | } 269 | 270 | export function isEqual(amount1: bigint, amount2: bigint) { 271 | if ( 272 | BigInt(amount1) === BigInt(amount2) || 273 | BigInt(amount1) - BigInt(amount2) === BigInt(1) || 274 | BigInt(amount1) - BigInt(amount2) === BigInt(-1) 275 | ) { 276 | return true; 277 | } 278 | return false; 279 | } 280 | -------------------------------------------------------------------------------- /tests/utils/web3.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { 3 | Connection, 4 | Signer, 5 | Transaction, 6 | TransactionInstruction, 7 | TransactionSignature, 8 | ConfirmOptions, 9 | } from "@solana/web3.js"; 10 | 11 | export async function accountExist( 12 | connection: anchor.web3.Connection, 13 | account: anchor.web3.PublicKey 14 | ) { 15 | const info = await connection.getAccountInfo(account); 16 | if (info == null || info.data.length == 0) { 17 | return false; 18 | } 19 | return true; 20 | } 21 | 22 | export async function sendTransaction( 23 | connection: Connection, 24 | ixs: TransactionInstruction[], 25 | signers: Array, 26 | options?: ConfirmOptions 27 | ): Promise { 28 | const tx = new Transaction(); 29 | for (var i = 0; i < ixs.length; i++) { 30 | tx.add(ixs[i]); 31 | } 32 | 33 | if (options == undefined) { 34 | options = { 35 | preflightCommitment: "confirmed", 36 | commitment: "confirmed", 37 | }; 38 | } 39 | 40 | const sendOpt = options && { 41 | skipPreflight: options.skipPreflight, 42 | preflightCommitment: options.preflightCommitment || options.commitment, 43 | }; 44 | 45 | tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; 46 | const signature = await connection.sendTransaction(tx, signers, sendOpt); 47 | 48 | const status = ( 49 | await connection.confirmTransaction(signature, options.commitment) 50 | ).value; 51 | 52 | if (status.err) { 53 | throw new Error( 54 | `Raw transaction ${signature} failed (${JSON.stringify(status)})` 55 | ); 56 | } 57 | return signature; 58 | } 59 | 60 | export async function getBlockTimestamp( 61 | connection: Connection 62 | ): Promise { 63 | let slot = await connection.getSlot(); 64 | return await connection.getBlockTime(slot); 65 | } 66 | -------------------------------------------------------------------------------- /tests/withdraw.test.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program, BN } from "@coral-xyz/anchor"; 3 | import { RaydiumCpSwap } from "../target/types/raydium_cp_swap"; 4 | import { 5 | deposit, 6 | getUserAndPoolVaultAmount, 7 | isEqual, 8 | setupDepositTest, 9 | withdraw, 10 | } from "./utils"; 11 | import { assert } from "chai"; 12 | 13 | describe("withdraw test", () => { 14 | anchor.setProvider(anchor.AnchorProvider.env()); 15 | const owner = anchor.Wallet.local().payer; 16 | const program = anchor.workspace.RaydiumCpSwap as Program; 17 | 18 | const confirmOptions = { 19 | skipPreflight: true, 20 | }; 21 | 22 | it("withdraw half of lp ", async () => { 23 | const { poolAddress, poolState } = await setupDepositTest( 24 | program, 25 | anchor.getProvider().connection, 26 | owner, 27 | { 28 | config_index: 0, 29 | tradeFeeRate: new BN(10), 30 | protocolFeeRate: new BN(1000), 31 | fundFeeRate: new BN(25000), 32 | create_fee: new BN(0), 33 | }, 34 | { transferFeeBasisPoints: 0, MaxFee: 0 } 35 | ); 36 | const liquidity = new BN(10000000000); 37 | await deposit( 38 | program, 39 | owner, 40 | poolState.ammConfig, 41 | poolState.token0Mint, 42 | poolState.token0Program, 43 | poolState.token1Mint, 44 | poolState.token1Program, 45 | liquidity, 46 | new BN(10000000000), 47 | new BN(20000000000) 48 | ); 49 | 50 | await withdraw( 51 | program, 52 | owner, 53 | poolState.ammConfig, 54 | poolState.token0Mint, 55 | poolState.token0Program, 56 | poolState.token1Mint, 57 | poolState.token1Program, 58 | liquidity.divn(2), 59 | new BN(10000000), 60 | new BN(1000000), 61 | confirmOptions 62 | ); 63 | const newPoolState = await program.account.poolState.fetch(poolAddress); 64 | assert(newPoolState.lpSupply.eq(liquidity.divn(2).add(poolState.lpSupply))); 65 | }); 66 | 67 | it("withdraw all lp ", async () => { 68 | const { poolAddress, poolState } = await setupDepositTest( 69 | program, 70 | anchor.getProvider().connection, 71 | owner, 72 | { 73 | config_index: 0, 74 | tradeFeeRate: new BN(10), 75 | protocolFeeRate: new BN(1000), 76 | fundFeeRate: new BN(25000), 77 | create_fee: new BN(0), 78 | }, 79 | { transferFeeBasisPoints: 0, MaxFee: 0 } 80 | ); 81 | const liquidity = new BN(10000000000); 82 | const { 83 | onwerToken0Account: ownerToken0AccountBefore, 84 | onwerToken1Account: ownerToken1AccountBefore, 85 | poolVault0TokenAccount: poolVault0TokenAccountBefore, 86 | poolVault1TokenAccount: poolVault1TokenAccountBefore, 87 | } = await getUserAndPoolVaultAmount( 88 | owner.publicKey, 89 | poolState.token0Mint, 90 | poolState.token0Program, 91 | poolState.token1Mint, 92 | poolState.token1Program, 93 | poolState.token0Vault, 94 | poolState.token1Vault 95 | ); 96 | 97 | await deposit( 98 | program, 99 | owner, 100 | poolState.ammConfig, 101 | poolState.token0Mint, 102 | poolState.token0Program, 103 | poolState.token1Mint, 104 | poolState.token1Program, 105 | liquidity, 106 | new BN(10000000000), 107 | new BN(20000000000) 108 | ); 109 | 110 | await withdraw( 111 | program, 112 | owner, 113 | poolState.ammConfig, 114 | poolState.token0Mint, 115 | poolState.token0Program, 116 | poolState.token1Mint, 117 | poolState.token1Program, 118 | liquidity, 119 | new BN(10000000), 120 | new BN(1000000), 121 | confirmOptions 122 | ); 123 | 124 | const newPoolState = await program.account.poolState.fetch(poolAddress); 125 | assert(newPoolState.lpSupply.eq(poolState.lpSupply)); 126 | 127 | const { 128 | onwerToken0Account: ownerToken0AccountAfter, 129 | onwerToken1Account: ownerToken1AccountAfter, 130 | poolVault0TokenAccount: poolVault0TokenAccountAfter, 131 | poolVault1TokenAccount: poolVault1TokenAccountAfter, 132 | } = await getUserAndPoolVaultAmount( 133 | owner.publicKey, 134 | poolState.token0Mint, 135 | poolState.token0Program, 136 | poolState.token1Mint, 137 | poolState.token1Program, 138 | poolState.token0Vault, 139 | poolState.token1Vault 140 | ); 141 | 142 | assert( 143 | isEqual(ownerToken0AccountBefore.amount, ownerToken0AccountAfter.amount) 144 | ); 145 | assert( 146 | isEqual(ownerToken1AccountBefore.amount, ownerToken1AccountAfter.amount) 147 | ); 148 | assert( 149 | isEqual( 150 | poolVault0TokenAccountBefore.amount, 151 | poolVault0TokenAccountAfter.amount 152 | ) 153 | ); 154 | assert( 155 | isEqual( 156 | poolVault1TokenAccountBefore.amount, 157 | poolVault1TokenAccountAfter.amount 158 | ) 159 | ); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | --------------------------------------------------------------------------------