From 0040493d17f2586cda146028696e7252b227f028 Mon Sep 17 00:00:00 2001 From: Tony Lee Date: Fri, 31 Jan 2025 13:09:51 -0500 Subject: [PATCH] Permissionless Pool Creation (#322) * first iteration of permissionless pool creation * permissionless pool creation * whitelist false for permissionless --- .../deepbook/sources/helper/constants.move | 10 +- packages/deepbook/sources/pool.move | 48 ++++- packages/deepbook/sources/registry.move | 80 ++++++++- .../deepbook/sources/state/governance.move | 9 +- packages/deepbook/tests/pool_tests.move | 169 ++++++++++++++++++ 5 files changed, 301 insertions(+), 15 deletions(-) diff --git a/packages/deepbook/sources/helper/constants.move b/packages/deepbook/sources/helper/constants.move index d4a775fc..e066e5d1 100644 --- a/packages/deepbook/sources/helper/constants.move +++ b/packages/deepbook/sources/helper/constants.move @@ -3,8 +3,8 @@ module deepbook::constants; -const CURRENT_VERSION: u64 = 1; // Update version during upgrades -const POOL_CREATION_FEE: u64 = 0 * 1_000_000; // 0 DEEP +const CURRENT_VERSION: u64 = 2; // Update version during upgrades +const POOL_CREATION_FEE: u64 = 500 * 1_000_000; // 500 DEEP const FLOAT_SCALING: u64 = 1_000_000_000; const FLOAT_SCALING_U128: u128 = 1_000_000_000; const MAX_U64: u64 = ((1u128 << 64) - 1) as u64; @@ -19,9 +19,11 @@ const FEE_PENALTY_MULTIPLIER: u64 = 250_000_000; // 25% // Restrictions on limit orders. // No restriction on the order. const NO_RESTRICTION: u8 = 0; -// Mandates that whatever amount of an order that can be executed in the current transaction, be filled and then the rest of the order canceled. +// Mandates that whatever amount of an order that can be executed in the current +// transaction, be filled and then the rest of the order canceled. const IMMEDIATE_OR_CANCEL: u8 = 1; -// Mandates that the entire order size be filled in the current transaction. Otherwise, the order is canceled. +// Mandates that the entire order size be filled in the current transaction. +// Otherwise, the order is canceled. const FILL_OR_KILL: u8 = 2; // Mandates that the entire order be passive. Otherwise, cancel the order. const POST_ONLY: u8 = 3; diff --git a/packages/deepbook/sources/pool.move b/packages/deepbook/sources/pool.move index 993e52e5..5da8b966 100644 --- a/packages/deepbook/sources/pool.move +++ b/packages/deepbook/sources/pool.move @@ -94,6 +94,42 @@ public struct DeepBurned< deep_burned: u64, } +// === Public-Mutative Functions * POOL CREATION * === +/// Create a new pool. The pool is registered in the registry. +/// Checks are performed to ensure the tick size, lot size, +/// and min size are valid. +/// The creation fee is transferred to the treasury address. +/// Returns the id of the pool created +public fun create_permissionless_pool( + registry: &mut Registry, + tick_size: u64, + lot_size: u64, + min_size: u64, + creation_fee: Coin, + ctx: &mut TxContext, +): ID { + assert!( + creation_fee.value() == constants::pool_creation_fee(), + EInvalidFee, + ); + let base_type = type_name::get(); + let quote_type = type_name::get(); + let whitelisted_pool = false; + let stable_pool = + registry.is_stablecoin(base_type) && registry.is_stablecoin(quote_type); + + create_pool( + registry, + tick_size, + lot_size, + min_size, + creation_fee, + whitelisted_pool, + stable_pool, + ctx, + ) +} + // === Public-Mutative Functions * EXCHANGE * === /// Place a limit order. Quantity is in base asset terms. /// For current version pay_with_deep must be true, so the fee will be paid with @@ -660,7 +696,6 @@ public fun burn_deep( /// Create a new pool. The pool is registered in the registry. /// Checks are performed to ensure the tick size, lot size, and min size are /// valid. -/// The creation fee is transferred to the treasury address. /// Returns the id of the pool created public fun create_pool_admin( registry: &mut Registry, @@ -768,6 +803,13 @@ public fun whitelisted( self.load_inner().state.governance().whitelisted() } +/// Accessor to check if the pool is a stablecoin pool. +public fun stable_pool( + self: &Pool, +): bool { + self.load_inner().state.governance().stable() +} + public fun registered_pool( self: &Pool, ): bool { @@ -1072,10 +1114,6 @@ public(package) fun create_pool( stable_pool: bool, ctx: &mut TxContext, ): ID { - assert!( - creation_fee.value() == constants::pool_creation_fee(), - EInvalidFee, - ); assert!(tick_size > 0, EInvalidTickSize); assert!(math::is_power_of_ten(tick_size), EInvalidTickSize); assert!(lot_size > 0, EInvalidLotSize); diff --git a/packages/deepbook/sources/registry.move b/packages/deepbook/sources/registry.move index 858250c7..d06c859e 100644 --- a/packages/deepbook/sources/registry.move +++ b/packages/deepbook/sources/registry.move @@ -7,6 +7,7 @@ module deepbook::registry; use deepbook::constants; use std::type_name::{Self, TypeName}; use sui::bag::{Self, Bag}; +use sui::dynamic_field; use sui::vec_set::{Self, VecSet}; use sui::versioned::{Self, Versioned}; @@ -17,6 +18,8 @@ const EPackageVersionNotEnabled: u64 = 3; const EVersionNotEnabled: u64 = 4; const EVersionAlreadyEnabled: u64 = 5; const ECannotDisableCurrentVersion: u64 = 6; +const ECoinAlreadyWhitelisted: u64 = 7; +const ECoinNotWhitelisted: u64 = 8; public struct REGISTRY has drop {} @@ -42,6 +45,8 @@ public struct PoolKey has copy, drop, store { quote: TypeName, } +public struct StableCoinKey has store, copy, drop {} + fun init(_: REGISTRY, ctx: &mut TxContext) { let registry_inner = RegistryInner { allowed_versions: vec_set::singleton(constants::current_version()), @@ -103,6 +108,78 @@ public fun disable_version( self.allowed_versions.remove(&version); } +/// Adds a stablecoin to the whitelist +/// Only Admin can add stablecoin +public fun add_stablecoin( + self: &mut Registry, + _cap: &DeepbookAdminCap, +) { + let _: &mut RegistryInner = self.load_inner_mut(); + let stable_type = type_name::get(); + if ( + !dynamic_field::exists_( + &self.id, + StableCoinKey {}, + ) + ) { + dynamic_field::add( + &mut self.id, + StableCoinKey {}, + vec_set::singleton(stable_type), + ); + } else { + let stable_coins: &mut VecSet = dynamic_field::borrow_mut( + &mut self.id, + StableCoinKey {}, + ); + assert!(!stable_coins.contains(&stable_type), ECoinAlreadyWhitelisted); + stable_coins.insert(stable_type); + }; +} + +/// Removes a stablecoin from the whitelist +/// Only Admin can remove stablecoin +public fun remove_stablecoin( + self: &mut Registry, + _cap: &DeepbookAdminCap, +) { + let _: &mut RegistryInner = self.load_inner_mut(); + let stable_type = type_name::get(); + assert!( + dynamic_field::exists_( + &self.id, + StableCoinKey {}, + ), + ECoinNotWhitelisted, + ); + let stable_coins: &mut VecSet = dynamic_field::borrow_mut( + &mut self.id, + StableCoinKey {}, + ); + assert!(stable_coins.contains(&stable_type), ECoinNotWhitelisted); + stable_coins.remove(&stable_type); +} + +/// Returns whether the given coin is whitelisted +public fun is_stablecoin(self: &Registry, stable_type: TypeName): bool { + let _: &RegistryInner = self.load_inner(); + if ( + !dynamic_field::exists_( + &self.id, + StableCoinKey {}, + ) + ) { + false + } else { + let stable_coins: &VecSet = dynamic_field::borrow( + &self.id, + StableCoinKey {}, + ); + + stable_coins.contains(&stable_type) + } +} + // === Public-Package Functions === public(package) fun load_inner_mut(self: &mut Registry): &mut RegistryInner { let inner: &mut RegistryInner = self.inner.load_value_mut(); @@ -116,7 +193,8 @@ public(package) fun load_inner_mut(self: &mut Registry): &mut RegistryInner { } /// Register a new pool in the registry. -/// Asserts if (Base, Quote) pool already exists or (Quote, Base) pool already exists. +/// Asserts if (Base, Quote) pool already exists or +/// (Quote, Base) pool already exists. public(package) fun register_pool( self: &mut Registry, pool_id: ID, diff --git a/packages/deepbook/sources/state/governance.move b/packages/deepbook/sources/state/governance.move index 3064bd2f..d02c1561 100644 --- a/packages/deepbook/sources/state/governance.move +++ b/packages/deepbook/sources/state/governance.move @@ -114,6 +114,10 @@ public(package) fun whitelisted(self: &Governance): bool { self.whitelisted } +public(package) fun stable(self: &Governance): bool { + self.stable +} + public(package) fun quorum(self: &Governance): u64 { self.quorum } @@ -312,11 +316,6 @@ public fun voting_power(self: &Governance): u64 { self.voting_power } -#[test_only] -public fun stable(self: &Governance): bool { - self.stable -} - #[test_only] public fun proposals(self: &Governance): VecMap { self.proposals diff --git a/packages/deepbook/tests/pool_tests.move b/packages/deepbook/tests/pool_tests.move index b7cce387..435a8e2f 100644 --- a/packages/deepbook/tests/pool_tests.move +++ b/packages/deepbook/tests/pool_tests.move @@ -7,6 +7,7 @@ module deepbook::pool_tests; use deepbook::balance_manager::{BalanceManager, TradeCap}; use deepbook::balance_manager_tests::{ USDC, + USDT, SPAM, create_acct_and_share_with_funds, create_acct_and_share_with_funds_typed @@ -1022,6 +1023,94 @@ fun test_create_pool(whitelisted_pool: bool, stable_pool: bool) { end(test); } +#[test] +fun test_permissionless_pools() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + // Only 1 coin is stable + add_stablecoin(OWNER, registry_id, &mut test); + let pool_id_1 = setup_default_permissionless_pool( + OWNER, + registry_id, + &mut test, + ); + check_pool_attributes(pool_id_1, false, false, &mut test); + + let pool_id_2 = setup_default_permissionless_pool( + OWNER, + registry_id, + &mut test, + ); + check_pool_attributes(pool_id_2, false, false, &mut test); + + // Now both coins are stable + unregister_pool(pool_id_2, registry_id, &mut test); + add_stablecoin(OWNER, registry_id, &mut test); + let pool_id_2 = setup_default_permissionless_pool( + OWNER, + registry_id, + &mut test, + ); + check_pool_attributes(pool_id_2, false, true, &mut test); + + let pool_id_3 = setup_default_permissionless_pool( + OWNER, + registry_id, + &mut test, + ); + check_pool_attributes(pool_id_3, false, false, &mut test); + + end(test); +} + +#[ + test, + expected_failure( + abort_code = ::deepbook::registry::ECoinAlreadyWhitelisted, + ), +] +fun test_adding_duplicate_stablecoin_e() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + add_stablecoin(OWNER, registry_id, &mut test); + add_stablecoin(OWNER, registry_id, &mut test); + + end(test); +} + +#[ + test, + expected_failure( + abort_code = ::deepbook::registry::ECoinNotWhitelisted, + ), +] +fun test_removing_not_whitelisted_stablecoin_e() { + let mut test = begin(OWNER); + let registry_id = setup_test(OWNER, &mut test); + + add_stablecoin(OWNER, registry_id, &mut test); + remove_stablecoin(OWNER, registry_id, &mut test); + + end(test); +} + +fun check_pool_attributes( + pool_id: ID, + whitelisted: bool, + stable: bool, + test: &mut Scenario, +) { + test.next_tx(OWNER); + { + let pool = test.take_shared_by_id>(pool_id); + assert!(pool.whitelisted() == whitelisted, 0); + assert!(pool.stable_pool() == stable, 0); + return_shared(pool); + } +} + #[test_only] public(package) fun setup_test(owner: address, test: &mut Scenario): ID { test.next_tx(owner); @@ -1231,6 +1320,22 @@ public(package) fun setup_pool_with_default_fees_return_fee< pool_id } +#[test_only] +public(package) fun setup_default_permissionless_pool( + sender: address, + registry_id: ID, + test: &mut Scenario, +): ID { + setup_permissionsless_pool( + sender, + constants::tick_size(), // tick size + constants::lot_size(), // lot size + constants::min_size(), // min size + registry_id, + test, + ) +} + #[test_only] /// Place a limit order public(package) fun place_limit_order( @@ -4763,6 +4868,38 @@ fun setup_pool( pool_id } +fun setup_permissionsless_pool( + sender: address, + tick_size: u64, + lot_size: u64, + min_size: u64, + registry_id: ID, + test: &mut Scenario, +): ID { + test.next_tx(sender); + let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); + let mut registry = test.take_shared_by_id(registry_id); + let pool_id; + { + pool_id = + pool::create_permissionless_pool( + &mut registry, + tick_size, + lot_size, + min_size, + mint_for_testing( + constants::pool_creation_fee(), + test.ctx(), + ), + test.ctx(), + ); + }; + return_shared(registry); + test_utils::destroy(admin_cap); + + pool_id +} + fun get_mid_price( pool_id: ID, test: &mut Scenario, @@ -5081,3 +5218,35 @@ fun adjust_tick_size_admin( return_shared(pool); return_shared(clock); } + +fun add_stablecoin(sender: address, registry_id: ID, test: &mut Scenario) { + test.next_tx(sender); + let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); + let mut registry = test.take_shared_by_id(registry_id); + { + registry::add_stablecoin( + &mut registry, + &admin_cap, + ); + }; + return_shared(registry); + test_utils::destroy(admin_cap); +} + +fun remove_stablecoin( + sender: address, + registry_id: ID, + test: &mut Scenario, +) { + test.next_tx(sender); + let admin_cap = registry::get_admin_cap_for_testing(test.ctx()); + let mut registry = test.take_shared_by_id(registry_id); + { + registry::remove_stablecoin( + &mut registry, + &admin_cap, + ); + }; + return_shared(registry); + test_utils::destroy(admin_cap); +}