├── .gitignore └── packages └── cetus_clmm ├── Move.toml ├── sources ├── utils.move ├── acl.move ├── pool_creator.move ├── position_snapshot.move ├── math │ ├── tick_math.move │ └── clmm_math.move ├── partner.move └── rewarder.move ├── Move.lock ├── tests ├── tick_math_tests.move ├── position_snapshot.move ├── acl_tests.move ├── pool_restore_tests.move ├── rewarder_unittests.move ├── partner_tests.move ├── swap_cases.move ├── config_tests.move └── pool_creator_tests.move ├── CHANGELOG.md └── restore.md /.gitignore: -------------------------------------------------------------------------------- 1 | packages/*/build/* 2 | -------------------------------------------------------------------------------- /packages/cetus_clmm/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "CetusClmm" 3 | version = "mainnet-v1.3.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | MoveSTL = { git = "https://github.com/CetusProtocol/move-stl.git", rev = "mainnet-v1.3.0", override = true } 8 | IntegerMate = { git = "https://github.com/CetusProtocol/integer-mate.git", rev = "mainnet-v1.3.0", override = true } 9 | 10 | [addresses] 11 | cetus_clmm = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb" 12 | -------------------------------------------------------------------------------- /packages/cetus_clmm/sources/utils.move: -------------------------------------------------------------------------------- 1 | module cetus_clmm::utils; 2 | 3 | use std::string::{Self, String}; 4 | 5 | /// Convert u64 to String. 6 | public fun str(mut num: u64): String { 7 | if (num == 0) { 8 | return string::utf8(b"0") 9 | }; 10 | let mut remainder: u8; 11 | let mut digits = vector::empty(); 12 | while (num > 0) { 13 | remainder = (num % 10 as u8); 14 | num = num / 10; 15 | vector::push_back(&mut digits, remainder + 48); 16 | }; 17 | vector::reverse(&mut digits); 18 | string::utf8(digits) 19 | } 20 | 21 | 22 | #[test_only] 23 | use std::u64::to_string; 24 | 25 | #[test] 26 | fun test_str() { 27 | assert!(str(0) == string::utf8(b"0"), 1); 28 | assert!(str(1) == string::utf8(b"1"), 2); 29 | assert!(str(10) == string::utf8(b"10"), 3); 30 | assert!(str(100) == string::utf8(b"100"), 4); 31 | assert!(str(999) == string::utf8(b"999"), 5); 32 | assert!(str(123456789) == string::utf8(b"123456789"), 6); 33 | assert!(str(0) == to_string(0), 7); 34 | assert!(str(1) == to_string(1), 8); 35 | assert!(str(10) == to_string(10), 9); 36 | assert!(str(100) == to_string(100), 10); 37 | assert!(str(999) == to_string(999), 11); 38 | assert!(str(123456789) == to_string(123456789), 12); 39 | } 40 | -------------------------------------------------------------------------------- /packages/cetus_clmm/Move.lock: -------------------------------------------------------------------------------- 1 | # @generated by Move, please check-in and do not edit manually. 2 | 3 | [move] 4 | version = 3 5 | manifest_digest = "BB32CB49D331364BB110023A60089D483740CCD4877DB1E4E39C202DFD08E724" 6 | deps_digest = "52B406A7A21811BEF51751CF88DA0E76DAEFFEAC888D4F4060B1A72BBE7D8D35" 7 | dependencies = [ 8 | { id = "Bridge", name = "Bridge" }, 9 | { id = "IntegerMate", name = "IntegerMate" }, 10 | { id = "MoveSTL", name = "MoveSTL" }, 11 | { id = "MoveStdlib", name = "MoveStdlib" }, 12 | { id = "Sui", name = "Sui" }, 13 | { id = "SuiSystem", name = "SuiSystem" }, 14 | ] 15 | 16 | [[move.package]] 17 | id = "Bridge" 18 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "4e8b6eda7d6411d80c62f39ac8a4f028e8d174c4", subdir = "crates/sui-framework/packages/bridge" } 19 | 20 | dependencies = [ 21 | { id = "MoveStdlib", name = "MoveStdlib" }, 22 | { id = "Sui", name = "Sui" }, 23 | { id = "SuiSystem", name = "SuiSystem" }, 24 | ] 25 | 26 | [[move.package]] 27 | id = "IntegerMate" 28 | source = { git = "https://github.com/CetusProtocol/integer-mate.git", rev = "mainnet-v1.3.0", subdir = "" } 29 | 30 | dependencies = [ 31 | { id = "Bridge", name = "Bridge" }, 32 | { id = "MoveStdlib", name = "MoveStdlib" }, 33 | { id = "Sui", name = "Sui" }, 34 | { id = "SuiSystem", name = "SuiSystem" }, 35 | ] 36 | 37 | [[move.package]] 38 | id = "MoveSTL" 39 | source = { git = "https://github.com/CetusProtocol/move-stl.git", rev = "mainnet-v1.3.0", subdir = "" } 40 | 41 | dependencies = [ 42 | { id = "Bridge", name = "Bridge" }, 43 | { id = "MoveStdlib", name = "MoveStdlib" }, 44 | { id = "Sui", name = "Sui" }, 45 | { id = "SuiSystem", name = "SuiSystem" }, 46 | ] 47 | 48 | [[move.package]] 49 | id = "MoveStdlib" 50 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "4e8b6eda7d6411d80c62f39ac8a4f028e8d174c4", subdir = "crates/sui-framework/packages/move-stdlib" } 51 | 52 | [[move.package]] 53 | id = "Sui" 54 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "4e8b6eda7d6411d80c62f39ac8a4f028e8d174c4", subdir = "crates/sui-framework/packages/sui-framework" } 55 | 56 | dependencies = [ 57 | { id = "MoveStdlib", name = "MoveStdlib" }, 58 | ] 59 | 60 | [[move.package]] 61 | id = "SuiSystem" 62 | source = { git = "https://github.com/MystenLabs/sui.git", rev = "4e8b6eda7d6411d80c62f39ac8a4f028e8d174c4", subdir = "crates/sui-framework/packages/sui-system" } 63 | 64 | dependencies = [ 65 | { id = "MoveStdlib", name = "MoveStdlib" }, 66 | { id = "Sui", name = "Sui" }, 67 | ] 68 | 69 | [move.toolchain-version] 70 | compiler-version = "1.58.1" 71 | edition = "2024" 72 | flavor = "sui" 73 | 74 | [env] 75 | 76 | [env.mainnet] 77 | chain-id = "35834a8a" 78 | original-published-id = "0x1eabed72c53feb3805120a081dc15963c204dc8d091542592abaf7a35689b2fb" 79 | latest-published-id = "0x25ebb9a7c50eb17b3fa9c5a30fb8b5ad8f97caaf4928943acbcff7153dfee5e3" 80 | published-version = "14" 81 | -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/tick_math_tests.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::tick_math_tests; 3 | 4 | use cetus_clmm::tick_math::{ 5 | get_tick_at_sqrt_price, 6 | max_tick, 7 | min_tick, 8 | get_sqrt_price_at_tick, 9 | tick_bound, 10 | is_valid_index 11 | }; 12 | use integer_mate::i32; 13 | 14 | const MAX_SQRT_PRICE_X64: u128 = 79226673515401279992447579055; 15 | 16 | const MIN_SQRT_PRICE_X64: u128 = 4295048016; 17 | 18 | #[test] 19 | fun test_get_sqrt_price_at_tick() { 20 | // min tick 21 | assert!(get_sqrt_price_at_tick(i32::neg_from(tick_bound())) == 4295048016u128, 2); 22 | // max tick 23 | assert!( 24 | get_sqrt_price_at_tick(i32::from(tick_bound())) == 79226673515401279992447579055u128, 25 | 1, 26 | ); 27 | assert!(get_sqrt_price_at_tick(i32::neg_from(435444u32)) == 6469134034u128, 3); 28 | assert!(get_sqrt_price_at_tick(i32::from(408332u32)) == 13561044167458152057771544136u128, 4); 29 | } 30 | 31 | #[test] 32 | fun test_tick_swap_sqrt_price() { 33 | let mut t = i32::from(400800); 34 | while (i32::lte(t, i32::from(401200))) { 35 | let sqrt_price = get_sqrt_price_at_tick(t); 36 | let tick = get_tick_at_sqrt_price(sqrt_price); 37 | assert!(i32::eq(t, tick) == true, 0); 38 | t = i32::add(t, i32::from(1)); 39 | } 40 | } 41 | 42 | #[test] 43 | fun test_get_tick_at_sqrt_price_1() { 44 | assert!(i32::eq(get_tick_at_sqrt_price(6469134034u128), i32::neg_from(435444)) == true, 0); 45 | assert!( 46 | i32::eq(get_tick_at_sqrt_price(13561044167458152057771544136u128), i32::from(408332u32)) == true, 47 | 0, 48 | ); 49 | } 50 | 51 | #[test] 52 | #[expected_failure] 53 | fun test_get_sqrt_price_at_invalid_upper_tick() { 54 | get_sqrt_price_at_tick(i32::add(max_tick(), i32::from(1))); 55 | } 56 | 57 | #[test] 58 | #[expected_failure] 59 | fun test_get_sqrt_price_at_invalid_lower_tick() { 60 | get_sqrt_price_at_tick(i32::sub(min_tick(), i32::from(1))); 61 | } 62 | 63 | #[test] 64 | #[expected_failure] 65 | fun test_get_tick_at_invalid_lower_sqrt_price() { 66 | get_tick_at_sqrt_price(MAX_SQRT_PRICE_X64 + 1); 67 | } 68 | 69 | #[test] 70 | #[expected_failure] 71 | fun test_get_tick_at_invalid_upper_sqrt_price() { 72 | get_tick_at_sqrt_price(MIN_SQRT_PRICE_X64 - 1); 73 | } 74 | 75 | #[test] 76 | fun test_is_valid_index() { 77 | assert!(is_valid_index(i32::from(0), 1), 0); 78 | assert!(is_valid_index(i32::from(1), 1), 1); 79 | assert!(is_valid_index(i32::from(2), 1), 2); 80 | assert!(!is_valid_index(i32::from(3), 2), 3); 81 | assert!(!is_valid_index(i32::from(448080), 2), 4); 82 | assert!(!is_valid_index(i32::neg_from(448080), 2), 4); 83 | assert!(is_valid_index(i32::from(443636), 2), 4); 84 | assert!(is_valid_index(i32::neg_from(443636), 2), 4); 85 | assert!(!is_valid_index(i32::from(443636), 10), 4); 86 | assert!(!is_valid_index(i32::neg_from(443636), 10), 4); 87 | } 88 | -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/position_snapshot.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::position_snapshot_tests; 3 | 4 | 5 | use cetus_clmm::position::new_position_info_custom; 6 | use integer_mate::i32; 7 | use cetus_clmm::position::new_position_reward_for_test; 8 | use cetus_clmm::tick_math::get_sqrt_price_at_tick; 9 | use cetus_clmm::position_snapshot; 10 | use std::unit_test::assert_eq; 11 | 12 | #[test] 13 | fun test_calculate_remove_liquidity() { 14 | let position_info = new_position_info_custom( 15 | 591619209017, 16 | i32::from_u32(0), 17 | i32::from_u32(1000), 18 | 0, 19 | 0, 20 | 0, 21 | 0, 22 | vector[ 23 | new_position_reward_for_test(2 << 89, 0), 24 | new_position_reward_for_test(2 << 90, 0), 25 | new_position_reward_for_test(3 << 91, 0), 26 | ], 27 | 0, 28 | 0, 29 | ); 30 | let current_sqrt_price = get_sqrt_price_at_tick(i32::from_u32(0)); 31 | let mut ctx = tx_context::dummy(); 32 | let position_liquidity_snapshot = position_snapshot::new(current_sqrt_price, 5000,&mut ctx); 33 | let remove_liquidity = position_liquidity_snapshot.calculate_remove_liquidity(&position_info); 34 | assert!(remove_liquidity == 591619209017 * 5000 / 1000000 + 1, 1); 35 | transfer::public_share_object(position_liquidity_snapshot); 36 | } 37 | 38 | #[test] 39 | fun test_snapshot_add_and_remove() { 40 | let position_info = new_position_info_custom( 41 | 591619209017, 42 | i32::from_u32(0), 43 | i32::from_u32(1000), 44 | 0, 45 | 0, 46 | 0, 47 | 0, 48 | vector[ 49 | new_position_reward_for_test(2 << 89, 0), 50 | new_position_reward_for_test(2 << 90, 0), 51 | new_position_reward_for_test(3 << 91, 0), 52 | ], 53 | 0, 54 | 0, 55 | ); 56 | let current_sqrt_price = get_sqrt_price_at_tick(i32::from_u32(0)); 57 | let mut ctx = tx_context::dummy(); 58 | let mut position_liquidity_snapshot = position_snapshot::new(current_sqrt_price, 5000,&mut ctx); 59 | position_liquidity_snapshot.add(object::id_from_address(@1234), 5000, position_info); 60 | let snapshot = position_liquidity_snapshot.get(object::id_from_address(@1234)); 61 | assert!(snapshot.liquidity() == 591619209017, 1); 62 | assert_eq!(snapshot.value_cut(),5000); 63 | assert_eq!(snapshot.position_id(), object::id_from_address(@1234)); 64 | let (tick_lower_index, tick_upper_index) = snapshot.tick_range(); 65 | assert!(tick_lower_index == i32::from_u32(0), 2); 66 | assert!(tick_upper_index == i32::from_u32(1000), 3); 67 | let (fee_owned_a, fee_owned_b) = snapshot.fee_owned(); 68 | assert!(fee_owned_a == 0, 4); 69 | assert!(fee_owned_b == 0, 5); 70 | let rewards = snapshot.rewards(); 71 | assert!(rewards.length() == 3, 6); 72 | assert!(position_liquidity_snapshot.total_value_cut() == 5000, 7); 73 | assert!(position_liquidity_snapshot.current_sqrt_price() == current_sqrt_price, 8); 74 | assert!(position_liquidity_snapshot.remove_percent() == 5000, 9); 75 | assert!(position_liquidity_snapshot.contains(object::id_from_address(@1234)), 10); 76 | position_liquidity_snapshot.remove(object::id_from_address(@1234)); 77 | assert!(!position_liquidity_snapshot.contains(object::id_from_address(@1234)), 11); 78 | transfer::public_share_object(position_liquidity_snapshot); 79 | } 80 | 81 | #[test] 82 | #[expected_failure(abort_code = cetus_clmm::position_snapshot::EPositionSnapshotAlreadyExists)] 83 | fun test_add_snapshot_already_exists(){ 84 | let mut ctx = tx_context::dummy(); 85 | let mut snapshot = position_snapshot::new(get_sqrt_price_at_tick(i32::from_u32(0)), 5000, &mut ctx); 86 | let position_info = new_position_info_custom( 87 | 591619209017, 88 | i32::from_u32(0), 89 | i32::from_u32(1000), 90 | 0, 91 | 0, 92 | 0, 93 | 0, 94 | vector[ 95 | new_position_reward_for_test(2 << 89, 0), 96 | new_position_reward_for_test(2 << 90, 0), 97 | new_position_reward_for_test(3 << 91, 0), 98 | ], 99 | 0, 100 | 0, 101 | ); 102 | snapshot.add(object::id_from_address(@1234), 500, position_info); 103 | snapshot.add(object::id_from_address(@1234), 500, position_info); 104 | transfer::public_share_object(snapshot); 105 | } 106 | 107 | #[test] 108 | #[expected_failure(abort_code = cetus_clmm::position_snapshot::EPositionSnapshotNotFound)] 109 | fun test_snapshot_remove_not_exists(){ 110 | let mut ctx = tx_context::dummy(); 111 | let mut snapshot = position_snapshot::new(get_sqrt_price_at_tick(i32::from_u32(0)), 5000, &mut ctx); 112 | snapshot.remove(object::id_from_address(@1234)); 113 | transfer::public_share_object(snapshot); 114 | } 115 | 116 | #[test] 117 | #[expected_failure(abort_code = cetus_clmm::position_snapshot::EPositionSnapshotNotFound)] 118 | fun test_snapshot_get_not_exists(){ 119 | let mut ctx = tx_context::dummy(); 120 | let snapshot = position_snapshot::new(get_sqrt_price_at_tick(i32::from_u32(0)), 5000, &mut ctx); 121 | snapshot.get(object::id_from_address(@1234)); 122 | transfer::public_share_object(snapshot); 123 | } -------------------------------------------------------------------------------- /packages/cetus_clmm/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## (2024-10-28) 4 | - Online Version: 8 5 | 6 | ### `config` Module Updates 7 | - Add `is_pool_manager` public method 8 | 9 | ### `factory` Module Updates 10 | ### New Share Objects & Structs 11 | - Share Objects: 12 | - `DenyCoinList` 13 | - `PermissionPairManager` 14 | - Structs: 15 | - `PoolKey` 16 | - `PoolCreationCap` 17 | 18 | ### New Events 19 | - DenyCoinList Events: 20 | - `AddAllowedListEvent` 21 | - `RemoveAllowedListEvent` 22 | - `AddDeniedListEvent` 23 | - `RemoveDeniedListEvent` 24 | - Permission Pair Events: 25 | - `InitPermissionPairManagerEvent` 26 | - `RegisterPermissionPairEvent` 27 | - `UnregisterPermissionPairEvent` 28 | - `AddAllowedPairConfigEvent` 29 | - `RemoveAllowedPairConfigEvent` 30 | - PoolCreationCap Events: 31 | - `MintPoolCreationCap` 32 | - `MintPoolCreationCapByAdmin` 33 | 34 | ### New Methods 35 | #### Public Methods 36 | - Init Entry Function: 37 | - `init_manager_and_whitelist` 38 | - DenyCoinList Management: 39 | - `in_allowed_list` 40 | - `in_denied_list` 41 | - `is_allowed_coin` 42 | - `add_allowed_list` 43 | - `remove_allowed_list` 44 | - `add_denied_list` 45 | - `remove_denied_list` 46 | - Pool Creation: 47 | - `permission_pair_cap` 48 | - `is_permission_pair` 49 | - `add_allowed_pair_config` 50 | - `remove_allowed_pair_config` 51 | - `mint_pool_creation_cap` 52 | - `mint_pool_creation_cap_by_admin` 53 | - `register_permission_pair` 54 | - `unregister_permission_pair` 55 | 56 | #### Private Methods 57 | - `add_denied_coin` 58 | - `unregister_permission_pair_internal` 59 | - `register_permission_pair_internal` 60 | - `create_pool_v2_internal` 61 | 62 | #### New public(friend) Methods 63 | - `create_pool_v2_` 64 | 65 | ### Method Changes 66 | - Modified `create_pool` to require permission 67 | - Deprecated `create_pool_with_liquidity` 68 | 69 | ## New Module 70 | ### `pool_creator` Module 71 | - Add `create_pool_v2_by_creation_cap` method 72 | - Add `create_pool_v2` method 73 | 74 | ## Breaking Changes & Migration Guide 75 | ### Deprecated Methods 76 | The `create_pool_with_liquidity` method has been deprecated. Instead, users should: 77 | - For registered pools: Use `create_pool_v2_by_creation_cap` method 78 | - For non-registered pools: Use `create_pool_v2` method 79 | 80 | ### New Requirements 81 | - Mandatory full-range liquidity provision for new pool creation 82 | - Token issuers can register pool creation permissions by specified with quote coin and tick_spacing 83 | - All pool creation functionality has been migrated to the new `pool_creator` module 84 | - Users should update their integration to use the new v2 methods 85 | 86 | ## 2024-11-20 87 | - Online Version: 9 88 | 89 | ### Added 90 | * Add amount check to `create_pool_v2` 91 | * Performed code optimizations 92 | * Added new event `CollectRewardV2Event` to pool module 93 | * No longer enforce the full-range liquidity constraint in the pool creation process. 94 | * Deprecated `create_pool_v2_by_creation_cap` method and implemented new method `create_pool_v2_with_creation_cap`. The `create_pool_v2` method is no longer intended for manager use. 95 | 96 | 97 | ## 2025-02-07 98 | - Online Version: 10 99 | ### Added 100 | - Support flash loan 101 | 102 | - Reward num per pool up to 5 103 | 104 | ### Changed 105 | 106 | - Ignore remaining rewarder check when update_emission if new emission speed slower than old 107 | 108 | ``` 109 | public(friend) fun update_emission( 110 | vault: &RewarderGlobalVault, 111 | manager: &mut RewarderManager, 112 | liquidity: u128, 113 | emissions_per_second: u128, 114 | timestamp: u64, 115 | ) { 116 | ... 117 | let old_emission = rewarder.emissions_per_second; 118 | if (emissions_per_second > 0 && emissions_per_second > old_emission) { 119 | ... 120 | } 121 | rewarder.emissions_per_second = emissions_per_second; 122 | } 123 | ``` 124 | ### Optimized 125 | - Optimized gas by adding a check to return early if current_sqrt_price == target_sqrt_price. 126 | 127 | ```rust 128 | if (liquidity == 0 || current_sqrt_price == target_sqrt_price) { 129 | return ( 130 | amount_in, 131 | amount_out, 132 | next_sqrt_price, 133 | fee_amount, 134 | ) 135 | }; 136 | if (a2b) { 137 | assert!(current_sqrt_price > target_sqrt_price, EINVALID_SQRT_PRICE_INPUT) 138 | } else { 139 | assert!(current_sqrt_price < target_sqrt_price, EINVALID_SQRT_PRICE_INPUT) 140 | }; 141 | ``` 142 | 143 | ### Fixed 144 | ### Security 145 | 146 | 147 | ## 2025-03-20 148 | - Online Version: 11 149 | 150 | Update `inter-mate` library version 151 | ### Added 152 | ### Changed 153 | 154 | ### Fixed 155 | ### Security 156 | 157 | 158 | ## 2025-06-02 159 | - Online Version: 12 160 | ### Added 161 | - Add `ProtocolFeeCollectCap` 162 | - Add `collect_protocol_fee_v2` method 163 | - Add pool restore related methods 164 | - Add Fine-Grained Pool Controls, add `PoolStatus` struct 165 | ### Changed 166 | - Update `clmm_math.get_liquidity_by_amount` method 167 | 168 | ### Fixed 169 | ### Security 170 | 171 | ## 2025-09-12 172 | - Online Version: 13 173 | ### Added 174 | - Add `AddLiquidityV2Event`, `RemoveLiquidityV2Event` 175 | - Fix asymptotic audit 176 | - Add `remove_liquidity_with_slippage` 177 | 178 | ## 2025-10-14 179 | - Online Version: 14 180 | # Added 181 | - Add partner.`claim_ref_fee_coin` 182 | - Add pool_creator.`create_pool_v2_with_creation_cap` 183 | - Add pool_creator.`create_pool_v3` 184 | - Add factory.`create_pool_v3_` 185 | ### Changed 186 | - Fix `AddLiquidityV2Event` 187 | -------------------------------------------------------------------------------- /packages/cetus_clmm/sources/acl.move: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cetus Technology Limited 2 | 3 | /// Fork @https://github.com/pentagonxyz/movemate.git 4 | /// 5 | /// `acl` is a simple access control module, where `member` represents a member and `role` represents a type 6 | /// of permission. A member can have multiple permissions. 7 | module cetus_clmm::acl; 8 | 9 | use move_stl::linked_table::{Self, LinkedTable}; 10 | use std::option::is_some; 11 | 12 | /// Error when role number is too large 13 | const ERoleNumberTooLarge: u64 = 0; 14 | /// Error when role is not found 15 | const ERoleNotFound: u64 = 1; 16 | /// Error when member is not found 17 | const EMemberNotFound: u64 = 2; 18 | 19 | /// ACL (Access Control List) struct that manages permissions for members 20 | /// Contains a mapping of addresses to their permission bitmasks 21 | /// Each bit in the permission bitmask represents a specific role/permission 22 | /// The first 128 bits are available for different roles 23 | public struct ACL has store { 24 | permissions: LinkedTable, 25 | } 26 | 27 | /// Member struct representing a member in the ACL system 28 | /// * `address` - The address of the member 29 | /// * `permission` - A bitmask of the member's permissions, where each bit represents a specific role 30 | public struct Member has copy, drop, store { 31 | address: address, 32 | permission: u128, 33 | } 34 | 35 | 36 | 37 | /// Create a new ACL instance 38 | /// * `ctx` - Transaction context used to create the LinkedTable 39 | /// Returns an empty ACL with no members or permissions 40 | public fun new(ctx: &mut TxContext): ACL { 41 | ACL { permissions: linked_table::new(ctx) } 42 | } 43 | 44 | /// Check if a member has a role in the ACL 45 | /// * `acl` - The ACL instance to check 46 | /// * `member` - The address of the member to check 47 | /// * `role` - The role to check for 48 | /// Returns true if the member has the role, false otherwise 49 | public fun has_role(acl: &ACL, member: address, role: u8): bool { 50 | assert!(role < 128, ERoleNumberTooLarge); 51 | linked_table::contains(&acl.permissions, member) && *linked_table::borrow( 52 | &acl.permissions, 53 | member 54 | ) & (1 << role) > 0 55 | } 56 | 57 | /// Set roles for a member in the ACL 58 | /// * `acl` - The ACL instance to update 59 | /// * `member` - The address of the member to set roles for 60 | /// * `permissions` - Permissions for the member, represented as a `u128` with each bit representing the presence of (or lack of) each role 61 | public fun set_roles(acl: &mut ACL, member: address, permissions: u128) { 62 | if (linked_table::contains(&acl.permissions, member)) { 63 | *linked_table::borrow_mut(&mut acl.permissions, member) = permissions 64 | } else { 65 | linked_table::push_back(&mut acl.permissions, member, permissions); 66 | } 67 | } 68 | 69 | /// Add a role for a member in the ACL 70 | /// * `acl` - The ACL instance to update 71 | /// * `member` - The address of the member to add the role to 72 | /// * `role` - The role to add 73 | public fun add_role(acl: &mut ACL, member: address, role: u8) { 74 | assert!(role < 128, ERoleNumberTooLarge); 75 | if (linked_table::contains(&acl.permissions, member)) { 76 | let perms = linked_table::borrow_mut(&mut acl.permissions, member); 77 | *perms = *perms | (1 << role); 78 | } else { 79 | linked_table::push_back(&mut acl.permissions, member, 1 << role); 80 | } 81 | } 82 | 83 | /// Revoke a role for a member in the ACL 84 | /// * `acl` - The ACL instance to update 85 | /// * `member` - The address of the member to remove the role from 86 | /// * `role` - The role to remove 87 | public fun remove_role(acl: &mut ACL, member: address, role: u8) { 88 | assert!(role < 128, ERoleNumberTooLarge); 89 | if (has_role(acl, member, role)) { 90 | let perms = linked_table::borrow_mut(&mut acl.permissions, member); 91 | *perms = *perms ^ (1 << role); 92 | }else{ 93 | abort ERoleNotFound 94 | } 95 | } 96 | 97 | /// Remove all roles of member 98 | /// * `acl` - The ACL instance to update 99 | /// * `member` - The address of the member to remove 100 | public fun remove_member(acl: &mut ACL, member: address) { 101 | if (linked_table::contains(&acl.permissions, member)) { 102 | let _ = linked_table::remove(&mut acl.permissions, member); 103 | }else{ 104 | abort EMemberNotFound 105 | } 106 | } 107 | 108 | /// Get all members 109 | /// * `acl` - The ACL instance to get members from 110 | /// Returns a vector of all members in the ACL 111 | public fun get_members(acl: &ACL): vector { 112 | let mut members = vector::empty(); 113 | let mut next_member_address = linked_table::head(&acl.permissions); 114 | while (is_some(&next_member_address)) { 115 | let address = *option::borrow(&next_member_address); 116 | let node = linked_table::borrow_node(&acl.permissions, address); 117 | vector::push_back( 118 | &mut members, 119 | Member { 120 | address, 121 | permission: *linked_table::borrow_value(node), 122 | }, 123 | ); 124 | next_member_address = linked_table::next(node); 125 | }; 126 | members 127 | } 128 | 129 | /// Get the permission of member by address 130 | /// * `acl` - The ACL instance to get permission from 131 | /// * `address` - The address of the member to get permission for 132 | /// Returns the permission of the member 133 | public fun get_permission(acl: &ACL, address: address): u128 { 134 | if (!linked_table::contains(&acl.permissions, address)) { 135 | 0 136 | } else { 137 | *linked_table::borrow(&acl.permissions, address) 138 | } 139 | } -------------------------------------------------------------------------------- /packages/cetus_clmm/restore.md: -------------------------------------------------------------------------------- 1 | # 🛠️ CLMM Pools Recovery Process 2 | 3 | ## Background 4 | 5 | Due to an exploit, some CLMM pools suffered partial fund loss and cannot be fully restored. Before reopening the pools for trading, several recovery steps are required to restore pool state and adjust user positions proportionally. 6 | 7 | This document outlines the steps and functions needed to perform the recovery process. 8 | 9 | --- 10 | 11 | ## 📋 Recovery Steps 12 | --- 13 | 14 | ### 1. Remove Malicious Positions 15 | 16 | **Function**: `emergency_remove_malicious_position` 17 | 18 | ```rust 19 | public fun emergency_remove_malicious_position( 20 | config: &mut GlobalConfig, 21 | pool: &mut Pool, 22 | position_id: ID, 23 | ctx: &mut TxContext 24 | ) 25 | ``` 26 | 27 | **Description**: 28 | 29 | * Removes malicious positions that were created during the attack. 30 | * Decreases the associated liquidity from both ticks of the position. 31 | * Can be called multiple times to remove multiple malicious positions. 32 | 33 | --- 34 | 35 | ### 2. Restore Pool State 36 | 37 | **Function**: `emergency_restore_pool_state` 38 | 39 | ```rust 40 | public fun emergency_restore_pool_state( 41 | config: &mut GlobalConfig, 42 | pool: &mut Pool, 43 | target_sqrt_price: u128, 44 | current_liquidity: u128, 45 | clk: &Clock, 46 | ctx: &mut TxContext 47 | ) 48 | ``` 49 | 50 | **Description**: 51 | 52 | * Brings the pool’s internal price and liquidity back to a known-good pre-attack state. 53 | * Swaps the pool to a `target_sqrt_price` representing the expected price. 54 | * Verifies that the resulting `current_liquidity` matches the expected value to ensure state consistency. 55 | 56 | --- 57 | 58 | ### 3. Inject Governance Funds 59 | 60 | **Function**: `governance_fund_injection` 61 | 62 | ```rust 63 | public fun governance_fund_injection( 64 | config: &mut GlobalConfig, 65 | pool: &mut Pool, 66 | coin_a: Coin, 67 | coin_b: Coin, 68 | ctx: &mut TxContext 69 | ) 70 | ``` 71 | 72 | **Description**: 73 | 74 | * Governance-only function. 75 | * Injects additional token liquidity into the pool to restore value lost during the attack. 76 | 77 | 78 | ### 4. Snapshot All Positions init 79 | 80 | **Function**: `init_position_snapshot` 81 | ```rust 82 | public entry fun init_position_snapshot( 83 | config: &GlobalConfig, 84 | pool: &mut Pool, 85 | remove_percent: u64, 86 | ctx: &mut TxContext, 87 | ) 88 | ``` 89 | **Argument**: 90 | 91 | ```rust 92 | remove_percent: u128 93 | ``` 94 | 95 | 96 | **Description**: 97 | 98 | * Since pool funds cannot be fully restored, each position’s liquidity must be reduced by a specified percentage before reopening. 99 | * This function snapshots the current state of each user’s position to preserve the original data, which is useful for future liquidation tracking or reconciliation. 100 | 101 | 102 | --- 103 | 104 | ### 5. Apply Liquidity Cut 105 | 106 | **Function**: `apply_liquidity_cut` 107 | 108 | ```rust 109 | public fun apply_liquidity_cut( 110 | config: &GlobalConfig, 111 | pool: &mut Pool, 112 | position_id: ID, 113 | clock: &Clock, 114 | ctx: &mut TxContext, 115 | ) 116 | ``` 117 | 118 | **Description**: 119 | 120 | * Applies a proportional reduction to a position’s liquidity based on the `remove_percent` specified earlier. 121 | * Must be called **after** restoring pool state and **before** reopening trading. 122 | * Snapshots the position before applying the cut to allow future auditing or accounting. 123 | 124 | 125 | ## 🔒 Fine-Grained Pool Controls 126 | 127 | 128 | ### ✨ Feature Overview 129 | 130 | To enhance the security and flexibility of CLMM operations, we introduce a fine-grained status control mechanism. This feature allows governance to selectively disable or enable specific functionalities on a per-pool basis. 131 | 132 | --- 133 | 134 | ## 🧱 Data Structures 135 | 136 | ### `Status` 137 | 138 | ```move 139 | struct Status has copy, drop, store { 140 | disable_add_liquidity: bool, 141 | disable_remove_liquidity: bool, 142 | disable_swap: bool, 143 | disable_flash_loan: bool, 144 | disable_collect_fee: bool, 145 | disable_collect_reward: bool, 146 | } 147 | ``` 148 | 149 | * Each field toggles a core operation of the CLMM pool. 150 | * `true` = disabled, `false` = enabled. 151 | 152 | ### `PoolStatus` 153 | 154 | ```move 155 | struct PoolStatus has key, store { 156 | id: UID, 157 | status: Status, 158 | } 159 | ``` 160 | 161 | * Maintains the status config per pool. 162 | * Stored and accessed via the pool dynamic field by name `pool_status` 163 | 164 | --- 165 | 166 | ## 🚦 Integration Points 167 | 168 | Each core pool method (e.g. `add_liquidity`, `swap`, `collect_fee`, etc.) must **check the corresponding status flag** before proceeding. 169 | 170 | ### Example (in `add_liquidity`): 171 | 172 | ```move 173 | assert!(is_allow_add_liquidity(pool), EOperationNotPermitted); 174 | ``` 175 | 176 | This pattern ensures that pool-level behaviors can be dynamically governed or paused. 177 | 178 | --- 179 | 180 | ## 🔐 Governance Control 181 | 182 | Only authorized governance (e.g., a multisig or acl role manager) should be allowed to: 183 | 184 | * Initialize `PoolStatus` 185 | * Update the `Status` values dynamically 186 | 187 | method: 188 | 189 | ```move 190 | public fun set_pool_status( 191 | config: &GlobalConfig, 192 | pool: &mut Pool, 193 | disable_add_liquidity: bool, 194 | disable_remove_liquidity: bool, 195 | disable_swap: bool, 196 | disable_flash_loan: bool, 197 | disable_collect_fee: bool, 198 | disable_collect_reward: bool, 199 | ctx: &mut TxContext, 200 | ) 201 | ``` 202 | 203 | --- -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/acl_tests.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::acl_tests; 3 | 4 | use cetus_clmm::acl; 5 | use cetus_clmm::acl::{add_role, remove_role, has_role, set_roles, get_members, get_permission, new}; 6 | 7 | public struct TestACL has key, store { 8 | id: UID, 9 | acl: ACL 10 | } 11 | 12 | use std::debug; 13 | use cetus_clmm::acl::ACL; 14 | use cetus_clmm::acl::remove_member; 15 | 16 | 17 | #[test] 18 | fun test_end_to_end() { 19 | let mut ctx = tx_context::dummy(); 20 | let mut acl = TestACL { 21 | id: object::new(&mut ctx), 22 | acl: acl::new(&mut ctx), 23 | }; 24 | 25 | add_role(&mut acl.acl, @0x1234, 12); 26 | add_role(&mut acl.acl, @0x1234, 99); 27 | add_role(&mut acl.acl, @0x1234, 88); 28 | add_role(&mut acl.acl, @0x1234, 123); 29 | add_role(&mut acl.acl, @0x1234, 2); 30 | add_role(&mut acl.acl, @0x1234, 1); 31 | remove_role(&mut acl.acl, @0x1234, 2); 32 | set_roles(&mut acl.acl, @0x5678, (1 << 123) | (1 << 2) | (1 << 1)); 33 | let mut i = 0; 34 | while (i < 128) { 35 | let mut has = has_role(&acl.acl, @0x1234, i); 36 | assert!(if (i == 12 || i == 99 || i == 88 || i == 123 || i == 1) has else !has, 0); 37 | has = has_role(&acl.acl, @0x5678, i); 38 | assert!(if (i == 123 || i == 2 || i == 1) has else !has, 1); 39 | i = i + 1; 40 | }; 41 | 42 | let members = get_members(&acl.acl); 43 | debug::print(&members); 44 | 45 | transfer::transfer(acl, tx_context::sender(&ctx)); 46 | } 47 | 48 | #[test] 49 | fun test_add_role_has_role() { 50 | let mut ctx = tx_context::dummy(); 51 | let mut acl = TestACL { 52 | id: object::new(&mut ctx), 53 | acl: new(&mut ctx), 54 | }; 55 | assert!(!has_role(&acl.acl, @0x1234, 9), 0); 56 | add_role(&mut acl.acl, @0x1234, 9); 57 | assert!(has_role(&acl.acl, @0x1234, 9), 1); 58 | transfer::transfer(acl, tx_context::sender(&ctx)); 59 | } 60 | 61 | #[test] 62 | fun test_remove_role() { 63 | let mut ctx = tx_context::dummy(); 64 | let mut acl = TestACL { 65 | id: object::new(&mut ctx), 66 | acl: new(&mut ctx), 67 | }; 68 | 69 | assert!(!has_role(&acl.acl, @0x1234, 9), 0); 70 | add_role(&mut acl.acl, @0x1234, 9); 71 | assert!(has_role(&acl.acl, @0x1234, 9), 1); 72 | remove_role(&mut acl.acl, @0x1234, 9); 73 | assert!(!has_role(&acl.acl, @0x1234, 9), 2); 74 | transfer::transfer(acl, tx_context::sender(&ctx)); 75 | } 76 | 77 | #[test] 78 | #[expected_failure(abort_code = cetus_clmm::acl::ERoleNotFound)] 79 | fun test_remove_role_not_exist() { 80 | let mut ctx = tx_context::dummy(); 81 | let mut acl = TestACL { 82 | id: object::new(&mut ctx), 83 | acl: new(&mut ctx), 84 | }; 85 | remove_role(&mut acl.acl, @0x1234, 9); 86 | transfer::transfer(acl, tx_context::sender(&ctx)); 87 | } 88 | #[test] 89 | fun test_set_role() { 90 | let mut ctx = tx_context::dummy(); 91 | let mut acl = TestACL { 92 | id: object::new(&mut ctx), 93 | acl: new(&mut ctx), 94 | }; 95 | add_role(&mut acl.acl, @0x1234, 10); 96 | set_roles(&mut acl.acl, @0x1234, 5); 97 | assert!(!has_role(&acl.acl, @0x1234, 10), 0); 98 | assert!(has_role(&acl.acl, @0x1234, 0), 1); 99 | assert!(has_role(&acl.acl, @0x1234, 2), 2); 100 | assert!(get_permission(&acl.acl, @0x1234) == 5, 3); 101 | transfer::transfer(acl, tx_context::sender(&ctx)); 102 | } 103 | 104 | #[test] 105 | fun test_remove_member() { 106 | let mut ctx = tx_context::dummy(); 107 | let mut acl = TestACL { 108 | id: object::new(&mut ctx), 109 | acl: new(&mut ctx), 110 | }; 111 | add_role(&mut acl.acl, @0x1234, 10); 112 | add_role(&mut acl.acl, @0x5678, 10); 113 | assert!(has_role(&acl.acl, @0x5678, 10), 1); 114 | assert!(has_role(&acl.acl, @0x1234, 10), 2); 115 | remove_member(&mut acl.acl, @0x1234); 116 | assert!(!has_role(&acl.acl, @0x1234, 10), 2); 117 | assert!(has_role(&acl.acl, @0x5678, 10), 3); 118 | transfer::transfer(acl, tx_context::sender(&ctx)); 119 | } 120 | 121 | #[test] 122 | #[expected_failure(abort_code = cetus_clmm::acl::EMemberNotFound)] 123 | fun test_remove_member_not_exist() { 124 | let mut ctx = tx_context::dummy(); 125 | let mut acl = TestACL { 126 | id: object::new(&mut ctx), 127 | acl: new(&mut ctx), 128 | }; 129 | remove_member(&mut acl.acl, @0x1234); 130 | transfer::transfer(acl, tx_context::sender(&ctx)); 131 | } 132 | 133 | #[test] 134 | #[expected_failure(abort_code = cetus_clmm::acl::ERoleNumberTooLarge)] 135 | fun test_role_number_too_large() { 136 | let mut ctx = tx_context::dummy(); 137 | let mut acl = TestACL { 138 | id: object::new(&mut ctx), 139 | acl: new(&mut ctx), 140 | }; 141 | add_role(&mut acl.acl, @0x1234, 1); 142 | assert!(acl.acl.has_role(@0x1234, 128), 1); 143 | transfer::transfer(acl, tx_context::sender(&ctx)); 144 | } 145 | 146 | #[test] 147 | #[expected_failure(abort_code = cetus_clmm::acl::ERoleNumberTooLarge)] 148 | fun test_add_role_number_too_large() { 149 | let mut ctx = tx_context::dummy(); 150 | let mut acl = TestACL { 151 | id: object::new(&mut ctx), 152 | acl: new(&mut ctx), 153 | }; 154 | add_role(&mut acl.acl, @0x1234, 128); 155 | transfer::transfer(acl, tx_context::sender(&ctx)); 156 | } 157 | 158 | #[test] 159 | #[expected_failure(abort_code = cetus_clmm::acl::ERoleNumberTooLarge)] 160 | fun test_remove_role_number_too_large() { 161 | let mut ctx = tx_context::dummy(); 162 | let mut acl = TestACL { 163 | id: object::new(&mut ctx), 164 | acl: new(&mut ctx), 165 | }; 166 | remove_role(&mut acl.acl, @0x1234, 128); 167 | transfer::transfer(acl, tx_context::sender(&ctx)); 168 | } 169 | 170 | #[test] 171 | fun test_get_permission() { 172 | let mut ctx = tx_context::dummy(); 173 | let mut acl = TestACL { 174 | id: object::new(&mut ctx), 175 | acl: new(&mut ctx), 176 | }; 177 | add_role(&mut acl.acl, @0x1234, 10); 178 | assert!(get_permission(&acl.acl, @0x1234) == 1 << 10, 1); 179 | assert!(get_permission(&acl.acl, @0x1235) == 0, 2); 180 | transfer::transfer(acl, tx_context::sender(&ctx)); 181 | } -------------------------------------------------------------------------------- /packages/cetus_clmm/sources/pool_creator.move: -------------------------------------------------------------------------------- 1 | module cetus_clmm::pool_creator; 2 | 3 | use cetus_clmm::config::GlobalConfig; 4 | use cetus_clmm::factory::{Self, Pools, permission_pair_cap, PoolCreationCap}; 5 | use cetus_clmm::position::Position; 6 | use cetus_clmm::tick_math::{Self, get_sqrt_price_at_tick}; 7 | use integer_mate::i32; 8 | use std::string::String; 9 | use sui::clock::Clock; 10 | use sui::coin::{Self, Coin, CoinMetadata}; 11 | 12 | const EPoolIsPermission: u64 = 1; 13 | #[allow(unused_const)] 14 | const EInvalidTickLower: u64 = 2; 15 | #[allow(unused_const)] 16 | const EInvalidTickUpper: u64 = 3; 17 | const ECapNotMatchWithPoolKey: u64 = 4; 18 | const EInitSqrtPriceNotBetweenLowerAndUpper: u64 = 5; 19 | const EMethodDeprecated: u64 = 6; 20 | 21 | /// DEPRECATED 22 | public fun create_pool_v2_by_creation_cap( 23 | _config: &GlobalConfig, 24 | _pools: &mut Pools, 25 | _cap: &PoolCreationCap, 26 | _tick_spacing: u32, 27 | _initialize_price: u128, 28 | _url: String, 29 | _coin_a: Coin, 30 | _coin_b: Coin, 31 | _metadata_a: &CoinMetadata, 32 | _metadata_b: &CoinMetadata, 33 | _fix_amount_a: bool, 34 | _clock: &Clock, 35 | _ctx: &mut TxContext, 36 | ): (Position, Coin, Coin) { 37 | abort EMethodDeprecated 38 | } 39 | 40 | #[allow(unused_variable)] 41 | public fun create_pool_v2_with_creation_cap( 42 | config: &GlobalConfig, 43 | pools: &mut Pools, 44 | cap: &PoolCreationCap, 45 | tick_spacing: u32, 46 | initialize_price: u128, 47 | url: String, 48 | tick_lower_idx: u32, 49 | tick_upper_idx: u32, 50 | coin_a: Coin, 51 | coin_b: Coin, 52 | metadata_a: &CoinMetadata, 53 | metadata_b: &CoinMetadata, 54 | fix_amount_a: bool, 55 | clock: &Clock, 56 | ctx: &mut TxContext, 57 | ): (Position, Coin, Coin) { 58 | abort EMethodDeprecated 59 | } 60 | 61 | #[allow(unused_variable)] 62 | public fun create_pool_v2( 63 | config: &GlobalConfig, 64 | pools: &mut Pools, 65 | tick_spacing: u32, 66 | initialize_price: u128, 67 | url: String, 68 | tick_lower_idx: u32, 69 | tick_upper_idx: u32, 70 | coin_a: Coin, 71 | coin_b: Coin, 72 | metadata_a: &CoinMetadata, 73 | metadata_b: &CoinMetadata, 74 | fix_amount_a: bool, 75 | clock: &Clock, 76 | ctx: &mut TxContext, 77 | ): (Position, Coin, Coin){ 78 | abort EMethodDeprecated 79 | } 80 | /// Create pool with creation cap 81 | /// * `config` - The global configuration 82 | /// * `pools` - The mutable reference to the `Pools` object 83 | /// * `cap` - The reference to the `PoolCreationCap` object 84 | /// * `tick_spacing` - The tick spacing 85 | /// * `initialize_price` - The initial price 86 | /// * `url` - The URL of the pool 87 | /// * `tick_lower_idx` - The lower tick index 88 | /// * `tick_upper_idx` - The upper tick index 89 | /// * `coin_a` - The coin A 90 | /// * `coin_b` - The coin B 91 | /// * `fix_amount_a` - Whether to fix the amount of the coin A 92 | /// * `clock` - The clock object 93 | /// * `ctx` - The transaction context 94 | /// * Returns the position, coin A, and coin B 95 | public fun create_pool_v3_with_creation_cap( 96 | config: &GlobalConfig, 97 | pools: &mut Pools, 98 | cap: &PoolCreationCap, 99 | tick_spacing: u32, 100 | initialize_price: u128, 101 | url: String, 102 | tick_lower_idx: u32, 103 | tick_upper_idx: u32, 104 | coin_a: Coin, 105 | coin_b: Coin, 106 | fix_amount_a: bool, 107 | clock: &Clock, 108 | ctx: &mut TxContext, 109 | ): (Position, Coin, Coin) { 110 | let (amount_a, amount_b) = (coin::value(&coin_a), coin::value(&coin_b)); 111 | assert!( 112 | permission_pair_cap(pools, tick_spacing) == object::id(cap), 113 | ECapNotMatchWithPoolKey, 114 | ); 115 | let lower_sqrt_price = get_sqrt_price_at_tick(i32::from_u32(tick_lower_idx)); 116 | let upper_sqrt_price = get_sqrt_price_at_tick(i32::from_u32(tick_upper_idx)); 117 | assert!( 118 | lower_sqrt_price < initialize_price && upper_sqrt_price > initialize_price, 119 | EInitSqrtPriceNotBetweenLowerAndUpper, 120 | ); 121 | 122 | let (position, coin_a, coin_b) = factory::create_pool_v3_( 123 | config, 124 | pools, 125 | tick_spacing, 126 | initialize_price, 127 | url, 128 | tick_lower_idx, 129 | tick_upper_idx, 130 | coin_a, 131 | coin_b, 132 | amount_a, 133 | amount_b, 134 | fix_amount_a, 135 | clock, 136 | ctx, 137 | ); 138 | (position, coin_a, coin_b) 139 | } 140 | 141 | /// Create pool with custom tick range 142 | /// * `config` - The global configuration 143 | /// * `pools` - The mutable reference to the `Pools` object 144 | /// * `tick_spacing` - The tick spacing 145 | /// * `initialize_price` - The initial price 146 | /// * `url` - The URL of the pool 147 | /// * `tick_lower_idx` - The lower tick index 148 | /// * `tick_upper_idx` - The upper tick index 149 | /// * `coin_a` - The coin A 150 | /// * `coin_b` - The coin B 151 | /// * `fix_amount_a` - Whether to fix the amount of the coin A 152 | /// * `clock` - The clock object 153 | /// * `ctx` - The transaction context 154 | /// * Returns the position, coin A, and coin B 155 | public fun create_pool_v3( 156 | config: &GlobalConfig, 157 | pools: &mut Pools, 158 | tick_spacing: u32, 159 | initialize_price: u128, 160 | url: String, 161 | tick_lower_idx: u32, 162 | tick_upper_idx: u32, 163 | coin_a: Coin, 164 | coin_b: Coin, 165 | fix_amount_a: bool, 166 | clock: &Clock, 167 | ctx: &mut TxContext, 168 | ): (Position, Coin, Coin) { 169 | let lower_sqrt_price = get_sqrt_price_at_tick(i32::from_u32(tick_lower_idx)); 170 | let upper_sqrt_price = get_sqrt_price_at_tick(i32::from_u32(tick_upper_idx)); 171 | assert!( 172 | lower_sqrt_price < initialize_price && upper_sqrt_price > initialize_price, 173 | EInitSqrtPriceNotBetweenLowerAndUpper, 174 | ); 175 | 176 | assert!( 177 | !factory::is_permission_pair(pools, tick_spacing), 178 | EPoolIsPermission, 179 | ); 180 | let (amount_a, amount_b) = (coin::value(&coin_a), coin::value(&coin_b)); 181 | let (position, coin_a, coin_b) = factory::create_pool_v3_( 182 | config, 183 | pools, 184 | tick_spacing, 185 | initialize_price, 186 | url, 187 | tick_lower_idx, 188 | tick_upper_idx, 189 | coin_a, 190 | coin_b, 191 | amount_a, 192 | amount_b, 193 | fix_amount_a, 194 | clock, 195 | ctx, 196 | ); 197 | (position, coin_a, coin_b) 198 | } 199 | 200 | /// Get the full range tick range 201 | /// * `tick_spacing` - The tick spacing 202 | /// * Returns the full range tick range 203 | public fun full_range_tick_range(tick_spacing: u32): (u32, u32) { 204 | let mod = i32::from_u32(tick_math::tick_bound() % tick_spacing); 205 | let full_min_tick = i32::add(tick_math::min_tick(), mod); 206 | let full_max_tick = i32::sub(tick_math::max_tick(), mod); 207 | (i32::as_u32(full_min_tick), i32::as_u32(full_max_tick)) 208 | } 209 | -------------------------------------------------------------------------------- /packages/cetus_clmm/sources/position_snapshot.move: -------------------------------------------------------------------------------- 1 | module cetus_clmm::position_snapshot; 2 | 3 | use cetus_clmm::position::{Self, PositionInfo}; 4 | use integer_mate::full_math_u128; 5 | use integer_mate::i32::I32; 6 | use move_stl::linked_table; 7 | 8 | // Parts Per Million 9 | const PPM: u64 = 1000000; 10 | 11 | const EPositionSnapshotAlreadyExists: u64 = 1; 12 | const EPositionSnapshotNotFound: u64 = 2; 13 | 14 | /// PositionLiquiditySnapshot struct that stores the snapshot of the position 15 | /// * `id` - The unique identifier for this PositionLiquiditySnapshot object 16 | /// * `current_sqrt_price` - The current sqrt price 17 | /// * `remove_percent` - The remove percent 18 | /// * `total_value_cut` - The total value cut 19 | /// * `snapshots` - A linked table storing the snapshots of the position 20 | public struct PositionLiquiditySnapshot has key, store { 21 | id: UID, 22 | current_sqrt_price: u128, 23 | remove_percent: u64, 24 | total_value_cut: u64, 25 | snapshots: linked_table::LinkedTable, 26 | } 27 | 28 | /// PositionSnapshot of a position 29 | /// * `position_id` - position id 30 | /// * `liquidity` - liquidity of the position 31 | /// * `tick_lower_index` - lower tick index 32 | /// * `tick_upper_index` - upper tick index 33 | /// * `fee_owned_a` - The fee owned by the position a 34 | /// * `fee_owned_b` - The fee owned by the position b 35 | /// * `rewards` - The rewards of the position 36 | /// * `value_cut` - The value cut of the position 37 | public struct PositionSnapshot has copy, drop, store { 38 | position_id: ID, 39 | liquidity: u128, 40 | tick_lower_index: I32, 41 | tick_upper_index: I32, 42 | fee_owned_a: u64, 43 | fee_owned_b: u64, 44 | rewards: vector, 45 | value_cut: u64, 46 | } 47 | 48 | /// Create a new PositionLiquiditySnapshot 49 | /// * `current_sqrt_price` - The current sqrt price 50 | /// * `remove_percent` - The remove percent 51 | /// * `ctx` - The transaction context 52 | /// * Returns a new PositionLiquiditySnapshot 53 | public(package) fun new( 54 | current_sqrt_price: u128, 55 | remove_percent: u64, 56 | ctx: &mut TxContext, 57 | ): PositionLiquiditySnapshot { 58 | PositionLiquiditySnapshot { 59 | id: object::new(ctx), 60 | current_sqrt_price, 61 | remove_percent, 62 | total_value_cut: 0, 63 | snapshots: linked_table::new(ctx), 64 | } 65 | } 66 | 67 | /// Get the remove percent of the PositionLiquiditySnapshot 68 | /// * `snapshot` - The PositionLiquiditySnapshot 69 | /// * Returns the remove percent 70 | public fun remove_percent(snapshot: &PositionLiquiditySnapshot): u64 { 71 | snapshot.remove_percent 72 | } 73 | 74 | /// Get the current sqrt price of the PositionLiquiditySnapshot 75 | /// * `snapshot` - The PositionLiquiditySnapshot 76 | /// * Returns the current sqrt price 77 | public fun current_sqrt_price(snapshot: &PositionLiquiditySnapshot): u128 { 78 | snapshot.current_sqrt_price 79 | } 80 | 81 | /// Get the total value cut of the PositionLiquiditySnapshot 82 | /// * `snapshot` - The PositionLiquiditySnapshot 83 | /// * Returns the total value cut 84 | public fun total_value_cut(snapshot: &PositionLiquiditySnapshot): u64 { 85 | snapshot.total_value_cut 86 | } 87 | 88 | /// Get the value cut of the PositionSnapshot 89 | /// * `snapshot` - The PositionSnapshot 90 | /// * Returns the value cut 91 | public fun value_cut(snapshot: &PositionSnapshot): u64 { 92 | snapshot.value_cut 93 | } 94 | 95 | /// Get the rewards of the PositionSnapshot 96 | /// * `snapshot` - The PositionSnapshot 97 | /// * Returns the rewards 98 | public fun rewards(snapshot: &PositionSnapshot): vector { 99 | snapshot.rewards 100 | } 101 | 102 | /// Get the fee owned of the PositionSnapshot 103 | /// * `snapshot` - The PositionSnapshot 104 | /// * Returns 105 | public fun fee_owned(snapshot: &PositionSnapshot): (u64, u64) { 106 | (snapshot.fee_owned_a, snapshot.fee_owned_b) 107 | } 108 | 109 | /// Get the tick range of the PositionSnapshot 110 | /// * `snapshot` - The PositionSnapshot 111 | /// * Returns the tick range 112 | public fun tick_range(snapshot: &PositionSnapshot): (I32, I32) { 113 | (snapshot.tick_lower_index, snapshot.tick_upper_index) 114 | } 115 | 116 | /// Get the liquidity of the PositionSnapshot 117 | /// * `snapshot` - The PositionSnapshot 118 | /// * Returns the liquidity 119 | public fun liquidity(snapshot: &PositionSnapshot): u128 { 120 | snapshot.liquidity 121 | } 122 | 123 | /// Get the position id of the PositionSnapshot 124 | /// * `snapshot` - The PositionSnapshot 125 | /// * Returns the position id 126 | public fun position_id(snapshot: &PositionSnapshot): ID { 127 | snapshot.position_id 128 | } 129 | 130 | /// Calculate the remove liquidity 131 | /// * `snapshot` - The PositionLiquiditySnapshot 132 | /// * `position_info` - The position info 133 | /// * Returns the remove liquidity 134 | public fun calculate_remove_liquidity( 135 | snapshot: &PositionLiquiditySnapshot, 136 | position_info: &PositionInfo, 137 | ): u128 { 138 | let liquidity = position::info_liquidity(position_info); 139 | full_math_u128::mul_div_ceil((snapshot.remove_percent as u128), liquidity, (PPM as u128)) 140 | } 141 | 142 | /// Add a new PositionSnapshot to the PositionLiquiditySnapshot 143 | /// * `snapshot` - The PositionLiquiditySnapshot 144 | /// * `position_id` - The position id 145 | /// * `value_cut` - The value cut 146 | /// * `position_info` - The position info 147 | public(package) fun add( 148 | snapshot: &mut PositionLiquiditySnapshot, 149 | position_id: ID, 150 | value_cut: u64, 151 | position_info: PositionInfo, 152 | ) { 153 | assert!( 154 | !linked_table::contains(&snapshot.snapshots, position_id), 155 | EPositionSnapshotAlreadyExists, 156 | ); 157 | let mut rewards = vector::empty(); 158 | let mut idx = 0; 159 | let reward_infos = position::info_rewards(&position_info); 160 | while (idx < vector::length(reward_infos)) { 161 | let reward = vector::borrow(reward_infos, idx); 162 | let amount_owned = position::reward_amount_owned(reward); 163 | vector::push_back(&mut rewards, amount_owned); 164 | idx = idx + 1; 165 | }; 166 | let (tick_lower_index, tick_upper_index) = position::info_tick_range(&position_info); 167 | let (fee_owned_a, fee_owned_b) = position::info_fee_owned(&position_info); 168 | let position_snapshot = PositionSnapshot { 169 | position_id, 170 | liquidity: position::info_liquidity(&position_info), 171 | tick_lower_index, 172 | tick_upper_index, 173 | fee_owned_a, 174 | fee_owned_b, 175 | rewards, 176 | value_cut, 177 | }; 178 | snapshot.total_value_cut = snapshot.total_value_cut + value_cut; 179 | linked_table::push_back(&mut snapshot.snapshots, position_id, position_snapshot); 180 | } 181 | 182 | /// Get the PositionSnapshot from the PositionLiquiditySnapshot 183 | /// * `snapshot` - The PositionLiquiditySnapshot 184 | /// * `position_id` - The position id 185 | /// * Returns the PositionSnapshot 186 | public fun get(snapshot: &PositionLiquiditySnapshot, position_id: ID): PositionSnapshot { 187 | assert!(linked_table::contains(&snapshot.snapshots, position_id), EPositionSnapshotNotFound); 188 | *linked_table::borrow(&snapshot.snapshots, position_id) 189 | } 190 | 191 | /// Check if the PositionLiquiditySnapshot contains the PositionSnapshot 192 | /// * `snapshot` - The PositionLiquiditySnapshot 193 | /// * `position_id` - The position id 194 | /// * Returns true if the PositionLiquiditySnapshot contains the PositionSnapshot for the position id, false otherwise 195 | public fun contains(snapshot: &PositionLiquiditySnapshot, position_id: ID): bool { 196 | linked_table::contains(&snapshot.snapshots, position_id) 197 | } 198 | 199 | /// Remove the PositionSnapshot from the PositionLiquiditySnapshot 200 | /// * `snapshot` - The PositionLiquiditySnapshot 201 | /// * `position_id` - The position id 202 | public(package) fun remove(snapshot: &mut PositionLiquiditySnapshot, position_id: ID) { 203 | assert!(linked_table::contains(&snapshot.snapshots, position_id), EPositionSnapshotNotFound); 204 | let position_snapshot = linked_table::remove(&mut snapshot.snapshots, position_id); 205 | snapshot.total_value_cut = snapshot.total_value_cut - position_snapshot.value_cut; 206 | } 207 | -------------------------------------------------------------------------------- /packages/cetus_clmm/sources/math/tick_math.move: -------------------------------------------------------------------------------- 1 | module cetus_clmm::tick_math; 2 | 3 | use integer_mate::full_math_u128; 4 | use integer_mate::i128; 5 | use integer_mate::i32::{Self, I32}; 6 | 7 | const TICK_BOUND: u32 = 443636; 8 | 9 | const MAX_SQRT_PRICE_X64: u128 = 79226673515401279992447579055; 10 | 11 | const MIN_SQRT_PRICE_X64: u128 = 4295048016; 12 | 13 | /// Error codes for the tick_math module 14 | /// When tick is out of bounds [-443636, 443636] 15 | const EINVALID_TICK: u64 = 1; 16 | /// When sqrt_price is out of bounds [4295048016, 79226673515401279992447579055] 17 | const EINVALID_SQRT_PRICE: u64 = 2; 18 | /// When tick_spacing is 0 19 | const EINVALID_TICK_SPACING: u64 = 3; 20 | 21 | public fun max_sqrt_price(): u128 { 22 | MAX_SQRT_PRICE_X64 23 | } 24 | 25 | public fun min_sqrt_price(): u128 { 26 | MIN_SQRT_PRICE_X64 27 | } 28 | 29 | public fun max_tick(): i32::I32 { 30 | i32::from(TICK_BOUND) 31 | } 32 | 33 | public fun min_tick(): i32::I32 { 34 | i32::neg_from(TICK_BOUND) 35 | } 36 | 37 | public fun tick_bound(): u32 { 38 | TICK_BOUND 39 | } 40 | 41 | public fun get_sqrt_price_at_tick(tick: i32::I32): u128 { 42 | assert!(i32::gte(tick, min_tick()) && i32::lte(tick, max_tick()), EINVALID_TICK); 43 | if (i32::is_neg(tick)) { 44 | get_sqrt_price_at_negative_tick(tick) 45 | } else { 46 | get_sqrt_price_at_positive_tick(tick) 47 | } 48 | } 49 | 50 | public fun is_valid_index(index: I32, tick_spacing: u32): bool { 51 | assert!(tick_spacing > 0, EINVALID_TICK_SPACING); 52 | let in_range = i32::gte(index, min_tick()) && i32::lte(index, max_tick()); 53 | in_range && (i32::mod(index, i32::from(tick_spacing)) == i32::from(0)) 54 | } 55 | 56 | #[allow(dead_code)] 57 | public fun get_tick_at_sqrt_price(sqrt_price: u128): i32::I32 { 58 | assert!( 59 | sqrt_price >= MIN_SQRT_PRICE_X64 && sqrt_price <= MAX_SQRT_PRICE_X64, 60 | EINVALID_SQRT_PRICE, 61 | ); 62 | let mut r = sqrt_price; 63 | let mut msb = 0; 64 | 65 | let mut f: u8 = as_u8(r >= 0x10000000000000000) << 6; // If r >= 2^64, f = 64 else 0 66 | msb = msb | f; 67 | r = r >> f; 68 | f = as_u8(r >= 0x100000000) << 5; // 2^32 69 | msb = msb | f; 70 | r = r >> f; 71 | f = as_u8(r >= 0x10000) << 4; // 2^16 72 | msb = msb | f; 73 | r = r >> f; 74 | f = as_u8(r >= 0x100) << 3; // 2^8 75 | msb = msb | f; 76 | r = r >> f; 77 | f = as_u8(r >= 0x10) << 2; // 2^4 78 | msb = msb | f; 79 | r = r >> f; 80 | f = as_u8(r >= 0x4) << 1; // 2^2 81 | msb = msb | f; 82 | r = r >> f; 83 | f = as_u8(r >= 0x2) << 0; // 2^0 84 | msb = msb | f; 85 | 86 | let mut log_2_x32 = i128::shl(i128::sub(i128::from((msb as u128)), i128::from(64)), 32); 87 | 88 | r = if (msb >= 64) { 89 | sqrt_price >> (msb - 63) 90 | } else { 91 | sqrt_price << (63 - msb) 92 | }; 93 | 94 | let mut shift = 31; 95 | while (shift >= 18) { 96 | r = ((r * r) >> 63); 97 | f = ((r >> 64) as u8); 98 | log_2_x32 = i128::or(log_2_x32, i128::shl(i128::from((f as u128)), shift)); 99 | r = r >> f; 100 | shift = shift - 1; 101 | }; 102 | 103 | let log_sqrt_10001 = i128::mul(log_2_x32, i128::from(59543866431366u128)); 104 | 105 | let tick_low = i128::as_i32( 106 | i128::shr(i128::sub(log_sqrt_10001, i128::from(184467440737095516u128)), 64), 107 | ); 108 | let tick_high = i128::as_i32( 109 | i128::shr(i128::add(log_sqrt_10001, i128::from(15793534762490258745u128)), 64), 110 | ); 111 | 112 | if (i32::eq(tick_low, tick_high)) { 113 | return tick_low 114 | } else if (get_sqrt_price_at_tick(tick_high) <= sqrt_price) { 115 | return tick_high 116 | } else { 117 | return tick_low 118 | } 119 | } 120 | 121 | fun as_u8(b: bool): u8 { 122 | if (b) { 123 | 1 124 | } else { 125 | 0 126 | } 127 | } 128 | 129 | fun get_sqrt_price_at_negative_tick(tick: i32::I32): u128 { 130 | let abs_tick = i32::as_u32(i32::abs(tick)); 131 | let mut ratio = if (abs_tick & 0x1 != 0) { 132 | 18445821805675392311u128 133 | } else { 134 | 18446744073709551616u128 135 | }; 136 | if (abs_tick & 0x2 != 0) { 137 | ratio = full_math_u128::mul_shr(ratio, 18444899583751176498u128, 64u8) 138 | }; 139 | if (abs_tick & 0x4 != 0) { 140 | ratio = full_math_u128::mul_shr(ratio, 18443055278223354162u128, 64u8); 141 | }; 142 | if (abs_tick & 0x8 != 0) { 143 | ratio = full_math_u128::mul_shr(ratio, 18439367220385604838u128, 64u8); 144 | }; 145 | if (abs_tick & 0x10 != 0) { 146 | ratio = full_math_u128::mul_shr(ratio, 18431993317065449817u128, 64u8); 147 | }; 148 | if (abs_tick & 0x20 != 0) { 149 | ratio = full_math_u128::mul_shr(ratio, 18417254355718160513u128, 64u8); 150 | }; 151 | if (abs_tick & 0x40 != 0) { 152 | ratio = full_math_u128::mul_shr(ratio, 18387811781193591352u128, 64u8); 153 | }; 154 | if (abs_tick & 0x80 != 0) { 155 | ratio = full_math_u128::mul_shr(ratio, 18329067761203520168u128, 64u8); 156 | }; 157 | if (abs_tick & 0x100 != 0) { 158 | ratio = full_math_u128::mul_shr(ratio, 18212142134806087854u128, 64u8); 159 | }; 160 | if (abs_tick & 0x200 != 0) { 161 | ratio = full_math_u128::mul_shr(ratio, 17980523815641551639u128, 64u8); 162 | }; 163 | if (abs_tick & 0x400 != 0) { 164 | ratio = full_math_u128::mul_shr(ratio, 17526086738831147013u128, 64u8); 165 | }; 166 | if (abs_tick & 0x800 != 0) { 167 | ratio = full_math_u128::mul_shr(ratio, 16651378430235024244u128, 64u8); 168 | }; 169 | if (abs_tick & 0x1000 != 0) { 170 | ratio = full_math_u128::mul_shr(ratio, 15030750278693429944u128, 64u8); 171 | }; 172 | if (abs_tick & 0x2000 != 0) { 173 | ratio = full_math_u128::mul_shr(ratio, 12247334978882834399u128, 64u8); 174 | }; 175 | if (abs_tick & 0x4000 != 0) { 176 | ratio = full_math_u128::mul_shr(ratio, 8131365268884726200u128, 64u8); 177 | }; 178 | if (abs_tick & 0x8000 != 0) { 179 | ratio = full_math_u128::mul_shr(ratio, 3584323654723342297u128, 64u8); 180 | }; 181 | if (abs_tick & 0x10000 != 0) { 182 | ratio = full_math_u128::mul_shr(ratio, 696457651847595233u128, 64u8); 183 | }; 184 | if (abs_tick & 0x20000 != 0) { 185 | ratio = full_math_u128::mul_shr(ratio, 26294789957452057u128, 64u8); 186 | }; 187 | if (abs_tick & 0x40000 != 0) { 188 | ratio = full_math_u128::mul_shr(ratio, 37481735321082u128, 64u8); 189 | }; 190 | 191 | ratio 192 | } 193 | 194 | fun get_sqrt_price_at_positive_tick(tick: i32::I32): u128 { 195 | let abs_tick = i32::as_u32(i32::abs(tick)); 196 | let mut ratio = if (abs_tick & 0x1 != 0) { 197 | 79232123823359799118286999567u128 198 | } else { 199 | 79228162514264337593543950336u128 200 | }; 201 | 202 | if (abs_tick & 0x2 != 0) { 203 | ratio = full_math_u128::mul_shr(ratio, 79236085330515764027303304731u128, 96u8) 204 | }; 205 | if (abs_tick & 0x4 != 0) { 206 | ratio = full_math_u128::mul_shr(ratio, 79244008939048815603706035061u128, 96u8) 207 | }; 208 | if (abs_tick & 0x8 != 0) { 209 | ratio = full_math_u128::mul_shr(ratio, 79259858533276714757314932305u128, 96u8) 210 | }; 211 | if (abs_tick & 0x10 != 0) { 212 | ratio = full_math_u128::mul_shr(ratio, 79291567232598584799939703904u128, 96u8) 213 | }; 214 | if (abs_tick & 0x20 != 0) { 215 | ratio = full_math_u128::mul_shr(ratio, 79355022692464371645785046466u128, 96u8) 216 | }; 217 | if (abs_tick & 0x40 != 0) { 218 | ratio = full_math_u128::mul_shr(ratio, 79482085999252804386437311141u128, 96u8) 219 | }; 220 | if (abs_tick & 0x80 != 0) { 221 | ratio = full_math_u128::mul_shr(ratio, 79736823300114093921829183326u128, 96u8) 222 | }; 223 | if (abs_tick & 0x100 != 0) { 224 | ratio = full_math_u128::mul_shr(ratio, 80248749790819932309965073892u128, 96u8) 225 | }; 226 | if (abs_tick & 0x200 != 0) { 227 | ratio = full_math_u128::mul_shr(ratio, 81282483887344747381513967011u128, 96u8) 228 | }; 229 | if (abs_tick & 0x400 != 0) { 230 | ratio = full_math_u128::mul_shr(ratio, 83390072131320151908154831281u128, 96u8) 231 | }; 232 | if (abs_tick & 0x800 != 0) { 233 | ratio = full_math_u128::mul_shr(ratio, 87770609709833776024991924138u128, 96u8) 234 | }; 235 | if (abs_tick & 0x1000 != 0) { 236 | ratio = full_math_u128::mul_shr(ratio, 97234110755111693312479820773u128, 96u8) 237 | }; 238 | if (abs_tick & 0x2000 != 0) { 239 | ratio = full_math_u128::mul_shr(ratio, 119332217159966728226237229890u128, 96u8) 240 | }; 241 | if (abs_tick & 0x4000 != 0) { 242 | ratio = full_math_u128::mul_shr(ratio, 179736315981702064433883588727u128, 96u8) 243 | }; 244 | if (abs_tick & 0x8000 != 0) { 245 | ratio = full_math_u128::mul_shr(ratio, 407748233172238350107850275304u128, 96u8) 246 | }; 247 | if (abs_tick & 0x10000 != 0) { 248 | ratio = full_math_u128::mul_shr(ratio, 2098478828474011932436660412517u128, 96u8) 249 | }; 250 | if (abs_tick & 0x20000 != 0) { 251 | ratio = full_math_u128::mul_shr(ratio, 55581415166113811149459800483533u128, 96u8) 252 | }; 253 | if (abs_tick & 0x40000 != 0) { 254 | ratio = full_math_u128::mul_shr(ratio, 38992368544603139932233054999993551u128, 96u8) 255 | }; 256 | 257 | ratio >> 32 258 | } 259 | 260 | -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/pool_restore_tests.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::pool_restore_tests; 3 | 4 | use cetus_clmm::config; 5 | use cetus_clmm::pool; 6 | use sui::test_scenario; 7 | use integer_mate::i32; 8 | use cetus_clmm::tick_math::get_sqrt_price_at_tick; 9 | use std::string; 10 | use sui::clock; 11 | use cetus_clmm::pool_tests::add_liquidity; 12 | use sui::clock::Clock; 13 | use cetus_clmm::config::AdminCap; 14 | use cetus_clmm::config::GlobalConfig; 15 | use cetus_clmm::pool::Pool; 16 | use cetus_clmm::position::Position; 17 | use sui::test_scenario::Scenario; 18 | use std::unit_test::assert_eq; 19 | use sui::coin; 20 | 21 | public struct CoinA {} 22 | public struct CoinB {} 23 | 24 | const EMERGENCY_RESTORE_NEED_VERSION: u64 = 9223372036854775807; 25 | const EMERGENCY_RESTORE_VERSION: u64 = 18446744073709551000; 26 | 27 | fun prepare(): (Clock, AdminCap, GlobalConfig, Pool, Position, Scenario) { 28 | let mut scenerio = test_scenario::begin(@0x1234); 29 | let ctx = scenerio.ctx(); 30 | let (cap, config) = config::new_global_config_for_test( 31 | ctx, 32 | 2000, 33 | ); 34 | let clock = clock::create_for_testing(ctx); 35 | let mut pool = pool::new_for_test( 36 | 100, 37 | get_sqrt_price_at_tick(i32::zero()), 38 | 2000, 39 | string::utf8(b""), 40 | 0, 41 | &clock, 42 | ctx, 43 | ); 44 | let (tick_lower, tick_upper) = (i32::neg_from(1000), i32::from(1000)); 45 | let mut position_nft = pool::open_position( 46 | &config, 47 | &mut pool, 48 | i32::as_u32(tick_lower), 49 | i32::as_u32(tick_upper), 50 | ctx, 51 | ); 52 | add_liquidity(&config, &mut pool, &mut position_nft, 100000000, &clock); 53 | (clock, cap, config, pool, position_nft, scenerio) 54 | } 55 | 56 | #[test] 57 | #[expected_failure(abort_code = cetus_clmm::pool::EDeprecatedFunction)] 58 | fun test_emergency_restore(){ 59 | let (clock, admin_cap, mut config, mut pool, position_nft, mut scenerio) = prepare(); 60 | let origin_position_info = pool::borrow_position_info(&pool, object::id(&position_nft)); 61 | let origin_liquidity = origin_position_info.info_liquidity(); 62 | 63 | config::add_role(&admin_cap, &mut config, @0x1234, 0); 64 | 65 | // construct attack position 66 | let (tick_lower, tick_upper) = (i32::from(300000), i32::from(300100)); 67 | let mut attack_position_nft = pool::open_position( 68 | &config, 69 | &mut pool, 70 | i32::as_u32(tick_lower), 71 | i32::as_u32(tick_upper), 72 | scenerio.ctx(), 73 | ); 74 | add_liquidity(&config, &mut pool, &mut attack_position_nft, 9381000597014909076, &clock); 75 | pool.pause_pool(); 76 | 77 | config::update_package_version_for_test(&admin_cap, &mut config, EMERGENCY_RESTORE_VERSION); 78 | // init position snapshow, remove 10% 79 | pool::init_position_snapshot(&config, &mut pool, 100000, scenerio.ctx()); 80 | 81 | // emergency restore 82 | pool::emergency_remove_malicious_position(&mut config, &mut pool, object::id(&attack_position_nft), scenerio.ctx()); 83 | 84 | // pool restore 85 | let current_sqrt_price = pool::current_sqrt_price(&pool); 86 | let current_liquidity = pool::liquidity(&pool); 87 | pool::emergency_restore_pool_state(&mut config, &mut pool, current_sqrt_price, current_liquidity, &clock, scenerio.ctx()); 88 | 89 | assert!(!pool::is_position_exist(&pool, object::id(&attack_position_nft)), 0); 90 | // apply liquidity cut 91 | pool::apply_liquidity_cut(&config, &mut pool, object::id(&position_nft), 32000000, &clock, scenerio.ctx()); 92 | let om_position_info = pool::borrow_position_info(&pool, object::id(&position_nft)); 93 | assert!(om_position_info.info_liquidity() != origin_liquidity, 0); 94 | assert!(pool::is_attacked_position(&pool, object::id(&position_nft)), 0); 95 | let snapshot = pool::get_position_snapshot_by_position_id(&pool, object::id(&position_nft)); 96 | assert!(snapshot.liquidity() == origin_liquidity, 0); 97 | 98 | clock.destroy_for_testing(); 99 | transfer::public_share_object(position_nft); 100 | transfer::public_share_object(attack_position_nft); 101 | transfer::public_share_object(admin_cap); 102 | transfer::public_share_object(config); 103 | transfer::public_share_object(pool); 104 | scenerio.end(); 105 | 106 | } 107 | 108 | 109 | #[test] 110 | #[expected_failure(abort_code = cetus_clmm::pool::EDeprecatedFunction)] 111 | fun test_emergency_restore_cannot_close_position(){ 112 | let (clock, admin_cap, mut config, mut pool, position_nft, mut scenerio) = prepare(); 113 | let origin_position_info = pool::borrow_position_info(&pool, object::id(&position_nft)); 114 | let origin_liquidity = origin_position_info.info_liquidity(); 115 | 116 | config::add_role(&admin_cap, &mut config, @0x1234, 0); 117 | 118 | // construct attack position 119 | let (tick_lower, tick_upper) = (i32::from(300000), i32::from(300100)); 120 | let mut attack_position_nft = pool::open_position( 121 | &config, 122 | &mut pool, 123 | i32::as_u32(tick_lower), 124 | i32::as_u32(tick_upper), 125 | scenerio.ctx(), 126 | ); 127 | add_liquidity(&config, &mut pool, &mut attack_position_nft, 9381000597014909076, &clock); 128 | // pool::pause(&config, &mut pool, scenerio.ctx()); 129 | pool.pause_pool(); 130 | 131 | config::update_package_version_for_test(&admin_cap, &mut config, EMERGENCY_RESTORE_VERSION); 132 | // init position snapshow, remove 10% 133 | pool::init_position_snapshot(&config, &mut pool, 100000, scenerio.ctx()); 134 | 135 | // emergency restore 136 | pool::emergency_remove_malicious_position(&mut config, &mut pool, object::id(&attack_position_nft), scenerio.ctx()); 137 | 138 | // pool restore 139 | let current_sqrt_price = pool::current_sqrt_price(&pool); 140 | let current_liquidity = pool::liquidity(&pool); 141 | pool::emergency_restore_pool_state(&mut config, &mut pool, current_sqrt_price, current_liquidity, &clock, scenerio.ctx()); 142 | 143 | assert!(!pool::is_position_exist(&pool, object::id(&attack_position_nft)), 0); 144 | // apply liquidity cut 145 | pool::apply_liquidity_cut(&config, &mut pool, object::id(&position_nft), 32000000, &clock, scenerio.ctx()); 146 | let om_position_info = pool::borrow_position_info(&pool, object::id(&position_nft)); 147 | assert!(om_position_info.info_liquidity() != origin_liquidity, 0); 148 | 149 | config::update_package_version(&admin_cap, &mut config, 10); 150 | // pool::unpause(&config, &mut pool, scenerio.ctx()); 151 | pool.unpause_pool(); 152 | pool::close_position(&config, &mut pool, position_nft); 153 | 154 | clock.destroy_for_testing(); 155 | transfer::public_share_object(attack_position_nft); 156 | transfer::public_share_object(admin_cap); 157 | transfer::public_share_object(config); 158 | transfer::public_share_object(pool); 159 | scenerio.end(); 160 | 161 | } 162 | 163 | #[test] 164 | fun test_init_position_snapshot(){ 165 | let (clock, admin_cap, mut config, mut pool, position_nft, mut scenerio) = prepare(); 166 | 167 | config::add_role(&admin_cap, &mut config, @0x1234, 0); 168 | // pool::pause(&config, &mut pool, scenerio.ctx()); 169 | pool.pause_pool(); 170 | config::update_package_version_for_test(&admin_cap, &mut config, EMERGENCY_RESTORE_VERSION); 171 | 172 | // init position snapshow, remove 10% 173 | pool::init_position_snapshot(&config, &mut pool, 100000, scenerio.ctx()); 174 | let lp = pool.position_liquidity_snapshot(); 175 | assert!(lp.remove_percent() == 100000, 0); 176 | assert_eq!(lp.current_sqrt_price(), pool.current_sqrt_price()); 177 | 178 | clock.destroy_for_testing(); 179 | transfer::public_share_object(position_nft); 180 | transfer::public_share_object(admin_cap); 181 | transfer::public_share_object(config); 182 | transfer::public_share_object(pool); 183 | scenerio.end(); 184 | } 185 | 186 | #[test] 187 | #[expected_failure(abort_code = cetus_clmm::pool::EPoolNotPaused)] 188 | fun test_init_position_snapshot_not_paused(){ 189 | let (clock, admin_cap, mut config, mut pool, position_nft, mut scenerio) = prepare(); 190 | 191 | config::add_role(&admin_cap, &mut config, @0x1234, 0); 192 | config::update_package_version_for_test(&admin_cap, &mut config, EMERGENCY_RESTORE_VERSION); 193 | 194 | // init position snapshow, remove 10% 195 | pool::init_position_snapshot(&config, &mut pool, 100000, scenerio.ctx()); 196 | 197 | clock.destroy_for_testing(); 198 | transfer::public_share_object(position_nft); 199 | transfer::public_share_object(admin_cap); 200 | transfer::public_share_object(config); 201 | transfer::public_share_object(pool); 202 | scenerio.end(); 203 | } 204 | 205 | #[test] 206 | #[expected_failure(abort_code = cetus_clmm::pool::EInvalidRemovePercent)] 207 | fun test_init_position_snapshot_ppm_error(){ 208 | let (clock, admin_cap, mut config, mut pool, position_nft, mut scenerio) = prepare(); 209 | 210 | config::add_role(&admin_cap, &mut config, @0x1234, 0); 211 | // pool::pause(&config, &mut pool, scenerio.ctx()); 212 | pool.pause_pool(); 213 | config::update_package_version_for_test(&admin_cap, &mut config, EMERGENCY_RESTORE_VERSION); 214 | 215 | // init position snapshow, remove 10% 216 | pool::init_position_snapshot(&config, &mut pool, 1000001, scenerio.ctx()); 217 | 218 | clock.destroy_for_testing(); 219 | transfer::public_share_object(position_nft); 220 | transfer::public_share_object(admin_cap); 221 | transfer::public_share_object(config); 222 | transfer::public_share_object(pool); 223 | scenerio.end(); 224 | } 225 | 226 | #[test] 227 | #[expected_failure(abort_code = cetus_clmm::pool::EDeprecatedFunction)] 228 | fun test_governance_fund_injection(){ 229 | let (clock, admin_cap, mut config, mut pool, position_nft, mut scenerio) = prepare(); 230 | 231 | config::add_role(&admin_cap, &mut config, @0x1234, 0); 232 | // pool::pause(&config, &mut pool, scenerio.ctx()); 233 | pool.pause_pool(); 234 | config::update_package_version_for_test(&admin_cap, &mut config, EMERGENCY_RESTORE_VERSION); 235 | 236 | // init position snapshow, remove 10% 237 | let coin_a = coin::mint_for_testing(1000000000, scenerio.ctx() ); 238 | let coin_b = coin::mint_for_testing(1000000000, scenerio.ctx() ); 239 | let (balance_a, balance_b) = pool.balances(); 240 | let value_a = balance_a.value(); 241 | let value_b = balance_b.value(); 242 | pool::governance_fund_injection(&mut config, &mut pool, coin_a, coin_b, scenerio.ctx()); 243 | let (balance_a, balance_b) = pool.balances(); 244 | assert!(balance_a.value() == value_a + 1000000000, 0); 245 | assert!(balance_b.value() == value_b + 1000000000, 0); 246 | 247 | clock.destroy_for_testing(); 248 | transfer::public_share_object(position_nft); 249 | transfer::public_share_object(admin_cap); 250 | transfer::public_share_object(config); 251 | transfer::public_share_object(pool); 252 | scenerio.end(); 253 | } 254 | 255 | #[test] 256 | #[expected_failure(abort_code = cetus_clmm::pool::EDeprecatedFunction)] 257 | fun test_governance_fund_withdrawal(){ 258 | let (clock, admin_cap, mut config, mut pool, position_nft, mut scenerio) = prepare(); 259 | 260 | config::add_role(&admin_cap, &mut config, @0x1234, 0); 261 | // pool::pause(&config, &mut pool, scenerio.ctx()); 262 | pool.pause_pool(); 263 | config::update_package_version(&admin_cap, &mut config, EMERGENCY_RESTORE_NEED_VERSION); 264 | 265 | // init position snapshow, remove 10% 266 | let coin_a = coin::mint_for_testing(1000000000, scenerio.ctx() ); 267 | let coin_b = coin::mint_for_testing(1000000000, scenerio.ctx() ); 268 | let (balance_a, balance_b) = pool.balances(); 269 | let value_a = balance_a.value(); 270 | let value_b = balance_b.value(); 271 | pool::governance_fund_injection(&mut config, &mut pool, coin_a, coin_b, scenerio.ctx()); 272 | let (balance_a, balance_b) = pool.balances(); 273 | assert!(balance_a.value() == value_a + 1000000000, 0); 274 | assert!(balance_b.value() == value_b + 1000000000, 0); 275 | pool::governance_fund_withdrawal(&mut config, &mut pool, 1000000000, 1000000000, scenerio.ctx()); 276 | let (balance_a, balance_b) = pool.balances(); 277 | assert!(balance_a.value() == value_a, 0); 278 | assert!(balance_b.value() == value_b, 0); 279 | 280 | clock.destroy_for_testing(); 281 | transfer::public_share_object(position_nft); 282 | transfer::public_share_object(admin_cap); 283 | transfer::public_share_object(config); 284 | transfer::public_share_object(pool); 285 | scenerio.end(); 286 | } -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/rewarder_unittests.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::rewarder_unittests; 3 | 4 | use sui::coin; 5 | use sui::balance; 6 | use sui::transfer::{public_transfer}; 7 | use cetus_clmm::rewarder::{Self, RewarderManager, add_rewarder, borrow_rewarder, borrow_mut_rewarder, settle, update_emission, deposit_reward, emergent_withdraw, balance_of}; 8 | use std::type_name; 9 | use cetus_clmm::config::new_global_config_for_test; 10 | use integer_mate::full_math_u128; 11 | use sui::transfer::public_share_object; 12 | 13 | const POINTS_EMISSIONS_PER_SECOND: u128 = 1000000 << 64; 14 | 15 | public struct RewarderCoin {} 16 | 17 | public struct RewarderCoin2 {} 18 | 19 | public struct RewarderCoin3 {} 20 | 21 | public struct RewarderCoin4 {} 22 | 23 | public struct RewarderCoin5 {} 24 | 25 | public struct RewarderCoin6 {} 26 | 27 | #[test_only] 28 | public struct TestPool has key { 29 | id: UID, 30 | position: RewarderManager, 31 | } 32 | 33 | #[test_only] 34 | public fun return_manager(m: RewarderManager, ctx: &mut TxContext) { 35 | let p = TestPool { 36 | id: object::new(ctx), 37 | position: m, 38 | }; 39 | transfer::share_object(p); 40 | } 41 | 42 | #[test] 43 | fun test_add_rewarder() { 44 | let mut ctx = tx_context::dummy(); 45 | let mut manager = rewarder::new(); 46 | add_rewarder(&mut manager); 47 | assert!(vector::length(&rewarder::rewarders(&manager)) == 1, 0); 48 | assert!(option::extract(&mut rewarder::rewarder_index(&manager)) == 0, 0); 49 | let rewarder = borrow_rewarder(&manager); 50 | assert!(rewarder.emissions_per_second() == 0, 1); 51 | assert!(rewarder.growth_global() == 0, 2); 52 | assert!(rewarder.reward_coin() == type_name::with_defining_ids(), 3); 53 | 54 | add_rewarder(&mut manager); 55 | assert!(vector::length(&rewarder::rewarders(&manager)) == 2, 4); 56 | assert!(option::extract(&mut rewarder::rewarder_index(&manager)) == 1, 5); 57 | let rewarder = borrow_rewarder(&manager); 58 | assert!(rewarder.emissions_per_second() == 0, 6); 59 | assert!(rewarder.growth_global() == 0, 7); 60 | assert!(rewarder.reward_coin() == type_name::with_defining_ids(), 8); 61 | return_manager(manager, &mut ctx); 62 | } 63 | 64 | #[test] 65 | #[expected_failure(abort_code = cetus_clmm::rewarder::ERewardAlreadyExist)] 66 | fun test_add_rewarder_failure_with_rewarder_exist() { 67 | let mut ctx = tx_context::dummy(); 68 | let mut manager = rewarder::new(); 69 | add_rewarder(&mut manager); 70 | add_rewarder(&mut manager); 71 | return_manager(manager, &mut ctx); 72 | } 73 | 74 | #[test] 75 | #[expected_failure(abort_code = cetus_clmm::rewarder::ERewardSoltIsFull)] 76 | fun test_add_rewarder_failure_with_rewarder_slod_is_full() { 77 | let mut ctx = tx_context::dummy(); 78 | let mut manager = rewarder::new(); 79 | add_rewarder(&mut manager); 80 | add_rewarder(&mut manager); 81 | add_rewarder(&mut manager); 82 | add_rewarder(&mut manager); 83 | add_rewarder(&mut manager); 84 | add_rewarder(&mut manager); 85 | return_manager(manager, &mut ctx); 86 | } 87 | 88 | #[test] 89 | #[expected_failure(abort_code = cetus_clmm::rewarder::EInvalidTime)] 90 | fun test_settle_failure_with_invalid_time() { 91 | let mut ctx = tx_context::dummy(); 92 | let mut manager = rewarder::new(); 93 | settle(&mut manager, 1000000000, 1000000); 94 | settle(&mut manager, 1000000000, 10000); 95 | return_manager(manager, &mut ctx); 96 | } 97 | 98 | #[test] 99 | fun test_settle() { 100 | let mut ctx = tx_context::dummy(); 101 | let mut manager = rewarder::new(); 102 | let (cap, config) = new_global_config_for_test(&mut ctx, 20000); 103 | let mut vault = rewarder::new_vault_for_test(&mut ctx); 104 | let liquidity = 1000000000; 105 | settle(&mut manager, liquidity, 1000000); 106 | assert!(manager.points_released() == 1000000 * POINTS_EMISSIONS_PER_SECOND, 0); 107 | assert!( 108 | manager.points_growth_global() == full_math_u128::mul_div_floor( 109 | (1000000 as u128), 110 | POINTS_EMISSIONS_PER_SECOND, 111 | liquidity 112 | ), 113 | 1, 114 | ); 115 | assert!(manager.last_update_time() == 1000000, 2); 116 | let last_points_released = manager.points_released(); 117 | let last_points_growth_global = manager.points_growth_global(); 118 | add_rewarder(&mut manager); 119 | let balances = coin::into_balance( 120 | coin::mint_for_testing(1000000 * 24 * 3600, &mut ctx), 121 | ); 122 | deposit_reward(&config, &mut vault, balances); 123 | update_emission(&vault, &mut manager, liquidity, 1000000 << 64, 2000000); 124 | assert!( 125 | manager.points_released() == last_points_released + (2000000 - 1000000) * POINTS_EMISSIONS_PER_SECOND, 126 | 3, 127 | ); 128 | assert!( 129 | manager.points_growth_global() == last_points_growth_global + full_math_u128::mul_div_floor( 130 | (1000000 as u128), 131 | POINTS_EMISSIONS_PER_SECOND, 132 | liquidity 133 | ), 134 | 4, 135 | ); 136 | assert!(manager.last_update_time() == 2000000, 5); 137 | let last_points_released = manager.points_released(); 138 | let last_points_growth_global = manager.points_growth_global(); 139 | settle(&mut manager, 2 * liquidity, 3000000); 140 | assert!( 141 | manager.points_released() == last_points_released + (3000000 - 2000000) * POINTS_EMISSIONS_PER_SECOND, 142 | 3, 143 | ); 144 | assert!( 145 | manager.points_growth_global() == last_points_growth_global + full_math_u128::mul_div_floor( 146 | (1000000 as u128), 147 | POINTS_EMISSIONS_PER_SECOND, 148 | 2 * liquidity 149 | ), 150 | 4, 151 | ); 152 | assert!(manager.last_update_time() == 3000000, 5); 153 | let rewarder = borrow_rewarder(&manager); 154 | assert!(rewarder.emissions_per_second() == 1000000 << 64, 6); 155 | assert!( 156 | rewarder.growth_global() == full_math_u128::mul_div_floor( 157 | (1000000 as u128), 158 | 1000000 << 64, 159 | 2 * liquidity 160 | ), 161 | 7, 162 | ); 163 | 164 | return_manager(manager, &mut ctx); 165 | public_transfer(cap, tx_context::sender(&ctx)); 166 | transfer::public_share_object(config); 167 | transfer::public_share_object(vault); 168 | } 169 | 170 | #[test] 171 | #[expected_failure(abort_code = cetus_clmm::rewarder::ERewardAmountInsufficient)] 172 | fun test_update_emission_failure_with_reward_amount_insufficient() { 173 | let mut ctx = tx_context::dummy(); 174 | let (cap, config) = new_global_config_for_test(&mut ctx, 20000); 175 | let mut manager = rewarder::new(); 176 | let mut vault = rewarder::new_vault_for_test(&mut ctx); 177 | add_rewarder(&mut manager); 178 | let balances = coin::into_balance( 179 | coin::mint_for_testing(1000000 * 3600, &mut ctx), 180 | ); 181 | deposit_reward(&config, &mut vault, balances); 182 | update_emission(&vault, &mut manager, 100, 1000000 << 64, 10); 183 | return_manager(manager, &mut ctx); 184 | public_transfer(cap, tx_context::sender(&ctx)); 185 | transfer::public_share_object(config); 186 | transfer::public_share_object(vault); 187 | } 188 | 189 | #[test] 190 | fun test_update_emission() { 191 | let mut ctx = tx_context::dummy(); 192 | let (cap, config) = new_global_config_for_test(&mut ctx, 20000); 193 | let mut manager = rewarder::new(); 194 | let mut vault = rewarder::new_vault_for_test(&mut ctx); 195 | add_rewarder(&mut manager); 196 | let balances = coin::into_balance( 197 | coin::mint_for_testing(1000000 * 48 * 3600, &mut ctx), 198 | ); 199 | deposit_reward(&config, &mut vault, balances); 200 | update_emission(&vault, &mut manager, 2 << 50, 1000000 << 64, 1000000000); 201 | assert!( 202 | manager.points_growth_global() == full_math_u128::mul_div_floor( 203 | (1000000000 as u128), 204 | POINTS_EMISSIONS_PER_SECOND, 205 | 2 << 50 206 | ), 207 | 1, 208 | ); 209 | assert!(manager.last_update_time() == 1000000000, 2); 210 | assert!(manager.points_released() == (1000000000 as u128) * POINTS_EMISSIONS_PER_SECOND, 3); 211 | let rewarder = *borrow_rewarder(&manager); 212 | assert!(rewarder.emissions_per_second() == 1000000 << 64, 4); 213 | assert!(rewarder.growth_global() == 0, 5); 214 | let last_points_released = manager.points_released(); 215 | let last_points_growth_global = manager.points_growth_global(); 216 | update_emission(&vault, &mut manager, 2 << 50, 2000000 << 64, 2000000000); 217 | assert!( 218 | manager.points_released() == last_points_released + (2000000000 - 1000000000) * POINTS_EMISSIONS_PER_SECOND, 219 | 6, 220 | ); 221 | assert!( 222 | manager.points_growth_global() == last_points_growth_global + full_math_u128::mul_div_floor( 223 | (1000000000 as u128), 224 | POINTS_EMISSIONS_PER_SECOND, 225 | 2 << 50 226 | ), 227 | 7, 228 | ); 229 | assert!(manager.last_update_time() == 2000000000, 8); 230 | let last_rewarder = *borrow_rewarder(&manager); 231 | assert!(last_rewarder.emissions_per_second() == 2000000 << 64, 9); 232 | assert!( 233 | last_rewarder.growth_global() == full_math_u128::mul_div_floor( 234 | (1000000000 as u128), 235 | 1000000 << 64, 236 | 2 << 50 237 | ), 238 | 10, 239 | ); 240 | update_emission(&vault, &mut manager, 2 << 50, 0, 3000000000); 241 | let rewarder = borrow_rewarder(&manager); 242 | assert!(rewarder.emissions_per_second() == 0, 11); 243 | assert!( 244 | rewarder.growth_global() == last_rewarder.growth_global() + full_math_u128::mul_div_floor( 245 | (1000000000 as u128), 246 | 2000000 << 64, 247 | 2 << 50 248 | ), 249 | 12, 250 | ); 251 | update_emission(&vault, &mut manager, 2 << 50, 1000000 << 64, 4000000000); 252 | 253 | return_manager(manager, &mut ctx); 254 | public_transfer(cap, tx_context::sender(&ctx)); 255 | transfer::public_share_object(config); 256 | transfer::public_share_object(vault); 257 | } 258 | 259 | #[test] 260 | fun test_deposit_reward() { 261 | let mut ctx = tx_context::dummy(); 262 | let (cap, config) = new_global_config_for_test(&mut ctx, 20000); 263 | let mut vault = rewarder::new_vault_for_test(&mut ctx); 264 | let amount = 1000000 * 48 * 3600; 265 | let balances = coin::into_balance(coin::mint_for_testing(amount, &mut ctx)); 266 | assert!(deposit_reward(&config, &mut vault, balances) == amount, 1); 267 | assert!(balance_of(&vault) == amount, 2); 268 | let balances = coin::into_balance(coin::mint_for_testing(amount * 3, &mut ctx)); 269 | assert!(deposit_reward(&config, &mut vault, balances) == amount * 4, 2); 270 | assert!(balance_of(&vault) == 4 * amount, 2); 271 | let balances = coin::into_balance(coin::mint_for_testing(amount * 2, &mut ctx)); 272 | assert!(deposit_reward(&config, &mut vault, balances) == amount * 2, 3); 273 | assert!(balance_of(&vault) == 2 * amount, 2); 274 | transfer::public_share_object(vault); 275 | public_transfer(cap, tx_context::sender(&ctx)); 276 | public_share_object(config); 277 | } 278 | 279 | #[test] 280 | fun test_withdraw_reward() { 281 | let mut ctx = tx_context::dummy(); 282 | let amount = 1000000 * 48 * 3600; 283 | let (cap, config) = new_global_config_for_test(&mut ctx, 20000); 284 | let mut vault = rewarder::new_vault_for_test(&mut ctx); 285 | let balances = coin::into_balance(coin::mint_for_testing(amount, &mut ctx)); 286 | assert!(deposit_reward(&config, &mut vault, balances) == amount, 1); 287 | let return_balance = rewarder::withdraw_reward(&mut vault, amount); 288 | assert!(balance::value(&return_balance) == amount, 1); 289 | transfer::public_share_object(vault); 290 | public_transfer(coin::from_balance(return_balance, &mut ctx), tx_context::sender(&ctx)); 291 | public_transfer(cap, tx_context::sender(&ctx)); 292 | transfer::public_share_object(config); 293 | } 294 | 295 | #[test] 296 | fun test_emergent_withdraw() { 297 | let mut ctx = tx_context::dummy(); 298 | let amount = 1000000 * 48 * 3600; 299 | let (cap, config) = new_global_config_for_test(&mut ctx, 20000); 300 | let mut vault = rewarder::new_vault_for_test(&mut ctx); 301 | let balances = coin::into_balance(coin::mint_for_testing(amount, &mut ctx)); 302 | assert!(deposit_reward(&config, &mut vault, balances) == amount, 1); 303 | let return_balance = emergent_withdraw(&cap, &config, &mut vault, amount); 304 | assert!(balance::value(&return_balance) == amount, 1); 305 | transfer::public_share_object(vault); 306 | public_transfer(coin::from_balance(return_balance, &mut ctx), tx_context::sender(&ctx)); 307 | public_transfer(cap, tx_context::sender(&ctx)); 308 | transfer::public_share_object(config); 309 | } 310 | 311 | #[test] 312 | fun test_borrow() { 313 | let mut ctx = tx_context::dummy(); 314 | let mut manager = rewarder::new(); 315 | add_rewarder(&mut manager); 316 | borrow_rewarder(&manager); 317 | borrow_mut_rewarder(&mut manager); 318 | return_manager(manager, &mut ctx); 319 | } 320 | 321 | #[test] 322 | #[expected_failure(abort_code = cetus_clmm::rewarder::ERewardNotExist)] 323 | fun test_borrow_failure_with_reward_not_exist() { 324 | let mut ctx = tx_context::dummy(); 325 | let mut manager = rewarder::new(); 326 | add_rewarder(&mut manager); 327 | borrow_rewarder(&manager); 328 | return_manager(manager, &mut ctx); 329 | } 330 | 331 | #[test] 332 | #[expected_failure(abort_code = cetus_clmm::rewarder::ERewardNotExist)] 333 | fun test_borrow_mut_failure_with_reward_not_exist() { 334 | let mut ctx = tx_context::dummy(); 335 | let mut manager = rewarder::new(); 336 | add_rewarder(&mut manager); 337 | borrow_mut_rewarder(&mut manager); 338 | return_manager(manager, &mut ctx); 339 | } 340 | -------------------------------------------------------------------------------- /packages/cetus_clmm/sources/partner.move: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cetus Technology Limited 2 | 3 | /// "Partner" is a module of "clmmpool" that defines a "Partner" object. When a partner participates in a swap 4 | /// transaction, they pass this object and will receive a share of the swap fee that belongs to them. 5 | module cetus_clmm::partner; 6 | 7 | use cetus_clmm::config::{GlobalConfig, check_partner_manager_role, checked_package_version}; 8 | use std::string::{Self, String}; 9 | use std::type_name; 10 | use sui::bag::{Self, Bag}; 11 | use sui::balance::{Self, Balance}; 12 | use sui::clock::{Self, Clock}; 13 | use sui::coin::{Self, Coin}; 14 | use sui::event; 15 | use sui::vec_map::{Self, VecMap}; 16 | 17 | /// The partner rate denominator. 18 | #[allow(unused_const)] 19 | const PARTNER_RATE_DENOMINATOR: u64 = 10000; 20 | /// The max partner fee rate. 21 | const MAX_PARTNER_FEE_RATE: u64 = 10000; 22 | 23 | // =============== Errors ================= 24 | const EPartnerAlreadyExist: u64 = 1; 25 | const EInvalidTime: u64 = 2; 26 | const EInvalidPartnerRefFeeRate: u64 = 3; 27 | const EInvalidPartnerCap: u64 = 4; 28 | const EInvalidCoinType: u64 = 5; 29 | const EInvalidPartnerName: u64 = 6; 30 | const EDeprecatedFunction: u64 = 7; 31 | 32 | // =============== Structs ================= 33 | 34 | /// Partners struct that stores a mapping of partner names to their IDs 35 | /// * `id` - The unique identifier for this Partners object 36 | /// * `partners` - A VecMap storing partner names (as String) mapped to their unique IDs 37 | public struct Partners has key { 38 | id: UID, 39 | partners: VecMap, 40 | } 41 | 42 | /// PartnerCap is used to claim the parter fee generated when swap from partners which is owned by third parties. 43 | /// * `id` - The unique identifier for this PartnerCap object 44 | /// * `name` - The name of the partner 45 | /// * `partner_id` - The ID of the partner 46 | public struct PartnerCap has key, store { 47 | id: UID, 48 | name: String, 49 | partner_id: ID, 50 | } 51 | 52 | /// Partner is used to store the partner info. 53 | /// * `id` - The unique identifier for this Partner object 54 | /// * `name` - The name of the partner 55 | /// * `ref_fee_rate` - The reference fee rate for the partner 56 | /// * `start_time` - The start time of the partner's validity period 57 | /// * `end_time` - The end time of the partner's validity period 58 | /// * `balances` - A Bag storing the partner's balances for different coin types 59 | public struct Partner has key, store { 60 | id: UID, 61 | name: String, 62 | ref_fee_rate: u64, 63 | start_time: u64, 64 | end_time: u64, 65 | balances: Bag, 66 | } 67 | 68 | // ============= Events ================= 69 | /// Emit when publish the module. 70 | /// * `partners_id` - The unique identifier for this Partners object 71 | public struct InitPartnerEvent has copy, drop { 72 | partners_id: ID, 73 | } 74 | 75 | /// Emit when create partner. 76 | /// * `recipient` - The address of the recipient 77 | /// * `partner_id` - The unique identifier for this Partner object 78 | /// * `partner_cap_id` - The unique identifier for this PartnerCap object 79 | /// * `ref_fee_rate` - The reference fee rate for the partner 80 | /// * `name` - The name of the partner 81 | /// * `start_time` - The start time of the partner's validity period 82 | /// * `end_time` - The end time of the partner's validity period 83 | public struct CreatePartnerEvent has copy, drop { 84 | recipient: address, 85 | partner_id: ID, 86 | partner_cap_id: ID, 87 | ref_fee_rate: u64, 88 | name: String, 89 | start_time: u64, 90 | end_time: u64, 91 | } 92 | 93 | /// Emit when update partner ref fee rate. 94 | /// * `partner_id` - The unique identifier for this Partner object 95 | /// * `old_fee_rate` - The old reference fee rate for the partner 96 | /// * `new_fee_rate` - The new reference fee rate for the partner 97 | public struct UpdateRefFeeRateEvent has copy, drop { 98 | partner_id: ID, 99 | old_fee_rate: u64, 100 | new_fee_rate: u64, 101 | } 102 | 103 | /// Emit when update partner time range. 104 | /// * `partner_id` - The unique identifier for this Partner object 105 | /// * `start_time` - The start time of the partner's validity period 106 | /// * `end_time` - The end time of the partner's validity period 107 | public struct UpdateTimeRangeEvent has copy, drop { 108 | partner_id: ID, 109 | start_time: u64, 110 | end_time: u64, 111 | } 112 | 113 | /// Emit when receive ref fee. 114 | /// * `partner_id` - The unique identifier for this Partner object 115 | /// * `amount` - The amount of the fee 116 | /// * `type_name` - The type name of the fee 117 | public struct ReceiveRefFeeEvent has copy, drop { 118 | partner_id: ID, 119 | amount: u64, 120 | type_name: String, 121 | } 122 | 123 | /// Emit when claim ref fee. 124 | /// * `partner_id` - The unique identifier for this Partner object 125 | /// * `amount` - The amount of the fee 126 | /// * `type_name` - The type name of the fee 127 | public struct ClaimRefFeeEvent has copy, drop { 128 | partner_id: ID, 129 | amount: u64, 130 | type_name: String, 131 | } 132 | 133 | /// Initialize the `Partners` object to store partner information 134 | /// * `ctx` - The transaction context used to create the object 135 | fun init(ctx: &mut TxContext) { 136 | let partners = Partners { 137 | id: object::new(ctx), 138 | partners: vec_map::empty(), 139 | }; 140 | let partners_id = object::id(&partners); 141 | transfer::share_object(partners); 142 | event::emit(InitPartnerEvent { 143 | partners_id, 144 | }); 145 | } 146 | 147 | /// Create one partner. 148 | /// * `config` - The global configuration 149 | /// * `partners` - The mutable reference to the `Partners` object 150 | /// * `name` - The name of the partner 151 | /// * `ref_fee_rate` - The reference fee rate for the partner 152 | /// * `start_time` - The start time of the partner's validity period 153 | /// * `end_time` - The end time of the partner's validity period 154 | /// * `recipient` - The address of the recipient 155 | /// * `clock` - The clock object 156 | /// * `ctx` - The transaction context 157 | public fun create_partner( 158 | config: &GlobalConfig, 159 | partners: &mut Partners, 160 | name: String, 161 | ref_fee_rate: u64, 162 | start_time: u64, 163 | end_time: u64, 164 | recipient: address, 165 | clock: &Clock, 166 | ctx: &mut TxContext, 167 | ) { 168 | // Check params 169 | assert!(end_time > start_time, EInvalidTime); 170 | assert!(start_time >= clock::timestamp_ms(clock) / 1000, EInvalidTime); 171 | assert!(ref_fee_rate < MAX_PARTNER_FEE_RATE, EInvalidPartnerRefFeeRate); 172 | assert!(!string::is_empty(&name), EInvalidPartnerName); 173 | assert!(!vec_map::contains(&partners.partners, &name), EPartnerAlreadyExist); 174 | 175 | checked_package_version(config); 176 | check_partner_manager_role(config, tx_context::sender(ctx)); 177 | 178 | // Crate partner and cap 179 | let partner = Partner { 180 | id: object::new(ctx), 181 | name, 182 | ref_fee_rate, 183 | start_time, 184 | end_time, 185 | balances: bag::new(ctx), 186 | }; 187 | let partner_cap = PartnerCap { 188 | id: object::new(ctx), 189 | partner_id: object::id(&partner), 190 | name, 191 | }; 192 | let (partner_id, partner_cap_id) = (object::id(&partner), object::id(&partner_cap)); 193 | vec_map::insert( 194 | &mut partners.partners, 195 | name, 196 | partner_id, 197 | ); 198 | transfer::share_object(partner); 199 | transfer::transfer(partner_cap, recipient); 200 | 201 | event::emit(CreatePartnerEvent { 202 | recipient, 203 | partner_id, 204 | partner_cap_id, 205 | ref_fee_rate, 206 | name, 207 | start_time, 208 | end_time, 209 | }); 210 | } 211 | 212 | /// Get partner name. 213 | /// * `partner` - The reference to the `Partner` object 214 | /// * Returns the name of the partner 215 | public fun name(partner: &Partner): String { 216 | partner.name 217 | } 218 | 219 | /// get partner ref_fee_rate. 220 | /// * `partner` - The reference to the `Partner` object 221 | /// * Returns the reference fee rate for the partner 222 | public fun ref_fee_rate(partner: &Partner): u64 { 223 | partner.ref_fee_rate 224 | } 225 | 226 | /// get partner start_time. 227 | /// * `partner` - The reference to the `Partner` object 228 | /// * Returns the start time of the partner's validity period 229 | public fun start_time(partner: &Partner): u64 { 230 | partner.start_time 231 | } 232 | 233 | /// get partner end_time. 234 | /// * `partner` - The reference to the `Partner` object 235 | /// * Returns the end time of the partner's validity period 236 | public fun end_time(partner: &Partner): u64 { 237 | partner.end_time 238 | } 239 | 240 | /// get partner balances. 241 | /// * `partner` - The reference to the `Partner` object 242 | /// * Returns the balances of the partner 243 | public fun balances(partner: &Partner): &Bag { 244 | &partner.balances 245 | } 246 | 247 | /// check the parter is valid or not, and return the partner ref_fee_rate. 248 | /// * `partner` - The reference to the `Partner` object 249 | /// * `current_time` - The current time 250 | /// * Returns the current reference fee rate for the partner 251 | public fun current_ref_fee_rate(partner: &Partner, current_time: u64): u64 { 252 | if (partner.start_time > current_time || partner.end_time <= current_time) { 253 | return 0 254 | }; 255 | partner.ref_fee_rate 256 | } 257 | 258 | /// Update partner ref fee rate. 259 | /// * `config` - The global configuration 260 | /// * `partner` - The mutable reference to the `Partner` object 261 | /// * `new_fee_rate` - The new reference fee rate for the partner 262 | /// * `ctx` - The transaction context 263 | public fun update_ref_fee_rate( 264 | config: &GlobalConfig, 265 | partner: &mut Partner, 266 | new_fee_rate: u64, 267 | ctx: &TxContext, 268 | ) { 269 | assert!(new_fee_rate < MAX_PARTNER_FEE_RATE, EInvalidPartnerRefFeeRate); 270 | 271 | checked_package_version(config); 272 | check_partner_manager_role(config, tx_context::sender(ctx)); 273 | 274 | let old_fee_rate = partner.ref_fee_rate; 275 | partner.ref_fee_rate = new_fee_rate; 276 | event::emit(UpdateRefFeeRateEvent { 277 | partner_id: object::id(partner), 278 | old_fee_rate, 279 | new_fee_rate, 280 | }); 281 | } 282 | 283 | /// Update partner time range. 284 | /// * `config` - The global configuration 285 | /// * `partner` - The mutable reference to the `Partner` object 286 | /// * `start_time` - The start time of the partner's validity period 287 | /// * `end_time` - The end time of the partner's validity period 288 | /// * `clock` - The clock object 289 | /// * `ctx` - The transaction context 290 | public fun update_time_range( 291 | config: &GlobalConfig, 292 | partner: &mut Partner, 293 | start_time: u64, 294 | end_time: u64, 295 | clock: &Clock, 296 | ctx: &mut TxContext, 297 | ) { 298 | assert!(end_time > start_time, EInvalidTime); 299 | assert!(end_time > clock::timestamp_ms(clock) / 1000, EInvalidTime); 300 | 301 | checked_package_version(config); 302 | check_partner_manager_role(config, tx_context::sender(ctx)); 303 | 304 | partner.start_time = start_time; 305 | partner.end_time = end_time; 306 | event::emit(UpdateTimeRangeEvent { 307 | partner_id: object::id(partner), 308 | start_time, 309 | end_time, 310 | }); 311 | } 312 | 313 | /// Receive ref fee. 314 | /// This method is called when swap and partner is provided. 315 | /// * `partner` - The mutable reference to the `Partner` object 316 | /// * `fee` - The balance of the fee 317 | public fun receive_ref_fee(_partner: &mut Partner, _fee: Balance) { 318 | abort EDeprecatedFunction 319 | } 320 | 321 | /// Receive ref fee. 322 | /// This method is called when swap and partner is provided. 323 | /// * `partner` - The mutable reference to the `Partner` object 324 | /// * `fee` - The balance of the fee 325 | public(package) fun receive_ref_fee_internal(partner: &mut Partner, fee: Balance) { 326 | let amount = balance::value(&fee); 327 | let type_name = type_name::with_defining_ids(); 328 | let key = string::from_ascii(type_name::into_string(type_name)); 329 | if (bag::contains(&partner.balances, key)) { 330 | let current_balance = bag::borrow_mut>(&mut partner.balances, key); 331 | balance::join(current_balance, fee); 332 | } else { 333 | bag::add>(&mut partner.balances, key, fee); 334 | }; 335 | 336 | event::emit(ReceiveRefFeeEvent { 337 | partner_id: object::id(partner), 338 | amount, 339 | type_name: key, 340 | }); 341 | } 342 | 343 | #[allow(lint(self_transfer))] 344 | /// The `PartnerCap` owner claim the parter fee by CoinType. 345 | /// * `config` - The global configuration 346 | /// * `partner_cap` - The reference to the `PartnerCap` object 347 | /// * `partner` - The mutable reference to the `Partner` object 348 | /// * `ctx` - The transaction context 349 | public fun claim_ref_fee( 350 | config: &GlobalConfig, 351 | partner_cap: &PartnerCap, 352 | partner: &mut Partner, 353 | ctx: &mut TxContext, 354 | ) { 355 | let fee_coin = claim_ref_fee_coin(config, partner_cap, partner, ctx); 356 | transfer::public_transfer>(fee_coin, ctx.sender()); 357 | } 358 | 359 | public fun claim_ref_fee_coin( 360 | config: &GlobalConfig, 361 | partner_cap: &PartnerCap, 362 | partner: &mut Partner, 363 | ctx: &mut TxContext, 364 | ): Coin { 365 | checked_package_version(config); 366 | assert!(partner_cap.partner_id == object::id(partner), EInvalidPartnerCap); 367 | 368 | let type_name = type_name::with_defining_ids(); 369 | let key = string::from_ascii(type_name::into_string(type_name)); 370 | 371 | assert!(bag::contains(&partner.balances, key), EInvalidCoinType); 372 | 373 | let current_balance = bag::remove>( 374 | &mut partner.balances, 375 | key, 376 | ); 377 | let amount = balance::value(¤t_balance); 378 | let fee_coin = coin::from_balance(current_balance, ctx); 379 | 380 | event::emit(ClaimRefFeeEvent { 381 | partner_id: object::id(partner), 382 | amount, 383 | type_name: key, 384 | }); 385 | fee_coin 386 | } 387 | 388 | 389 | #[test_only] 390 | public fun create_partner_for_test( 391 | name: String, 392 | ref_fee_rate: u64, 393 | start_time: u64, 394 | end_time: u64, 395 | clock: &Clock, 396 | ctx: &mut TxContext, 397 | ): (PartnerCap, Partner) { 398 | // Check params 399 | assert!(end_time > start_time, EInvalidTime); 400 | assert!(start_time >= clock::timestamp_ms(clock) / 1000, EInvalidTime); 401 | assert!(ref_fee_rate < MAX_PARTNER_FEE_RATE, EInvalidPartnerRefFeeRate); 402 | assert!(!string::is_empty(&name), EInvalidPartnerName); 403 | 404 | // Crate partner and cap 405 | let partner = Partner { 406 | id: object::new(ctx), 407 | name, 408 | ref_fee_rate, 409 | start_time, 410 | end_time, 411 | balances: bag::new(ctx), 412 | }; 413 | let partner_cap = PartnerCap { 414 | id: object::new(ctx), 415 | partner_id: object::id(&partner), 416 | name, 417 | }; 418 | (partner_cap, partner) 419 | } 420 | 421 | #[test_only] 422 | use sui::test_scenario; 423 | #[test_only] 424 | use std::unit_test::assert_eq; 425 | 426 | #[test_only] 427 | public fun create_partners_for_test(ctx: &mut TxContext): Partners { 428 | Partners { 429 | id: object::new(ctx), 430 | partners: vec_map::empty(), 431 | } 432 | } 433 | 434 | #[test_only] 435 | public fun return_partners(partners: Partners) { 436 | transfer::share_object(partners); 437 | } 438 | 439 | #[test] 440 | fun test_init() { 441 | let mut sc = test_scenario::begin(@0x23); 442 | init(sc.ctx()); 443 | sc.next_tx(@0x24); 444 | let partners = test_scenario::take_shared(&sc); 445 | assert_eq!(partners.partners.length(), 0); 446 | test_scenario::return_shared(partners); 447 | test_scenario::end(sc); 448 | } 449 | -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/partner_tests.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::partner_tests; 3 | 4 | use cetus_clmm::config; 5 | use cetus_clmm::partner::{create_partner, create_partners_for_test, return_partners, update_ref_fee_rate, update_time_range}; 6 | use sui::clock; 7 | use sui::coin; 8 | use std::string; 9 | use std::type_name; 10 | use sui::transfer::{public_share_object, public_transfer}; 11 | use cetus_clmm::partner::create_partner_for_test; 12 | use cetus_clmm::partner; 13 | use sui::bag; 14 | use sui::balance; 15 | use std::string::String; 16 | use sui::balance::Balance; 17 | use sui::test_scenario; 18 | use cetus_clmm::partner::Partner; 19 | use std::unit_test::assert_eq; 20 | 21 | #[test] 22 | fun test_create_partner() { 23 | let mut sc = test_scenario::begin(@1988); 24 | let ctx = sc.ctx(); 25 | let mut clk = clock::create_for_testing(ctx); 26 | let (cap, config) = config::new_global_config_for_test(ctx, 2000); 27 | let mut partners = create_partners_for_test(ctx); 28 | create_partner( 29 | &config, 30 | &mut partners, 31 | string::utf8(b"partner"), 32 | 1000, 33 | 1000, 34 | 10010, 35 | @1234, 36 | &clk, 37 | ctx, 38 | ); 39 | public_transfer(cap, tx_context::sender(ctx)); 40 | public_share_object(config); 41 | return_partners(partners); 42 | sc.next_tx(@1988); 43 | let partner = test_scenario::take_shared(&sc); 44 | assert!(partner::name(&partner) == string::utf8(b"partner"), 0); 45 | assert!(partner::ref_fee_rate(&partner) == 1000, 0); 46 | assert_eq!(partner.current_ref_fee_rate(clk.timestamp_ms()/ 1000), 0); 47 | test_scenario::return_shared(partner); 48 | sc.next_tx(@1988); 49 | 50 | clk.increment_for_testing(1001 * 1000); 51 | let partner = test_scenario::take_shared(&sc); 52 | assert_eq!(partner::ref_fee_rate(&partner), 1000); 53 | assert_eq!(partner.current_ref_fee_rate(clk.timestamp_ms()/ 1000), 1000); 54 | test_scenario::return_shared(partner); 55 | sc.next_tx(@1988); 56 | clk.increment_for_testing(10011 * 1000); 57 | let partner = test_scenario::take_shared(&sc); 58 | assert_eq!(partner::ref_fee_rate(&partner), 1000); 59 | assert_eq!(partner.current_ref_fee_rate(clk.timestamp_ms()/ 1000), 0); 60 | test_scenario::return_shared(partner); 61 | test_scenario::end(sc); 62 | clock::destroy_for_testing(clk); 63 | } 64 | 65 | #[test] 66 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidTime)] 67 | fun test_create_partner_time_error() { 68 | let mut ctx = tx_context::dummy(); 69 | let clk = clock::create_for_testing(&mut ctx); 70 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 71 | let mut partners = create_partners_for_test(&mut ctx); 72 | create_partner( 73 | &config, 74 | &mut partners, 75 | string::utf8(b"partner"), 76 | 1000, 77 | 1000, 78 | 1000, 79 | @1234, 80 | &clk, 81 | &mut ctx, 82 | ); 83 | public_transfer(cap, tx_context::sender(&ctx)); 84 | public_share_object(config); 85 | return_partners(partners); 86 | clock::destroy_for_testing(clk); 87 | } 88 | 89 | #[test] 90 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidTime)] 91 | fun test_create_partner_startime_error() { 92 | let mut ctx = tx_context::dummy(); 93 | let mut clk = clock::create_for_testing(&mut ctx); 94 | clock::set_for_testing(&mut clk, 1684205966000); 95 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 96 | let mut partners = create_partners_for_test(&mut ctx); 97 | create_partner( 98 | &config, 99 | &mut partners, 100 | string::utf8(b"partner"), 101 | 1000, 102 | 1684205965, 103 | 1684205990, 104 | @1234, 105 | &clk, 106 | &mut ctx, 107 | ); 108 | public_transfer(cap, tx_context::sender(&ctx)); 109 | public_share_object(config); 110 | return_partners(partners); 111 | clock::destroy_for_testing(clk); 112 | } 113 | 114 | #[test] 115 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidPartnerRefFeeRate)] 116 | fun test_create_partner_ref_fee_error() { 117 | let mut ctx = tx_context::dummy(); 118 | let mut clk = clock::create_for_testing(&mut ctx); 119 | clock::set_for_testing(&mut clk, 1684205966000); 120 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 121 | let mut partners = create_partners_for_test(&mut ctx); 122 | create_partner( 123 | &config, 124 | &mut partners, 125 | string::utf8(b"partner"), 126 | 10000, 127 | 1684205975, 128 | 1684205990, 129 | @1234, 130 | &clk, 131 | &mut ctx, 132 | ); 133 | public_transfer(cap, tx_context::sender(&ctx)); 134 | public_share_object(config); 135 | return_partners(partners); 136 | clock::destroy_for_testing(clk); 137 | } 138 | 139 | #[test] 140 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidPartnerName)] 141 | fun test_create_partner_name_error() { 142 | let mut ctx = tx_context::dummy(); 143 | let mut clk = clock::create_for_testing(&mut ctx); 144 | clock::set_for_testing(&mut clk, 1684205966000); 145 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 146 | let mut partners = create_partners_for_test(&mut ctx); 147 | create_partner( 148 | &config, 149 | &mut partners, 150 | string::utf8(b""), 151 | 5000, 152 | 1684205975, 153 | 1684205990, 154 | @1234, 155 | &clk, 156 | &mut ctx, 157 | ); 158 | public_transfer(cap, tx_context::sender(&ctx)); 159 | public_share_object(config); 160 | return_partners(partners); 161 | clock::destroy_for_testing(clk); 162 | } 163 | 164 | #[test] 165 | #[expected_failure(abort_code = cetus_clmm::partner::EPartnerAlreadyExist)] 166 | fun test_create_partner_exists_error() { 167 | let mut ctx = tx_context::dummy(); 168 | let mut clk = clock::create_for_testing(&mut ctx); 169 | clock::set_for_testing(&mut clk, 1684205966000); 170 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 171 | let mut partners = create_partners_for_test(&mut ctx); 172 | create_partner( 173 | &config, 174 | &mut partners, 175 | string::utf8(b"partner"), 176 | 5000, 177 | 1684205975, 178 | 1684205990, 179 | @1234, 180 | &clk, 181 | &mut ctx, 182 | ); 183 | create_partner( 184 | &config, 185 | &mut partners, 186 | string::utf8(b"partner"), 187 | 5000, 188 | 1684205975, 189 | 1684205990, 190 | @1234, 191 | &clk, 192 | &mut ctx, 193 | ); 194 | public_transfer(cap, tx_context::sender(&ctx)); 195 | public_share_object(config); 196 | return_partners(partners); 197 | clock::destroy_for_testing(clk); 198 | } 199 | 200 | #[test] 201 | fun test_update_ref_fee_rate() { 202 | let mut ctx = tx_context::dummy(); 203 | let clk = clock::create_for_testing(&mut ctx); 204 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 205 | let (partner_cap, mut partner) = create_partner_for_test( 206 | string::utf8(b"TestPartner"), 207 | 2000, 208 | 1, 209 | 10000000000, 210 | &clk, 211 | &mut ctx, 212 | ); 213 | update_ref_fee_rate(&config, &mut partner, 2000, &ctx); 214 | assert!(partner::ref_fee_rate(&partner) == 2000, 0); 215 | transfer::public_transfer(partner_cap, tx_context::sender(&ctx)); 216 | public_transfer(cap, tx_context::sender(&ctx)); 217 | public_share_object(config); 218 | transfer::public_share_object(partner); 219 | clock::destroy_for_testing(clk); 220 | } 221 | 222 | #[test] 223 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidPartnerRefFeeRate)] 224 | fun test_update_ref_fee_rate_invalid() { 225 | let mut ctx = tx_context::dummy(); 226 | let clk = clock::create_for_testing(&mut ctx); 227 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 228 | let (partner_cap, mut partner) = create_partner_for_test( 229 | string::utf8(b"TestPartner"), 230 | 2000, 231 | 1, 232 | 10000000000, 233 | &clk, 234 | &mut ctx, 235 | ); 236 | update_ref_fee_rate(&config, &mut partner, 10000, &ctx); 237 | assert!(partner::ref_fee_rate(&partner) == 10000, 0); 238 | transfer::public_transfer(partner_cap, tx_context::sender(&ctx)); 239 | public_transfer(cap, tx_context::sender(&ctx)); 240 | public_share_object(config); 241 | transfer::public_share_object(partner); 242 | clock::destroy_for_testing(clk); 243 | } 244 | 245 | #[test] 246 | fun test_update_time_range() { 247 | let mut ctx = tx_context::dummy(); 248 | let clk = clock::create_for_testing(&mut ctx); 249 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 250 | let (partner_cap, mut partner) = create_partner_for_test( 251 | string::utf8(b"TestPartner"), 252 | 2000, 253 | 1, 254 | 10000000000, 255 | &clk, 256 | &mut ctx, 257 | ); 258 | update_time_range(&config, &mut partner, 2000, 2000000000, &clk, &mut ctx); 259 | assert!(partner::start_time(&partner) == 2000, 0); 260 | assert!(partner::end_time(&partner) == 2000000000, 0); 261 | transfer::public_transfer(partner_cap, tx_context::sender(&ctx)); 262 | public_transfer(cap, tx_context::sender(&ctx)); 263 | public_share_object(config); 264 | transfer::public_share_object(partner); 265 | clock::destroy_for_testing(clk); 266 | } 267 | 268 | #[test] 269 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidTime)] 270 | fun test_update_time_range_invalid_time() { 271 | let mut ctx = tx_context::dummy(); 272 | let clk = clock::create_for_testing(&mut ctx); 273 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 274 | let (partner_cap, mut partner) = create_partner_for_test( 275 | string::utf8(b"TestPartner"), 276 | 2000, 277 | 1, 278 | 10000000000, 279 | &clk, 280 | &mut ctx, 281 | ); 282 | update_time_range(&config, &mut partner, 1750955032, 1750925032, &clk, &mut ctx); 283 | assert!(partner::start_time(&partner) == 1750925032, 0); 284 | assert!(partner::end_time(&partner) == 1750925032, 0); 285 | transfer::public_transfer(partner_cap, tx_context::sender(&ctx)); 286 | public_transfer(cap, tx_context::sender(&ctx)); 287 | public_share_object(config); 288 | transfer::public_share_object(partner); 289 | clock::destroy_for_testing(clk); 290 | } 291 | 292 | #[test] 293 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidTime)] 294 | fun test_update_time_range_invalid_time_2() { 295 | let mut ctx = tx_context::dummy(); 296 | let mut clk = clock::create_for_testing(&mut ctx); 297 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 298 | let (partner_cap, mut partner) = create_partner_for_test( 299 | string::utf8(b"TestPartner"), 300 | 2000, 301 | 1, 302 | 10000000000, 303 | &clk, 304 | &mut ctx, 305 | ); 306 | clk.increment_for_testing(1750955032 * 1000); 307 | update_time_range(&config, &mut partner,1750925032, 1750954032, &clk, &mut ctx); 308 | assert!(partner::start_time(&partner) == 1750925032, 0); 309 | assert!(partner::end_time(&partner) == 1750925032, 0); 310 | transfer::public_transfer(partner_cap, tx_context::sender(&ctx)); 311 | public_transfer(cap, tx_context::sender(&ctx)); 312 | public_share_object(config); 313 | transfer::public_share_object(partner); 314 | clock::destroy_for_testing(clk); 315 | } 316 | 317 | public struct CoinA {} 318 | 319 | public struct CoinB {} 320 | 321 | #[test] 322 | fun test_receiver_and_claim_ref_fee() { 323 | let mut ctx = tx_context::dummy(); 324 | let clk = clock::create_for_testing(&mut ctx); 325 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 326 | let (partner_cap, mut partner) = create_partner_for_test( 327 | string::utf8(b"TestPartner"), 328 | 2000, 329 | 1, 330 | 10000000000, 331 | &clk, 332 | &mut ctx, 333 | ); 334 | let coin_a = coin::mint_for_testing(1000000000, &mut ctx); 335 | partner::receive_ref_fee_internal(&mut partner, coin::into_balance(coin_a)); 336 | 337 | // receive second time 338 | let coin_a = coin::mint_for_testing(1000000000, &mut ctx); 339 | partner::receive_ref_fee_internal(&mut partner, coin::into_balance(coin_a)); 340 | let type_name = type_name::with_defining_ids(); 341 | let key = string::from_ascii(type_name::into_string(type_name)); 342 | let balance_a = bag::borrow>(partner::balances(&partner), key); 343 | assert!(balance::value(balance_a) == 2000000000, 0); 344 | partner::claim_ref_fee(&config, &partner_cap, &mut partner, &mut ctx); 345 | transfer::public_transfer(partner_cap, tx_context::sender(&ctx)); 346 | public_transfer(cap, tx_context::sender(&ctx)); 347 | public_share_object(config); 348 | transfer::public_share_object(partner); 349 | clock::destroy_for_testing(clk); 350 | } 351 | 352 | #[test] 353 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidCoinType)] 354 | fun test_receiver_and_claim_ref_fee_invalid_coin_type() { 355 | let mut ctx = tx_context::dummy(); 356 | let clk = clock::create_for_testing(&mut ctx); 357 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 358 | let (partner_cap, mut partner) = create_partner_for_test( 359 | string::utf8(b"TestPartner"), 360 | 2000, 361 | 1, 362 | 10000000000, 363 | &clk, 364 | &mut ctx, 365 | ); 366 | let coin_a = coin::mint_for_testing(1000000000, &mut ctx); 367 | partner::receive_ref_fee_internal(&mut partner, coin::into_balance(coin_a)); 368 | 369 | // receive second time 370 | let coin_a = coin::mint_for_testing(1000000000, &mut ctx); 371 | partner::receive_ref_fee_internal(&mut partner, coin::into_balance(coin_a)); 372 | let type_name = type_name::with_defining_ids(); 373 | let key = string::from_ascii(type_name::into_string(type_name)); 374 | let balance_a = bag::borrow>(partner::balances(&partner), key); 375 | assert!(balance::value(balance_a) == 2000000000, 0); 376 | partner::claim_ref_fee(&config, &partner_cap, &mut partner, &mut ctx); 377 | transfer::public_transfer(partner_cap, tx_context::sender(&ctx)); 378 | public_transfer(cap, tx_context::sender(&ctx)); 379 | public_share_object(config); 380 | transfer::public_share_object(partner); 381 | clock::destroy_for_testing(clk); 382 | } 383 | 384 | 385 | 386 | #[test] 387 | #[expected_failure(abort_code = cetus_clmm::partner::EInvalidPartnerCap)] 388 | fun test_claim_ref_fee_wrong_partner_cap() { 389 | let mut ctx = tx_context::dummy(); 390 | let clk = clock::create_for_testing(&mut ctx); 391 | let (cap, config) = config::new_global_config_for_test(&mut ctx, 2000); 392 | let (partner_cap, mut partner) = create_partner_for_test( 393 | string::utf8(b"TestPartner"), 394 | 2000, 395 | 1, 396 | 10000000000, 397 | &clk, 398 | &mut ctx, 399 | ); 400 | 401 | let (partner_cap2, partner2) = create_partner_for_test( 402 | string::utf8(b"TestPartner2"), 403 | 2000, 404 | 1, 405 | 10000000000, 406 | &clk, 407 | &mut ctx, 408 | ); 409 | let coin_a = coin::mint_for_testing(1000000000, &mut ctx); 410 | partner::receive_ref_fee_internal(&mut partner, coin::into_balance(coin_a)); 411 | 412 | // receive second time 413 | let coin_a = coin::mint_for_testing(1000000000, &mut ctx); 414 | partner::receive_ref_fee_internal(&mut partner, coin::into_balance(coin_a)); 415 | let type_name = type_name::with_defining_ids(); 416 | let key = string::from_ascii(type_name::into_string(type_name)); 417 | let balance_a = bag::borrow>(partner::balances(&partner), key); 418 | assert!(balance::value(balance_a) == 2000000000, 0); 419 | partner::claim_ref_fee(&config, &partner_cap2, &mut partner, &mut ctx); 420 | transfer::public_transfer(partner_cap, tx_context::sender(&ctx)); 421 | transfer::public_transfer(partner_cap2, tx_context::sender(&ctx)); 422 | public_transfer(cap, tx_context::sender(&ctx)); 423 | public_share_object(config); 424 | transfer::public_share_object(partner); 425 | transfer::public_share_object(partner2); 426 | clock::destroy_for_testing(clk); 427 | } 428 | -------------------------------------------------------------------------------- /packages/cetus_clmm/sources/math/clmm_math.move: -------------------------------------------------------------------------------- 1 | module cetus_clmm::clmm_math; 2 | 3 | use cetus_clmm::tick_math; 4 | use integer_mate::full_math_u128; 5 | use integer_mate::full_math_u64; 6 | use integer_mate::i32::{Self, I32}; 7 | use integer_mate::math_u128; 8 | use integer_mate::math_u256; 9 | 10 | /// Error when token amount exceeds maximum allowed value 11 | const ETOKEN_AMOUNT_MAX_EXCEEDED: u64 = 0; 12 | /// Error when token amount is below minimum required value 13 | const ETOKEN_AMOUNT_MIN_SUBCEEDED: u64 = 1; 14 | /// Error when multiplication operation results in overflow 15 | const EMULTIPLICATION_OVERFLOW: u64 = 2; 16 | /// Error when integer downcast operation results in overflow 17 | #[allow(unused_const)] 18 | const EINTEGER_DOWNCAST_OVERFLOW: u64 = 3; 19 | /// Error when sqrt price input is invalid 20 | const EINVALID_SQRT_PRICE_INPUT: u64 = 4; 21 | /// Error when subtraction operation results in underflow 22 | const ESUBTRACTION_UNDERFLOW: u64 = 5; 23 | /// Error when casting amount to u64 results in overflow 24 | const EAMOUNT_CAST_TO_U64_OVERFLOW: u64 = 6; 25 | /// Error when amount cast to u128 results in overflow 26 | const EAMOUNT_CAST_TO_U128_OVERFLOW: u64 = 7; 27 | /// Error when tick range is invalid 28 | const EINVALID_TICK_RANGE: u64 = 8; 29 | /// Error when fixed token type is invalid 30 | const EINVALID_FIXED_TOKEN_TYPE: u64 = 3018; 31 | 32 | /// The denominator used for fee rate calculations, where fee rate is expressed as parts per million (1000000 = 100%) 33 | const FEE_RATE_DENOMINATOR: u64 = 1000000; 34 | 35 | /// The maximum value that can be stored in a u64 integer (2^64 - 1) 36 | #[allow(unused_const)] 37 | const UINT64_MAX: u256 = 18446744073709551615; 38 | 39 | /// Returns the denominator used for fee rate calculations, where fee rate is expressed as parts per million (1000000 = 100%) 40 | public fun fee_rate_denominator(): u64 { 41 | FEE_RATE_DENOMINATOR 42 | } 43 | 44 | public fun get_liquidity_from_a( 45 | sqrt_price_0: u128, 46 | sqrt_price_1: u128, 47 | amount_a: u64, 48 | round_up: bool, 49 | ): u128 { 50 | assert!(sqrt_price_0 != sqrt_price_1, EINVALID_SQRT_PRICE_INPUT); 51 | let sqrt_price_diff = if (sqrt_price_0 > sqrt_price_1) { 52 | sqrt_price_0 - sqrt_price_1 53 | } else { 54 | sqrt_price_1 - sqrt_price_0 55 | }; 56 | 57 | // Here sqrt_price is Q32.64, less than 2**96, so the product is less than 2**192, multiply by amount_a is less than 2**256 58 | assert!(sqrt_price_0 <= tick_math::max_sqrt_price(), EINVALID_SQRT_PRICE_INPUT); 59 | assert!(sqrt_price_1 <= tick_math::max_sqrt_price(), EINVALID_SQRT_PRICE_INPUT); 60 | let numberator = full_math_u128::full_mul(sqrt_price_0, sqrt_price_1) * (amount_a as u256); 61 | let div_res = math_u256::div_round(numberator, ((sqrt_price_diff as u256) << 64), round_up); 62 | assert!(div_res <= std::u128::max_value!() as u256, EAMOUNT_CAST_TO_U128_OVERFLOW); 63 | (div_res as u128) 64 | } 65 | 66 | public fun get_liquidity_from_b( 67 | sqrt_price_0: u128, 68 | sqrt_price_1: u128, 69 | amount_b: u64, 70 | round_up: bool, 71 | ): u128 { 72 | assert!(sqrt_price_0 != sqrt_price_1, EINVALID_SQRT_PRICE_INPUT); 73 | let sqrt_price_diff = if (sqrt_price_0 > sqrt_price_1) { 74 | sqrt_price_0 - sqrt_price_1 75 | } else { 76 | sqrt_price_1 - sqrt_price_0 77 | }; 78 | 79 | let div_res = math_u256::div_round( 80 | ((amount_b as u256) << 64), 81 | (sqrt_price_diff as u256), 82 | round_up, 83 | ); 84 | assert!(div_res <= std::u128::max_value!() as u256, EAMOUNT_CAST_TO_U128_OVERFLOW); 85 | (div_res as u128) 86 | } 87 | 88 | public fun get_delta_a( 89 | sqrt_price_0: u128, 90 | sqrt_price_1: u128, 91 | liquidity: u128, 92 | round_up: bool, 93 | ): u64 { 94 | let sqrt_price_diff = if (sqrt_price_0 > sqrt_price_1) { 95 | sqrt_price_0 - sqrt_price_1 96 | } else { 97 | sqrt_price_1 - sqrt_price_0 98 | }; 99 | if (sqrt_price_diff == 0 || liquidity == 0) { 100 | return 0 101 | }; 102 | 103 | let (numberator, overflowing) = math_u256::checked_shlw( 104 | full_math_u128::full_mul(liquidity, sqrt_price_diff), 105 | ); 106 | if (overflowing) { 107 | abort EMULTIPLICATION_OVERFLOW 108 | }; 109 | let denominator = full_math_u128::full_mul(sqrt_price_0, sqrt_price_1); 110 | let quotient = math_u256::div_round(numberator, denominator, round_up); 111 | assert!(quotient <= std::u64::max_value!() as u256, EAMOUNT_CAST_TO_U64_OVERFLOW); 112 | (quotient as u64) 113 | } 114 | 115 | public fun get_delta_b( 116 | sqrt_price_0: u128, 117 | sqrt_price_1: u128, 118 | liquidity: u128, 119 | round_up: bool, 120 | ): u64 { 121 | let sqrt_price_diff = if (sqrt_price_0 > sqrt_price_1) { 122 | sqrt_price_0 - sqrt_price_1 123 | } else { 124 | sqrt_price_1 - sqrt_price_0 125 | }; 126 | if (sqrt_price_diff == 0 || liquidity == 0) { 127 | return 0 128 | }; 129 | 130 | let lo64_mask = 0x000000000000000000000000000000000000000000000000ffffffffffffffff; 131 | let product = full_math_u128::full_mul(liquidity, sqrt_price_diff); 132 | let should_round_up = (round_up) && ((product & lo64_mask) > 0); 133 | if (should_round_up) { 134 | assert!( 135 | ((product >> 64) + 1) <= std::u64::max_value!() as u256, 136 | EAMOUNT_CAST_TO_U64_OVERFLOW, 137 | ); 138 | return (((product >> 64) + 1) as u64) 139 | }; 140 | assert!((product >> 64) <= std::u64::max_value!() as u256, EAMOUNT_CAST_TO_U64_OVERFLOW); 141 | ((product >> 64) as u64) 142 | } 143 | 144 | public fun get_next_sqrt_price_a_up( 145 | sqrt_price: u128, 146 | liquidity: u128, 147 | amount: u64, 148 | by_amount_input: bool, 149 | ): u128 { 150 | if (amount == 0) { 151 | return sqrt_price 152 | }; 153 | 154 | let (numberator, overflowing) = math_u256::checked_shlw( 155 | full_math_u128::full_mul(sqrt_price, liquidity), 156 | ); 157 | if (overflowing) { 158 | abort EMULTIPLICATION_OVERFLOW 159 | }; 160 | 161 | let liquidity_shl_64 = (liquidity as u256) << 64; 162 | let product = full_math_u128::full_mul(sqrt_price, (amount as u128)); 163 | let new_sqrt_price = if (by_amount_input) { 164 | math_u256::div_round(numberator, (liquidity_shl_64 + product), true) 165 | } else { 166 | if (liquidity_shl_64 <= product) { 167 | abort ESUBTRACTION_UNDERFLOW 168 | }; 169 | math_u256::div_round(numberator, (liquidity_shl_64 - product), true) 170 | }; 171 | assert!(new_sqrt_price >= tick_math::min_sqrt_price() as u256, ETOKEN_AMOUNT_MIN_SUBCEEDED); 172 | assert!(new_sqrt_price <= tick_math::max_sqrt_price() as u256, ETOKEN_AMOUNT_MAX_EXCEEDED); 173 | 174 | new_sqrt_price as u128 175 | } 176 | 177 | public fun get_next_sqrt_price_b_down( 178 | sqrt_price: u128, 179 | liquidity: u128, 180 | amount: u64, 181 | by_amount_input: bool, 182 | ): u128 { 183 | let delta_sqrt_price = math_u128::checked_div_round( 184 | ((amount as u128) << 64), 185 | liquidity, 186 | !by_amount_input, 187 | ); 188 | let new_sqrt_price = if (by_amount_input) { 189 | sqrt_price + delta_sqrt_price 190 | } else { 191 | if (sqrt_price < delta_sqrt_price) { 192 | abort ESUBTRACTION_UNDERFLOW 193 | }; 194 | sqrt_price - delta_sqrt_price 195 | }; 196 | 197 | if (new_sqrt_price > tick_math::max_sqrt_price()) { 198 | abort ETOKEN_AMOUNT_MAX_EXCEEDED 199 | } else if (new_sqrt_price < tick_math::min_sqrt_price()) { 200 | abort ETOKEN_AMOUNT_MIN_SUBCEEDED 201 | }; 202 | 203 | new_sqrt_price 204 | } 205 | 206 | public fun get_next_sqrt_price_from_input( 207 | sqrt_price: u128, 208 | liquidity: u128, 209 | amount: u64, 210 | a_to_b: bool, 211 | ): u128 { 212 | if (a_to_b) { 213 | get_next_sqrt_price_a_up(sqrt_price, liquidity, amount, true) 214 | } else { 215 | get_next_sqrt_price_b_down(sqrt_price, liquidity, amount, true) 216 | } 217 | } 218 | 219 | public fun get_next_sqrt_price_from_output( 220 | sqrt_price: u128, 221 | liquidity: u128, 222 | amount: u64, 223 | a_to_b: bool, 224 | ): u128 { 225 | if (a_to_b) { 226 | get_next_sqrt_price_b_down(sqrt_price, liquidity, amount, false) 227 | } else { 228 | get_next_sqrt_price_a_up(sqrt_price, liquidity, amount, false) 229 | } 230 | } 231 | 232 | public fun get_delta_up_from_input( 233 | current_sqrt_price: u128, 234 | target_sqrt_price: u128, 235 | liquidity: u128, 236 | a_to_b: bool, 237 | ): u256 { 238 | let sqrt_price_diff = if (current_sqrt_price > target_sqrt_price) { 239 | current_sqrt_price - target_sqrt_price 240 | } else { 241 | target_sqrt_price - current_sqrt_price 242 | }; 243 | if (sqrt_price_diff == 0 || liquidity == 0) { 244 | return 0 245 | }; 246 | if (a_to_b) { 247 | let (numberator, overflowing) = math_u256::checked_shlw( 248 | full_math_u128::full_mul(liquidity, sqrt_price_diff), 249 | ); 250 | if (overflowing) { 251 | abort EMULTIPLICATION_OVERFLOW 252 | }; 253 | let denominator = full_math_u128::full_mul(current_sqrt_price, target_sqrt_price); 254 | math_u256::div_round(numberator, denominator, true) 255 | } else { 256 | let product = full_math_u128::full_mul(liquidity, sqrt_price_diff); 257 | let lo64_mask = 0x000000000000000000000000000000000000000000000000ffffffffffffffff; 258 | let should_round_up = (product & lo64_mask) > 0; 259 | if (should_round_up) { 260 | return (product >> 64) + 1 261 | }; 262 | product >> 64 263 | } 264 | } 265 | 266 | public fun get_delta_down_from_output( 267 | current_sqrt_price: u128, 268 | target_sqrt_price: u128, 269 | liquidity: u128, 270 | a_to_b: bool, 271 | ): u256 { 272 | let sqrt_price_diff = if (current_sqrt_price > target_sqrt_price) { 273 | current_sqrt_price - target_sqrt_price 274 | } else { 275 | target_sqrt_price - current_sqrt_price 276 | }; 277 | if (sqrt_price_diff == 0 || liquidity == 0) { 278 | return 0 279 | }; 280 | if (a_to_b) { 281 | let product = full_math_u128::full_mul(liquidity, sqrt_price_diff); 282 | product >> 64 283 | } else { 284 | let (numberator, overflowing) = math_u256::checked_shlw( 285 | full_math_u128::full_mul(liquidity, sqrt_price_diff), 286 | ); 287 | if (overflowing) { 288 | abort EMULTIPLICATION_OVERFLOW 289 | }; 290 | let denominator = full_math_u128::full_mul(current_sqrt_price, target_sqrt_price); 291 | math_u256::div_round(numberator, denominator, false) 292 | } 293 | } 294 | 295 | public fun compute_swap_step( 296 | current_sqrt_price: u128, 297 | target_sqrt_price: u128, 298 | liquidity: u128, 299 | amount: u64, 300 | fee_rate: u64, 301 | a2b: bool, 302 | by_amount_in: bool, 303 | ): (u64, u64, u128, u64) { 304 | let mut next_sqrt_price = target_sqrt_price; 305 | let mut amount_in: u64 = 0; 306 | let mut amount_out: u64 = 0; 307 | let mut fee_amount: u64 = 0; 308 | if (liquidity == 0 || current_sqrt_price == target_sqrt_price) { 309 | return (amount_in, amount_out, next_sqrt_price, fee_amount) 310 | }; 311 | if (a2b) { 312 | assert!(current_sqrt_price > target_sqrt_price, EINVALID_SQRT_PRICE_INPUT) 313 | } else { 314 | assert!(current_sqrt_price < target_sqrt_price, EINVALID_SQRT_PRICE_INPUT) 315 | }; 316 | 317 | if (by_amount_in) { 318 | let amount_remain = full_math_u64::mul_div_floor( 319 | amount, 320 | (FEE_RATE_DENOMINATOR - fee_rate), 321 | FEE_RATE_DENOMINATOR, 322 | ); 323 | let max_amount_in = get_delta_up_from_input( 324 | current_sqrt_price, 325 | target_sqrt_price, 326 | liquidity, 327 | a2b, 328 | ); 329 | if (max_amount_in > (amount_remain as u256)) { 330 | amount_in = amount_remain; 331 | fee_amount = amount - amount_remain; 332 | next_sqrt_price = 333 | get_next_sqrt_price_from_input( 334 | current_sqrt_price, 335 | liquidity, 336 | amount_remain, 337 | a2b, 338 | ); 339 | } else { 340 | // it will never overflow here, because max_amount_in < amount_remain and amount_remain's type is u64 341 | amount_in = (max_amount_in as u64); 342 | fee_amount = 343 | full_math_u64::mul_div_ceil(amount_in, fee_rate, (FEE_RATE_DENOMINATOR - fee_rate)); 344 | next_sqrt_price = target_sqrt_price; 345 | }; 346 | let amount_out_ = get_delta_down_from_output( 347 | current_sqrt_price, 348 | next_sqrt_price, 349 | liquidity, 350 | a2b, 351 | ); 352 | assert!(amount_out_ <= std::u64::max_value!() as u256, EAMOUNT_CAST_TO_U64_OVERFLOW); 353 | amount_out = (amount_out_ as u64); 354 | } else { 355 | let max_amount_out = get_delta_down_from_output( 356 | current_sqrt_price, 357 | target_sqrt_price, 358 | liquidity, 359 | a2b, 360 | ); 361 | if (max_amount_out > (amount as u256)) { 362 | amount_out = amount; 363 | next_sqrt_price = 364 | get_next_sqrt_price_from_output(current_sqrt_price, liquidity, amount, a2b); 365 | } else { 366 | amount_out = (max_amount_out as u64); 367 | next_sqrt_price = target_sqrt_price; 368 | }; 369 | let in_amount_ = get_delta_up_from_input( 370 | current_sqrt_price, 371 | next_sqrt_price, 372 | liquidity, 373 | a2b, 374 | ); 375 | assert!(in_amount_ <= std::u64::max_value!() as u256, EAMOUNT_CAST_TO_U64_OVERFLOW); 376 | amount_in = (in_amount_ as u64); 377 | fee_amount = 378 | full_math_u64::mul_div_ceil(amount_in, fee_rate, (FEE_RATE_DENOMINATOR - fee_rate)); 379 | }; 380 | 381 | (amount_in, amount_out, next_sqrt_price, fee_amount) 382 | } 383 | 384 | public fun get_amount_by_liquidity( 385 | tick_lower: I32, 386 | tick_upper: I32, 387 | current_tick_index: I32, 388 | current_sqrt_price: u128, 389 | liquidity: u128, 390 | round_up: bool, 391 | ): (u64, u64) { 392 | if (liquidity == 0) { 393 | return (0, 0) 394 | }; 395 | assert!(tick_lower.lt(tick_upper), EINVALID_TICK_RANGE); 396 | let lower_price = tick_math::get_sqrt_price_at_tick(tick_lower); 397 | let upper_price = tick_math::get_sqrt_price_at_tick(tick_upper); 398 | // Only coin a 399 | 400 | let (amount_a, amount_b) = if (i32::lt(current_tick_index, tick_lower)) { 401 | (get_delta_a(lower_price, upper_price, liquidity, round_up), 0) 402 | } else if (i32::lt(current_tick_index, tick_upper)) { 403 | ( 404 | get_delta_a(current_sqrt_price, upper_price, liquidity, round_up), 405 | get_delta_b(lower_price, current_sqrt_price, liquidity, round_up), 406 | ) 407 | } else { 408 | (0, get_delta_b(lower_price, upper_price, liquidity, round_up)) 409 | }; 410 | (amount_a, amount_b) 411 | } 412 | 413 | public fun get_liquidity_by_amount( 414 | lower_index: I32, 415 | upper_index: I32, 416 | _current_tick_index: I32, 417 | current_sqrt_price: u128, 418 | amount: u64, 419 | is_fixed_a: bool, 420 | ): (u128, u64, u64) { 421 | let lower_price = tick_math::get_sqrt_price_at_tick(lower_index); 422 | let upper_price = tick_math::get_sqrt_price_at_tick(upper_index); 423 | let mut amount_a: u64 = 0; 424 | let mut amount_b: u64 = 0; 425 | let mut _liquidity: u128 = 0; 426 | if (is_fixed_a) { 427 | assert!(current_sqrt_price < upper_price, EINVALID_FIXED_TOKEN_TYPE); 428 | if (current_sqrt_price <= lower_price) { 429 | // Only A 430 | _liquidity = get_liquidity_from_a(lower_price, upper_price, amount, false); 431 | } else { 432 | // Both A and B 433 | _liquidity = get_liquidity_from_a(current_sqrt_price, upper_price, amount, false); 434 | amount_b = get_delta_b(current_sqrt_price, lower_price, _liquidity, true); 435 | }; 436 | amount_a = amount; 437 | } else { 438 | assert!(current_sqrt_price > lower_price, EINVALID_FIXED_TOKEN_TYPE); 439 | if (current_sqrt_price >= upper_price) { 440 | // Only B 441 | _liquidity = get_liquidity_from_b(lower_price, upper_price, amount, false); 442 | } else { 443 | // Both A and B 444 | _liquidity = get_liquidity_from_b(lower_price, current_sqrt_price, amount, false); 445 | amount_a = get_delta_a(current_sqrt_price, upper_price, _liquidity, true); 446 | }; 447 | amount_b = amount; 448 | }; 449 | (_liquidity, amount_a, amount_b) 450 | } 451 | -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/swap_cases.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::swap_cases; 3 | 4 | use cetus_clmm::config::{Self, GlobalConfig}; 5 | use cetus_clmm::pool::{Self, add_liquidity_fix_coin, Pool}; 6 | use cetus_clmm::pool_tests; 7 | use cetus_clmm::tick_math::get_sqrt_price_at_tick; 8 | use integer_mate::i32; 9 | use std::string; 10 | use std::unit_test::assert_eq; 11 | use sui::balance; 12 | use sui::clock; 13 | use sui::test_scenario; 14 | 15 | public struct CETUS has drop {} 16 | public struct USDC has drop {} 17 | 18 | #[test] 19 | #[expected_failure(abort_code = cetus_clmm::pool::EPoolCurrentTickIndexOutOfRange)] 20 | fun test_swap_to_min_tick() { 21 | let mut sc = test_scenario::begin(@0x52); 22 | let ctx = test_scenario::ctx(&mut sc); 23 | let clock = clock::create_for_testing(ctx); 24 | let (admin_cap, config) = config::new_global_config_for_test(ctx, 2000); 25 | let mut pool = pool::new_for_test( 26 | 2, 27 | get_sqrt_price_at_tick(i32::from(46151)), 28 | 100, 29 | string::utf8(b""), 30 | 0, 31 | &clock, 32 | ctx, 33 | ); 34 | let mut pos = pool::open_position( 35 | &config, 36 | &mut pool, 37 | i32::neg_from(443636).as_u32(), 38 | i32::from(443636).as_u32(), 39 | ctx, 40 | ); 41 | let receipt = add_liquidity_fix_coin( 42 | &config, 43 | &mut pool, 44 | &mut pos, 45 | 1000000, 46 | true, 47 | &clock, 48 | ); 49 | let (pay_a, pay_b) = receipt.add_liquidity_pay_amount(); 50 | std::debug::print(&pay_a); 51 | std::debug::print(&pay_b); 52 | let balance_a = balance::create_for_testing(pay_a); 53 | let balance_b = balance::create_for_testing(pay_b); 54 | pool::repay_add_liquidity(&config, &mut pool, balance_a, balance_b, receipt); 55 | 56 | transfer::public_transfer(admin_cap, @0x52); 57 | transfer::public_share_object(pool); 58 | transfer::public_share_object(config); 59 | transfer::public_transfer(pos, @0x52); 60 | sc.next_tx(@0x52); 61 | let config = test_scenario::take_shared(&sc); 62 | let mut pool = test_scenario::take_shared>(&sc); 63 | std::debug::print(&pool.current_tick_index()); 64 | std::debug::print(&pool.liquidity()); 65 | let (s_a, s_b, receipt) = pool::flash_swap( 66 | &config, 67 | &mut pool, 68 | true, 69 | true, 70 | 18446744073709551614, 71 | // get_sqrt_price_at_tick(i32::neg_from(443636)), 72 | 4295048016, 73 | &clock, 74 | ); 75 | s_a.destroy_for_testing(); 76 | let pay_amount = receipt.swap_pay_amount(); 77 | std::debug::print(&pay_amount); 78 | std::debug::print(&s_b.value()); 79 | s_b.destroy_for_testing(); 80 | std::debug::print(&pool.current_tick_index()); 81 | assert_eq!(pool.current_tick_index(), i32::neg_from(443637)); 82 | let balance_a = balance::create_for_testing(pay_amount); 83 | let balance_b = balance::zero(); 84 | pool::repay_flash_swap(&config, &mut pool, balance_a, balance_b, receipt); 85 | 86 | pool_tests::swap( 87 | &mut pool, 88 | &config, 89 | false, 90 | true, 91 | 1000000, 92 | 79226673515401279992447579055, 93 | &clock, 94 | sc.ctx(), 95 | ); 96 | // std::debug::print(&pool.current_tick_index()); 97 | 98 | test_scenario::return_shared(config); 99 | test_scenario::return_shared(pool); 100 | clock.destroy_for_testing(); 101 | sc.end(); 102 | } 103 | 104 | 105 | #[test] 106 | #[expected_failure(abort_code = cetus_clmm::pool::EPoolCurrentTickIndexOutOfRange)] 107 | fun test_swap_to_max_tick() { 108 | let mut sc = test_scenario::begin(@0x52); 109 | let ctx = test_scenario::ctx(&mut sc); 110 | let clock = clock::create_for_testing(ctx); 111 | let (admin_cap, config) = config::new_global_config_for_test(ctx, 2000); 112 | let mut pool = pool::new_for_test( 113 | 2, 114 | get_sqrt_price_at_tick(i32::from(46151)), 115 | 100, 116 | string::utf8(b""), 117 | 0, 118 | &clock, 119 | ctx, 120 | ); 121 | let mut pos = pool::open_position( 122 | &config, 123 | &mut pool, 124 | i32::neg_from(443636).as_u32(), 125 | i32::from(443636).as_u32(), 126 | ctx, 127 | ); 128 | let receipt = add_liquidity_fix_coin( 129 | &config, 130 | &mut pool, 131 | &mut pos, 132 | 1000000, 133 | true, 134 | &clock, 135 | ); 136 | let (pay_a, pay_b) = receipt.add_liquidity_pay_amount(); 137 | let balance_a = balance::create_for_testing(pay_a); 138 | let balance_b = balance::create_for_testing(pay_b); 139 | pool::repay_add_liquidity(&config, &mut pool, balance_a, balance_b, receipt); 140 | 141 | transfer::public_transfer(admin_cap, @0x52); 142 | transfer::public_share_object(pool); 143 | transfer::public_share_object(config); 144 | transfer::public_transfer(pos, @0x52); 145 | sc.next_tx(@0x52); 146 | let config = test_scenario::take_shared(&sc); 147 | let mut pool = test_scenario::take_shared>(&sc); 148 | std::debug::print(&pool.current_tick_index()); 149 | let (s_a, s_b, receipt) = pool::flash_swap( 150 | &config, 151 | &mut pool, 152 | false, 153 | true, 154 | 18446744073709551614, 155 | 79226673515401279992447579055, 156 | &clock, 157 | ); 158 | s_a.destroy_for_testing(); 159 | let pay_amount = receipt.swap_pay_amount(); 160 | s_b.destroy_for_testing(); 161 | std::debug::print(&pool.current_tick_index()); 162 | assert_eq!(pool.current_tick_index(), i32::from(443636)); 163 | let balance_b = balance::create_for_testing(pay_amount); 164 | let balance_a = balance::zero(); 165 | pool::repay_flash_swap(&config, &mut pool, balance_a, balance_b, receipt); 166 | 167 | test_scenario::return_shared(config); 168 | test_scenario::return_shared(pool); 169 | clock.destroy_for_testing(); 170 | sc.end(); 171 | } 172 | 173 | #[test] 174 | fun swap_cross_tick_when_tick_spacing_is_1() { 175 | let mut sc = test_scenario::begin(@0x52); 176 | let ctx = test_scenario::ctx(&mut sc); 177 | let clock = clock::create_for_testing(ctx); 178 | let (admin_cap, config) = config::new_global_config_for_test(ctx, 2000); 179 | let mut pool = pool::new_for_test( 180 | 1, 181 | get_sqrt_price_at_tick(i32::from(0)), 182 | 0, 183 | string::utf8(b""), 184 | 0, 185 | &clock, 186 | ctx, 187 | ); 188 | let mut pos = pool::open_position( 189 | &config, 190 | &mut pool, 191 | i32::neg_from(10).as_u32(), 192 | i32::from(0).as_u32(), 193 | ctx, 194 | ); 195 | let receipt = add_liquidity_fix_coin( 196 | &config, 197 | &mut pool, 198 | &mut pos, 199 | 1000000, 200 | false, 201 | &clock, 202 | ); 203 | let (pay_a, pay_b) = receipt.add_liquidity_pay_amount(); 204 | std::debug::print(&pay_a); 205 | std::debug::print(&pay_b); 206 | let balance_a = balance::create_for_testing(pay_a); 207 | let balance_b = balance::create_for_testing(pay_b); 208 | pool::repay_add_liquidity(&config, &mut pool, balance_a, balance_b, receipt); 209 | 210 | let mut pos2 = pool::open_position( 211 | &config, 212 | &mut pool, 213 | i32::neg_from(20).as_u32(), 214 | i32::neg_from(11).as_u32(), 215 | ctx, 216 | ); 217 | let receipt = add_liquidity_fix_coin( 218 | &config, 219 | &mut pool, 220 | &mut pos2, 221 | 1000000, 222 | false, 223 | &clock, 224 | ); 225 | let (pay_a, pay_b) = receipt.add_liquidity_pay_amount(); 226 | std::debug::print(&pay_a); 227 | std::debug::print(&pay_b); 228 | let balance_a = balance::create_for_testing(pay_a); 229 | let balance_b = balance::create_for_testing(pay_b); 230 | pool::repay_add_liquidity(&config, &mut pool, balance_a, balance_b, receipt); 231 | 232 | transfer::public_transfer(admin_cap, @0x52); 233 | transfer::public_share_object(pool); 234 | transfer::public_share_object(config); 235 | transfer::public_transfer(pos, @0x52); 236 | transfer::public_transfer(pos2, @0x52); 237 | sc.next_tx(@0x52); 238 | let config = test_scenario::take_shared(&sc); 239 | let mut pool = test_scenario::take_shared>(&sc); 240 | // std::debug::print(&pool.current_tick_index()); 241 | // std::debug::print(&pool.liquidity()); 242 | let (s_a, s_b, receipt) = pool::flash_swap( 243 | &config, 244 | &mut pool, 245 | true, 246 | true, 247 | 1000502, 248 | get_sqrt_price_at_tick(i32::neg_from(10)), 249 | // 4295048016, 250 | &clock, 251 | ); 252 | std::debug::print(&999999999999110); 253 | std::debug::print(&s_b.value()); 254 | // std::debug::print(&s_a.value()); 255 | std::debug::print(&pool.current_tick_index()); 256 | s_a.destroy_for_testing(); 257 | s_b.destroy_for_testing(); 258 | 259 | let pay_amount = receipt.swap_pay_amount(); 260 | std::debug::print(&pay_amount); 261 | let balance_a = balance::create_for_testing(pay_amount); 262 | let balance_b = balance::zero(); 263 | pool::repay_flash_swap(&config, &mut pool, balance_a, balance_b, receipt); 264 | assert_eq!(pool.current_tick_index(), i32::neg_from(11)); 265 | std::debug::print(&999999999999110); 266 | pool_tests::swap( 267 | &mut pool, 268 | &config, 269 | true, 270 | true, 271 | 100000, 272 | 4295048016, 273 | &clock, 274 | sc.ctx(), 275 | ); 276 | // std::debug::print(&pool.current_tick_index()); 277 | 278 | test_scenario::return_shared(config); 279 | test_scenario::return_shared(pool); 280 | clock.destroy_for_testing(); 281 | sc.end(); 282 | } 283 | 284 | 285 | #[test] 286 | fun swap_cross_tick_when_tick_spacing_is_1_case_2() { 287 | let mut sc = test_scenario::begin(@0x52); 288 | let ctx = test_scenario::ctx(&mut sc); 289 | let clock = clock::create_for_testing(ctx); 290 | let (admin_cap, config) = config::new_global_config_for_test(ctx, 2000); 291 | let mut pool = pool::new_for_test( 292 | 1, 293 | get_sqrt_price_at_tick(i32::from(0)), 294 | 0, 295 | string::utf8(b""), 296 | 0, 297 | &clock, 298 | ctx, 299 | ); 300 | let mut pos = pool::open_position( 301 | &config, 302 | &mut pool, 303 | i32::neg_from(10).as_u32(), 304 | i32::from(0).as_u32(), 305 | ctx, 306 | ); 307 | let receipt = add_liquidity_fix_coin( 308 | &config, 309 | &mut pool, 310 | &mut pos, 311 | 1000000, 312 | false, 313 | &clock, 314 | ); 315 | let (pay_a, pay_b) = receipt.add_liquidity_pay_amount(); 316 | std::debug::print(&pay_a); 317 | std::debug::print(&pay_b); 318 | let balance_a = balance::create_for_testing(pay_a); 319 | let balance_b = balance::create_for_testing(pay_b); 320 | pool::repay_add_liquidity(&config, &mut pool, balance_a, balance_b, receipt); 321 | 322 | let mut pos2 = pool::open_position( 323 | &config, 324 | &mut pool, 325 | i32::neg_from(20).as_u32(), 326 | i32::neg_from(10).as_u32(), 327 | ctx, 328 | ); 329 | let receipt = add_liquidity_fix_coin( 330 | &config, 331 | &mut pool, 332 | &mut pos2, 333 | 1000000, 334 | false, 335 | &clock, 336 | ); 337 | let (pay_a, pay_b) = receipt.add_liquidity_pay_amount(); 338 | std::debug::print(&pay_a); 339 | std::debug::print(&pay_b); 340 | let balance_a = balance::create_for_testing(pay_a); 341 | let balance_b = balance::create_for_testing(pay_b); 342 | pool::repay_add_liquidity(&config, &mut pool, balance_a, balance_b, receipt); 343 | 344 | transfer::public_transfer(admin_cap, @0x52); 345 | transfer::public_share_object(pool); 346 | transfer::public_share_object(config); 347 | transfer::public_transfer(pos, @0x52); 348 | transfer::public_transfer(pos2, @0x52); 349 | sc.next_tx(@0x52); 350 | let config = test_scenario::take_shared(&sc); 351 | let mut pool = test_scenario::take_shared>(&sc); 352 | // std::debug::print(&pool.current_tick_index()); 353 | // std::debug::print(&pool.liquidity()); 354 | let (s_a, s_b, receipt) = pool::flash_swap( 355 | &config, 356 | &mut pool, 357 | true, 358 | true, 359 | 1000502, 360 | get_sqrt_price_at_tick(i32::neg_from(10)), 361 | // 4295048016, 362 | &clock, 363 | ); 364 | s_a.destroy_for_testing(); 365 | s_b.destroy_for_testing(); 366 | 367 | let pay_amount = receipt.swap_pay_amount(); 368 | std::debug::print(&pay_amount); 369 | let balance_a = balance::create_for_testing(pay_amount); 370 | let balance_b = balance::zero(); 371 | pool::repay_flash_swap(&config, &mut pool, balance_a, balance_b, receipt); 372 | assert_eq!(pool.current_tick_index(), i32::neg_from(11)); 373 | std::debug::print(&999999999999110); 374 | pool_tests::swap( 375 | &mut pool, 376 | &config, 377 | true, 378 | true, 379 | 400000, 380 | 4295048016, 381 | &clock, 382 | sc.ctx(), 383 | ); 384 | std::debug::print(&pool.current_tick_index()); 385 | 386 | test_scenario::return_shared(config); 387 | test_scenario::return_shared(pool); 388 | clock.destroy_for_testing(); 389 | sc.end(); 390 | } 391 | 392 | 393 | #[test] 394 | fun swap_cross_tick_when_tick_spacing_is_1_b2a() { 395 | let mut sc = test_scenario::begin(@0x52); 396 | let ctx = test_scenario::ctx(&mut sc); 397 | let clock = clock::create_for_testing(ctx); 398 | let (admin_cap, config) = config::new_global_config_for_test(ctx, 2000); 399 | let mut pool = pool::new_for_test( 400 | 1, 401 | get_sqrt_price_at_tick(i32::from(0)), 402 | 0, 403 | string::utf8(b""), 404 | 0, 405 | &clock, 406 | ctx, 407 | ); 408 | let mut pos = pool::open_position( 409 | &config, 410 | &mut pool, 411 | i32::neg_from(10).as_u32(), 412 | i32::from(0).as_u32(), 413 | ctx, 414 | ); 415 | let receipt = add_liquidity_fix_coin( 416 | &config, 417 | &mut pool, 418 | &mut pos, 419 | 1000000, 420 | false, 421 | &clock, 422 | ); 423 | let (pay_a, pay_b) = receipt.add_liquidity_pay_amount(); 424 | std::debug::print(&pay_a); 425 | std::debug::print(&pay_b); 426 | let balance_a = balance::create_for_testing(pay_a); 427 | let balance_b = balance::create_for_testing(pay_b); 428 | pool::repay_add_liquidity(&config, &mut pool, balance_a, balance_b, receipt); 429 | 430 | let mut pos2 = pool::open_position( 431 | &config, 432 | &mut pool, 433 | i32::neg_from(20).as_u32(), 434 | i32::neg_from(11).as_u32(), 435 | ctx, 436 | ); 437 | let receipt = add_liquidity_fix_coin( 438 | &config, 439 | &mut pool, 440 | &mut pos2, 441 | 1000000, 442 | false, 443 | &clock, 444 | ); 445 | let (pay_a, pay_b) = receipt.add_liquidity_pay_amount(); 446 | std::debug::print(&pay_a); 447 | std::debug::print(&pay_b); 448 | let balance_a = balance::create_for_testing(pay_a); 449 | let balance_b = balance::create_for_testing(pay_b); 450 | pool::repay_add_liquidity(&config, &mut pool, balance_a, balance_b, receipt); 451 | 452 | transfer::public_transfer(admin_cap, @0x52); 453 | transfer::public_share_object(pool); 454 | transfer::public_share_object(config); 455 | transfer::public_transfer(pos, @0x52); 456 | transfer::public_transfer(pos2, @0x52); 457 | sc.next_tx(@0x52); 458 | let config = test_scenario::take_shared(&sc); 459 | let mut pool = test_scenario::take_shared>(&sc); 460 | // std::debug::print(&pool.current_tick_index()); 461 | // std::debug::print(&pool.liquidity()); 462 | let (s_a, s_b, receipt) = pool::flash_swap( 463 | &config, 464 | &mut pool, 465 | true, 466 | true, 467 | 1000502, 468 | get_sqrt_price_at_tick(i32::neg_from(10)), 469 | // 4295048016, 470 | &clock, 471 | ); 472 | std::debug::print(&999999999999110); 473 | std::debug::print(&s_b.value()); 474 | // std::debug::print(&s_a.value()); 475 | std::debug::print(&pool.current_tick_index()); 476 | s_a.destroy_for_testing(); 477 | s_b.destroy_for_testing(); 478 | 479 | let pay_amount = receipt.swap_pay_amount(); 480 | std::debug::print(&pay_amount); 481 | let balance_a = balance::create_for_testing(pay_amount); 482 | let balance_b = balance::zero(); 483 | pool::repay_flash_swap(&config, &mut pool, balance_a, balance_b, receipt); 484 | assert_eq!(pool.current_tick_index(), i32::neg_from(11)); 485 | std::debug::print(&999999999999110); 486 | pool_tests::swap( 487 | &mut pool, 488 | &config, 489 | false, 490 | true, 491 | 400000, 492 | get_sqrt_price_at_tick(i32::from(10)), 493 | &clock, 494 | sc.ctx(), 495 | ); 496 | std::debug::print(&pool.current_tick_index()); 497 | 498 | test_scenario::return_shared(config); 499 | test_scenario::return_shared(pool); 500 | clock.destroy_for_testing(); 501 | sc.end(); 502 | } 503 | -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/config_tests.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::config_tests; 3 | 4 | use cetus_clmm::acl; 5 | use cetus_clmm::config::{ 6 | Self, 7 | check_fee_tier_manager_role, 8 | check_pool_manager_role, 9 | check_rewarder_manager_role, 10 | check_partner_manager_role, 11 | remove_role 12 | }; 13 | use sui::transfer::{public_share_object, public_transfer}; 14 | use sui::tx_context::sender; 15 | use sui::vec_map; 16 | use std::unit_test::assert_eq; 17 | use cetus_clmm::config::check_protocol_fee_claim_role; 18 | 19 | #[test] 20 | fun test_update_protocol_fee_rate() { 21 | let mut ctx = tx_context::dummy(); 22 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 23 | assert!(config::protocol_fee_rate(&config) == 1000, 0); 24 | config::update_protocol_fee_rate(&mut config, 2000, &ctx); 25 | assert!(config::protocol_fee_rate(&config) == 2000, 0); 26 | public_share_object(config); 27 | public_transfer(admin_cap, tx_context::sender(&ctx)); 28 | } 29 | 30 | 31 | #[test] 32 | #[expected_failure(abort_code = cetus_clmm::config::EInvalidProtocolFeeRate)] 33 | fun test_update_protocol_fee_rate_max_exceed() { 34 | let mut ctx = tx_context::dummy(); 35 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 36 | assert!(config::protocol_fee_rate(&config) == 1000, 0); 37 | config::update_protocol_fee_rate(&mut config, 10000, &ctx); 38 | assert!(config::protocol_fee_rate(&config) == 2000, 0); 39 | public_share_object(config); 40 | public_transfer(admin_cap, tx_context::sender(&ctx)); 41 | } 42 | 43 | #[test] 44 | #[expected_failure(abort_code = cetus_clmm::config::EFeeTierAlreadyExist)] 45 | fun test_add_fee_tier_already_exist(){ 46 | let mut ctx = tx_context::dummy(); 47 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 48 | config::add_fee_tier(&mut config, 2, 2000, &ctx); 49 | config::add_fee_tier(&mut config, 2, 2000, &ctx); 50 | public_share_object(config); 51 | public_transfer(admin_cap, tx_context::sender(&ctx)); 52 | } 53 | 54 | #[test] 55 | fun test_fee_tier() { 56 | let mut ctx = tx_context::dummy(); 57 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 58 | 59 | config::add_fee_tier(&mut config, 2, 2000, &ctx); 60 | let fee_tiers = config.fee_tiers(); 61 | let fee_tier = fee_tiers.get(&2); 62 | assert_eq!(fee_tier.tick_spacing(), 2); 63 | assert_eq!(fee_tier.fee_rate(), 2000); 64 | assert!(config::get_fee_rate(2, &config) == 2000, 0); 65 | config::update_fee_tier(&mut config, 2, 1000, &ctx); 66 | assert!(config::get_fee_rate(2, &config) == 1000, 0); 67 | config::delete_fee_tier(&mut config, 2, &ctx); 68 | let fee_tiers = config::fee_tiers(&config); 69 | assert!(vec_map::is_empty(fee_tiers), 0); 70 | 71 | public_share_object(config); 72 | public_transfer(admin_cap, tx_context::sender(&ctx)); 73 | } 74 | 75 | #[test] 76 | #[expected_failure(abort_code = cetus_clmm::config::EInvalidFeeRate)] 77 | fun test_fee_tier_invalid_fee_rate() { 78 | let mut ctx = tx_context::dummy(); 79 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 80 | 81 | config::add_fee_tier(&mut config, 2, 210000, &ctx); 82 | 83 | public_share_object(config); 84 | public_transfer(admin_cap, tx_context::sender(&ctx)); 85 | } 86 | 87 | #[test] 88 | #[expected_failure(abort_code = cetus_clmm::config::EInvalidTickSpacing)] 89 | fun test_fee_tier_invalid_tick_spacing() { 90 | let mut ctx = tx_context::dummy(); 91 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 92 | 93 | config::add_fee_tier(&mut config, 444444, 200000, &ctx); 94 | 95 | public_share_object(config); 96 | public_transfer(admin_cap, tx_context::sender(&ctx)); 97 | } 98 | 99 | #[test] 100 | #[expected_failure(abort_code = cetus_clmm::config::EInvalidTickSpacing)] 101 | fun test_fee_tier_invalid_tick_spacing_2() { 102 | let mut ctx = tx_context::dummy(); 103 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 104 | 105 | config::add_fee_tier(&mut config, 0, 200000, &ctx); 106 | 107 | public_share_object(config); 108 | public_transfer(admin_cap, tx_context::sender(&ctx)); 109 | } 110 | 111 | #[test] 112 | fun test_delete_fee_tier() { 113 | let mut ctx = tx_context::dummy(); 114 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 115 | 116 | config::add_fee_tier(&mut config, 200, 2000, &ctx); 117 | config::delete_fee_tier(&mut config, 200, &ctx); 118 | 119 | public_share_object(config); 120 | public_transfer(admin_cap, tx_context::sender(&ctx)); 121 | } 122 | 123 | #[test] 124 | #[expected_failure(abort_code = cetus_clmm::config::EFeeTierNotFound)] 125 | fun test_delete_fee_tier_not_found() { 126 | let mut ctx = tx_context::dummy(); 127 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 128 | 129 | config::add_fee_tier(&mut config, 200, 2000, &ctx); 130 | config::delete_fee_tier(&mut config, 2, &ctx); 131 | 132 | public_share_object(config); 133 | public_transfer(admin_cap, tx_context::sender(&ctx)); 134 | } 135 | 136 | #[test] 137 | fun test_update_fee_tier() { 138 | let mut ctx = tx_context::dummy(); 139 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 140 | 141 | config::add_fee_tier(&mut config, 200, 2000, &ctx); 142 | let fee_tier = config::get_fee_rate(200, &config); 143 | assert_eq!(fee_tier, 2000); 144 | config::update_fee_tier(&mut config, 200, 1000, &ctx); 145 | let fee_tier = config::get_fee_rate(200, &config); 146 | assert_eq!(fee_tier, 1000); 147 | 148 | public_share_object(config); 149 | public_transfer(admin_cap, tx_context::sender(&ctx)); 150 | } 151 | 152 | #[test] 153 | #[expected_failure(abort_code = cetus_clmm::config::EFeeTierNotFound)] 154 | fun test_update_fee_tier_not_found() { 155 | let mut ctx = tx_context::dummy(); 156 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 157 | 158 | config::add_fee_tier(&mut config, 200, 2000, &ctx); 159 | config::update_fee_tier(&mut config, 2, 1000, &ctx); 160 | 161 | public_share_object(config); 162 | public_transfer(admin_cap, tx_context::sender(&ctx)); 163 | } 164 | 165 | #[test] 166 | #[expected_failure(abort_code = cetus_clmm::config::EInvalidFeeRate)] 167 | fun test_update_fee_tier_fee_rate_max_exceed() { 168 | let mut ctx = tx_context::dummy(); 169 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 170 | 171 | config::add_fee_tier(&mut config, 200, 2000, &ctx); 172 | config::update_fee_tier(&mut config, 200, 220000, &ctx); 173 | 174 | public_share_object(config); 175 | public_transfer(admin_cap, tx_context::sender(&ctx)); 176 | } 177 | 178 | #[test] 179 | #[expected_failure(abort_code = cetus_clmm::config::EPackageVersionDeprecate)] 180 | fun test_package_version() { 181 | let mut ctx = tx_context::dummy(); 182 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 183 | config::update_package_version(&admin_cap, &mut config, 15); 184 | config::add_fee_tier(&mut config, 2, 2000, &ctx); 185 | public_share_object(config); 186 | public_transfer(admin_cap, tx_context::sender(&ctx)); 187 | } 188 | 189 | const ACL_POOL_MANAGER: u8 = 0; 190 | const ACL_FEE_TIER_MANAGER: u8 = 1; 191 | const ACL_CLAIM_PROTOCOL_FEE: u8 = 2; 192 | const ACL_PARTNER_MANAGER: u8 = 3; 193 | const ACL_REWARDER_MANAGER: u8 = 4; 194 | 195 | #[test] 196 | fun test_acl() { 197 | let mut ctx = tx_context::dummy(); 198 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 199 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_POOL_MANAGER), 0); 200 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_FEE_TIER_MANAGER), 0); 201 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_REWARDER_MANAGER), 0); 202 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_CLAIM_PROTOCOL_FEE), 0); 203 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_PARTNER_MANAGER), 0); 204 | 205 | assert!(!acl::has_role(config::acl(&config), @0x12345, ACL_POOL_MANAGER), 0); 206 | assert!(!acl::has_role(config::acl(&config), @0x12345, ACL_FEE_TIER_MANAGER), 0); 207 | assert!(!acl::has_role(config::acl(&config), @0x12345, ACL_REWARDER_MANAGER), 0); 208 | assert!(!acl::has_role(config::acl(&config), @0x12345, ACL_CLAIM_PROTOCOL_FEE), 0); 209 | assert!(!acl::has_role(config::acl(&config), @0x12345, ACL_PARTNER_MANAGER), 0); 210 | 211 | let roles = 0u128 | (1 << ACL_POOL_MANAGER) | (1 << ACL_FEE_TIER_MANAGER); 212 | config::set_roles(&admin_cap, &mut config, @0x12345, roles); 213 | assert!(acl::has_role(config::acl(&config), @0x12345, ACL_POOL_MANAGER), 0); 214 | assert!(acl::has_role(config::acl(&config), @0x12345, ACL_FEE_TIER_MANAGER), 0); 215 | 216 | config::remove_role(&admin_cap, &mut config, sender(&ctx), ACL_FEE_TIER_MANAGER); 217 | config::remove_role(&admin_cap, &mut config, sender(&ctx), ACL_CLAIM_PROTOCOL_FEE); 218 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_POOL_MANAGER), 0); 219 | assert!(!acl::has_role(config::acl(&config), sender(&ctx), ACL_FEE_TIER_MANAGER), 0); 220 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_REWARDER_MANAGER), 0); 221 | assert!(!acl::has_role(config::acl(&config), sender(&ctx), ACL_CLAIM_PROTOCOL_FEE), 0); 222 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_PARTNER_MANAGER), 0); 223 | 224 | config::remove_member(&admin_cap, &mut config, @0x12345); 225 | assert!(!acl::has_role(config::acl(&config), @0x12345, ACL_POOL_MANAGER), 0); 226 | assert!(!acl::has_role(config::acl(&config), @0x12345, ACL_FEE_TIER_MANAGER), 0); 227 | public_share_object(config); 228 | public_transfer(admin_cap, tx_context::sender(&ctx)); 229 | } 230 | 231 | #[test] 232 | #[expected_failure(abort_code = cetus_clmm::config::ENoFeeTierManagerPermission)] 233 | fun test_acl_expect_failure() { 234 | let mut ctx = tx_context::dummy(); 235 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 236 | config::remove_role(&admin_cap, &mut config, sender(&ctx), ACL_FEE_TIER_MANAGER); 237 | config::remove_role(&admin_cap, &mut config, sender(&ctx), ACL_CLAIM_PROTOCOL_FEE); 238 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_POOL_MANAGER), 0); 239 | assert!(!acl::has_role(config::acl(&config), sender(&ctx), ACL_FEE_TIER_MANAGER), 0); 240 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_REWARDER_MANAGER), 0); 241 | assert!(!acl::has_role(config::acl(&config), sender(&ctx), ACL_CLAIM_PROTOCOL_FEE), 0); 242 | assert!(acl::has_role(config::acl(&config), sender(&ctx), ACL_PARTNER_MANAGER), 0); 243 | config::add_fee_tier(&mut config, 2, 2000, &ctx); 244 | 245 | public_share_object(config); 246 | public_transfer(admin_cap, tx_context::sender(&ctx)); 247 | } 248 | 249 | #[test] 250 | fun test_check_role() { 251 | let mut ctx = tx_context::dummy(); 252 | let (admin_cap, config) = config::new_global_config_for_test(&mut ctx, 1000); 253 | check_fee_tier_manager_role(&config, sender(&ctx)); 254 | check_rewarder_manager_role(&config, sender(&ctx)); 255 | check_partner_manager_role(&config, sender(&ctx)); 256 | check_pool_manager_role(&config, sender(&ctx)); 257 | 258 | public_share_object(config); 259 | public_transfer(admin_cap, tx_context::sender(&ctx)); 260 | } 261 | 262 | #[test] 263 | #[expected_failure(abort_code = cetus_clmm::config::ENoFeeTierManagerPermission)] 264 | fun test_check_role_failure() { 265 | let mut ctx = tx_context::dummy(); 266 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 267 | check_fee_tier_manager_role(&config, sender(&ctx)); 268 | check_rewarder_manager_role(&config, sender(&ctx)); 269 | check_partner_manager_role(&config, sender(&ctx)); 270 | check_pool_manager_role(&config, sender(&ctx)); 271 | remove_role(&admin_cap, &mut config, sender(&ctx), ACL_FEE_TIER_MANAGER); 272 | check_fee_tier_manager_role(&config, sender(&ctx)); 273 | 274 | public_share_object(config); 275 | public_transfer(admin_cap, tx_context::sender(&ctx)); 276 | } 277 | 278 | #[test] 279 | #[expected_failure(abort_code = cetus_clmm::config::EPackageVersionDeprecate)] 280 | fun test_emergency_pause() { 281 | let mut ctx = tx_context::dummy(); 282 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 283 | config::add_role(&admin_cap, &mut config, sender(&ctx), 5); 284 | config::emergency_pause(&mut config, &ctx); 285 | config::checked_package_version(&config); 286 | public_share_object(config); 287 | public_transfer(admin_cap, tx_context::sender(&ctx)); 288 | } 289 | 290 | #[test] 291 | #[expected_failure(abort_code = cetus_clmm::config::ENoPartnerManagerPermission)] 292 | fun test_check_partner_manager_role() { 293 | let mut ctx = tx_context::dummy(); 294 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 295 | check_partner_manager_role(&config, sender(&ctx)); 296 | config::remove_member(&admin_cap, &mut config, sender(&ctx)); 297 | check_partner_manager_role(&config, sender(&ctx)); 298 | public_share_object(config); 299 | public_transfer(admin_cap, tx_context::sender(&ctx)); 300 | } 301 | 302 | #[test] 303 | #[expected_failure(abort_code = cetus_clmm::config::ENoProtocolFeeClaimPermission)] 304 | fun test_check_protocol_fee_claim_role() { 305 | let mut ctx = tx_context::dummy(); 306 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 307 | check_protocol_fee_claim_role(&config, sender(&ctx)); 308 | config::remove_member(&admin_cap, &mut config, sender(&ctx)); 309 | check_protocol_fee_claim_role(&config, sender(&ctx)); 310 | public_share_object(config); 311 | public_transfer(admin_cap, tx_context::sender(&ctx)); 312 | } 313 | 314 | #[test] 315 | #[expected_failure(abort_code = cetus_clmm::config::ENoRewarderManagerPermission)] 316 | fun test_check_rewarder_manager_role() { 317 | let mut ctx = tx_context::dummy(); 318 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 319 | check_rewarder_manager_role(&config, sender(&ctx)); 320 | config::remove_member(&admin_cap, &mut config, sender(&ctx)); 321 | check_rewarder_manager_role(&config, sender(&ctx)); 322 | public_share_object(config); 323 | public_transfer(admin_cap, tx_context::sender(&ctx)); 324 | } 325 | 326 | #[test] 327 | #[expected_failure(abort_code = cetus_clmm::config::ENoPoolManagerPemission)] 328 | fun test_check_pool_manager_role() { 329 | let mut ctx = tx_context::dummy(); 330 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 331 | config::check_pool_manager_role(&config, sender(&ctx)); 332 | config::remove_member(&admin_cap, &mut config, sender(&ctx)); 333 | config::check_pool_manager_role(&config, sender(&ctx)); 334 | public_share_object(config); 335 | public_transfer(admin_cap, tx_context::sender(&ctx)); 336 | } 337 | 338 | #[test] 339 | #[expected_failure(abort_code = cetus_clmm::config::ENoEmergencyPausePermission)] 340 | fun test_check_emergency_pause_role() { 341 | let mut ctx = tx_context::dummy(); 342 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 343 | config::check_emergency_pause_role(&config, sender(&ctx)); 344 | config::remove_member(&admin_cap, &mut config, sender(&ctx)); 345 | config::check_emergency_pause_role(&config, sender(&ctx)); 346 | public_share_object(config); 347 | public_transfer(admin_cap, tx_context::sender(&ctx)); 348 | } 349 | 350 | #[test] 351 | fun test_read_config() { 352 | let mut ctx = tx_context::dummy(); 353 | let (admin_cap, config) = config::new_global_config_for_test(&mut ctx, 1000); 354 | assert_eq!(config::max_protocol_fee_rate(), 3000); 355 | assert_eq!(config::get_protocol_fee_rate(&config), 1000); 356 | assert_eq!(config.get_members().length(),1); 357 | public_share_object(config); 358 | public_transfer(admin_cap, tx_context::sender(&ctx)); 359 | } 360 | 361 | #[test] 362 | #[expected_failure(abort_code = cetus_clmm::config::EInvalidPackageVersion)] 363 | fun test_check_emergency_restore_version() { 364 | let mut ctx = tx_context::dummy(); 365 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 366 | config::update_package_version(&admin_cap, &mut config, 18446744073709551000); 367 | config::check_emergency_restore_version(&config); 368 | public_share_object(config); 369 | public_transfer(admin_cap, tx_context::sender(&ctx)); 370 | } 371 | 372 | #[test] 373 | #[expected_failure(abort_code = cetus_clmm::config::EConfigVersionNotEqualEmergencyRestoreVersion)] 374 | fun test_check_emergency_restore_version_failure() { 375 | let mut ctx = tx_context::dummy(); 376 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 377 | config::update_package_version(&admin_cap, &mut config, 13); 378 | config::check_emergency_restore_version(&config); 379 | public_share_object(config); 380 | public_transfer(admin_cap, tx_context::sender(&ctx)); 381 | } 382 | 383 | #[test] 384 | #[expected_failure(abort_code = cetus_clmm::config::EProtocolNotEmergencyPause)] 385 | fun test_emergency_unpause() { 386 | let mut ctx = tx_context::dummy(); 387 | let (admin_cap, mut config) = config::new_global_config_for_test(&mut ctx, 1000); 388 | config::add_role(&admin_cap, &mut config, sender(&ctx), 5); 389 | config::emergency_unpause(&mut config, 13, &ctx); 390 | public_share_object(config); 391 | public_transfer(admin_cap, tx_context::sender(&ctx)); 392 | } 393 | -------------------------------------------------------------------------------- /packages/cetus_clmm/sources/rewarder.move: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cetus Technology Limited 2 | 3 | /// `Rewarder` is the liquidity incentive module of `clmmpool`, which is commonly known as `farming`. In `clmmpool`, 4 | /// liquidity is stored in a price range, so `clmmpool` uses a reward allocation method based on effective liquidity. 5 | /// The allocation rules are roughly as follows: 6 | /// 7 | /// 1. Each pool can configure multiple `Rewarders`, and each `Rewarder` releases rewards at a uniform speed according 8 | /// to its configured release rate. 9 | /// 2. During the time period when the liquidity price range contains the current price of the pool, the liquidity 10 | /// position can participate in the reward distribution for this time period (if the pool itself is configured with 11 | /// rewards), and the proportion of the distribution depends on the size of the liquidity value of the position. 12 | /// Conversely, if the price range of a position does not include the current price of the pool during a certain period 13 | /// of time, then this position will not receive any rewards during this period of time. This is similar to the 14 | /// calculation of transaction fees. 15 | module cetus_clmm::rewarder; 16 | 17 | use cetus_clmm::config::{GlobalConfig, checked_package_version, AdminCap}; 18 | use integer_mate::full_math_u128; 19 | use integer_mate::math_u128; 20 | use std::type_name::{Self, TypeName}; 21 | use sui::bag::{Self, Bag}; 22 | use sui::balance::{Self, Balance}; 23 | use sui::event::emit; 24 | 25 | /// Maximum number of rewarders that can be configured per pool 26 | const REWARDER_NUM: u64 = 5; 27 | /// Number of seconds in a day (24 hours * 60 minutes * 60 seconds) 28 | const DAYS_IN_SECONDS: u128 = 24 * 60 * 60; 29 | /// Points emitted per second in Q64.64 fixed point format (1M points per second) 30 | const POINTS_EMISSIONS_PER_SECOND: u128 = 1000000 << 64; 31 | 32 | const ERewardSoltIsFull: u64 = 1; 33 | const ERewardAlreadyExist: u64 = 2; 34 | const EInvalidTime: u64 = 3; 35 | const ERewardAmountInsufficient: u64 = 4; 36 | const ERewardNotExist: u64 = 5; 37 | const ERewardCoinNotEnough: u64 = 6; 38 | 39 | /// Manager the Rewards and Points. 40 | /// * `rewarders` - The rewarders 41 | /// * `points_released` - The points released 42 | /// * `points_growth_global` - The points growth global 43 | /// * `last_updated_time` - The last updated time 44 | public struct RewarderManager has store { 45 | rewarders: vector, 46 | points_released: u128, 47 | points_growth_global: u128, 48 | last_updated_time: u64, 49 | } 50 | 51 | /// Rewarder store the information of a rewarder. 52 | /// * `reward_coin` - The type of reward coin 53 | /// * `emissions_per_second` - The amount of reward coin emit per second 54 | /// * `growth_global` - Q64.X64, is reward emited per liquidity 55 | public struct Rewarder has copy, drop, store { 56 | reward_coin: TypeName, 57 | emissions_per_second: u128, 58 | growth_global: u128, 59 | } 60 | 61 | /// RewarderGlobalVault store the rewarder `Balance` in Bag globally. 62 | /// * `id` - The unique identifier for this RewarderGlobalVault object 63 | /// * `balances` - A bag storing the balances of the rewarders 64 | public struct RewarderGlobalVault has key, store { 65 | id: UID, 66 | balances: Bag, 67 | } 68 | 69 | /// Emit when `RewarderManager` is initialized. 70 | /// * `global_vault_id` - The unique identifier for this RewarderGlobalVault object 71 | public struct RewarderInitEvent has copy, drop { 72 | global_vault_id: ID, 73 | } 74 | 75 | /// Emit when deposit reward. 76 | /// * `reward_type` - The type of reward coin 77 | /// * `deposit_amount` - The amount of reward coin deposited 78 | /// * `after_amount` - The amount of reward coin after deposit 79 | public struct DepositEvent has copy, drop, store { 80 | reward_type: TypeName, 81 | deposit_amount: u64, 82 | after_amount: u64, 83 | } 84 | 85 | /// Emit when withdraw reward. 86 | /// * `reward_type` - The type of reward coin 87 | /// * `withdraw_amount` - The amount of reward coin withdrawn 88 | /// * `after_amount` - The amount of reward coin after withdrawal 89 | public struct EmergentWithdrawEvent has copy, drop, store { 90 | reward_type: TypeName, 91 | withdraw_amount: u64, 92 | after_amount: u64, 93 | } 94 | 95 | /// init the `RewarderGlobalVault 96 | /// * `ctx` - The transaction context 97 | fun init(ctx: &mut TxContext) { 98 | let vault = RewarderGlobalVault { 99 | id: object::new(ctx), 100 | balances: bag::new(ctx), 101 | }; 102 | let vault_id = object::id(&vault); 103 | transfer::share_object(vault); 104 | emit(RewarderInitEvent { 105 | global_vault_id: vault_id, 106 | }) 107 | } 108 | 109 | /// initialize the `RewarderManager`. 110 | /// * Returns the new `RewarderManager` 111 | public(package) fun new(): RewarderManager { 112 | RewarderManager { 113 | rewarders: vector::empty(), 114 | points_released: 0, 115 | points_growth_global: 0, 116 | last_updated_time: 0, 117 | } 118 | } 119 | 120 | /// get the rewarders 121 | /// * `manager` - The `RewarderManager` 122 | /// * Returns the rewarders 123 | public fun rewarders(manager: &RewarderManager): vector { 124 | manager.rewarders 125 | } 126 | 127 | /// get the reward_growth_globals 128 | /// * `manager` - The `RewarderManager` 129 | /// * Returns the reward growth globals 130 | public fun rewards_growth_global(manager: &RewarderManager): vector { 131 | let mut idx = 0; 132 | let mut res = vector::empty(); 133 | while (idx < vector::length(&manager.rewarders)) { 134 | vector::push_back(&mut res, vector::borrow(&manager.rewarders, idx).growth_global); 135 | idx = idx + 1; 136 | }; 137 | res 138 | } 139 | 140 | /// get the points_released 141 | /// * `manager` - The `RewarderManager` 142 | /// * Returns the points released 143 | public fun points_released(manager: &RewarderManager): u128 { 144 | manager.points_released 145 | } 146 | 147 | /// get the points_growth_global 148 | /// * `manager` - The `RewarderManager` 149 | /// * Returns the points growth global 150 | public fun points_growth_global(manager: &RewarderManager): u128 { 151 | manager.points_growth_global 152 | } 153 | 154 | /// get the last_updated_time 155 | /// * `manager` - The `RewarderManager` 156 | /// * Returns the last updated time 157 | public fun last_update_time(manager: &RewarderManager): u64 { 158 | manager.last_updated_time 159 | } 160 | 161 | /// get the rewarder coin Type. 162 | /// * `rewarder` - The `Rewarder` 163 | /// * Returns the rewarder coin type 164 | public fun reward_coin(rewarder: &Rewarder): TypeName { 165 | rewarder.reward_coin 166 | } 167 | 168 | /// get the rewarder emissions_per_second. 169 | /// * `rewarder` - The `Rewarder` 170 | /// * Returns the rewarder emissions per second 171 | public fun emissions_per_second(rewarder: &Rewarder): u128 { 172 | rewarder.emissions_per_second 173 | } 174 | 175 | /// get the rewarder growth_global. 176 | /// * `rewarder` - The `Rewarder` 177 | /// * Returns the rewarder growth global 178 | public fun growth_global(rewarder: &Rewarder): u128 { 179 | rewarder.growth_global 180 | } 181 | 182 | /// Get index of CoinType in `RewarderManager`, if not exists, return `None` 183 | /// * `manager` - The `RewarderManager` 184 | /// * Returns the index of the rewarder 185 | public fun rewarder_index(manager: &RewarderManager): Option { 186 | let mut idx = 0; 187 | while (idx < vector::length(&manager.rewarders)) { 188 | if (vector::borrow(&manager.rewarders, idx).reward_coin == type_name::with_defining_ids()) { 189 | return option::some(idx) 190 | }; 191 | idx = idx + 1; 192 | }; 193 | option::none() 194 | } 195 | 196 | /// Borrow `Rewarder` from `RewarderManager` 197 | /// * `manager` - The `RewarderManager` 198 | /// * Returns the rewarder 199 | public fun borrow_rewarder(manager: &RewarderManager): &Rewarder { 200 | let mut idx = 0; 201 | while (idx < vector::length(&manager.rewarders)) { 202 | if (vector::borrow(&manager.rewarders, idx).reward_coin == type_name::with_defining_ids()) { 203 | return vector::borrow(&manager.rewarders, idx) 204 | }; 205 | idx = idx + 1; 206 | }; 207 | abort ERewardNotExist 208 | } 209 | 210 | /// Borrow mutable `Rewarder` from `RewarderManager 211 | /// * `manager` - The `RewarderManager` 212 | /// * Returns the mutable rewarder 213 | public(package) fun borrow_mut_rewarder(manager: &mut RewarderManager): &mut Rewarder { 214 | let mut idx = 0; 215 | while (idx < vector::length(&manager.rewarders)) { 216 | if (vector::borrow(&manager.rewarders, idx).reward_coin == type_name::with_defining_ids()) { 217 | return vector::borrow_mut(&mut manager.rewarders, idx) 218 | }; 219 | idx = idx + 1; 220 | }; 221 | abort ERewardNotExist 222 | } 223 | 224 | /// Add rewarder into `RewarderManager` 225 | /// Only support at most REWARDER_NUM rewarders. 226 | /// * `manager` - The `RewarderManager` 227 | public(package) fun add_rewarder(manager: &mut RewarderManager) { 228 | assert!(option::is_none(&rewarder_index(manager)), ERewardAlreadyExist); 229 | let rewarder_infos = &mut manager.rewarders; 230 | assert!(vector::length(rewarder_infos) <= REWARDER_NUM - 1, ERewardSoltIsFull); 231 | let rewarder_type = type_name::with_defining_ids(); 232 | let rewarder = Rewarder { 233 | reward_coin: rewarder_type, 234 | emissions_per_second: 0, 235 | growth_global: 0, 236 | }; 237 | vector::push_back(rewarder_infos, rewarder); 238 | } 239 | 240 | /// Settle the reward. 241 | /// Update the last_updated_time, the growth_global of each rewarder and points_growth_global. 242 | /// Settlement is needed when swap, modify position liquidity, update emission speed. 243 | /// * `manager` - The `RewarderManager` 244 | /// * `liquidity` - The liquidity of the pool 245 | /// * `timestamp` - The timestamp 246 | public(package) fun settle(manager: &mut RewarderManager, liquidity: u128, timestamp: u64) { 247 | let last_time = manager.last_updated_time; 248 | manager.last_updated_time = timestamp; 249 | assert!(last_time <= timestamp, EInvalidTime); 250 | if (liquidity == 0 || timestamp == last_time) { 251 | return 252 | }; 253 | let time_delta = (timestamp - last_time); 254 | let mut idx = 0; 255 | while (idx < vector::length(&manager.rewarders)) { 256 | let emission = vector::borrow(&manager.rewarders, idx).emissions_per_second; 257 | if (emission == 0) { 258 | idx = idx + 1; 259 | continue 260 | }; 261 | let rewarder_growth_delta = full_math_u128::mul_div_floor( 262 | (time_delta as u128), 263 | emission, 264 | liquidity, 265 | ); 266 | let last_growth_global = vector::borrow(&manager.rewarders, idx).growth_global; 267 | let rewarder = vector::borrow_mut(&mut manager.rewarders, idx); 268 | rewarder.growth_global = math_u128::wrapping_add(last_growth_global, rewarder_growth_delta); 269 | idx = idx + 1; 270 | }; 271 | 272 | // update points 273 | let points_growth_delta = full_math_u128::mul_div_floor( 274 | (time_delta as u128), 275 | POINTS_EMISSIONS_PER_SECOND, 276 | liquidity, 277 | ); 278 | let points_released_delta = (time_delta as u128) * POINTS_EMISSIONS_PER_SECOND; 279 | manager.points_released = manager.points_released + points_released_delta; 280 | manager.points_growth_global = math_u128::wrapping_add(manager.points_growth_global, points_growth_delta); 281 | } 282 | 283 | /// Update the reward emission speed. 284 | /// The reward balance at least enough for one day should in `RewarderGlobalVault` when the emission speed is not zero. 285 | /// The reward settlement is needed when update the emission speed. 286 | /// emissions_per_second is Q64.X64 287 | /// Params 288 | /// - `vault`: `RewarderGlobalVault` 289 | /// - `manager`: `RewarderManager` 290 | /// - `liquidity`: The current pool liquidity. 291 | /// - `emissions_per_second`: The emission speed 292 | /// - `timestamp`: The timestamp 293 | public(package) fun update_emission( 294 | vault: &RewarderGlobalVault, 295 | manager: &mut RewarderManager, 296 | liquidity: u128, 297 | emissions_per_second: u128, 298 | timestamp: u64, 299 | ) { 300 | settle(manager, liquidity, timestamp); 301 | 302 | let rewarder = borrow_mut_rewarder(manager); 303 | let old_emission = rewarder.emissions_per_second; 304 | if (emissions_per_second > 0 && emissions_per_second > old_emission) { 305 | let emission_per_day = DAYS_IN_SECONDS * emissions_per_second; 306 | let reward_type = type_name::with_defining_ids(); 307 | assert!(bag::contains(&vault.balances, reward_type), ERewardAmountInsufficient); 308 | let rewarder_balance = bag::borrow>( 309 | &vault.balances, 310 | reward_type, 311 | ); 312 | // emissions_per_second is Q64.X64, so we need shift left 64 for rewarder_balance. 313 | assert!( 314 | ((balance::value(rewarder_balance) as u128) << 64) >= emission_per_day, 315 | ERewardAmountInsufficient, 316 | ); 317 | }; 318 | rewarder.emissions_per_second = emissions_per_second; 319 | } 320 | 321 | /// Withdraw Reward from `RewarderGlobalVault` 322 | /// This method is used for claim reward in pool and emergent_withdraw. 323 | /// * `vault` - The `RewarderGlobalVault` 324 | /// * `amount` - The amount of reward coin to withdraw 325 | /// * Returns the balance of the reward coin 326 | public(package) fun withdraw_reward( 327 | vault: &mut RewarderGlobalVault, 328 | amount: u64, 329 | ): Balance { 330 | assert!(bag::contains(&vault.balances, type_name::with_defining_ids()), ERewardCoinNotEnough); 331 | let reward_balance = bag::borrow_mut>( 332 | &mut vault.balances, 333 | type_name::with_defining_ids(), 334 | ); 335 | assert!(balance::value(reward_balance) >= amount, ERewardCoinNotEnough); 336 | balance::split(reward_balance, amount) 337 | } 338 | 339 | /// Deposit Reward into `RewarderGlobalVault` 340 | /// * `config` - The global config 341 | /// * `vault` - The `RewarderGlobalVault` 342 | /// * `balance` - The balance of the reward coin 343 | /// * Returns the amount of reward coin deposited 344 | public fun deposit_reward( 345 | config: &GlobalConfig, 346 | vault: &mut RewarderGlobalVault, 347 | balance: Balance, 348 | ): u64 { 349 | checked_package_version(config); 350 | let reward_type = type_name::with_defining_ids(); 351 | if (!bag::contains(&vault.balances, reward_type)) { 352 | bag::add(&mut vault.balances, reward_type, balance::zero()); 353 | }; 354 | let deposit_amount = balance::value(&balance); 355 | let reward_balance = bag::borrow_mut>( 356 | &mut vault.balances, 357 | type_name::with_defining_ids(), 358 | ); 359 | let after_amount = balance::join(reward_balance, balance); 360 | emit(DepositEvent { 361 | reward_type: type_name::with_defining_ids(), 362 | deposit_amount, 363 | after_amount, 364 | }); 365 | after_amount 366 | } 367 | 368 | /// Withdraw reward Balance of CoinType from vault by the protocol `AdminCap`. 369 | /// This function is only used for emergency. 370 | /// * `config` - The global config 371 | /// * `vault` - The `RewarderGlobalVault` 372 | /// * `amount` - The amount of reward coin to withdraw 373 | /// * Returns the balance of the reward coin 374 | public fun emergent_withdraw( 375 | _: &AdminCap, 376 | config: &GlobalConfig, 377 | vault: &mut RewarderGlobalVault, 378 | amount: u64, 379 | ): Balance { 380 | checked_package_version(config); 381 | let withdraw_balance = withdraw_reward(vault, amount); 382 | let after_amount = balance_of(vault); 383 | emit(EmergentWithdrawEvent { 384 | reward_type: type_name::with_defining_ids(), 385 | withdraw_amount: amount, 386 | after_amount, 387 | }); 388 | withdraw_balance 389 | } 390 | 391 | /// Get the balances in vault. 392 | /// * `vault` - The `RewarderGlobalVault` 393 | /// * Returns the balances 394 | public fun balances(vault: &RewarderGlobalVault): &Bag { 395 | &vault.balances 396 | } 397 | 398 | /// Get the balance value of CoinType in vault. 399 | /// * `vault` - The `RewarderGlobalVault` 400 | /// * Returns the balance value of the reward coin 401 | public fun balance_of(vault: &RewarderGlobalVault): u64 { 402 | let reward_type = type_name::with_defining_ids(); 403 | if (!bag::contains(&vault.balances, reward_type)) { 404 | return 0 405 | }; 406 | let balance = bag::borrow>(&vault.balances, reward_type); 407 | balance::value(balance) 408 | } 409 | 410 | #[test_only] 411 | public fun new_vault_for_test(ctx: &mut TxContext): RewarderGlobalVault { 412 | RewarderGlobalVault { 413 | id: object::new(ctx), 414 | balances: bag::new(ctx), 415 | } 416 | } 417 | 418 | #[test_only] 419 | public fun new_rewarder_for_test( 420 | emissions_per_second: u128, 421 | growth_global: u128, 422 | ): Rewarder { 423 | Rewarder { 424 | reward_coin: type_name::with_defining_ids(), 425 | emissions_per_second, 426 | growth_global, 427 | } 428 | } 429 | 430 | #[test_only] 431 | public fun update_for_swap_test( 432 | manager: &mut RewarderManager, 433 | rewarders: vector, 434 | points_released: u128, 435 | points_growth_global: u128, 436 | last_updated_time: u64, 437 | ) { 438 | manager.rewarders = rewarders; 439 | manager.points_released = points_released; 440 | manager.points_growth_global = points_growth_global; 441 | manager.last_updated_time = last_updated_time; 442 | } 443 | 444 | #[test] 445 | fun test_init() { 446 | let mut sc = sui::test_scenario::begin(@0x23); 447 | init(sc.ctx()); 448 | sc.next_tx(@0x23); 449 | let vault = sui::test_scenario::take_shared(&sc); 450 | sui::test_scenario::return_shared(vault); 451 | sc.end(); 452 | } -------------------------------------------------------------------------------- /packages/cetus_clmm/tests/pool_creator_tests.move: -------------------------------------------------------------------------------- 1 | #[test_only] 2 | module cetus_clmm::pool_creator_tests { 3 | use cetus_clmm::cetus; 4 | use cetus_clmm::config::new_global_config_for_test; 5 | use cetus_clmm::factory::{Self, new_pools_for_test, init_manager_and_whitelist}; 6 | use cetus_clmm::pool_creator::{ 7 | create_pool_v3, 8 | full_range_tick_range, 9 | create_pool_v3_with_creation_cap, 10 | create_pool_v2_by_creation_cap 11 | }; 12 | use cetus_clmm::tick_math::{tick_bound, get_sqrt_price_at_tick}; 13 | use cetus_clmm::usdc; 14 | use integer_mate::i32; 15 | use std::string; 16 | use sui::balance; 17 | use sui::clock; 18 | 19 | #[test] 20 | fun test_create_pool_v3() { 21 | let mut ctx = tx_context::dummy(); 22 | let clock = clock::create_for_testing(&mut ctx); 23 | let (admin_cap, mut config) = new_global_config_for_test(&mut ctx, 1000); 24 | config.add_fee_tier(200, 1000, &ctx); 25 | let coin_a = balance::create_for_testing(10000000).into_coin(&mut ctx); 26 | let coin_b = balance::create_for_testing(10000000).into_coin(&mut ctx); 27 | let mut pools = new_pools_for_test(&mut ctx); 28 | init_manager_and_whitelist(&config, &mut pools, &mut ctx); 29 | let (position, coin_a, coin_b) = create_pool_v3( 30 | &config, 31 | &mut pools, 32 | 200, 33 | get_sqrt_price_at_tick(i32::from(1000)), 34 | string::utf8(b"https://cetus.zone"), 35 | 0, 36 | 2000, 37 | coin_b, 38 | coin_a, 39 | false, 40 | &clock, 41 | &mut ctx, 42 | ); 43 | transfer::public_transfer(admin_cap, ctx.sender()); 44 | coin_a.into_balance().destroy_for_testing(); 45 | coin_b.into_balance().destroy_for_testing(); 46 | transfer::public_transfer(position, ctx.sender()); 47 | transfer::public_share_object(config); 48 | transfer::public_share_object(pools); 49 | clock.destroy_for_testing(); 50 | } 51 | 52 | #[test] 53 | #[ 54 | expected_failure( 55 | abort_code = cetus_clmm::pool_creator::EInitSqrtPriceNotBetweenLowerAndUpper, 56 | ), 57 | ] 58 | fun test_create_pool_v3_sqrt_price_error() { 59 | let mut ctx = tx_context::dummy(); 60 | let clock = clock::create_for_testing(&mut ctx); 61 | let (admin_cap, mut config) = new_global_config_for_test(&mut ctx, 1000); 62 | config.add_fee_tier(200, 1000, &ctx); 63 | let coin_a = balance::create_for_testing(10000000).into_coin(&mut ctx); 64 | let coin_b = balance::create_for_testing(10000000).into_coin(&mut ctx); 65 | let mut pools = new_pools_for_test(&mut ctx); 66 | init_manager_and_whitelist(&config, &mut pools, &mut ctx); 67 | let (position, coin_a, coin_b) = create_pool_v3( 68 | &config, 69 | &mut pools, 70 | 200, 71 | get_sqrt_price_at_tick(i32::neg_from(1000)), 72 | string::utf8(b"https://cetus.zone"), 73 | 0, 74 | 2000, 75 | coin_b, 76 | coin_a, 77 | false, 78 | &clock, 79 | &mut ctx, 80 | ); 81 | transfer::public_transfer(admin_cap, ctx.sender()); 82 | coin_a.into_balance().destroy_for_testing(); 83 | coin_b.into_balance().destroy_for_testing(); 84 | transfer::public_transfer(position, ctx.sender()); 85 | transfer::public_share_object(config); 86 | transfer::public_share_object(pools); 87 | clock.destroy_for_testing(); 88 | } 89 | 90 | #[test] 91 | #[expected_failure(abort_code = cetus_clmm::pool_creator::EPoolIsPermission)] 92 | fun test_create_pool_v3_pool_is_permissioned() { 93 | let mut ctx = tx_context::dummy(); 94 | let clock = clock::create_for_testing(&mut ctx); 95 | let (admin_cap, mut config) = new_global_config_for_test(&mut ctx, 1000); 96 | config.add_fee_tier(200, 1000, &ctx); 97 | let coin_a = balance::create_for_testing(10000000).into_coin(&mut ctx); 98 | let coin_b = balance::create_for_testing(10000000).into_coin(&mut ctx); 99 | let mut pools = new_pools_for_test(&mut ctx); 100 | init_manager_and_whitelist(&config, &mut pools, &mut ctx); 101 | let cap = factory::mint_pool_creation_cap_by_admin( 102 | &config, 103 | &mut pools, 104 | &mut ctx, 105 | ); 106 | factory::add_allowed_pair_config(&config, &mut pools, 200, &ctx); 107 | factory::register_permission_pair( 108 | &config, 109 | &mut pools, 110 | 200, 111 | &cap, 112 | &mut ctx, 113 | ); 114 | let (position, coin_a, coin_b) = create_pool_v3( 115 | &config, 116 | &mut pools, 117 | 200, 118 | get_sqrt_price_at_tick(i32::from(1000)), 119 | string::utf8(b"https://cetus.zone"), 120 | 0, 121 | 2000, 122 | coin_b, 123 | coin_a, 124 | false, 125 | &clock, 126 | &mut ctx, 127 | ); 128 | transfer::public_transfer(cap, ctx.sender()); 129 | transfer::public_transfer(admin_cap, ctx.sender()); 130 | coin_a.into_balance().destroy_for_testing(); 131 | coin_b.into_balance().destroy_for_testing(); 132 | transfer::public_transfer(position, ctx.sender()); 133 | transfer::public_share_object(config); 134 | transfer::public_share_object(pools); 135 | clock.destroy_for_testing(); 136 | } 137 | 138 | #[test] 139 | fun test_create_pool_v3_with_creation_cap() { 140 | let mut ctx = tx_context::dummy(); 141 | let clock = clock::create_for_testing(&mut ctx); 142 | let (admin_cap, mut config) = new_global_config_for_test(&mut ctx, 1000); 143 | config.add_fee_tier(200, 1000, &ctx); 144 | let coin_a = balance::create_for_testing(10000000).into_coin(&mut ctx); 145 | let coin_b = balance::create_for_testing(10000000).into_coin(&mut ctx); 146 | let mut pools = new_pools_for_test(&mut ctx); 147 | init_manager_and_whitelist(&config, &mut pools, &mut ctx); 148 | let cap = factory::mint_pool_creation_cap_by_admin( 149 | &config, 150 | &mut pools, 151 | &mut ctx, 152 | ); 153 | factory::add_allowed_pair_config(&config, &mut pools, 200, &ctx); 154 | factory::register_permission_pair( 155 | &config, 156 | &mut pools, 157 | 200, 158 | &cap, 159 | &mut ctx, 160 | ); 161 | let (position, coin_a, coin_b) = create_pool_v3_with_creation_cap( 162 | &config, 163 | &mut pools, 164 | &cap, 165 | 200, 166 | get_sqrt_price_at_tick(i32::from(1000)), 167 | string::utf8(b"https://cetus.zone"), 168 | 0, 169 | 2000, 170 | coin_b, 171 | coin_a, 172 | false, 173 | &clock, 174 | &mut ctx, 175 | ); 176 | transfer::public_transfer(cap, ctx.sender()); 177 | transfer::public_transfer(admin_cap, ctx.sender()); 178 | coin_a.into_balance().destroy_for_testing(); 179 | coin_b.into_balance().destroy_for_testing(); 180 | transfer::public_transfer(position, ctx.sender()); 181 | transfer::public_share_object(config); 182 | transfer::public_share_object(pools); 183 | clock.destroy_for_testing(); 184 | } 185 | 186 | #[test] 187 | #[expected_failure(abort_code = cetus_clmm::factory::ECapNotMatchWithCoinType)] 188 | fun test_create_pool_v3_with_creation_cap_error_cap_not_match_with_coin_type() { 189 | let mut ctx = tx_context::dummy(); 190 | let clock = clock::create_for_testing(&mut ctx); 191 | let (admin_cap, mut config) = new_global_config_for_test(&mut ctx, 1000); 192 | config.add_fee_tier(200, 1000, &ctx); 193 | let coin_a = balance::create_for_testing(10000000).into_coin(&mut ctx); 194 | let coin_b = balance::create_for_testing(10000000).into_coin(&mut ctx); 195 | let mut pools = new_pools_for_test(&mut ctx); 196 | init_manager_and_whitelist(&config, &mut pools, &mut ctx); 197 | let cap = factory::mint_pool_creation_cap_by_admin( 198 | &config, 199 | &mut pools, 200 | &mut ctx, 201 | ); 202 | factory::add_allowed_pair_config(&config, &mut pools, 200, &ctx); 203 | factory::register_permission_pair( 204 | &config, 205 | &mut pools, 206 | 200, 207 | &cap, 208 | &mut ctx, 209 | ); 210 | let (position, coin_a, coin_b) = create_pool_v3_with_creation_cap( 211 | &config, 212 | &mut pools, 213 | &cap, 214 | 200, 215 | get_sqrt_price_at_tick(i32::from(1000)), 216 | string::utf8(b"https://cetus.zone"), 217 | 0, 218 | 2000, 219 | coin_b, 220 | coin_a, 221 | false, 222 | &clock, 223 | &mut ctx, 224 | ); 225 | transfer::public_transfer(cap, ctx.sender()); 226 | transfer::public_transfer(admin_cap, ctx.sender()); 227 | coin_a.into_balance().destroy_for_testing(); 228 | coin_b.into_balance().destroy_for_testing(); 229 | transfer::public_transfer(position, ctx.sender()); 230 | transfer::public_share_object(config); 231 | transfer::public_share_object(pools); 232 | clock.destroy_for_testing(); 233 | } 234 | 235 | #[test] 236 | #[ 237 | expected_failure( 238 | abort_code = cetus_clmm::pool_creator::EInitSqrtPriceNotBetweenLowerAndUpper, 239 | ), 240 | ] 241 | fun test_create_pool_v3_with_creation_cap_sqrt_price_error() { 242 | let mut ctx = tx_context::dummy(); 243 | let clock = clock::create_for_testing(&mut ctx); 244 | let (admin_cap, mut config) = new_global_config_for_test(&mut ctx, 1000); 245 | config.add_fee_tier(200, 1000, &ctx); 246 | let coin_a = balance::create_for_testing(10000000).into_coin(&mut ctx); 247 | let coin_b = balance::create_for_testing(10000000).into_coin(&mut ctx); 248 | let mut pools = new_pools_for_test(&mut ctx); 249 | init_manager_and_whitelist(&config, &mut pools, &mut ctx); 250 | let cap = factory::mint_pool_creation_cap_by_admin( 251 | &config, 252 | &mut pools, 253 | &mut ctx, 254 | ); 255 | factory::add_allowed_pair_config(&config, &mut pools, 200, &ctx); 256 | factory::register_permission_pair( 257 | &config, 258 | &mut pools, 259 | 200, 260 | &cap, 261 | &mut ctx, 262 | ); 263 | let (position, coin_a, coin_b) = create_pool_v3_with_creation_cap( 264 | &config, 265 | &mut pools, 266 | &cap, 267 | 200, 268 | get_sqrt_price_at_tick(i32::neg_from(1000)), 269 | string::utf8(b"https://cetus.zone"), 270 | 0, 271 | 2000, 272 | coin_b, 273 | coin_a, 274 | false, 275 | &clock, 276 | &mut ctx, 277 | ); 278 | transfer::public_transfer(cap, ctx.sender()); 279 | transfer::public_transfer(admin_cap, ctx.sender()); 280 | coin_a.into_balance().destroy_for_testing(); 281 | coin_b.into_balance().destroy_for_testing(); 282 | transfer::public_transfer(position, ctx.sender()); 283 | transfer::public_share_object(config); 284 | transfer::public_share_object(pools); 285 | clock.destroy_for_testing(); 286 | } 287 | 288 | #[test] 289 | #[expected_failure(abort_code = cetus_clmm::pool_creator::EMethodDeprecated)] 290 | fun test_create_pool_v3_by_creation_cap() { 291 | let mut ctx = tx_context::dummy(); 292 | let clock = clock::create_for_testing(&mut ctx); 293 | let metadata_a = cetus::init_coin(&mut ctx); 294 | let metadata_b = usdc::init_coin(&mut ctx); 295 | let (admin_cap, mut config) = new_global_config_for_test(&mut ctx, 1000); 296 | config.add_fee_tier(200, 1000, &ctx); 297 | let coin_a = balance::create_for_testing(10000000).into_coin(&mut ctx); 298 | let coin_b = balance::create_for_testing(10000000).into_coin(&mut ctx); 299 | let mut pools = new_pools_for_test(&mut ctx); 300 | init_manager_and_whitelist(&config, &mut pools, &mut ctx); 301 | let cap = factory::mint_pool_creation_cap_by_admin( 302 | &config, 303 | &mut pools, 304 | &mut ctx, 305 | ); 306 | factory::add_allowed_pair_config(&config, &mut pools, 200, &ctx); 307 | factory::register_permission_pair( 308 | &config, 309 | &mut pools, 310 | 200, 311 | &cap, 312 | &mut ctx, 313 | ); 314 | let (position, coin_a, coin_b) = create_pool_v2_by_creation_cap( 315 | &config, 316 | &mut pools, 317 | &cap, 318 | 200, 319 | get_sqrt_price_at_tick(i32::from(1000)), 320 | string::utf8(b"https://cetus.zone"), 321 | coin_a, 322 | coin_b, 323 | &metadata_a, 324 | &metadata_b, 325 | false, 326 | &clock, 327 | &mut ctx, 328 | ); 329 | transfer::public_transfer(cap, ctx.sender()); 330 | transfer::public_transfer(admin_cap, ctx.sender()); 331 | coin_a.into_balance().destroy_for_testing(); 332 | coin_b.into_balance().destroy_for_testing(); 333 | transfer::public_transfer(position, ctx.sender()); 334 | transfer::public_share_object(config); 335 | transfer::public_share_object(pools); 336 | transfer::public_freeze_object(metadata_a); 337 | transfer::public_freeze_object(metadata_b); 338 | clock.destroy_for_testing(); 339 | } 340 | 341 | #[test] 342 | fun test_full_range_tick_range() { 343 | let (min_tick, max_tick) = full_range_tick_range(1); 344 | assert!(min_tick == i32::neg_from(tick_bound()).as_u32(), 0); 345 | assert!(max_tick == tick_bound(), 0); 346 | let (min_tick, max_tick) = full_range_tick_range(2); 347 | assert!(min_tick == i32::neg_from(tick_bound()).as_u32(), 0); 348 | assert!(max_tick == tick_bound(), 0); 349 | let (min_tick, max_tick) = full_range_tick_range(10); 350 | 351 | assert!(min_tick == i32::neg_from(443630).as_u32(), 0); 352 | assert!(max_tick == 443630, 0); 353 | let (min_tick, max_tick) = full_range_tick_range(200); 354 | assert!(min_tick == i32::neg_from(443600).as_u32(), 0); 355 | assert!(max_tick == 443600, 0); 356 | } 357 | 358 | #[test] 359 | #[expected_failure(abort_code = cetus_clmm::pool_creator::ECapNotMatchWithPoolKey)] 360 | fun test_create_pool_v3_with_creation_cap_not_match_with_pool_key() { 361 | let mut ctx = tx_context::dummy(); 362 | let clock = clock::create_for_testing(&mut ctx); 363 | let (admin_cap, mut config) = new_global_config_for_test(&mut ctx, 1000); 364 | config.add_fee_tier(200, 1000, &ctx); 365 | let coin_a = balance::create_for_testing(10000000).into_coin(&mut ctx); 366 | let coin_b = balance::create_for_testing(10000000).into_coin(&mut ctx); 367 | let mut pools = new_pools_for_test(&mut ctx); 368 | init_manager_and_whitelist(&config, &mut pools, &mut ctx); 369 | let cap = factory::mint_pool_creation_cap_by_admin( 370 | &config, 371 | &mut pools, 372 | &mut ctx, 373 | ); 374 | let error_cap = factory::mint_pool_creation_cap_by_admin( 375 | &config, 376 | &mut pools, 377 | &mut ctx, 378 | ); 379 | factory::add_allowed_pair_config(&config, &mut pools, 200, &ctx); 380 | factory::register_permission_pair( 381 | &config, 382 | &mut pools, 383 | 200, 384 | &cap, 385 | &mut ctx, 386 | ); 387 | let (position, coin_a, coin_b) = create_pool_v3_with_creation_cap( 388 | &config, 389 | &mut pools, 390 | &error_cap, 391 | 200, 392 | get_sqrt_price_at_tick(i32::from(1000)), 393 | string::utf8(b"https://cetus.zone"), 394 | 0, 395 | 2000, 396 | coin_b, 397 | coin_a, 398 | false, 399 | &clock, 400 | &mut ctx, 401 | ); 402 | transfer::public_transfer(cap, ctx.sender()); 403 | transfer::public_transfer(error_cap, ctx.sender()); 404 | transfer::public_transfer(admin_cap, ctx.sender()); 405 | coin_a.into_balance().destroy_for_testing(); 406 | coin_b.into_balance().destroy_for_testing(); 407 | transfer::public_transfer(position, ctx.sender()); 408 | transfer::public_share_object(config); 409 | transfer::public_share_object(pools); 410 | clock.destroy_for_testing(); 411 | } 412 | } 413 | 414 | #[test_only] 415 | module cetus_clmm::cetus { 416 | use sui::coin::{Self, CoinMetadata}; 417 | use sui::test_utils; 418 | 419 | public struct CETUS has drop {} 420 | 421 | #[allow(lint(self_transfer))] 422 | public fun init_coin(ctx: &mut TxContext): CoinMetadata { 423 | let (treasury_cap_cetus, metadata_cetus) = coin::create_currency( 424 | test_utils::create_one_time_witness(), 425 | 9, 426 | b"CETUS", 427 | b"CETUS", 428 | b"CETUS", 429 | option::none(), 430 | ctx, 431 | ); 432 | transfer::public_transfer(treasury_cap_cetus, ctx.sender()); 433 | metadata_cetus 434 | } 435 | } 436 | 437 | #[test_only] 438 | module cetus_clmm::usdc { 439 | use sui::coin::{Self, CoinMetadata}; 440 | use sui::test_utils; 441 | 442 | public struct USDC has drop {} 443 | 444 | #[allow(lint(self_transfer))] 445 | public fun init_coin(ctx: &mut TxContext): CoinMetadata { 446 | let (treasury_cap, metadata) = coin::create_currency( 447 | test_utils::create_one_time_witness(), 448 | 6, 449 | b"USDC", 450 | b"USDC", 451 | b"USDC", 452 | option::none(), 453 | ctx, 454 | ); 455 | transfer::public_transfer(treasury_cap, ctx.sender()); 456 | metadata 457 | } 458 | } 459 | --------------------------------------------------------------------------------