From 819f961939bd951b9382c97946abc6b4342d1e40 Mon Sep 17 00:00:00 2001 From: Cameron Fairchild Date: Wed, 6 Nov 2024 22:34:50 -0500 Subject: [PATCH] Hotfix/clear stake delta on drain (#934) * add comment also * chore: fmt * add back delta handling in helpers * track removals in stake delta * handle stake delta in ck swap func * add tests back * add tests for staking back * add back test for ck swap --- .../subtensor/src/coinbase/run_coinbase.rs | 9 +- pallets/subtensor/src/staking/helpers.rs | 6 + pallets/subtensor/src/staking/remove_stake.rs | 5 + pallets/subtensor/src/swap/swap_coldkey.rs | 21 +- pallets/subtensor/tests/coinbase.rs | 1296 ++++++++++++++++- pallets/subtensor/tests/staking.rs | 103 ++ pallets/subtensor/tests/swap_coldkey.rs | 38 + 7 files changed, 1470 insertions(+), 8 deletions(-) diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 0615ccc39..aa3fa17d2 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -303,8 +303,8 @@ impl Pallet { // --- 8 Iterate over each nominator. if total_viable_nominator_stake != 0 { for (nominator, nominator_stake) in Stake::::iter_prefix(hotkey) { - // --- 9 Check if the stake was manually increased by the user since the last emission drain for this hotkey. - // If it was, skip this nominator as they will not receive their proportion of the emission. + // --- 9 Skip emission for any stake the was added by the nominator since the last emission drain. + // This means the nominator will get emission on existing stake, but not on new stake, until the next emission drain. let viable_nominator_stake = nominator_stake.saturating_sub(Self::get_nonviable_stake(hotkey, &nominator)); @@ -331,7 +331,10 @@ impl Pallet { let hotkey_new_tao: u64 = hotkey_take.saturating_add(remainder); Self::increase_stake_on_hotkey_account(hotkey, hotkey_new_tao); - // --- 14 Record new tao creation event and return the amount created. + // --- 14 Reset the stake delta for the hotkey. + let _ = StakeDeltaSinceLastEmissionDrain::::clear_prefix(hotkey, u32::MAX, None); + + // --- 15 Record new tao creation event and return the amount created. total_new_tao = total_new_tao.saturating_add(hotkey_new_tao); total_new_tao } diff --git a/pallets/subtensor/src/staking/helpers.rs b/pallets/subtensor/src/staking/helpers.rs index 0328d94e6..9fd60ea51 100644 --- a/pallets/subtensor/src/staking/helpers.rs +++ b/pallets/subtensor/src/staking/helpers.rs @@ -297,6 +297,9 @@ impl Pallet { staking_hotkeys.retain(|h| h != hotkey); StakingHotkeys::::insert(coldkey, staking_hotkeys); + // Update stake delta + StakeDeltaSinceLastEmissionDrain::::remove(hotkey, coldkey); + current_stake } @@ -431,6 +434,9 @@ impl Pallet { // Add the balance to the coldkey account. Self::add_balance_to_coldkey_account(&delegate_coldkey_i, stake_i); + + // Remove stake delta + StakeDeltaSinceLastEmissionDrain::::remove(hotkey, &delegate_coldkey_i); } } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index 4118e8d07..587583f5e 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -76,6 +76,11 @@ impl Pallet { // We remove the balance from the hotkey. Self::decrease_stake_on_coldkey_hotkey_account(&coldkey, &hotkey, stake_to_be_removed); + // Track this removal in the stake delta. + StakeDeltaSinceLastEmissionDrain::::mutate(&hotkey, &coldkey, |stake_delta| { + *stake_delta = stake_delta.saturating_sub_unsigned(stake_to_be_removed as u128); + }); + // We add the balance to the coldkey. If the above fails we will not credit this coldkey. Self::add_balance_to_coldkey_account(&coldkey, stake_to_be_removed); diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index bcbd2a330..4742c3fca 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -169,7 +169,20 @@ impl Pallet { weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); } - // 4. Swap total coldkey stake. + // 4. Swap StakeDeltaSinceLastEmissionDrain + for hotkey in StakingHotkeys::::get(old_coldkey) { + let old_stake_delta = StakeDeltaSinceLastEmissionDrain::::get(&hotkey, old_coldkey); + let new_stake_delta = StakeDeltaSinceLastEmissionDrain::::get(&hotkey, new_coldkey); + StakeDeltaSinceLastEmissionDrain::::insert( + &hotkey, + new_coldkey, + new_stake_delta.saturating_add(old_stake_delta), + ); + StakeDeltaSinceLastEmissionDrain::::remove(&hotkey, old_coldkey); + weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); + } + + // 5. Swap total coldkey stake. // TotalColdkeyStake: MAP ( coldkey ) --> u64 | Total stake of the coldkey. let old_coldkey_stake: u64 = TotalColdkeyStake::::get(old_coldkey); // Get the stake of the new coldkey. @@ -183,7 +196,7 @@ impl Pallet { ); weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - // 5. Swap StakingHotkeys. + // 6. Swap StakingHotkeys. // StakingHotkeys: MAP ( coldkey ) --> Vec | Hotkeys staking for the coldkey. let old_staking_hotkeys: Vec = StakingHotkeys::::get(old_coldkey); let mut new_staking_hotkeys: Vec = StakingHotkeys::::get(new_coldkey); @@ -197,7 +210,7 @@ impl Pallet { StakingHotkeys::::insert(new_coldkey, new_staking_hotkeys); weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - // 6. Swap hotkey owners. + // 7. Swap hotkey owners. // Owner: MAP ( hotkey ) --> coldkey | Owner of the hotkey. // OwnedHotkeys: MAP ( coldkey ) --> Vec | Hotkeys owned by the coldkey. let old_owned_hotkeys: Vec = OwnedHotkeys::::get(old_coldkey); @@ -216,7 +229,7 @@ impl Pallet { OwnedHotkeys::::insert(new_coldkey, new_owned_hotkeys); weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2)); - // 7. Transfer remaining balance. + // 8. Transfer remaining balance. // Balance: MAP ( coldkey ) --> u64 | Balance of the coldkey. // Transfer any remaining balance from old_coldkey to new_coldkey let remaining_balance = Self::get_coldkey_balance(old_coldkey); diff --git a/pallets/subtensor/tests/coinbase.rs b/pallets/subtensor/tests/coinbase.rs index a6c1acde1..184f7d837 100644 --- a/pallets/subtensor/tests/coinbase.rs +++ b/pallets/subtensor/tests/coinbase.rs @@ -1,8 +1,11 @@ #![allow(unused, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used)] use crate::mock::*; mod mock; -// use frame_support::{assert_err, assert_ok}; +use frame_support::assert_ok; use sp_core::U256; +use substrate_fixed::types::I64F64; + +use pallet_subtensor::TargetStakesPerInterval; // Test the ability to hash all sorts of hotkeys. #[test] @@ -154,3 +157,1294 @@ fn test_set_and_get_hotkey_emission_tempo() { assert_eq!(updated_tempo, new_tempo); }); } + +// Test getting nonviable stake +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test coinbase test_get_nonviable_stake -- --nocapture +#[test] +fn test_get_nonviable_stake() { + new_test_ext(1).execute_with(|| { + let netuid = 1u16; + let delegate_coldkey = U256::from(1); + let delegate_hotkey = U256::from(2); + let delegator = U256::from(3); + + let owner_added_stake = 123; + let owner_removed_stake = 456; + let owner_stake = 1_000 + owner_removed_stake; + // Add more than removed to test that the delta is updated correctly + let owner_adds_more_stake = owner_removed_stake + 1; + + let delegator_added_stake = 999; + + // Set stake rate limit very high + TargetStakesPerInterval::::put(1e9 as u64); + + add_network(netuid, 0, 0); + register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); + // Give extra stake to the owner + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &delegate_coldkey, + &delegate_hotkey, + owner_stake, + ); + + // Register as a delegate + assert_ok!(SubtensorModule::become_delegate( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey + )); + + // Verify that the key starts with 0 nonviable stake + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + 0 + ); + + // Give the coldkey some balance; extra just in-case + SubtensorModule::add_balance_to_coldkey_account( + &delegate_coldkey, + owner_added_stake + owner_adds_more_stake, + ); + + // Add some stake + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_added_stake + )); + + // Verify the nonviable stake is the same as the added stake + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + owner_added_stake + ); + + // Add some stake from a delegator + SubtensorModule::add_balance_to_coldkey_account(&delegator, delegator_added_stake); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + delegator_added_stake + )); + + // Verify that the nonviable stake doesn't change when a different account adds stake + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + owner_added_stake + ); + + // Remove some stake + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_removed_stake + )); + + // The stake delta is negative, so the nonviable stake should be 0 + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + 0 + ); + + // Add more stake than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_adds_more_stake + )); + + // Verify that the nonviable stake is the net of the operations + assert_eq!( + SubtensorModule::get_nonviable_stake(&delegate_hotkey, &delegate_coldkey), + owner_adds_more_stake - owner_removed_stake + owner_added_stake + ); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_overflow -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_overflow() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let vali_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + vali_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 5e9 as u64); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 5e9 as u64); + let initial_stake = 5e9 as u64; + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + // 5. Set emission and verify initial states + let to_emit = 20_000e9 as u64; + SubtensorModule::set_emission_values(&[netuid], vec![to_emit]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), to_emit); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + initial_stake * 2 + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), to_emit); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let hotkey_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + hotkey_stake, + nominator1_stake, + nominator2_stake + ); + + // 9. Verify distribution + let total_emission = to_emit * 2; // to_emit per block for 2 blocks + let hotkey_emission = (I64F64::from_num(total_emission) / I64F64::from_num(u16::MAX) + * I64F64::from_num(vali_take)) + .to_num::(); + let remaining_emission = total_emission - hotkey_emission; + let nominator_emission = remaining_emission / 2; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: {}", + hotkey_emission, + nominator_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", hotkey_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + let expected_hotkey_stake = 4_000e9 as u64; + let eps = 0.5e9 as u64; + assert!( + hotkey_stake >= expected_hotkey_stake - eps + && hotkey_stake <= expected_hotkey_stake + eps, + "Hotkey stake mismatch - expected: {}, actual: {}", + expected_hotkey_stake, + hotkey_stake + ); + assert_eq!( + nominator1_stake, + initial_stake + nominator_emission, + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + initial_stake + nominator_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!( + total_stake, + initial_stake + initial_stake + total_emission, + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_no_deltas -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_no_deltas() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey), 200); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let hotkey_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + hotkey_stake, + nominator1_stake, + nominator2_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + let nominator_emission = remaining_emission / 2; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: {}", + hotkey_emission, + nominator_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", hotkey_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(hotkey_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + 100 + nominator_emission, + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + 100 + nominator_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!(total_stake, 200 + total_emission, "Total stake mismatch"); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_positive_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_positive_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + // Do an add_stake for nominator 1 + assert_ok!(SubtensorModule::do_add_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + 123 + )); // We should not expect this to impact the emissions + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let nominator1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + assert_eq!(nominator1_stake_before, 100 + 123); // The stake should include the added stake + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + 200 + 123 + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let hotkey_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + hotkey_stake, + nominator1_stake, + nominator2_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + let nominator_emission = remaining_emission / 2; + // Notice that nominator emission is equal for both nominators, even though nominator1 added stake + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: {}", + hotkey_emission, + nominator_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", hotkey_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(hotkey_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + 100 + 123 + nominator_emission, + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + 100 + nominator_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + // Includes the added stake from nominator1 + assert_eq!( + total_stake, + 200 + 123 + total_emission, + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_negative_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_negative_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + // Do an remove_stake for nominator 1 + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + 12 + )); // We should expect the emissions to be impacted; + // The viable stake should be the *new* stake for nominator 1 + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let nominator_1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + // Notice that nominator1 stake is the new stake, including the removed stake + assert_eq!(nominator_1_stake_before, 100 - 12); + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + 200 - 12 + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let delegate_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}, Total Hotkey Stake: {}", + delegate_stake, + nominator1_stake, + nominator2_stake, + total_hotkey_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + + let nominator_1_emission = remaining_emission * nominator1_stake / total_hotkey_stake; + let nominator_2_emission = remaining_emission * nominator2_stake / total_hotkey_stake; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: 1;{}, 2;{}", + hotkey_emission, + nominator_1_emission, + nominator_2_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", delegate_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(delegate_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + 100 - 12 + nominator_1_emission, + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + 100 + nominator_2_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!( + total_stake, + 200 - 12 + total_emission, + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_neutral_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_neutral_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + // Do an remove_stake for nominator 1 + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + 12 + )); + // Do an add_stake for nominator 1 of the same amount + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + 12 + )); // The viable stake should match the initial stake, because the delta is 0 + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let nominator1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + // Notice that nominator1 stake is the unchanged from the initial stake + assert_eq!(nominator1_stake_before, 100); + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!(SubtensorModule::get_total_stake_for_hotkey(&hotkey), 200); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let delegate_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}, Total Hotkey Stake: {}", + delegate_stake, + nominator1_stake, + nominator2_stake, + total_hotkey_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + + let nominator_1_emission = remaining_emission * nominator1_stake / total_hotkey_stake; + let nominator_2_emission = remaining_emission * nominator2_stake / total_hotkey_stake; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: 1;{}, 2;{}", + hotkey_emission, + nominator_1_emission, + nominator_2_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", delegate_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(delegate_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + 100 + nominator_1_emission, // We expect the emission to be calculated based on the initial stake + // Because the delta is 0. + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + 100 + nominator_2_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!(total_stake, 200 + total_emission, "Total stake mismatch"); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_net_positive_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_net_positive_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 100); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 100); + + let initial_nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let intial_total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let initial_nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + assert_eq!(initial_nominator1_stake, initial_nominator2_stake); // Initial stakes should be equal + + let removed_stake = 12; + // Do an add_stake for nominator 1 of MORE than was removed + let added_stake = removed_stake + 1; + let net_change: i128 = i128::from(added_stake) - i128::from(removed_stake); // Positive net change + + // Do an remove_stake for nominator 1 + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + removed_stake + )); + + // Do an add_stake for nominator 1 of MORE than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + added_stake + )); // We should expect the emissions to be impacted; + // The viable stake should be the same initial stake for nominator 1 + // NOT the new stake amount, because the delta is net positive + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let nominator_1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + // Notice that nominator1 stake is the new stake, including the removed stake + assert_eq!( + nominator_1_stake_before, + u64::try_from(100 + net_change).unwrap() + ); + + // 5. Set emission and verify initial states + SubtensorModule::set_emission_values(&[netuid], vec![10]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), 10); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + u64::try_from(200 + net_change).unwrap() + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 10); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let delegate_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}, Total Hotkey Stake: {}", + delegate_stake, + nominator1_stake, + nominator2_stake, + total_hotkey_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = 20; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + + // We expect to distribute using the initial stake for nominator 1; because the delta is net positive + // We also use the INITIAL total hotkey stake + let nominator_1_emission = + remaining_emission * initial_nominator1_stake / intial_total_hotkey_stake; + let nominator_2_emission = + remaining_emission * initial_nominator2_stake / intial_total_hotkey_stake; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: 1;{}, 2;{}", + hotkey_emission, + nominator_1_emission, + nominator_2_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", delegate_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Assertions + assert_eq!(delegate_stake, 2, "Hotkey stake mismatch"); + assert_eq!( + nominator1_stake, + u64::try_from( + net_change + .checked_add_unsigned(100 + nominator_1_emission as u128) + .unwrap() + ) + .unwrap(), + "Nominator1 stake mismatch" + ); + assert_eq!( + nominator2_stake, + initial_nominator2_stake + nominator_2_emission, + "Nominator2 stake mismatch" + ); + + // 10. Check total stake + assert_eq!( + total_stake, + u64::try_from( + net_change + .checked_add_unsigned(200 + total_emission as u128) + .unwrap() + ) + .unwrap(), + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --package pallet-subtensor --test coinbase test_coinbase_nominator_drainage_with_net_negative_delta -- --nocapture +#[test] +fn test_coinbase_nominator_drainage_with_net_negative_delta() { + new_test_ext(1).execute_with(|| { + // 1. Set up the network and accounts + let netuid: u16 = 1; + let hotkey = U256::from(0); + let coldkey = U256::from(3); + let nominator1 = U256::from(1); + let nominator2 = U256::from(2); + + log::debug!("Setting up network with netuid: {}", netuid); + log::debug!("Hotkey: {:?}, Coldkey: {:?}", hotkey, coldkey); + log::debug!("Nominators: {:?}, {:?}", nominator1, nominator2); + + // 2. Create network and register neuron + add_network(netuid, 1, 0); + register_ok_neuron(netuid, hotkey, coldkey, 100000); + SubtensorModule::create_account_if_non_existent(&coldkey, &hotkey); + + log::debug!("Network created and neuron registered"); + + // 3. Set up balances and stakes + SubtensorModule::add_balance_to_coldkey_account(&coldkey, 1000); + SubtensorModule::add_balance_to_coldkey_account(&nominator1, 1500); + SubtensorModule::add_balance_to_coldkey_account(&nominator2, 1500); + + log::debug!("Balances added to accounts"); + + // 4. Make the hotkey a delegate + let val_take = (u16::MAX as u64 / 10); + assert_ok!(SubtensorModule::do_become_delegate( + RuntimeOrigin::signed(coldkey), + hotkey, + val_take as u16 + )); + + log::debug!("Hotkey became a delegate with minimum take"); + + // Add stakes for nominators + // Add the stake directly to their coldkey-hotkey account + // This bypasses the accounting in stake delta + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator1, &hotkey, 300); + SubtensorModule::increase_stake_on_coldkey_hotkey_account(&nominator2, &hotkey, 300); + + let initial_nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let intial_total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let initial_nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + assert_eq!(initial_nominator1_stake, initial_nominator2_stake); // Initial stakes should be equal + + let removed_stake = 220; + // Do an add_stake for nominator 1 of LESS than was removed + let added_stake = removed_stake - 188; + let net_change: i128 = i128::from(added_stake) - i128::from(removed_stake); // Negative net change + assert!(net_change < 0); + + // Do an remove_stake for nominator 1 + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + removed_stake + )); + + // Do an add_stake for nominator 1 of MORE than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(nominator1), + hotkey, + added_stake + )); // We should expect the emissions to be impacted; + // The viable stake should be the LESS than the initial stake for nominator 1 + // Which IS the new stake amount, because the delta is net negative + + // Log the stakes for hotkey, nominator1, and nominator2 + log::debug!( + "Initial stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}", + SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey), + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey) + ); + log::debug!("Stakes added for nominators"); + + let total_stake_before = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator_1_stake_before = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + // Notice that nominator1 stake is the new stake, including the removed stake + assert_eq!( + nominator_1_stake_before, + u64::try_from(300 + net_change).unwrap() + ); + + // 5. Set emission and verify initial states + let to_emit = 10_000e9 as u64; + SubtensorModule::set_emission_values(&[netuid], vec![to_emit]).unwrap(); + assert_eq!(SubtensorModule::get_subnet_emission_value(netuid), to_emit); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + assert_eq!( + SubtensorModule::get_total_stake_for_hotkey(&hotkey), + u64::try_from(600 + net_change).unwrap() + ); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + + log::debug!("Emission set and initial states verified"); + + // 6. Set hotkey emission tempo + SubtensorModule::set_hotkey_emission_tempo(1); + log::debug!("Hotkey emission tempo set to 1"); + + // 7. Simulate blocks and check emissions + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), to_emit); + log::debug!( + "After first block, pending emission: {}", + SubtensorModule::get_pending_emission(netuid) + ); + + next_block(); + assert_eq!(SubtensorModule::get_pending_emission(netuid), 0); + assert_eq!(SubtensorModule::get_pending_hotkey_emission(&hotkey), 0); + log::debug!("After second block, pending emission drained"); + + // 8. Check final stakes + let delegate_stake = SubtensorModule::get_stake_for_coldkey_and_hotkey(&coldkey, &hotkey); + let total_hotkey_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + let nominator1_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator1, &hotkey); + let nominator2_stake = + SubtensorModule::get_stake_for_coldkey_and_hotkey(&nominator2, &hotkey); + + log::debug!( + "Final stakes - Hotkey: {}, Nominator1: {}, Nominator2: {}, Total Hotkey Stake: {}", + delegate_stake, + nominator1_stake, + nominator2_stake, + total_hotkey_stake + ); + + // 9. Verify distribution + let min_take = val_take; + let total_emission = to_emit * 2; // 10 per block for 2 blocks + let hotkey_emission = total_emission * min_take / u16::MAX as u64; + let remaining_emission = total_emission - hotkey_emission; + + // We expect to distribute using the NEW stake for nominator 1; because the delta is net negative + // We also use the INITIAL total hotkey stake + // Note: nominator_1_stake_before is the new stake for nominator 1, before the epochs run + let nominator_1_emission = + remaining_emission * nominator_1_stake_before / total_stake_before; + let nominator_2_emission = + remaining_emission * initial_nominator2_stake / total_stake_before; + + log::debug!( + "Calculated emissions - Hotkey: {}, Each Nominator: 1;{}, 2;{}", + hotkey_emission, + nominator_1_emission, + nominator_2_emission + ); + + // Debug: Print the actual stakes + log::debug!("Actual hotkey stake: {}", delegate_stake); + log::debug!("Actual nominator1 stake: {}", nominator1_stake); + log::debug!("Actual nominator2 stake: {}", nominator2_stake); + + // Debug: Check the total stake for the hotkey + let total_stake = SubtensorModule::get_total_stake_for_hotkey(&hotkey); + log::debug!("Total stake for hotkey: {}", total_stake); + + // Do a fuzzy check on the final stakes + let eps = 0.2e9 as u64; + + let expected_delegate_stake: u64 = 2_000e9 as u64; + assert!( + expected_delegate_stake - eps <= delegate_stake + && expected_delegate_stake + eps >= delegate_stake, + "Hotkey stake mismatch - Expected: {}, Actual: {}", + expected_delegate_stake, + delegate_stake + ); + + let expected_1_stake = u64::try_from( + net_change + .checked_add_unsigned((initial_nominator1_stake + nominator_1_emission) as u128) + .unwrap(), + ) + .unwrap(); + assert!( + expected_1_stake - eps <= nominator1_stake + && expected_1_stake + eps >= nominator1_stake, + "Nominator1 stake mismatch - Expected: {}, Actual: {}", + expected_1_stake, + nominator1_stake + ); + let expected_2_stake = initial_nominator2_stake + nominator_2_emission; + assert!( + expected_2_stake - eps <= nominator2_stake + && expected_2_stake + eps >= nominator2_stake, + "Nominator2 stake mismatch - Expected: {}, Actual: {}", + expected_2_stake, + nominator2_stake + ); + + // 10. Check total stake + assert_eq!( + total_stake, + u64::try_from( + net_change + .checked_add_unsigned( + (initial_nominator2_stake + initial_nominator1_stake + total_emission) + as u128 + ) + .unwrap() + ) + .unwrap(), + "Total stake mismatch" + ); + + log::debug!("Test completed"); + }); +} diff --git a/pallets/subtensor/tests/staking.rs b/pallets/subtensor/tests/staking.rs index f053c7ca6..a55db996b 100644 --- a/pallets/subtensor/tests/staking.rs +++ b/pallets/subtensor/tests/staking.rs @@ -2306,3 +2306,106 @@ fn test_get_total_delegated_stake_exclude_owner_stake() { ); }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test staking -- test_stake_delta_tracks_adds_and_removes --exact --nocapture +#[test] +fn test_stake_delta_tracks_adds_and_removes() { + new_test_ext(1).execute_with(|| { + let netuid = 1u16; + let delegate_coldkey = U256::from(1); + let delegate_hotkey = U256::from(2); + let delegator = U256::from(3); + + let owner_stake = 1000; + let owner_added_stake = 123; + let owner_removed_stake = 456; + // Add more than removed to test that the delta is updated correctly + let owner_adds_more_stake = owner_removed_stake + 1; + + let delegator_added_stake = 999; + + // Set stake rate limit very high + TargetStakesPerInterval::::put(1e9 as u64); + + add_network(netuid, 0, 0); + register_ok_neuron(netuid, delegate_hotkey, delegate_coldkey, 0); + // Give extra stake to the owner + SubtensorModule::increase_stake_on_coldkey_hotkey_account( + &delegate_coldkey, + &delegate_hotkey, + owner_stake, + ); + + // Register as a delegate + assert_ok!(SubtensorModule::become_delegate( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey + )); + + // Verify that the stake delta is empty + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + 0 + ); + + // Give the coldkey some balance; extra just in case + SubtensorModule::add_balance_to_coldkey_account( + &delegate_coldkey, + owner_added_stake + owner_adds_more_stake, + ); + + // Add some stake + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_added_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + ); + + // Add some stake from a delegator + SubtensorModule::add_balance_to_coldkey_account(&delegator, delegator_added_stake); + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegator), + delegate_hotkey, + delegator_added_stake + )); + + // Verify that the stake delta is unchanged for the owner + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + ); + + // Remove some stake + assert_ok!(SubtensorModule::remove_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_removed_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake).saturating_sub_unsigned(owner_removed_stake.into()) + ); + + // Add more stake than was removed + assert_ok!(SubtensorModule::add_stake( + RuntimeOrigin::signed(delegate_coldkey), + delegate_hotkey, + owner_adds_more_stake + )); + + // Verify that the stake delta is correct + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(delegate_hotkey, delegate_coldkey), + i128::from(owner_added_stake) + .saturating_add_unsigned((owner_adds_more_stake - owner_removed_stake).into()) + ); + }); +} diff --git a/pallets/subtensor/tests/swap_coldkey.rs b/pallets/subtensor/tests/swap_coldkey.rs index 0fe601cab..37467646f 100644 --- a/pallets/subtensor/tests/swap_coldkey.rs +++ b/pallets/subtensor/tests/swap_coldkey.rs @@ -1628,3 +1628,41 @@ fn test_coldkey_swap_no_identity_no_changes_newcoldkey_exists() { assert!(Identities::::get(new_coldkey).is_some()); }); } + +// SKIP_WASM_BUILD=1 RUST_LOG=info cargo test --test swap_coldkey -- test_coldkey_swap_stake_delta --exact --nocapture +#[test] +fn test_coldkey_swap_stake_delta() { + new_test_ext(1).execute_with(|| { + let old_coldkey = U256::from(3); + let new_coldkey = U256::from(4); + let hotkey = U256::from(5); + + let netuid = 1; + let burn_cost = 10; + let tempo = 1; + + // Give the old coldkey a stake delta on hotkey + StakeDeltaSinceLastEmissionDrain::::insert(hotkey, old_coldkey, 123); + // Give the new coldkey a stake delta on hotkey + StakeDeltaSinceLastEmissionDrain::::insert(hotkey, new_coldkey, 456); + let expected_stake_delta = 123 + 456; + // Add StakingHotkeys entry + StakingHotkeys::::insert(old_coldkey, vec![hotkey]); + + // Give balance for the swap fees + SubtensorModule::add_balance_to_coldkey_account(&old_coldkey, 100e9 as u64); + + // Perform the coldkey swap + assert_ok!(SubtensorModule::do_swap_coldkey(&old_coldkey, &new_coldkey)); + + // Ensure the stake delta is correctly transferred + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(hotkey, new_coldkey), + expected_stake_delta + ); + assert_eq!( + StakeDeltaSinceLastEmissionDrain::::get(hotkey, old_coldkey), + 0 + ); + }); +}