From 9479d89e0c10fad25c90fbaee2bceb10c1918817 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Sat, 20 Sep 2025 17:21:10 -0700 Subject: [PATCH 01/13] convet a to t --- pallets/swap/src/pallet/impls.rs | 129 ++++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 11 deletions(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index c0a109bfb5..a600b392ce 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1232,23 +1232,125 @@ impl Pallet { to_close .sort_by(|a, b| (a.owner == protocol_account).cmp(&(b.owner == protocol_account))); + let mut user_refunded_tao = TaoCurrency::ZERO; + let mut burned_tao = TaoCurrency::ZERO; + let mut burned_alpha = AlphaCurrency::ZERO; + + // Helper: build a very lax sqrt price limit. + // Mirrors the wrapper’s transformation: price_limit / 1e9, then sqrt(). + let compute_limit = || { + SqrtPrice::saturating_from_num(u64::MAX) + .safe_div(SqrtPrice::saturating_from_num(1_000_000_000u64)) + .checked_sqrt(SqrtPrice::saturating_from_num(0.0000000001f64)) + }; + for CloseItem { owner, pos_id } in to_close.into_iter() { match Self::do_remove_liquidity(netuid, &owner, pos_id) { Ok(rm) => { - if rm.tao > TaoCurrency::ZERO { - T::BalanceOps::increase_balance(&owner, rm.tao); - } - if owner != protocol_account { - 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); + // α withdrawn from the pool = principal + accrued fees + let alpha_total_from_pool: AlphaCurrency = + rm.alpha.saturating_add(rm.fee_alpha); + + if owner == protocol_account { + // ---------------- PROTOCOL: burn everything ---------------- + if rm.tao > TaoCurrency::ZERO { + burned_tao = burned_tao.saturating_add(rm.tao); + } + if alpha_total_from_pool > AlphaCurrency::ZERO { + burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); + } + + log::debug!( + "dissolve_all_lp: burned protocol position: netuid={:?}, pos_id={:?}, τ={:?}, α_principal={:?}, α_fees={:?}", + netuid, + pos_id, + rm.tao, + rm.alpha, + rm.fee_alpha + ); + } else { + // ---------------- USER: refund τ and convert α → τ ---------------- + + // 1) Refund τ principal directly. + if rm.tao > TaoCurrency::ZERO { + T::BalanceOps::increase_balance(&owner, rm.tao); + user_refunded_tao = user_refunded_tao.saturating_add(rm.tao); + T::BalanceOps::decrease_provided_tao_reserve(netuid, rm.tao); + } + + // 2) Convert ALL α withdrawn (principal + fees) to τ and refund τ to user. + if alpha_total_from_pool > AlphaCurrency::ZERO { + // α → τ via AMM swap (sell α). Drop trading fees on forced dissolve. + let sell_amount: u64 = alpha_total_from_pool.into(); + + if let Some(limit_sqrt_price) = compute_limit() { + match Self::do_swap( + netuid, + OrderType::Sell, + sell_amount, + limit_sqrt_price, + true, + false, + ) { + Ok(sres) => { + // Credit τ output to the user. + let tao_out: TaoCurrency = sres.amount_paid_out.into(); + if tao_out > TaoCurrency::ZERO { + T::BalanceOps::increase_balance(&owner, tao_out); + user_refunded_tao = + user_refunded_tao.saturating_add(tao_out); + } + } + Err(e) => { + // Could not convert α -> τ; log and continue dissolving others. + // (No α is credited to the user; α already removed from pool is effectively burned.) + log::debug!( + "dissolve_all_lp: α→τ swap failed on dissolve: netuid={:?}, owner={:?}, pos_id={:?}, α={:?}, err={:?}", + netuid, + owner, + pos_id, + alpha_total_from_pool, + e + ); + } + } + } else { + log::debug!( + "dissolve_all_lp: invalid price limit during α→τ on dissolve: netuid={:?}, owner={:?}, pos_id={:?}, α={:?}", + netuid, + owner, + pos_id, + alpha_total_from_pool + ); + } + + // Provided‑α reserve (user‑provided liquidity) decreased by what left the pool. + T::BalanceOps::decrease_provided_alpha_reserve( + netuid, + alpha_total_from_pool, + ); } + + log::debug!( + "dissolve_all_lp: user dissolved: netuid={:?}, owner={:?}, pos_id={:?}, τ_refunded={:?}, α_total_converted={:?} (α_principal={:?}, α_fees={:?})", + netuid, + owner, + pos_id, + rm.tao, + alpha_total_from_pool, + rm.alpha, + rm.fee_alpha + ); } } Err(e) => { + // Keep dissolving other positions even if this one fails. 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={:?}, owner={:?}, pos_id={:?}, err={:?}", + netuid, + owner, + pos_id, + e ); continue; } @@ -1277,7 +1379,11 @@ impl Pallet { EnabledUserLiquidity::::remove(netuid); log::debug!( - "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V3, positions closed; τ principal refunded; α burned; state cleared" + "dissolve_all_liquidity_providers: netuid={:?}, users_refunded_total_τ={:?}; protocol_burned: τ={:?}, α={:?}; state cleared", + netuid, + user_refunded_tao, + burned_tao, + burned_alpha ); return Ok(()); @@ -1305,7 +1411,8 @@ impl Pallet { EnabledUserLiquidity::::remove(netuid); log::debug!( - "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V2-or-nonV3, state_cleared" + "dissolve_all_liquidity_providers: netuid={:?}, mode=V2-or-nonV3, state_cleared", + netuid ); Ok(()) From 2b0a5f6f8ab07885250372535e5966736df61309 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Sat, 20 Sep 2025 17:26:16 -0700 Subject: [PATCH 02/13] fix comments --- pallets/swap/src/pallet/impls.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index a600b392ce..c0f0f8f12e 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1280,7 +1280,6 @@ impl Pallet { // 2) Convert ALL α withdrawn (principal + fees) to τ and refund τ to user. if alpha_total_from_pool > AlphaCurrency::ZERO { - // α → τ via AMM swap (sell α). Drop trading fees on forced dissolve. let sell_amount: u64 = alpha_total_from_pool.into(); if let Some(limit_sqrt_price) = compute_limit() { @@ -1293,7 +1292,6 @@ impl Pallet { false, ) { Ok(sres) => { - // Credit τ output to the user. let tao_out: TaoCurrency = sres.amount_paid_out.into(); if tao_out > TaoCurrency::ZERO { T::BalanceOps::increase_balance(&owner, tao_out); @@ -1302,8 +1300,6 @@ impl Pallet { } } Err(e) => { - // Could not convert α -> τ; log and continue dissolving others. - // (No α is credited to the user; α already removed from pool is effectively burned.) log::debug!( "dissolve_all_lp: α→τ swap failed on dissolve: netuid={:?}, owner={:?}, pos_id={:?}, α={:?}, err={:?}", netuid, @@ -1324,7 +1320,6 @@ impl Pallet { ); } - // Provided‑α reserve (user‑provided liquidity) decreased by what left the pool. T::BalanceOps::decrease_provided_alpha_reserve( netuid, alpha_total_from_pool, @@ -1344,7 +1339,6 @@ impl Pallet { } } Err(e) => { - // Keep dissolving other positions even if this one fails. log::debug!( "dissolve_all_lp: force-close failed: netuid={:?}, owner={:?}, pos_id={:?}, err={:?}", netuid, From 98ef9f617d915bae7d3802ad9f1327135b4f5fdd Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:48:45 -0700 Subject: [PATCH 03/13] add stake instead of swap --- common/src/lib.rs | 5 +- pallets/subtensor/src/lib.rs | 12 ++++ pallets/swap/src/mock.rs | 20 ++++++ pallets/swap/src/pallet/impls.rs | 114 +++++++++++++++++-------------- 4 files changed, 97 insertions(+), 54 deletions(-) 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/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/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 c0f0f8f12e..4b4a7076b8 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, @@ -1233,15 +1233,36 @@ impl Pallet { .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 mut burned_tao = TaoCurrency::ZERO; let mut burned_alpha = AlphaCurrency::ZERO; - // Helper: build a very lax sqrt price limit. - // Mirrors the wrapper’s transformation: price_limit / 1e9, then sqrt(). - let compute_limit = || { - SqrtPrice::saturating_from_num(u64::MAX) - .safe_div(SqrtPrice::saturating_from_num(1_000_000_000u64)) - .checked_sqrt(SqrtPrice::saturating_from_num(0.0000000001f64)) + let trust: Vec = T::SubnetInfo::get_validator_trust(netuid.into()); + let permit: Vec = T::SubnetInfo::get_validator_permit(netuid.into()); + + if trust.len() != permit.len() { + log::debug!( + "dissolve_all_lp: ValidatorTrust/Permit length mismatch: netuid={:?}, trust_len={}, permit_len={}", + netuid, + trust.len(), + permit.len() + ); + return Err(sp_runtime::DispatchError::Other( + "validator_meta_len_mismatch", + )); + } + + // 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() { @@ -1259,14 +1280,12 @@ impl Pallet { if alpha_total_from_pool > AlphaCurrency::ZERO { burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); } - log::debug!( - "dissolve_all_lp: burned protocol position: netuid={:?}, pos_id={:?}, τ={:?}, α_principal={:?}, α_fees={:?}", + "dissolve_all_lp: burned protocol pos: netuid={:?}, pos_id={:?}, τ={:?}, α_total={:?}", netuid, pos_id, rm.tao, - rm.alpha, - rm.fee_alpha + alpha_total_from_pool ); } else { // ---------------- USER: refund τ and convert α → τ ---------------- @@ -1278,41 +1297,40 @@ impl Pallet { T::BalanceOps::decrease_provided_tao_reserve(netuid, rm.tao); } - // 2) Convert ALL α withdrawn (principal + fees) to τ and refund τ to user. + // 2) Stake ALL withdrawn α (principal + fees) to the best permitted validator. if alpha_total_from_pool > AlphaCurrency::ZERO { - let sell_amount: u64 = alpha_total_from_pool.into(); + 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); - if let Some(limit_sqrt_price) = compute_limit() { - match Self::do_swap( + log::debug!( + "dissolve_all_lp: user dissolved & staked α: netuid={:?}, owner={:?}, pos_id={:?}, α_staked={:?}, target_uid={}", netuid, - OrderType::Sell, - sell_amount, - limit_sqrt_price, - true, - false, - ) { - Ok(sres) => { - let tao_out: TaoCurrency = sres.amount_paid_out.into(); - if tao_out > TaoCurrency::ZERO { - T::BalanceOps::increase_balance(&owner, tao_out); - user_refunded_tao = - user_refunded_tao.saturating_add(tao_out); - } - } - Err(e) => { - log::debug!( - "dissolve_all_lp: α→τ swap failed on dissolve: netuid={:?}, owner={:?}, pos_id={:?}, α={:?}, err={:?}", - netuid, - owner, - pos_id, - alpha_total_from_pool, - e - ); - } - } + owner, + pos_id, + alpha_total_from_pool, + target_uid + ); } else { + // No permitted validators; burn to avoid balance drift. + burned_alpha = + burned_alpha.saturating_add(alpha_total_from_pool); log::debug!( - "dissolve_all_lp: invalid price limit during α→τ on dissolve: netuid={:?}, owner={:?}, pos_id={:?}, α={:?}", + "dissolve_all_lp: no permitted validators; α burned: netuid={:?}, owner={:?}, pos_id={:?}, α_total={:?}", netuid, owner, pos_id, @@ -1325,17 +1343,6 @@ impl Pallet { alpha_total_from_pool, ); } - - log::debug!( - "dissolve_all_lp: user dissolved: netuid={:?}, owner={:?}, pos_id={:?}, τ_refunded={:?}, α_total_converted={:?} (α_principal={:?}, α_fees={:?})", - netuid, - owner, - pos_id, - rm.tao, - alpha_total_from_pool, - rm.alpha, - rm.fee_alpha - ); } } Err(e) => { @@ -1373,9 +1380,10 @@ impl Pallet { EnabledUserLiquidity::::remove(netuid); log::debug!( - "dissolve_all_liquidity_providers: netuid={:?}, users_refunded_total_τ={:?}; protocol_burned: τ={:?}, α={:?}; state cleared", + "dissolve_all_liquidity_providers: netuid={:?}, users_refunded_total_τ={:?}, users_staked_total_α={:?}; protocol_burned: τ={:?}, α={:?}; state cleared", netuid, user_refunded_tao, + user_staked_alpha, burned_tao, burned_alpha ); From b2041c52de5984779a89b99ee81246c60d9e868e Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:50:08 -0700 Subject: [PATCH 04/13] clippy --- pallets/swap/src/pallet/impls.rs | 43 ++++++++------------------------ 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 4b4a7076b8..a077182666 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1241,11 +1241,10 @@ impl Pallet { let permit: Vec = T::SubnetInfo::get_validator_permit(netuid.into()); if trust.len() != permit.len() { + let trust_len = trust.len(); + let permit_len = permit.len(); log::debug!( - "dissolve_all_lp: ValidatorTrust/Permit length mismatch: netuid={:?}, trust_len={}, permit_len={}", - netuid, - trust.len(), - permit.len() + "dissolve_all_lp: ValidatorTrust/Permit length mismatch: netuid={netuid:?}, trust_len={trust_len}, permit_len={permit_len}" ); return Err(sp_runtime::DispatchError::Other( "validator_meta_len_mismatch", @@ -1280,12 +1279,9 @@ impl Pallet { if alpha_total_from_pool > AlphaCurrency::ZERO { burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); } + let tao = rm.tao; log::debug!( - "dissolve_all_lp: burned protocol pos: netuid={:?}, pos_id={:?}, τ={:?}, α_total={:?}", - netuid, - pos_id, - rm.tao, - alpha_total_from_pool + "dissolve_all_lp: burned protocol pos: netuid={netuid:?}, pos_id={pos_id:?}, τ={tao:?}, α_total={alpha_total_from_pool:?}" ); } else { // ---------------- USER: refund τ and convert α → τ ---------------- @@ -1318,23 +1314,14 @@ impl Pallet { user_staked_alpha.saturating_add(alpha_total_from_pool); log::debug!( - "dissolve_all_lp: user dissolved & staked α: netuid={:?}, owner={:?}, pos_id={:?}, α_staked={:?}, target_uid={}", - netuid, - owner, - pos_id, - alpha_total_from_pool, - target_uid + "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. burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); log::debug!( - "dissolve_all_lp: no permitted validators; α burned: netuid={:?}, owner={:?}, pos_id={:?}, α_total={:?}", - netuid, - owner, - pos_id, - alpha_total_from_pool + "dissolve_all_lp: no permitted validators; α burned: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, α_total={alpha_total_from_pool:?}" ); } @@ -1347,11 +1334,7 @@ impl Pallet { } Err(e) => { log::debug!( - "dissolve_all_lp: force-close failed: netuid={:?}, owner={:?}, pos_id={:?}, err={:?}", - netuid, - owner, - pos_id, - e + "dissolve_all_lp: force-close failed: netuid={netuid:?}, owner={owner:?}, pos_id={pos_id:?}, err={e:?}" ); continue; } @@ -1380,12 +1363,7 @@ impl Pallet { EnabledUserLiquidity::::remove(netuid); log::debug!( - "dissolve_all_liquidity_providers: netuid={:?}, users_refunded_total_τ={:?}, users_staked_total_α={:?}; protocol_burned: τ={:?}, α={:?}; state cleared", - netuid, - user_refunded_tao, - user_staked_alpha, - burned_tao, - burned_alpha + "dissolve_all_liquidity_providers: netuid={netuid:?}, users_refunded_total_τ={user_refunded_tao:?}, users_staked_total_α={user_staked_alpha:?}; protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" ); return Ok(()); @@ -1413,8 +1391,7 @@ impl Pallet { EnabledUserLiquidity::::remove(netuid); log::debug!( - "dissolve_all_liquidity_providers: netuid={:?}, mode=V2-or-nonV3, state_cleared", - netuid + "dissolve_all_liquidity_providers: netuid={netuid:?}, mode=V2-or-nonV3, state_cleared" ); Ok(()) From 47120ba97ad6fbeedf0071fe57e2f9abdcbd74b8 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 23 Sep 2025 09:59:15 -0700 Subject: [PATCH 05/13] bump spec --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 254bec73a3..20f4bac2b3 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: 318, + spec_version: 319, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From e8cdc1c0e7eb0725da8cd44eb76db0f036ad981b Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:25:47 -0700 Subject: [PATCH 06/13] blank commit From dda8b2421e15bba61687add4d4a895fbfaa0d99c Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:29:56 -0700 Subject: [PATCH 07/13] test_dissolve_v3_green_path_refund_tao_stake_alpha --- pallets/swap/src/pallet/tests.rs | 174 +++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index dc7f08baa8..afd70e0b66 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -2536,3 +2536,177 @@ 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 { + // Avoid double counting when validator == user's hotkey. + 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 { + // Net effect: user's hot ledger returns to its original balance. + assert_eq!( + alpha_after_hot, alpha_before_hot, + "When validator == hotkey, user's hot ledger must net back to its original balance" + ); + + // Totals without double-counting the same ledger. + 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" + ); + + // Totals across distinct ledgers must be conserved. + 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" + ); + } + + // --- Assert: All positions (user + protocol) removed and V3 state cleared --- + 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" + ); + + // Ticks / liquidity / price / flags cleared + 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)); + + // Fee globals cleared + assert!(!FeeGlobalTao::::contains_key(netuid)); + assert!(!FeeGlobalAlpha::::contains_key(netuid)); + + // Active tick bitmap cleared + assert!( + TickIndexBitmapWords::::iter_prefix((netuid,)) + .next() + .is_none(), + "active tick bitmap words must be cleared" + ); + + // Knobs removed + assert!(!FeeRate::::contains_key(netuid)); + assert!(!EnabledUserLiquidity::::contains_key(netuid)); + }); +} From 91e99596e9aea42f3ffcd7db5e274e24e47178c1 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:42:59 -0700 Subject: [PATCH 08/13] add clear_protocol_liquidity --- pallets/subtensor/src/coinbase/root.rs | 1 + pallets/swap-interface/src/lib.rs | 1 + pallets/swap/src/pallet/impls.rs | 195 ++++++++++++++----------- 3 files changed, 111 insertions(+), 86 deletions(-) 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/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/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index a077182666..6def1d1a7f 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1216,26 +1216,29 @@ 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(); - - // Non‑protocol first - to_close - .sort_by(|a, b| (a.owner == protocol_account).cmp(&(b.owner == protocol_account))); + if to_close.is_empty() { + log::debug!( + "dissolve_all_lp: no user positions; netuid={netuid:?}, protocol liquidity untouched" + ); + return Ok(()); + } let mut user_refunded_tao = TaoCurrency::ZERO; let mut user_staked_alpha = AlphaCurrency::ZERO; - let mut burned_tao = TaoCurrency::ZERO; - let mut burned_alpha = AlphaCurrency::ZERO; let trust: Vec = T::SubnetInfo::get_validator_trust(netuid.into()); let permit: Vec = T::SubnetInfo::get_validator_permit(netuid.into()); @@ -1271,65 +1274,50 @@ impl Pallet { let alpha_total_from_pool: AlphaCurrency = rm.alpha.saturating_add(rm.fee_alpha); - if owner == protocol_account { - // ---------------- PROTOCOL: burn everything ---------------- - if rm.tao > TaoCurrency::ZERO { - burned_tao = burned_tao.saturating_add(rm.tao); - } - if alpha_total_from_pool > AlphaCurrency::ZERO { - burned_alpha = burned_alpha.saturating_add(alpha_total_from_pool); - } - let tao = rm.tao; - log::debug!( - "dissolve_all_lp: burned protocol pos: netuid={netuid:?}, pos_id={pos_id:?}, τ={tao:?}, α_total={alpha_total_from_pool:?}" - ); - } else { - // ---------------- USER: refund τ and convert α → τ ---------------- - - // 1) Refund τ principal directly. - if rm.tao > TaoCurrency::ZERO { - T::BalanceOps::increase_balance(&owner, rm.tao); - user_refunded_tao = user_refunded_tao.saturating_add(rm.tao); - T::BalanceOps::decrease_provided_tao_reserve(netuid, rm.tao); - } + // ---------------- USER: refund τ and convert α → stake ---------------- - // 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, + // 1) Refund τ principal directly. + if rm.tao > TaoCurrency::ZERO { + T::BalanceOps::increase_balance(&owner, rm.tao); + user_refunded_tao = user_refunded_tao.saturating_add(rm.tao); + T::BalanceOps::decrease_provided_tao_reserve(netuid, rm.tao); + } + + // 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", + ), )?; - 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. - burned_alpha = - burned_alpha.saturating_add(alpha_total_from_pool); - 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( + // 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) => { @@ -1341,41 +1329,74 @@ impl Pallet { } } - // 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:?}, users_refunded_total_τ={user_refunded_tao:?}, users_staked_total_α={user_staked_alpha:?}; protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; 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); @@ -1386,12 +1407,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(()) @@ -1494,6 +1514,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)] From cd1ba1499a75adcaae8dc206032281d21fc5e424 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:48:13 -0700 Subject: [PATCH 09/13] remove back check --- pallets/swap/src/pallet/impls.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 6def1d1a7f..9a41283426 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1243,17 +1243,6 @@ impl Pallet { let trust: Vec = T::SubnetInfo::get_validator_trust(netuid.into()); let permit: Vec = T::SubnetInfo::get_validator_permit(netuid.into()); - if trust.len() != permit.len() { - let trust_len = trust.len(); - let permit_len = permit.len(); - log::debug!( - "dissolve_all_lp: ValidatorTrust/Permit length mismatch: netuid={netuid:?}, trust_len={trust_len}, permit_len={permit_len}" - ); - return Err(sp_runtime::DispatchError::Other( - "validator_meta_len_mismatch", - )); - } - // 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; From 8621e16062476c4726dd7d4fbc2db1c61ce72cb9 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:19:23 -0700 Subject: [PATCH 10/13] Update tests.rs --- pallets/swap/src/pallet/tests.rs | 145 ++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 33 deletions(-) diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index afd70e0b66..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); @@ -2593,7 +2599,6 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { ::BalanceOps::alpha_balance(netuid.into(), &cold, &validator_hotkey); let alpha_before_total = if validator_hotkey == hot { - // Avoid double counting when validator == user's hotkey. alpha_before_hot + alpha_before_owner } else { alpha_before_hot + alpha_before_owner + alpha_before_val @@ -2635,13 +2640,10 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { ); if validator_hotkey == hot { - // Net effect: user's hot ledger returns to its original balance. assert_eq!( alpha_after_hot, alpha_before_hot, "When validator == hotkey, user's hot ledger must net back to its original balance" ); - - // Totals without double-counting the same ledger. let alpha_after_total = alpha_after_hot + alpha_after_owner; assert_eq!( alpha_after_total, alpha_before_total, @@ -2659,13 +2661,11 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { 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" ); - // Totals across distinct ledgers must be conserved. let alpha_after_total = alpha_after_hot + alpha_after_owner + alpha_after_val; assert_eq!( alpha_after_total, alpha_before_total, @@ -2673,9 +2673,10 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { ); } - // --- Assert: All positions (user + protocol) removed and V3 state cleared --- - let protocol_id = Pallet::::protocol_account_id(); + // 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::>(); @@ -2684,7 +2685,6 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { "protocol positions must be removed" ); - // Ticks / liquidity / price / flags cleared assert!(Ticks::::iter_prefix(netuid).next().is_none()); assert!(Ticks::::get(netuid, TickIndex::MIN).is_none()); assert!(Ticks::::get(netuid, TickIndex::MAX).is_none()); @@ -2693,11 +2693,9 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { assert!(!AlphaSqrtPrice::::contains_key(netuid)); assert!(!SwapV3Initialized::::contains_key(netuid)); - // Fee globals cleared assert!(!FeeGlobalTao::::contains_key(netuid)); assert!(!FeeGlobalAlpha::::contains_key(netuid)); - // Active tick bitmap cleared assert!( TickIndexBitmapWords::::iter_prefix((netuid,)) .next() @@ -2705,8 +2703,89 @@ fn test_dissolve_v3_green_path_refund_tao_stake_alpha_and_clear_state() { "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)); }); } From 2d1fed52ae1c75cfd4d458b4c562ad165923b1d7 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:31:00 -0700 Subject: [PATCH 11/13] use root --- pallets/swap/src/pallet/impls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 9a41283426..bfe15234ed 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1286,7 +1286,7 @@ impl Pallet { T::BalanceOps::increase_stake( &owner, &validator_hotkey, - netuid, + NetUid::ROOT, alpha_total_from_pool, )?; From 09a028765f0e4a942a113be7f0056f53be0d914a Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:35:26 -0700 Subject: [PATCH 12/13] Update impls.rs --- pallets/swap/src/pallet/impls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index bfe15234ed..9a41283426 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -1286,7 +1286,7 @@ impl Pallet { T::BalanceOps::increase_stake( &owner, &validator_hotkey, - NetUid::ROOT, + netuid, alpha_total_from_pool, )?; From 321f198a9f1af959af04f46d216d2808ceeb380f Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:44:54 -0700 Subject: [PATCH 13/13] bump spec --- runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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,