Skip to content

Commit 5786add

Browse files
authored
Merge pull request #2106 from opentensor/subnet-burn-cost
Subnet Burn Cost
2 parents a75c6f5 + bdf4bac commit 5786add

File tree

2 files changed

+213
-37
lines changed

2 files changed

+213
-37
lines changed

pallets/subtensor/src/staking/remove_stake.rs

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -449,43 +449,58 @@ impl<T: Config> Pallet<T> {
449449
let owner_coldkey: T::AccountId = SubnetOwner::<T>::get(netuid);
450450
let lock_cost: TaoCurrency = Self::get_subnet_locked_balance(netuid);
451451

452-
// 3) Compute owner's received emission in TAO at current price.
452+
// Determine if this subnet is eligible for a lock refund (legacy).
453+
let reg_at: u64 = NetworkRegisteredAt::<T>::get(netuid);
454+
let start_block: u64 = NetworkRegistrationStartBlock::<T>::get();
455+
let should_refund_owner: bool = reg_at < start_block;
456+
457+
// 3) Compute owner's received emission in TAO at current price (ONLY if we may refund).
453458
// Emission::<T> is Vec<AlphaCurrency>. We:
454459
// - sum emitted α,
455460
// - apply owner fraction to get owner α,
456461
// - price that α using a *simulated* AMM swap.
457-
let total_emitted_alpha_u128: u128 =
458-
Emission::<T>::get(netuid)
459-
.into_iter()
460-
.fold(0u128, |acc, e_alpha| {
461-
let e_u64: u64 = Into::<u64>::into(e_alpha);
462-
acc.saturating_add(e_u64 as u128)
463-
});
464-
465-
let owner_fraction: U96F32 = Self::get_float_subnet_owner_cut();
466-
let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha_u128)
467-
.saturating_mul(owner_fraction)
468-
.floor()
469-
.saturating_to_num::<u64>();
470-
471-
let owner_emission_tao: TaoCurrency = if owner_alpha_u64 > 0 {
472-
match T::SwapInterface::sim_swap(netuid.into(), OrderType::Sell, owner_alpha_u64) {
473-
Ok(sim) => TaoCurrency::from(sim.amount_paid_out),
474-
Err(e) => {
475-
log::debug!(
476-
"destroy_alpha_in_out_stakes: sim_swap owner α→τ failed (netuid={netuid:?}, alpha={owner_alpha_u64}, err={e:?}); falling back to price multiply.",
477-
);
478-
let cur_price: U96F32 = T::SwapInterface::current_alpha_price(netuid.into());
479-
let val_u64: u64 = U96F32::from_num(owner_alpha_u64)
480-
.saturating_mul(cur_price)
481-
.floor()
482-
.saturating_to_num::<u64>();
483-
TaoCurrency::from(val_u64)
484-
}
462+
let mut owner_emission_tao: TaoCurrency = TaoCurrency::ZERO;
463+
if should_refund_owner && !lock_cost.is_zero() {
464+
let total_emitted_alpha_u128: u128 =
465+
Emission::<T>::get(netuid)
466+
.into_iter()
467+
.fold(0u128, |acc, e_alpha| {
468+
let e_u64: u64 = Into::<u64>::into(e_alpha);
469+
acc.saturating_add(e_u64 as u128)
470+
});
471+
472+
if total_emitted_alpha_u128 > 0 {
473+
let owner_fraction: U96F32 = Self::get_float_subnet_owner_cut();
474+
let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha_u128)
475+
.saturating_mul(owner_fraction)
476+
.floor()
477+
.saturating_to_num::<u64>();
478+
479+
owner_emission_tao = if owner_alpha_u64 > 0 {
480+
match T::SwapInterface::sim_swap(
481+
netuid.into(),
482+
OrderType::Sell,
483+
owner_alpha_u64,
484+
) {
485+
Ok(sim) => TaoCurrency::from(sim.amount_paid_out),
486+
Err(e) => {
487+
log::debug!(
488+
"destroy_alpha_in_out_stakes: sim_swap owner α→τ failed (netuid={netuid:?}, alpha={owner_alpha_u64}, err={e:?}); falling back to price multiply.",
489+
);
490+
let cur_price: U96F32 =
491+
T::SwapInterface::current_alpha_price(netuid.into());
492+
let val_u64: u64 = U96F32::from_num(owner_alpha_u64)
493+
.saturating_mul(cur_price)
494+
.floor()
495+
.saturating_to_num::<u64>();
496+
TaoCurrency::from(val_u64)
497+
}
498+
}
499+
} else {
500+
TaoCurrency::ZERO
501+
};
485502
}
486-
} else {
487-
TaoCurrency::ZERO
488-
};
503+
}
489504

