From 22a4d3f6ed9a693ea0b8a6a7d31dffcff21fa7a9 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:57:51 -0700 Subject: [PATCH 1/2] burn lock for new subnets, refund for old --- pallets/subtensor/src/staking/remove_stake.rs | 93 ++++++++++++------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 9d610ea88f..99e4a52be1 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -449,43 +449,58 @@ impl Pallet { let owner_coldkey: T::AccountId = SubnetOwner::::get(netuid); let lock_cost: TaoCurrency = Self::get_subnet_locked_balance(netuid); - // 3) Compute owner's received emission in TAO at current price. + // Determine if this subnet is eligible for a lock refund (legacy). + let reg_at: u64 = NetworkRegisteredAt::::get(netuid); + let start_block: u64 = NetworkRegistrationStartBlock::::get(); + let should_refund_owner: bool = reg_at < start_block; + + // 3) Compute owner's received emission in TAO at current price (ONLY if we may refund). // Emission:: is Vec. We: // - sum emitted α, // - apply owner fraction to get owner α, // - price that α using a *simulated* AMM swap. - let total_emitted_alpha_u128: u128 = - Emission::::get(netuid) - .into_iter() - .fold(0u128, |acc, e_alpha| { - let e_u64: u64 = Into::::into(e_alpha); - acc.saturating_add(e_u64 as u128) - }); - - let owner_fraction: U96F32 = Self::get_float_subnet_owner_cut(); - let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha_u128) - .saturating_mul(owner_fraction) - .floor() - .saturating_to_num::(); - - let owner_emission_tao: TaoCurrency = if owner_alpha_u64 > 0 { - match T::SwapInterface::sim_swap(netuid.into(), OrderType::Sell, owner_alpha_u64) { - Ok(sim) => TaoCurrency::from(sim.amount_paid_out), - Err(e) => { - log::debug!( - "destroy_alpha_in_out_stakes: sim_swap owner α→τ failed (netuid={netuid:?}, alpha={owner_alpha_u64}, err={e:?}); falling back to price multiply.", - ); - let cur_price: U96F32 = T::SwapInterface::current_alpha_price(netuid.into()); - let val_u64: u64 = U96F32::from_num(owner_alpha_u64) - .saturating_mul(cur_price) - .floor() - .saturating_to_num::(); - TaoCurrency::from(val_u64) - } + let mut owner_emission_tao: TaoCurrency = TaoCurrency::ZERO; + if should_refund_owner && !lock_cost.is_zero() { + let total_emitted_alpha_u128: u128 = + Emission::::get(netuid) + .into_iter() + .fold(0u128, |acc, e_alpha| { + let e_u64: u64 = Into::::into(e_alpha); + acc.saturating_add(e_u64 as u128) + }); + + if total_emitted_alpha_u128 > 0 { + let owner_fraction: U96F32 = Self::get_float_subnet_owner_cut(); + let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha_u128) + .saturating_mul(owner_fraction) + .floor() + .saturating_to_num::(); + + owner_emission_tao = if owner_alpha_u64 > 0 { + match T::SwapInterface::sim_swap( + netuid.into(), + OrderType::Sell, + owner_alpha_u64, + ) { + Ok(sim) => TaoCurrency::from(sim.amount_paid_out), + Err(e) => { + log::debug!( + "destroy_alpha_in_out_stakes: sim_swap owner α→τ failed (netuid={netuid:?}, alpha={owner_alpha_u64}, err={e:?}); falling back to price multiply.", + ); + let cur_price: U96F32 = + T::SwapInterface::current_alpha_price(netuid.into()); + let val_u64: u64 = U96F32::from_num(owner_alpha_u64) + .saturating_mul(cur_price) + .floor() + .saturating_to_num::(); + TaoCurrency::from(val_u64) + } + } + } else { + TaoCurrency::ZERO + }; } - } else { - TaoCurrency::ZERO - }; + } // 4) Enumerate all α entries on this subnet to build distribution weights and cleanup lists. // - collect keys to remove, @@ -594,13 +609,19 @@ impl Pallet { SubnetAlphaInProvided::::remove(netuid); SubnetAlphaOut::::remove(netuid); - // 8) Refund remaining lock to subnet owner: - // refund = max(0, lock_cost(τ) − owner_received_emission_in_τ). - let refund: TaoCurrency = lock_cost.saturating_sub(owner_emission_tao); - // Clear the locked balance on the subnet. Self::set_subnet_locked_balance(netuid, TaoCurrency::ZERO); + // 8) Finalize lock handling: + // - Legacy subnets (registered before NetworkRegistrationStartBlock) receive: + // refund = max(0, lock_cost(τ) − owner_received_emission_in_τ). + // - New subnets: no refund. + let refund: TaoCurrency = if should_refund_owner { + lock_cost.saturating_sub(owner_emission_tao) + } else { + TaoCurrency::ZERO + }; + if !refund.is_zero() { Self::add_balance_to_coldkey_account(&owner_coldkey, refund.to_u64()); } From bdf4bac9a96de3b40ce46e3c93fa752251ec61b9 Mon Sep 17 00:00:00 2001 From: John Reed <87283488+JohnReedV@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:58:08 -0700 Subject: [PATCH 2/2] add/update tests --- pallets/subtensor/src/tests/networks.rs | 157 +++++++++++++++++++++++- 1 file changed, 156 insertions(+), 1 deletion(-) diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 42de84f54f..e24c1a53c7 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -71,6 +71,10 @@ fn dissolve_refunds_full_lock_cost_when_no_emission() { let hot = U256::from(4); let net = add_dynamic_network(&hot, &cold); + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(net); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + let lock: TaoCurrency = TaoCurrency::from(1_000_000); SubtensorModule::set_subnet_locked_balance(net, lock); SubnetTAO::::insert(net, TaoCurrency::from(0)); @@ -126,6 +130,10 @@ fn dissolve_two_stakers_pro_rata_distribution() { let oh = U256::from(51); let net = add_dynamic_network(&oh, &oc); + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(net); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + let (s1_hot, s1_cold, a1) = (U256::from(201), U256::from(301), 300u128); let (s2_hot, s2_cold, a2) = (U256::from(202), U256::from(302), 700u128); @@ -134,7 +142,7 @@ fn dissolve_two_stakers_pro_rata_distribution() { let pot: u64 = 10_000; SubnetTAO::::insert(net, TaoCurrency::from(pot)); - SubtensorModule::set_subnet_locked_balance(net, 5_000.into()); // owner refund path present but emission = 0 + SubtensorModule::set_subnet_locked_balance(net, 5_000.into()); // owner refund path present; emission = 0 // Cold-key balances before let s1_before = SubtensorModule::get_coldkey_balance(&s1_cold); @@ -199,6 +207,10 @@ fn dissolve_owner_cut_refund_logic() { let oh = U256::from(71); let net = add_dynamic_network(&oh, &oc); + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(net); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + // One staker and a TAO pot (not relevant to refund amount). let sh = U256::from(77); let sc = U256::from(88); @@ -683,6 +695,10 @@ fn destroy_alpha_out_multiple_stakers_pro_rata() { let owner_hot = U256::from(20); let netuid = add_dynamic_network(&owner_hot, &owner_cold); + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + // 2. Two stakers on that subnet let (c1, h1) = (U256::from(111), U256::from(211)); let (c2, h2) = (U256::from(222), U256::from(333)); @@ -779,6 +795,10 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { SubtensorModule::set_max_registrations_per_block(netuid, 1_000u16); SubtensorModule::set_target_registrations_per_interval(netuid, 1_000u16); + // Mark this subnet as *legacy* so owner refund path is enabled. + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + // Runtime-exact min amount = min_stake + fee let min_amount = { let min_stake = DefaultMinStake::::get(); @@ -914,6 +934,141 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { }); } +#[test] +fn destroy_alpha_out_refund_gating_by_registration_block() { + // ────────────────────────────────────────────────────────────────────── + // Case A: LEGACY subnet → refund applied + // ────────────────────────────────────────────────────────────────────── + new_test_ext(0).execute_with(|| { + // Owner + subnet + let owner_cold = U256::from(10_000); + let owner_hot = U256::from(20_000); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Mark as *legacy*: registered_at < start_block + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + // Lock and (nonzero) emissions + let lock_u64: u64 = 50_000; + SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(lock_u64)); + Emission::::insert( + netuid, + vec![AlphaCurrency::from(1_500u64), AlphaCurrency::from(3_000u64)], // total 4_500 α + ); + // Owner cut ≈ 50% + SubnetOwnerCut::::put(32_768u16); + + // Compute expected refund using the same math as the pallet + let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut(); + let total_emitted_alpha: u64 = 1_500 + 3_000; // 4_500 α + let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha) + .saturating_mul(frac) + .floor() + .saturating_to_num::(); + + // Prefer sim_swap; fall back to current price if unavailable. + let owner_emission_tao_u64: u64 = ::SwapInterface::sim_swap( + netuid.into(), + OrderType::Sell, + owner_alpha_u64, + ) + .map(|res| res.amount_paid_out) + .unwrap_or_else(|_| { + let price: U96F32 = + ::SwapInterface::current_alpha_price(netuid.into()); + U96F32::from_num(owner_alpha_u64) + .saturating_mul(price) + .floor() + .saturating_to_num::() + }); + + let expected_refund: u64 = lock_u64.saturating_sub(owner_emission_tao_u64); + + // Balances before + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + // Run the path under test + assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + + // Owner received their refund… + let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); + assert_eq!(owner_after, owner_before + expected_refund); + + // …and the lock is always cleared to zero by destroy_alpha_in_out_stakes. + assert_eq!( + SubtensorModule::get_subnet_locked_balance(netuid), + TaoCurrency::from(0u64) + ); + }); + + // ────────────────────────────────────────────────────────────────────── + // Case B: NON‑LEGACY subnet → NO refund; + // ────────────────────────────────────────────────────────────────────── + new_test_ext(0).execute_with(|| { + // Owner + subnet + let owner_cold = U256::from(1_111); + let owner_hot = U256::from(2_222); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Explicitly set start_block <= registered_at to make it non‑legacy. + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at); + + // Lock and emissions present (should be ignored for refund) + let lock_u64: u64 = 42_000; + SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(lock_u64)); + Emission::::insert(netuid, vec![AlphaCurrency::from(5_000u64)]); + SubnetOwnerCut::::put(32_768u16); // ~50% + + // Balances before + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + + // Run the path under test + assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + + // No refund for non‑legacy + let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); + assert_eq!(owner_after, owner_before); + + // Lock is still cleared to zero by the routine + assert_eq!( + SubtensorModule::get_subnet_locked_balance(netuid), + TaoCurrency::from(0u64) + ); + }); + + // ────────────────────────────────────────────────────────────────────── + // Case C: LEGACY subnet but lock = 0 → no refund; + // ────────────────────────────────────────────────────────────────────── + new_test_ext(0).execute_with(|| { + // Owner + subnet + let owner_cold = U256::from(9_999); + let owner_hot = U256::from(8_888); + let netuid = add_dynamic_network(&owner_hot, &owner_cold); + + // Mark as *legacy* + let reg_at = NetworkRegisteredAt::::get(netuid); + NetworkRegistrationStartBlock::::put(reg_at.saturating_add(1)); + + // lock = 0; emissions present (must not matter) + SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(0u64)); + Emission::::insert(netuid, vec![AlphaCurrency::from(10_000u64)]); + SubnetOwnerCut::::put(32_768u16); // ~50% + + let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold); + assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid)); + let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold); + + // No refund possible when lock = 0 + assert_eq!(owner_after, owner_before); + assert_eq!( + SubtensorModule::get_subnet_locked_balance(netuid), + TaoCurrency::from(0u64) + ); + }); +} + #[test] fn prune_none_with_no_networks() { new_test_ext(0).execute_with(|| {