Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 57 additions & 36 deletions pallets/subtensor/src/staking/remove_stake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,43 +449,58 @@ impl<T: Config> Pallet<T> {
let owner_coldkey: T::AccountId = SubnetOwner::<T>::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::<T>::get(netuid);
let start_block: u64 = NetworkRegistrationStartBlock::<T>::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::<T> is Vec<AlphaCurrency>. We:
// - sum emitted α,
// - apply owner fraction to get owner α,
// - price that α using a *simulated* AMM swap.
let total_emitted_alpha_u128: u128 =
Emission::<T>::get(netuid)
.into_iter()
.fold(0u128, |acc, e_alpha| {
let e_u64: u64 = Into::<u64>::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::<u64>();

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::<u64>();
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::<T>::get(netuid)
.into_iter()
.fold(0u128, |acc, e_alpha| {
let e_u64: u64 = Into::<u64>::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::<u64>();

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::<u64>();
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,
Expand Down Expand Up @@ -594,13 +609,19 @@ impl<T: Config> Pallet<T> {
SubnetAlphaInProvided::<T>::remove(netuid);
SubnetAlphaOut::<T>::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());
}
Expand Down
157 changes: 156 additions & 1 deletion pallets/subtensor/src/tests/networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Test>::get(net);
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));

let lock: TaoCurrency = TaoCurrency::from(1_000_000);
SubtensorModule::set_subnet_locked_balance(net, lock);
SubnetTAO::<Test>::insert(net, TaoCurrency::from(0));
Expand Down Expand Up @@ -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::<Test>::get(net);
NetworkRegistrationStartBlock::<Test>::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);

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

let pot: u64 = 10_000;
SubnetTAO::<Test>::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);
Expand Down Expand Up @@ -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::<Test>::get(net);
NetworkRegistrationStartBlock::<Test>::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);
Expand Down Expand Up @@ -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::<Test>::get(netuid);
NetworkRegistrationStartBlock::<Test>::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));
Expand Down Expand Up @@ -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::<Test>::get(netuid);
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));

// Runtime-exact min amount = min_stake + fee
let min_amount = {
let min_stake = DefaultMinStake::<Test>::get();
Expand Down Expand Up @@ -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::<Test>::get(netuid);
NetworkRegistrationStartBlock::<Test>::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::<Test>::insert(
netuid,
vec![AlphaCurrency::from(1_500u64), AlphaCurrency::from(3_000u64)], // total 4_500 α
);
// Owner cut ≈ 50%
SubnetOwnerCut::<Test>::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::<u64>();

// Prefer sim_swap; fall back to current price if unavailable.
let owner_emission_tao_u64: u64 = <Test as pallet::Config>::SwapInterface::sim_swap(
netuid.into(),
OrderType::Sell,
owner_alpha_u64,
)
.map(|res| res.amount_paid_out)
.unwrap_or_else(|_| {
let price: U96F32 =
<Test as pallet::Config>::SwapInterface::current_alpha_price(netuid.into());
U96F32::from_num(owner_alpha_u64)
.saturating_mul(price)
.floor()
.saturating_to_num::<u64>()
});

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::<Test>::get(netuid);
NetworkRegistrationStartBlock::<Test>::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::<Test>::insert(netuid, vec![AlphaCurrency::from(5_000u64)]);
SubnetOwnerCut::<Test>::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::<Test>::get(netuid);
NetworkRegistrationStartBlock::<Test>::put(reg_at.saturating_add(1));

// lock = 0; emissions present (must not matter)
SubtensorModule::set_subnet_locked_balance(netuid, TaoCurrency::from(0u64));
Emission::<Test>::insert(netuid, vec![AlphaCurrency::from(10_000u64)]);
SubnetOwnerCut::<Test>::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(|| {
Expand Down
Loading