490505
// 4) Enumerate all α entries on this subnet to build distribution weights and cleanup lists.
491506
// - collect keys to remove,
@@ -594,13 +609,19 @@ impl<T: Config> Pallet<T> {
594609
SubnetAlphaInProvided::<T>::remove(netuid);
595610
SubnetAlphaOut::<T>::remove(netuid);
596611

597-
// 8) Refund remaining lock to subnet owner:
598-
// refund = max(0, lock_cost(τ) − owner_received_emission_in_τ).
599-
let refund: TaoCurrency = lock_cost.saturating_sub(owner_emission_tao);
600-
601612
// Clear the locked balance on the subnet.
602613
Self::set_subnet_locked_balance(netuid, TaoCurrency::ZERO);
603614

615+
// 8) Finalize lock handling:
616+
// - Legacy subnets (registered before NetworkRegistrationStartBlock) receive:
617+
// refund = max(0, lock_cost(τ) − owner_received_emission_in_τ).
618+
// - New subnets: no refund.
619+
let refund: TaoCurrency = if should_refund_owner {
620+
lock_cost.saturating_sub(owner_emission_tao)
621+
} else {
622+
TaoCurrency::ZERO
623+
};
624+
604625
if !refund.is_zero() {
605626
Self::add_balance_to_coldkey_account(&owner_coldkey, refund.to_u64());
606627
}

pallets/subtensor/src/tests/networks.rs

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ fn dissolve_refunds_full_lock_cost_when_no_emission() {
7171
let hot = U256::from(4);
7272
let net = add_dynamic_network(&hot, &cold);
7373

74+
// Mark this subnet as *legacy* so owner refund path is enabled.
75+
let reg_at = NetworkRegisteredAt::<Test>::get(net);
76+
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));
77+
7478
let lock: TaoCurrency = TaoCurrency::from(1_000_000);
7579
SubtensorModule::set_subnet_locked_balance(net, lock);
7680
SubnetTAO::<Test>::insert(net, TaoCurrency::from(0));
@@ -126,6 +130,10 @@ fn dissolve_two_stakers_pro_rata_distribution() {
126130
let oh = U256::from(51);
127131
let net = add_dynamic_network(&oh, &oc);
128132

133+
// Mark this subnet as *legacy* so owner refund path is enabled.
134+
let reg_at = NetworkRegisteredAt::<Test>::get(net);
135+
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));
136+
129137
let (s1_hot, s1_cold, a1) = (U256::from(201), U256::from(301), 300u128);
130138
let (s2_hot, s2_cold, a2) = (U256::from(202), U256::from(302), 700u128);
131139

