diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index 57d086b538..e72c77a0fb 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -274,7 +274,6 @@ parameter_types! { impl pallet_subtensor_swap::Config for Test { type RuntimeEvent = RuntimeEvent; - type AdminOrigin = EnsureRoot; type SubnetInfo = SubtensorModule; type BalanceOps = SubtensorModule; type ProtocolId = SwapProtocolId; diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index cb5c4bb0a8..15b2dfe7fb 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2439,6 +2439,10 @@ impl> fn mechanism(netuid: u16) -> u16 { SubnetMechanism::::get(netuid) } + + fn is_owner(account_id: &T::AccountId, netuid: u16) -> bool { + SubnetOwner::::get(netuid) == *account_id + } } impl> diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 1f30b115ef..4dd4ff202a 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -430,7 +430,6 @@ parameter_types! { impl pallet_subtensor_swap::Config for Test { type RuntimeEvent = RuntimeEvent; - type AdminOrigin = EnsureRoot; type SubnetInfo = SubtensorModule; type BalanceOps = SubtensorModule; type ProtocolId = SwapProtocolId; diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index c4735d7050..78d80a8e1b 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -47,6 +47,7 @@ pub trait SubnetInfo { fn alpha_reserve(netuid: u16) -> u64; fn exists(netuid: u16) -> bool; fn mechanism(netuid: u16) -> u16; + fn is_owner(account_id: &AccountId, netuid: u16) -> bool; } pub trait BalanceOps { diff --git a/pallets/swap/src/benchmarking.rs b/pallets/swap/src/benchmarking.rs index 6f3327d00e..117a2dd86f 100644 --- a/pallets/swap/src/benchmarking.rs +++ b/pallets/swap/src/benchmarking.rs @@ -10,8 +10,8 @@ use substrate_fixed::types::U64F64; use crate::{ NetUid, pallet::{ - AlphaSqrtPrice, Call, Config, CurrentLiquidity, CurrentTick, Pallet, Positions, - SwapV3Initialized, + AlphaSqrtPrice, Call, Config, CurrentLiquidity, CurrentTick, EnabledUserLiquidity, Pallet, + Positions, SwapV3Initialized, }, position::{Position, PositionId}, tick::TickIndex, @@ -125,5 +125,17 @@ mod benchmarks { ); } + #[benchmark] + fn set_enabled_user_liquidity() { + let netuid = NetUid::from(101); + + assert!(!EnabledUserLiquidity::::get(netuid)); + + #[extrinsic_call] + set_enabled_user_liquidity(RawOrigin::Root, netuid.into()); + + assert!(EnabledUserLiquidity::::get(netuid)); + } + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index e439f5be67..9c9e9bc1ce 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -8,7 +8,7 @@ use frame_support::{ PalletId, parameter_types, traits::{ConstU32, Everything}, }; -use frame_system::{self as system, EnsureRoot}; +use frame_system::{self as system}; use sp_core::H256; use sp_runtime::{ BuildStorage, @@ -16,6 +16,8 @@ use sp_runtime::{ }; use subtensor_swap_interface::{BalanceOps, SubnetInfo}; +use crate::{NetUid, pallet::EnabledUserLiquidity}; + construct_runtime!( pub enum Test { System: frame_system = 0, @@ -27,6 +29,8 @@ pub type Block = frame_system::mocking::MockBlock; pub type AccountId = u32; pub const OK_COLDKEY_ACCOUNT_ID: AccountId = 1; pub const OK_HOTKEY_ACCOUNT_ID: AccountId = 1000; +pub const NOT_SUBNET_OWNER: AccountId = 666; +pub const NON_EXISTENT_NETUID: u16 = 999; parameter_types! { pub const BlockHashCount: u64 = 250; @@ -91,13 +95,17 @@ impl SubnetInfo for MockLiquidityProvider { } } - fn exists(_netuid: u16) -> bool { - true + fn exists(netuid: u16) -> bool { + netuid != NON_EXISTENT_NETUID } fn mechanism(netuid: u16) -> u16 { if netuid == 0 { 0 } else { 1 } } + + fn is_owner(account_id: &AccountId, _netuid: u16) -> bool { + *account_id != NOT_SUBNET_OWNER + } } pub struct MockBalanceOps; @@ -148,7 +156,6 @@ impl BalanceOps for MockBalanceOps { impl crate::pallet::Config for Test { type RuntimeEvent = RuntimeEvent; - type AdminOrigin = EnsureRoot; type SubnetInfo = MockLiquidityProvider; type BalanceOps = MockBalanceOps; type ProtocolId = SwapProtocolId; @@ -165,6 +172,13 @@ pub fn new_test_ext() -> sp_io::TestExternalities { .build_storage() .unwrap(); let mut ext = sp_io::TestExternalities::new(storage); - ext.execute_with(|| System::set_block_number(1)); + ext.execute_with(|| { + System::set_block_number(1); + + for netuid in 0u16..=100 { + // enable V3 for this range of netuids + EnabledUserLiquidity::::set(NetUid::from(netuid), true); + } + }); ext } diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index e3c7b3a4ed..8d3b4ca521 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -676,6 +676,11 @@ impl Pallet { tick_high: TickIndex, liquidity: u64, ) -> Result<(PositionId, u64, u64), Error> { + ensure!( + EnabledUserLiquidity::::get(netuid), + Error::::UserLiquidityDisabled + ); + let (position, tao, alpha) = Self::add_liquidity_not_insert( netuid, coldkey_account_id, @@ -747,22 +752,6 @@ impl Pallet { let current_price = AlphaSqrtPrice::::get(netuid); let (tao, alpha) = position.to_token_amounts(current_price)?; - // If this is a user transaction, withdraw balances and update reserves - // TODO this should be returned (tao, alpha) from this function to prevent - // mutation of outside storage - the logic should be passed to the user of - // subtensor_swap_interface - // if !protocol { - // let current_price = self.state_ops.get_alpha_sqrt_price(); - // let (tao, alpha) = position.to_token_amounts(current_price)?; - // self.state_ops.withdraw_balances(coldkey_account_id, tao, alpha)?; - - // // Update reserves - // let new_tao_reserve = self.state_ops.get_tao_reserve().saturating_add(tao); - // self.state_ops.set_tao_reserve(new_tao_reserve); - // let new_alpha_reserve = self.state_ops.get_alpha_reserve().saturating_add(alpha); - // self.state_ops.set_alpha_reserve(new_alpha_reserve); - // } - SwapV3Initialized::::set(netuid, true); Ok((position, tao, alpha)) @@ -776,6 +765,11 @@ impl Pallet { coldkey_account_id: &T::AccountId, position_id: PositionId, ) -> Result> { + ensure!( + EnabledUserLiquidity::::get(netuid), + Error::::UserLiquidityDisabled + ); + let Some(mut position) = Positions::::get((netuid, coldkey_account_id, position_id)) else { return Err(Error::::LiquidityNotFound); @@ -801,18 +795,6 @@ impl Pallet { // Remove user position Positions::::remove((netuid, coldkey_account_id, position_id)); - { - // TODO we move this logic to the outside depender to prevent mutating its state - // // Deposit balances - // self.state_ops.deposit_balances(account_id, tao, alpha); - - // // Update reserves - // let new_tao_reserve = self.state_ops.get_tao_reserve().saturating_sub(tao); - // self.state_ops.set_tao_reserve(new_tao_reserve); - // let new_alpha_reserve = self.state_ops.get_alpha_reserve().saturating_sub(alpha); - // self.state_ops.set_alpha_reserve(new_alpha_reserve); - } - Ok(UpdateLiquidityResult { tao, alpha, @@ -828,6 +810,11 @@ impl Pallet { position_id: PositionId, liquidity_delta: i64, ) -> Result> { + ensure!( + EnabledUserLiquidity::::get(netuid), + Error::::UserLiquidityDisabled + ); + // Find the position let Some(mut position) = Positions::::get((netuid, coldkey_account_id, position_id)) else { @@ -1138,7 +1125,7 @@ pub enum SwapStepAction { #[cfg(test)] mod tests { use approx::assert_abs_diff_eq; - use frame_support::{assert_err, assert_ok}; + use frame_support::{assert_err, assert_noop, assert_ok}; use sp_arithmetic::helpers_128bit; use super::*; @@ -2441,4 +2428,77 @@ mod tests { } }); } + + #[test] + fn test_user_liquidity_disabled() { + new_test_ext().execute_with(|| { + // Use a netuid above 100 since our mock enables liquidity for 0-100 + let netuid = NetUid::from(101); + let tick_low = TickIndex::new_unchecked(-1000); + let tick_high = TickIndex::new_unchecked(1000); + let position_id = 1; + let liquidity = 1_000_000_000; + let liquidity_delta = 500_000_000; + + assert!(!EnabledUserLiquidity::::get(netuid)); + + assert_noop!( + Swap::do_add_liquidity( + netuid, + &OK_COLDKEY_ACCOUNT_ID, + &OK_HOTKEY_ACCOUNT_ID, + tick_low, + tick_high, + liquidity + ), + Error::::UserLiquidityDisabled + ); + + assert_noop!( + Swap::do_remove_liquidity(netuid, &OK_COLDKEY_ACCOUNT_ID, position_id.into()), + Error::::UserLiquidityDisabled + ); + + assert_noop!( + Swap::modify_position( + RuntimeOrigin::signed(OK_COLDKEY_ACCOUNT_ID), + OK_HOTKEY_ACCOUNT_ID, + netuid.into(), + position_id, + liquidity_delta + ), + Error::::UserLiquidityDisabled + ); + + assert_ok!(Swap::set_enabled_user_liquidity( + RuntimeOrigin::root(), + netuid.into() + )); + + let position_id = Swap::do_add_liquidity( + netuid, + &OK_COLDKEY_ACCOUNT_ID, + &OK_HOTKEY_ACCOUNT_ID, + tick_low, + tick_high, + liquidity, + ) + .unwrap() + .0; + + assert_ok!(Swap::do_modify_position( + netuid.into(), + &OK_COLDKEY_ACCOUNT_ID, + &OK_HOTKEY_ACCOUNT_ID, + position_id, + liquidity_delta, + )); + + assert_ok!(Swap::do_remove_liquidity( + netuid.into(), + &OK_COLDKEY_ACCOUNT_ID, + position_id, + )); + }); + } } diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 082ce508fb..f506174101 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -20,6 +20,7 @@ mod impls; #[frame_support::pallet] mod pallet { use super::*; + use frame_system::{ensure_root, ensure_signed}; #[pallet::pallet] pub struct Pallet(_); @@ -30,9 +31,6 @@ mod pallet { /// Because this pallet emits events, it depends on the runtime's definition of an event. type RuntimeEvent: From> + IsType<::RuntimeEvent>; - /// The origin which may configure the swap parameters - type AdminOrigin: EnsureOrigin; - /// Implementor of /// [`SubnetInfo`](subtensor_swap_interface::SubnetInfo). type SubnetInfo: SubnetInfo; @@ -103,6 +101,12 @@ mod pallet { #[pallet::storage] pub type CurrentLiquidity = StorageMap<_, Twox64Concat, NetUid, u64, ValueQuery>; + /// Indicates whether a subnet has been switched to V3 swap from V2. + /// If `true`, the subnet is permanently on V3 swap mode allowing add/remove liquidity + /// operations. Once set to `true` for a subnet, it cannot be changed back to `false`. + #[pallet::storage] + pub type EnabledUserLiquidity = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + /// Storage for user positions, using subnet ID and account ID as keys /// The value is a bounded vector of Position structs with details about the liquidity positions #[pallet::storage] @@ -140,6 +144,10 @@ mod pallet { /// Event emitted when the fee rate has been updated for a subnet FeeRateSet { netuid: NetUid, rate: u16 }, + /// Event emitted when user liquidity operations are enabled for a subnet. + /// This indicates a permanent switch from V2 to V3 swap. + UserLiquidityEnabled { netuid: NetUid }, + /// Event emitted when liquidity is added to a subnet's liquidity pool. LiquidityAdded { /// The coldkey account that owns the position @@ -215,6 +223,9 @@ mod pallet { /// The subnet does not exist. SubNetworkDoesNotExist, + + /// User liquidity operations are disabled for this subnet + UserLiquidityDisabled, } #[pallet::call] @@ -226,7 +237,13 @@ mod pallet { #[pallet::call_index(0)] #[pallet::weight(::WeightInfo::set_fee_rate())] pub fn set_fee_rate(origin: OriginFor, netuid: u16, rate: u16) -> DispatchResult { - T::AdminOrigin::ensure_origin(origin)?; + if ensure_root(origin.clone()).is_err() { + let account_id: T::AccountId = ensure_signed(origin)?; + ensure!( + T::SubnetInfo::is_owner(&account_id, netuid), + DispatchError::BadOrigin + ); + } // Ensure that the subnet exists. ensure!( @@ -246,6 +263,35 @@ mod pallet { Ok(()) } + /// Enable user liquidity operations for a specific subnet. This permanently switches the + /// subnet from V2 to V3 swap mode. Once enabled, it cannot be disabled. + /// + /// Only callable by the admin origin + #[pallet::call_index(4)] + #[pallet::weight(::WeightInfo::set_enabled_user_liquidity())] + pub fn set_enabled_user_liquidity(origin: OriginFor, netuid: u16) -> DispatchResult { + if ensure_root(origin.clone()).is_err() { + let account_id: T::AccountId = ensure_signed(origin)?; + ensure!( + T::SubnetInfo::is_owner(&account_id, netuid), + DispatchError::BadOrigin + ); + } + + ensure!( + T::SubnetInfo::exists(netuid), + Error::::SubNetworkDoesNotExist + ); + + let netuid = netuid.into(); + + EnabledUserLiquidity::::insert(netuid, true); + + Self::deposit_event(Event::UserLiquidityEnabled { netuid }); + + Ok(()) + } + /// Add liquidity to a specific price range for a subnet. /// /// Parameters: @@ -445,37 +491,76 @@ mod pallet { #[cfg(test)] mod tests { + use frame_support::{assert_noop, assert_ok}; + use sp_runtime::DispatchError; + use crate::{ NetUid, mock::*, - pallet::{Error, FeeRate, Pallet as SwapModule}, + pallet::{EnabledUserLiquidity, Error, FeeRate}, }; - use frame_support::{assert_noop, assert_ok}; #[test] fn test_set_fee_rate() { new_test_ext().execute_with(|| { - // Create a test subnet let netuid = 1u16; let fee_rate = 500; // 0.76% fee - // Set fee rate (requires admin/root origin) - assert_ok!(SwapModule::::set_fee_rate( - RuntimeOrigin::root(), + assert_noop!( + Swap::set_fee_rate(RuntimeOrigin::signed(666), netuid.into(), fee_rate), + DispatchError::BadOrigin + ); + + assert_ok!(Swap::set_fee_rate(RuntimeOrigin::root(), netuid, fee_rate)); + + // Check that fee rate was set correctly + assert_eq!(FeeRate::::get(NetUid::from(netuid)), fee_rate); + + let fee_rate = fee_rate * 2; + assert_ok!(Swap::set_fee_rate( + RuntimeOrigin::signed(1), netuid, fee_rate )); - - // Check that fee rate was set correctly - let netuid_struct = NetUid::from(netuid); - assert_eq!(FeeRate::::get(netuid_struct), fee_rate); + assert_eq!(FeeRate::::get(NetUid::from(netuid)), fee_rate); // Verify fee rate validation - should fail if too high let too_high_fee = MaxFeeRate::get() + 1; assert_noop!( - SwapModule::::set_fee_rate(RuntimeOrigin::root(), netuid, too_high_fee), + Swap::set_fee_rate(RuntimeOrigin::root(), netuid, too_high_fee), Error::::FeeRateTooHigh ); }); } + + #[test] + fn test_set_enabled_user_liquidity() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(101); + + assert!(!EnabledUserLiquidity::::get(netuid)); + + assert_ok!(Swap::set_enabled_user_liquidity( + RuntimeOrigin::root(), + netuid.into() + )); + + assert!(EnabledUserLiquidity::::get(netuid)); + + assert_noop!( + Swap::set_enabled_user_liquidity(RuntimeOrigin::signed(666), netuid.into()), + DispatchError::BadOrigin + ); + + assert_ok!(Swap::set_enabled_user_liquidity( + RuntimeOrigin::signed(1), + netuid.into() + )); + + assert_noop!( + Swap::set_enabled_user_liquidity(RuntimeOrigin::root(), NON_EXISTENT_NETUID), + Error::::SubNetworkDoesNotExist + ); + }); + } } diff --git a/pallets/swap/src/weights.rs b/pallets/swap/src/weights.rs index 7577383d19..045bd551cc 100644 --- a/pallets/swap/src/weights.rs +++ b/pallets/swap/src/weights.rs @@ -18,6 +18,7 @@ pub trait WeightInfo { fn add_liquidity() -> Weight; fn remove_liquidity() -> Weight; fn modify_position() -> Weight; + fn set_enabled_user_liquidity() -> Weight; } /// Default weights for pallet_subtensor_swap. @@ -50,6 +51,13 @@ impl WeightInfo for DefaultWeight { .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(4)) } + + fn set_enabled_user_liquidity() -> Weight { + // Conservative weight estimate: one read and one write + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } } // For backwards compatibility and tests @@ -77,4 +85,10 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4)) .saturating_add(RocksDbWeight::get().writes(4)) } + + fn set_enabled_user_liquidity() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(RocksDbWeight::get().reads(1)) + .saturating_add(RocksDbWeight::get().writes(1)) + } } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index c89c075bc1..b6b0e4c0f7 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1131,7 +1131,6 @@ parameter_types! { impl pallet_subtensor_swap::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type AdminOrigin = EnsureRoot; type SubnetInfo = SubtensorModule; type BalanceOps = SubtensorModule; type ProtocolId = SwapProtocolId;