diff --git a/common/src/lib.rs b/common/src/lib.rs index 6122ef99fa..a5d09ad974 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -9,7 +9,7 @@ use runtime_common::prod_or_fast; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; use sp_runtime::{ - MultiSignature, + MultiSignature, Vec, traits::{IdentifyAccount, Verify}, }; use subtensor_macros::freeze_struct; @@ -175,6 +175,9 @@ pub trait SubnetInfo { fn mechanism(netuid: NetUid) -> u16; fn is_owner(account_id: &AccountId, netuid: NetUid) -> bool; fn is_subtoken_enabled(netuid: NetUid) -> bool; + fn get_validator_trust(netuid: NetUid) -> Vec; + fn get_validator_permit(netuid: NetUid) -> Vec; + fn hotkey_of_uid(netuid: NetUid, uid: u16) -> Option; } pub trait BalanceOps { diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index 4cb9f177e1..6b09c9ed46 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -375,6 +375,7 @@ impl Pallet { // 2. --- Perform the cleanup before removing the network. T::SwapInterface::dissolve_all_liquidity_providers(netuid)?; Self::destroy_alpha_in_out_stakes(netuid)?; + T::SwapInterface::clear_protocol_liquidity(netuid)?; T::CommitmentsInterface::purge_netuid(netuid); // 3. --- Remove the network diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index f53ee4f58a..7de32221a0 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -2146,6 +2146,18 @@ impl> fn is_subtoken_enabled(netuid: NetUid) -> bool { SubtokenEnabled::::get(netuid) } + + fn get_validator_trust(netuid: NetUid) -> Vec { + ValidatorTrust::::get(netuid) + } + + fn get_validator_permit(netuid: NetUid) -> Vec { + ValidatorPermit::::get(netuid) + } + + fn hotkey_of_uid(netuid: NetUid, uid: u16) -> Option { + Keys::::try_get(netuid, uid).ok() + } } impl> diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index d247b28d35..4998bbe379 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -36,6 +36,7 @@ pub trait SwapHandler { fn is_user_liquidity_enabled(netuid: NetUid) -> bool; fn dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult; fn toggle_user_liquidity(netuid: NetUid, enabled: bool); + fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; } #[derive(Debug, PartialEq)] diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index 40aac6d796..c79cb95d32 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -120,6 +120,26 @@ impl SubnetInfo for MockLiquidityProvider { fn is_subtoken_enabled(netuid: NetUid) -> bool { netuid.inner() != SUBTOKEN_DISABLED_NETUID } + + fn get_validator_trust(netuid: NetUid) -> Vec { + match netuid.into() { + 123u16 => vec![4000, 3000, 2000, 1000], + WRAPPING_FEES_NETUID => vec![8000, 7000, 6000, 5000], + _ => vec![1000, 800, 600, 400], + } + } + + fn get_validator_permit(netuid: NetUid) -> Vec { + match netuid.into() { + 123u16 => vec![true, true, false, true], + WRAPPING_FEES_NETUID => vec![true, true, true, true], + _ => vec![true, true, true, true], + } + } + + fn hotkey_of_uid(_netuid: NetUid, uid: u16) -> Option { + Some(uid as AccountId) + } } pub struct MockBalanceOps; diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index c0a109bfb5..9a41283426 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -5,7 +5,7 @@ use frame_support::storage::{TransactionOutcome, transactional}; use frame_support::{ensure, pallet_prelude::DispatchError, traits::Get}; use safe_math::*; use sp_arithmetic::helpers_128bit; -use sp_runtime::{DispatchResult, traits::AccountIdConversion}; +use sp_runtime::{DispatchResult, Vec, traits::AccountIdConversion}; use substrate_fixed::types::{I64F64, U64F64, U96F32}; use subtensor_runtime_common::{ AlphaCurrency, BalanceOps, Currency, NetUid, SubnetInfo, TaoCurrency, @@ -1216,80 +1216,176 @@ impl Pallet { /// Dissolve all LPs and clean state. pub fn do_dissolve_all_liquidity_providers(netuid: NetUid) -> DispatchResult { if SwapV3Initialized::::get(netuid) { - // 1) Snapshot (owner, position_id). + // 1) Snapshot only *non‑protocol* positions: (owner, position_id). struct CloseItem { owner: A, pos_id: PositionId, } + let protocol_account = Self::protocol_account_id(); + let mut to_close: sp_std::vec::Vec> = sp_std::vec::Vec::new(); for ((owner, pos_id), _pos) in Positions::::iter_prefix((netuid,)) { - to_close.push(CloseItem { owner, pos_id }); + if owner != protocol_account { + to_close.push(CloseItem { owner, pos_id }); + } } - let protocol_account = Self::protocol_account_id(); + if to_close.is_empty() { + log::debug!( + "dissolve_all_lp: no user positions; netuid={netuid:?}, protocol liquidity untouched" + ); + return Ok(()); + } - // Non‑protocol first - to_close - .sort_by(|a, b| (a.owner == protocol_account).cmp(&(b.owner == protocol_account))); + let mut user_refunded_tao = TaoCurrency::ZERO; + let mut user_staked_alpha = AlphaCurrency::ZERO; + + let trust: Vec = T::SubnetInfo::get_validator_trust(netuid.into()); + let permit: Vec = T::SubnetInfo::get_validator_permit(netuid.into()); + + // Helper: pick target validator uid, only among permitted validators, by highest trust. + let pick_target_uid = |trust: &Vec, permit: &Vec| -> Option { + let mut best_uid: Option = None; + let mut best_trust: u16 = 0; + for (i, (&t, &p)) in trust.iter().zip(permit.iter()).enumerate() { + if p && (best_uid.is_none() || t > best_trust) { + best_uid = Some(i); + best_trust = t; + } + } + best_uid.map(|i| i as u16) + }; for CloseItem { owner, pos_id } in to_close.into_iter() { match Self::do_remove_liquidity(netuid, &owner, pos_id) { Ok(rm) => { + // α withdrawn from the pool = principal + accrued fees + let alpha_total_from_pool: AlphaCurrency = + rm.alpha.saturating_add(rm.fee_alpha); + + // ---------------- USER: refund τ and convert α → stake ---------------- + + // 1) Refund τ principal directly. if rm.tao > TaoCurrency::ZERO { T::BalanceOps::increase_balance(&owner, rm.tao); - } - if owner != protocol_account { + user_refunded_tao = user_refunded_tao.saturating_add(rm.tao); T::BalanceOps::decrease_provided_tao_reserve(netuid, rm.tao); - let alpha_burn = rm.alpha.saturating_add(rm.fee_alpha); - if alpha_burn > AlphaCurrency::ZERO { - T::BalanceOps::decrease_provided_alpha_reserve(netuid, alpha_burn); + } + + // 2) Stake ALL withdrawn α (principal + fees) to the best permitted validator. + if alpha_total_from_pool > AlphaCurrency::ZERO { + if let Some(target_uid) = pick_target_uid(&trust, &permit) { + let validator_hotkey: T::AccountId = + T::SubnetInfo::hotkey_of_uid(netuid.into(), target_uid).ok_or( + sp_runtime::DispatchError::Other( + "validator_hotkey_missing", + ), + )?; + + // Stake α from LP owner (coldkey) to chosen validator (hotkey). + T::BalanceOps::increase_stake( + &owner, + &validator_hotkey, + netuid, + alpha_total_from_pool, + )?; + + user_staked_alpha = + user_staked_alpha.saturating_add(alpha_total_from_pool); + + log::debug!( + "dissolve_all_lp: user dissolved & staked α: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, α_staked={alpha_total_from_pool:?}, target_uid={target_uid}" + ); + } else { + // No permitted validators; burn to avoid balance drift. + log::debug!( + "dissolve_all_lp: no permitted validators; α burned: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, α_total={alpha_total_from_pool:?}" + ); } + + T::BalanceOps::decrease_provided_alpha_reserve( + netuid, + alpha_total_from_pool, + ); } } Err(e) => { log::debug!( - "dissolve_all_lp: force-closing failed position: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, err={e:?}" + "dissolve_all_lp: force-close failed: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, err={e:?}" ); continue; } } } - // 3) Clear active tick index entries, then all swap state. - let active_ticks: sp_std::vec::Vec = - Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); - for ti in active_ticks { - ActiveTickIndexManager::::remove(netuid, ti); - } + log::debug!( + "dissolve_all_liquidity_providers (users-only): netuid={netuid:?}, users_refunded_total_τ={user_refunded_tao:?}, users_staked_total_α={user_staked_alpha:?}; protocol liquidity untouched" + ); - let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); - let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); + return Ok(()); + } - FeeGlobalTao::::remove(netuid); - FeeGlobalAlpha::::remove(netuid); - CurrentLiquidity::::remove(netuid); - CurrentTick::::remove(netuid); - AlphaSqrtPrice::::remove(netuid); - SwapV3Initialized::::remove(netuid); + log::debug!( + "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V2-or-nonV3, leaving all liquidity/state intact" + ); - let _ = TickIndexBitmapWords::::clear_prefix((netuid,), u32::MAX, None); - FeeRate::::remove(netuid); - EnabledUserLiquidity::::remove(netuid); + Ok(()) + } - log::debug!( - "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V3, positions closed; τ principal refunded; α burned; state cleared" - ); + /// Clear **protocol-owned** liquidity and wipe all swap state for `netuid`. + pub fn do_clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { + let protocol_account = Self::protocol_account_id(); - return Ok(()); + // 1) Force-close only protocol positions, burning proceeds. + let mut burned_tao = TaoCurrency::ZERO; + let mut burned_alpha = AlphaCurrency::ZERO; + + // Collect protocol position IDs first to avoid mutating while iterating. + let protocol_pos_ids: sp_std::vec::Vec = Positions::::iter_prefix((netuid,)) + .filter_map(|((owner, pos_id), _)| { + if owner == protocol_account { + Some(pos_id) + } else { + None + } + }) + .collect(); + + for pos_id in protocol_pos_ids { + match Self::do_remove_liquidity(netuid, &protocol_account, pos_id) { + Ok(rm) => { + let alpha_total_from_pool: AlphaCurrency = + rm.alpha.saturating_add(rm.fee_alpha); + let tao = rm.tao; + + if tao > TaoCurrency::ZERO { + burned_tao = burned_tao.saturating_add(tao); + } + if alpha_total_from_pool > AlphaCurrency::ZERO { + burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); + } + + log::debug!( + "clear_protocol_liquidity: burned protocol pos: netuid={netuid:?}, pos_id={pos_id:?}, τ={tao:?}, α_total={alpha_total_from_pool:?}" + ); + } + Err(e) => { + log::debug!( + "clear_protocol_liquidity: force-close failed: netuid={netuid:?}, pos_id={pos_id:?}, err={e:?}" + ); + continue; + } + } } - // V2 / non‑V3: ensure V3 residues are cleared (safe no‑ops). - let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); + // 2) Clear active tick index entries, then all swap state (idempotent even if empty/non‑V3). let active_ticks: sp_std::vec::Vec = Ticks::::iter_prefix(netuid).map(|(ti, _)| ti).collect(); for ti in active_ticks { ActiveTickIndexManager::::remove(netuid, ti); } + + let _ = Positions::::clear_prefix((netuid,), u32::MAX, None); let _ = Ticks::::clear_prefix(netuid, u32::MAX, None); FeeGlobalTao::::remove(netuid); @@ -1300,12 +1396,11 @@ impl Pallet { SwapV3Initialized::::remove(netuid); let _ = TickIndexBitmapWords::::clear_prefix((netuid,), u32::MAX, None); - FeeRate::::remove(netuid); EnabledUserLiquidity::::remove(netuid); log::debug!( - "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V2-or-nonV3, state_cleared" + "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" ); Ok(()) @@ -1408,6 +1503,9 @@ impl SwapHandler for Pallet { fn toggle_user_liquidity(netuid: NetUid, enabled: bool) { EnabledUserLiquidity::::insert(netuid, enabled) } + fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { + Self::do_clear_protocol_liquidity(netuid) + } } #[derive(Debug, PartialEq)] diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index dc7f08baa8..72c33d698f 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -1982,7 +1982,6 @@ fn test_swap_subtoken_disabled() { }); } -/// V3 path: protocol + user positions exist, fees accrued, everything must be removed. #[test] fn test_liquidate_v3_removes_positions_ticks_and_state() { new_test_ext().execute_with(|| { @@ -1992,7 +1991,7 @@ fn test_liquidate_v3_removes_positions_ticks_and_state() { assert_ok!(Pallet::::maybe_initialize_v3(netuid)); assert!(SwapV3Initialized::::get(netuid)); - // Enable user LP (mock usually enables for 0..=100, but be explicit and consistent) + // Enable user LP assert_ok!(Swap::toggle_user_liquidity( RuntimeOrigin::root(), netuid.into(), @@ -2041,14 +2040,14 @@ fn test_liquidate_v3_removes_positions_ticks_and_state() { assert!(Ticks::::get(netuid, TickIndex::MAX).is_some()); assert!(CurrentLiquidity::::get(netuid) > 0); - // There should be some bitmap words (active ticks) after adding a position. let had_bitmap_words = TickIndexBitmapWords::::iter_prefix((netuid,)) .next() .is_some(); assert!(had_bitmap_words); - // ACT: Liquidate & reset swap state + // ACT: users-only liquidation then protocol clear assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); // ASSERT: positions cleared (both user and protocol) assert_eq!( @@ -2091,12 +2090,11 @@ fn test_liquidate_v3_removes_positions_ticks_and_state() { }); } -/// V3 path with user liquidity disabled at teardown: must still remove all positions and clear state. +/// V3 path with user liquidity disabled at teardown: +/// must still remove positions and clear state (after protocol clear). #[test] fn test_liquidate_v3_with_user_liquidity_disabled() { new_test_ext().execute_with(|| { - // Pick a netuid the mock treats as "disabled" by default (per your comment >100), - // then explicitly walk through enable -> add -> disable -> liquidate. let netuid = NetUid::from(101); assert_ok!(Pallet::::maybe_initialize_v3(netuid)); @@ -2125,15 +2123,16 @@ fn test_liquidate_v3_with_user_liquidity_disabled() { ) .expect("add liquidity"); - // Disable user LP *before* liquidation to validate that removal ignores this flag. + // Disable user LP *before* liquidation; removal must ignore this flag. assert_ok!(Swap::toggle_user_liquidity( RuntimeOrigin::root(), netuid.into(), false )); - // ACT + // Users-only dissolve, then clear protocol liquidity/state. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); // ASSERT: positions & ticks gone, state reset assert_eq!( @@ -2158,7 +2157,7 @@ fn test_liquidate_v3_with_user_liquidity_disabled() { assert!(!FeeGlobalTao::::contains_key(netuid)); assert!(!FeeGlobalAlpha::::contains_key(netuid)); - // `EnabledUserLiquidity` is removed by liquidation. + // `EnabledUserLiquidity` is removed by protocol clear stage. assert!(!EnabledUserLiquidity::::contains_key(netuid)); }); } @@ -2205,7 +2204,6 @@ fn test_liquidate_non_v3_uninitialized_ok_and_clears() { }); } -/// Idempotency: calling liquidation twice is safe (both V3 and non‑V3 flavors). #[test] fn test_liquidate_idempotent() { // V3 flavor @@ -2230,11 +2228,14 @@ fn test_liquidate_idempotent() { 123_456_789 )); - // 1st liquidation + // Users-only liquidations are idempotent. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - // 2nd liquidation (no state left) — must still succeed assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + // Now clear protocol liquidity/state—also idempotent. + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + // State remains empty assert!( Positions::::iter_prefix_values((netuid, OK_COLDKEY_ACCOUNT_ID)) @@ -2254,7 +2255,7 @@ fn test_liquidate_idempotent() { new_test_ext().execute_with(|| { let netuid = NetUid::from(8); - // Never initialize V3 + // Never initialize V3; both calls no-op and succeed. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); @@ -2286,7 +2287,7 @@ fn liquidate_v3_refunds_user_funds_and_clears_state() { )); assert_ok!(Pallet::::maybe_initialize_v3(netuid)); - // Use distinct cold/hot to demonstrate alpha refund goes to (owner, owner). + // Use distinct cold/hot to demonstrate alpha refund/stake accounting. let cold = OK_COLDKEY_ACCOUNT_ID; let hot = OK_HOTKEY_ACCOUNT_ID; @@ -2322,15 +2323,14 @@ fn liquidate_v3_refunds_user_funds_and_clears_state() { ::BalanceOps::increase_provided_tao_reserve(netuid.into(), tao_taken); ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), alpha_taken); - // Liquidate everything on the subnet. + // Users‑only liquidation. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); // Expect balances restored to BEFORE snapshots (no swaps ran -> zero fees). - // TAO: we withdrew 'need_tao' above and liquidation refunded it, so we should be back to 'tao_before'. let tao_after = ::BalanceOps::tao_balance(&cold); assert_eq!(tao_after, tao_before, "TAO principal must be refunded"); - // ALPHA: refund is credited to (coldkey=cold, hotkey=cold). Compare totals across both ledgers. + // ALPHA totals conserved to owner (distribution may differ). let alpha_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); let alpha_after_owner = @@ -2338,9 +2338,12 @@ fn liquidate_v3_refunds_user_funds_and_clears_state() { let alpha_after_total = alpha_after_hot + alpha_after_owner; assert_eq!( alpha_after_total, alpha_before_total, - "ALPHA principal must be refunded to the account (may be credited to (owner, owner))" + "ALPHA principal must be refunded/staked for the account (check totals)" ); + // Clear protocol liquidity and V3 state now. + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + // User position(s) are gone and all V3 state cleared. assert_eq!(Pallet::::count_positions(netuid, &cold), 0); assert!(Ticks::::iter_prefix(netuid).next().is_none()); @@ -2386,10 +2389,10 @@ fn refund_alpha_single_provider_exact() { .expect("decrease ALPHA"); ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), alpha_taken); - // --- Act: dissolve (calls refund_alpha inside). + // --- Act: users‑only dissolve. assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); - // --- Assert: refunded back to the owner (may credit to (cold,cold)). + // --- Assert: total α conserved to owner (may be staked to validator). let alpha_after_hot = ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); let alpha_after_owner = @@ -2397,9 +2400,12 @@ fn refund_alpha_single_provider_exact() { let alpha_after_total = alpha_after_hot + alpha_after_owner; assert_eq!( alpha_after_total, alpha_before_total, - "ALPHA principal must be conserved to the owner" + "ALPHA principal must be conserved to the account" ); + // Clear protocol liquidity and V3 state now. + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + // --- State is cleared. assert!(Ticks::::iter_prefix(netuid).next().is_none()); assert_eq!(Pallet::::count_positions(netuid, &cold), 0); @@ -2536,3 +2542,250 @@ fn refund_alpha_same_cold_multiple_hotkeys_conserved_to_owner() { ); }); } + +#[test] +fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { + new_test_ext().execute_with(|| { + // --- Setup --- + let netuid = NetUid::from(42); + let cold = OK_COLDKEY_ACCOUNT_ID; + let hot = OK_HOTKEY_ACCOUNT_ID; + + assert_ok!(Swap::toggle_user_liquidity( + RuntimeOrigin::root(), + netuid.into(), + true + )); + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + assert!(SwapV3Initialized::::get(netuid)); + + // Tight in‑range band so BOTH τ and α are required. + let ct = CurrentTick::::get(netuid); + let tick_low = ct.saturating_sub(10); + let tick_high = ct.saturating_add(10); + let liquidity: u64 = 1_250_000; + + // Add liquidity and capture required τ/α. + let (_pos_id, tao_needed, alpha_needed) = + Pallet::::do_add_liquidity(netuid, &cold, &hot, tick_low, tick_high, liquidity) + .expect("add in-range liquidity"); + assert!(tao_needed > 0, "in-range pos must require TAO"); + assert!(alpha_needed > 0, "in-range pos must require ALPHA"); + + // Determine the permitted validator with the highest trust (green path). + let trust = ::SubnetInfo::get_validator_trust(netuid.into()); + let permit = ::SubnetInfo::get_validator_permit(netuid.into()); + assert_eq!(trust.len(), permit.len(), "trust/permit must align"); + let target_uid: u16 = trust + .iter() + .zip(permit.iter()) + .enumerate() + .filter(|(_, (_t, p))| **p) + .max_by_key(|(_, (t, _))| *t) + .map(|(i, _)| i as u16) + .expect("at least one permitted validator"); + let validator_hotkey: ::AccountId = + ::SubnetInfo::hotkey_of_uid(netuid.into(), target_uid) + .expect("uid -> hotkey mapping must exist"); + + // --- Snapshot BEFORE we withdraw τ/α to fund the position --- + let tao_before = ::BalanceOps::tao_balance(&cold); + + let alpha_before_hot = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); + let alpha_before_owner = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); + let alpha_before_val = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &validator_hotkey); + + let alpha_before_total = if validator_hotkey == hot { + alpha_before_hot + alpha_before_owner + } else { + alpha_before_hot + alpha_before_owner + alpha_before_val + }; + + // --- Mirror extrinsic bookkeeping: withdraw τ & α; bump provided reserves --- + let tao_taken = ::BalanceOps::decrease_balance(&cold, tao_needed.into()) + .expect("decrease TAO"); + let alpha_taken = ::BalanceOps::decrease_stake( + &cold, + &hot, + netuid.into(), + alpha_needed.into(), + ) + .expect("decrease ALPHA"); + + ::BalanceOps::increase_provided_tao_reserve(netuid.into(), tao_taken); + ::BalanceOps::increase_provided_alpha_reserve(netuid.into(), alpha_taken); + + // --- Act: dissolve (GREEN PATH: permitted validators exist) --- + assert_ok!(Pallet::::do_dissolve_all_liquidity_providers(netuid)); + + // --- Assert: τ principal refunded to user --- + let tao_after = ::BalanceOps::tao_balance(&cold); + assert_eq!(tao_after, tao_before, "TAO principal must be refunded"); + + // --- α ledger assertions --- + let alpha_after_hot = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &hot); + let alpha_after_owner = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &cold); + let alpha_after_val = + ::BalanceOps::alpha_balance(netuid.into(), &cold, &validator_hotkey); + + // Owner ledger must be unchanged in the green path. + assert_eq!( + alpha_after_owner, alpha_before_owner, + "Owner α ledger must be unchanged (staked to validator, not refunded)" + ); + + if validator_hotkey == hot { + assert_eq!( + alpha_after_hot, alpha_before_hot, + "When validator == hotkey, user's hot ledger must net back to its original balance" + ); + let alpha_after_total = alpha_after_hot + alpha_after_owner; + assert_eq!( + alpha_after_total, alpha_before_total, + "Total α for the coldkey must be conserved (validator==hotkey)" + ); + } else { + assert!( + alpha_before_hot >= alpha_after_hot, + "hot ledger should not increase" + ); + assert!( + alpha_after_val >= alpha_before_val, + "validator ledger should not decrease" + ); + + let hot_loss = alpha_before_hot - alpha_after_hot; + let val_gain = alpha_after_val - alpha_before_val; + assert_eq!( + val_gain, hot_loss, + "α that left the user's hot ledger must equal α credited to the validator ledger" + ); + + let alpha_after_total = alpha_after_hot + alpha_after_owner + alpha_after_val; + assert_eq!( + alpha_after_total, alpha_before_total, + "Total α for the coldkey must be conserved" + ); + } + + // Now clear protocol liquidity & state and assert full reset. + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + + let protocol_id = Pallet::::protocol_account_id(); + assert_eq!(Pallet::::count_positions(netuid, &cold), 0); + let prot_positions_after = + Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); + assert!( + prot_positions_after.is_empty(), + "protocol positions must be removed" + ); + + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); + assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); + assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!CurrentTick::::contains_key(netuid)); + assert!(!AlphaSqrtPrice::::contains_key(netuid)); + assert!(!SwapV3Initialized::::contains_key(netuid)); + + assert!(!FeeGlobalTao::::contains_key(netuid)); + assert!(!FeeGlobalAlpha::::contains_key(netuid)); + + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none(), + "active tick bitmap words must be cleared" + ); + + assert!(!FeeRate::::contains_key(netuid)); + assert!(!EnabledUserLiquidity::::contains_key(netuid)); + }); +} + +#[test] +fn test_clear_protocol_liquidity_green_path() { + new_test_ext().execute_with(|| { + // --- Arrange --- + let netuid = NetUid::from(55); + + // Ensure the "user liquidity enabled" flag exists so we can verify it's removed later. + assert_ok!(Pallet::::toggle_user_liquidity( + RuntimeOrigin::root(), + netuid, + true + )); + + // Initialize V3 state; this should set price/tick flags and create a protocol position. + assert_ok!(Pallet::::maybe_initialize_v3(netuid)); + assert!( + SwapV3Initialized::::get(netuid), + "V3 must be initialized" + ); + + // Sanity: protocol positions exist before clearing. + let protocol_id = Pallet::::protocol_account_id(); + let prot_positions_before = + Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); + assert!( + !prot_positions_before.is_empty(), + "protocol positions should exist after V3 init" + ); + + // --- Act --- + // Green path: just clear protocol liquidity and wipe all V3 state. + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + + // --- Assert: all protocol positions removed --- + let prot_positions_after = + Positions::::iter_prefix_values((netuid, protocol_id)).collect::>(); + assert!( + prot_positions_after.is_empty(), + "protocol positions must be removed by do_clear_protocol_liquidity" + ); + + // --- Assert: V3 data wiped (idempotent even if some maps were empty) --- + // Ticks / active tick bitmap + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none(), + "active tick bitmap words must be cleared" + ); + + // Fee globals + assert!(!FeeGlobalTao::::contains_key(netuid)); + assert!(!FeeGlobalAlpha::::contains_key(netuid)); + + // Price / tick / liquidity / flags + assert!(!AlphaSqrtPrice::::contains_key(netuid)); + assert!(!CurrentTick::::contains_key(netuid)); + assert!(!CurrentLiquidity::::contains_key(netuid)); + assert!(!SwapV3Initialized::::contains_key(netuid)); + + // Knobs removed + assert!(!FeeRate::::contains_key(netuid)); + assert!(!EnabledUserLiquidity::::contains_key(netuid)); + + // --- And it's idempotent --- + assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + assert!( + Positions::::iter_prefix_values((netuid, protocol_id)) + .next() + .is_none() + ); + assert!(Ticks::::iter_prefix(netuid).next().is_none()); + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none() + ); + assert!(!SwapV3Initialized::::contains_key(netuid)); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 20f4bac2b3..df11059c2a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -220,7 +220,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 319, + spec_version: 320, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,