@@ -134,7 +142,7 @@ fn dissolve_two_stakers_pro_rata_distribution() {
134142

135143
let pot: u64 = 10_000;
136144
SubnetTAO::<Test>::insert(net, TaoCurrency::from(pot));
137-
SubtensorModule::set_subnet_locked_balance(net, 5_000.into()); // owner refund path present but emission = 0
145+
SubtensorModule::set_subnet_locked_balance(net, 5_000.into()); // owner refund path present; emission = 0
138146

139147
// Cold-key balances before
140148
let s1_before = SubtensorModule::get_coldkey_balance(&s1_cold);
@@ -199,6 +207,10 @@ fn dissolve_owner_cut_refund_logic() {
199207
let oh = U256::from(71);
200208
let net = add_dynamic_network(&oh, &oc);
201209

210+
// Mark this subnet as *legacy* so owner refund path is enabled.
211+
let reg_at = NetworkRegisteredAt::<Test>::get(net);
212+
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));
213+
202214
// One staker and a TAO pot (not relevant to refund amount).
203215
let sh = U256::from(77);
204216
let sc = U256::from(88);
@@ -683,6 +695,10 @@ fn destroy_alpha_out_multiple_stakers_pro_rata() {
683695
let owner_hot = U256::from(20);
684696
let netuid = add_dynamic_network(&owner_hot, &owner_cold);
685697

698+
// Mark this subnet as *legacy* so owner refund path is enabled.
699+
let reg_at = NetworkRegisteredAt::<Test>::get(netuid);
700+
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));
701+
686702
// 2. Two stakers on that subnet
687703
let (c1, h1) = (U256::from(111), U256::from(211));
688704
let (c2, h2) = (U256::from(222), U256::from(333));
@@ -779,6 +795,10 @@ fn destroy_alpha_out_many_stakers_complex_distribution() {
779795
SubtensorModule::set_max_registrations_per_block(netuid, 1_000u16);
780796
SubtensorModule::set_target_registrations_per_interval(netuid, 1_000u16);
781797

798+
// Mark this subnet as *legacy* so owner refund path is enabled.
799+
let reg_at = NetworkRegisteredAt::<Test>::get(netuid);
800+
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));
801+
782802
// Runtime-exact min amount = min_stake + fee
783803
let min_amount = {
784804
let min_stake = DefaultMinStake::<Test>::get();
@@ -914,6 +934,141 @@ fn destroy_alpha_out_many_stakers_complex_distribution() {
914934
});
915935
}
916936

937+
#[test]
938+
fn destroy_alpha_out_refund_gating_by_registration_block() {
939+
// ──────────────────────────────────────────────────────────────────────
940+
// Case A: LEGACY subnet → refund applied
941+
// ──────────────────────────────────────────────────────────────────────
942+
new_test_ext(0).execute_with(|| {
943+
// Owner + subnet
944+
let owner_cold = U256::from(10_000);
945+
let owner_hot = U256::from(20_000);
946+
let netuid = add_dynamic_network(&owner_hot, &owner_cold);
947+
948+
// Mark as *legacy*: registered_at < start_block
949+
let reg_at = NetworkRegisteredAt::<Test>::get(netuid);
950+
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));
951+
952+
// Lock and (nonzero) emissions
953+
let lock_u64: u64 = 50_000;
954+
SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(lock_u64));
955+
Emission::<Test>::insert(
956+
netuid,
957+
vec![AlphaCurrency::from(1_500u64), AlphaCurrency::from(3_000u64)], // total 4_500 α
958+
);
959+
// Owner cut ≈ 50%
960+
SubnetOwnerCut::<Test>::put(32_768u16);
961+
962+
// Compute expected refund using the same math as the pallet
963+
let frac: U96F32 = SubtensorModule::get_float_subnet_owner_cut();
964+
let total_emitted_alpha: u64 = 1_500 + 3_000; // 4_500 α
965+
let owner_alpha_u64: u64 = U96F32::from_num(total_emitted_alpha)
966+
.saturating_mul(frac)
967+
.floor()
968+
.saturating_to_num::<u64>();
969+
970+
// Prefer sim_swap; fall back to current price if unavailable.
971+
let owner_emission_tao_u64: u64 = <Test as pallet::Config>::SwapInterface::sim_swap(
972+
netuid.into(),
973+
OrderType::Sell,
974+
owner_alpha_u64,
975+
)
976+
.map(|res| res.amount_paid_out)
977+
.unwrap_or_else(|_| {
978+
let price: U96F32 =
979+
<Test as pallet::Config>::SwapInterface::current_alpha_price(netuid.into());
980+
U96F32::from_num(owner_alpha_u64)
981+
.saturating_mul(price)
982+
.floor()
983+
.saturating_to_num::<u64>()
984+
});
985+
986+
let expected_refund: u64 = lock_u64.saturating_sub(owner_emission_tao_u64);
987+
988+
// Balances before
989+
let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold);
990+
991+
// Run the path under test
992+
assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid));
993+
994+
// Owner received their refund…
995+
let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold);
996+
assert_eq!(owner_after, owner_before + expected_refund);
997+
998+
// …and the lock is always cleared to zero by destroy_alpha_in_out_stakes.
999+
assert_eq!(
1000+
SubtensorModule::get_subnet_locked_balance(netuid),
1001+
TaoCurrency::from(0u64)
1002+
);
1003+
});
1004+
1005+
// ──────────────────────────────────────────────────────────────────────
1006+
// Case B: NON‑LEGACY subnet → NO refund;
1007+
// ──────────────────────────────────────────────────────────────────────
1008+
new_test_ext(0).execute_with(|| {
1009+
// Owner + subnet
1010+
let owner_cold = U256::from(1_111);
1011+
let owner_hot = U256::from(2_222);
1012+
let netuid = add_dynamic_network(&owner_hot, &owner_cold);
1013+
1014+
// Explicitly set start_block <= registered_at to make it non‑legacy.
1015+
let reg_at = NetworkRegisteredAt::<Test>::get(netuid);
1016+
NetworkRegistrationStartBlock::<Test>::put(reg_at);
1017+
1018+
// Lock and emissions present (should be ignored for refund)
1019+
let lock_u64: u64 = 42_000;
1020+
SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(lock_u64));
1021+
Emission::<Test>::insert(netuid, vec![AlphaCurrency::from(5_000u64)]);
1022+
SubnetOwnerCut::<Test>::put(32_768u16); // ~50%
1023+
1024+
// Balances before
1025+
let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold);
1026+
1027+
// Run the path under test
1028+
assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid));
1029+
1030+
// No refund for non‑legacy
1031+
let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold);
1032+
assert_eq!(owner_after, owner_before);
1033+
1034+
// Lock is still cleared to zero by the routine
1035+
assert_eq!(
1036+
SubtensorModule::get_subnet_locked_balance(netuid),
1037+
TaoCurrency::from(0u64)
1038+
);
1039+
});
1040+
1041+
// ──────────────────────────────────────────────────────────────────────
1042+
// Case C: LEGACY subnet but lock = 0 → no refund;
1043+
// ──────────────────────────────────────────────────────────────────────
1044+
new_test_ext(0).execute_with(|| {
1045+
// Owner + subnet
1046+
let owner_cold = U256::from(9_999);
1047+
let owner_hot = U256::from(8_888);
1048+
let netuid = add_dynamic_network(&owner_hot, &owner_cold);
1049+
1050+
// Mark as *legacy*
1051+
let reg_at = NetworkRegisteredAt::<Test>::get(netuid);
1052+
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));
1053+
1054+
// lock = 0; emissions present (must not matter)
1055+
SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(0u64));
1056+
Emission::<Test>::insert(netuid, vec![AlphaCurrency::from(10_000u64)]);
1057+
SubnetOwnerCut::<Test>::put(32_768u16); // ~50%
1058+
1059+
let owner_before = SubtensorModule::get_coldkey_balance(&owner_cold);
1060+
assert_ok!(SubtensorModule::destroy_alpha_in_out_stakes(netuid));
1061+
let owner_after = SubtensorModule::get_coldkey_balance(&owner_cold);
1062+
1063+
// No refund possible when lock = 0
1064+
assert_eq!(owner_after, owner_before);
1065+
assert_eq!(
1066+
SubtensorModule::get_subnet_locked_balance(netuid),
1067+
TaoCurrency::from(0u64)
1068+
);
1069+
});
1070+
}
1071+
9171072
#[test]
9181073
fn prune_none_with_no_networks() {
9191074
new_test_ext(0).execute_with(|| {

0 commit comments

Comments
 (0)