diff --git a/eco-tests/src/lib.rs b/eco-tests/src/lib.rs index 980de0f3da..4613d7dbe2 100644 --- a/eco-tests/src/lib.rs +++ b/eco-tests/src/lib.rs @@ -7,3 +7,6 @@ mod tests; #[cfg(test)] mod tests_taocom_indexer; + +#[cfg(test)] +mod tests_mentat_indexer; diff --git a/eco-tests/src/mock.rs b/eco-tests/src/mock.rs index 5ce8bc9b6b..2a064c8840 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -612,11 +612,19 @@ pub fn add_balance_to_coldkey_account(coldkey: &U256, tao: TaoBalance) { mod api_mocks { use codec::Compact; use pallet_subtensor::rpc_info::delegate_info::DelegateInfo; + use pallet_subtensor::rpc_info::dynamic_info::DynamicInfo; + use pallet_subtensor::rpc_info::metagraph::{Metagraph, SelectiveMetagraph}; + use pallet_subtensor::rpc_info::show_subnet::SubnetState; use pallet_subtensor::rpc_info::stake_info::StakeInfo; + use pallet_subtensor::rpc_info::subnet_info::{ + SubnetHyperparams, SubnetHyperparamsV2, SubnetHyperparamsV3, SubnetInfo, SubnetInfov2, + }; use pallet_subtensor_swap_runtime_api::{SimSwapResult, SubnetPrice, SwapRuntimeApi}; use sp_runtime::AccountId32; - use subtensor_custom_rpc_runtime_api::{DelegateInfoRuntimeApi, StakeInfoRuntimeApi}; - use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; + use subtensor_custom_rpc_runtime_api::{ + DelegateInfoRuntimeApi, StakeInfoRuntimeApi, SubnetInfoRuntimeApi, + }; + use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, TaoBalance}; use super::Block; @@ -660,6 +668,32 @@ mod api_mocks { } } + impl SubnetInfoRuntimeApi for MockApi { + fn get_subnet_info(_netuid: NetUid) -> Option> { None } + fn get_subnets_info() -> Vec>> { Vec::new() } + fn get_subnet_info_v2(_netuid: NetUid) -> Option> { None } + fn get_subnets_info_v2() -> Vec>> { Vec::new() } + #[allow(deprecated)] + fn get_subnet_hyperparams(_netuid: NetUid) -> Option { None } + #[allow(deprecated)] + fn get_subnet_hyperparams_v2(_netuid: NetUid) -> Option { None } + fn get_subnet_hyperparams_v3(_netuid: NetUid) -> Option { None } + fn get_all_dynamic_info() -> Vec>> { Vec::new() } + fn get_all_metagraphs() -> Vec>> { Vec::new() } + fn get_metagraph(_netuid: NetUid) -> Option> { None } + fn get_all_mechagraphs() -> Vec>> { Vec::new() } + fn get_mechagraph(_netuid: NetUid, _mecid: MechId) -> Option> { None } + fn get_dynamic_info(_netuid: NetUid) -> Option> { None } + fn get_subnet_state(_netuid: NetUid) -> Option> { None } + fn get_selective_metagraph(_netuid: NetUid, _metagraph_indexes: Vec) -> Option> { None } + fn get_coldkey_auto_stake_hotkey(_coldkey: AccountId32, _netuid: NetUid) -> Option { None } + fn get_selective_mechagraph(_netuid: NetUid, _subid: MechId, _metagraph_indexes: Vec) -> Option> { None } + fn get_subnet_to_prune() -> Option { None } + fn get_subnet_account_id(_netuid: NetUid) -> Option { None } + fn get_next_epoch_start_block(_netuid: NetUid) -> Option { None } + fn get_block_emission() -> TaoBalance { TaoBalance::from(0u64) } + } + impl SwapRuntimeApi for MockApi { fn current_alpha_price(_netuid: NetUid) -> u64 { 0 } fn current_alpha_price_all() -> Vec { Vec::new() } diff --git a/eco-tests/src/tests_mentat_indexer.rs b/eco-tests/src/tests_mentat_indexer.rs new file mode 100644 index 0000000000..8b0f6ae69f --- /dev/null +++ b/eco-tests/src/tests_mentat_indexer.rs @@ -0,0 +1,610 @@ +//! Indexer-contract tests for Mentat +//! Any modification in these tests will notify the member responsible +//! for the communication between protocol and the Mentat team. + + +#![allow(clippy::unwrap_used)] +#![allow(clippy::arithmetic_side_effects)] + +use frame_system as system; +use pallet_subtensor::*; +use pallet_subtensor_proxy::{Proxies, RealPaysFee}; +use pallet_subtensor_swap::FeeRate; +use pallet_subtensor_swap_runtime_api::SwapRuntimeApi; +use sp_core::U256; +use sp_runtime::traits::Block as BlockT; +use substrate_fixed::types::I96F32; +use subtensor_custom_rpc_runtime_api::{StakeInfoRuntimeApi, SubnetInfoRuntimeApi}; +use subtensor_runtime_common::{AlphaBalance, MechId, NetUid, NetUidStorageIndex, TaoBalance}; + +use super::helpers::*; +use super::mock::*; + +// --------------------------------------------------------------------------- +// Storage queries — SubtensorModule +// --------------------------------------------------------------------------- + +#[test] +fn indexer_hotkey_ownership() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(1); + + let _: U256 = Owner::::get(hotkey); + }); +} + +#[test] +fn indexer_staking_hotkeys() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + let _: Vec = StakingHotkeys::::get(coldkey); + }); +} + +#[test] +fn indexer_alpha_shares_and_stake() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + + let _: AlphaBalance = TotalHotkeyAlpha::::get(hotkey, netuid); + }); +} + +#[test] +fn indexer_subnet_pool_data() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u16 = TotalNetworks::::get(); + let _: TaoBalance = SubnetTAO::::get(netuid); + let _: AlphaBalance = SubnetAlphaIn::::get(netuid); + let _: AlphaBalance = SubnetAlphaOut::::get(netuid); + let _: I96F32 = SubnetMovingPrice::::get(netuid); + }); +} + +#[test] +fn indexer_subnet_metadata() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: MechId = MechanismCountCurrent::::get(netuid); + let _: u64 = NetworkImmunityPeriod::::get(); + let _: u64 = NetworkRegisteredAt::::get(netuid); + let _: Option = SubnetIdentitiesV3::::get(netuid); + }); +} + +#[test] +fn indexer_neuron_per_subnet_vectors() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: Vec = Active::::get(netuid); + let _: Vec = Dividends::::get(netuid); + let _: Vec = Emission::::get(netuid); + let _: Vec = Incentive::::get(NetUidStorageIndex::from(netuid)); + let _: Vec = ValidatorTrust::::get(netuid); + }); +} + +#[test] +fn indexer_neuron_uid_maps() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let uid: u16 = 0; + + let _: u16 = SubnetworkN::::get(netuid); + let _: U256 = Keys::::get(netuid, uid); + }); +} + +#[test] +fn indexer_childkey_and_parentkey_graph() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + + let _: Vec<(u64, U256)> = ChildKeys::::get(hotkey, netuid); + let _: Vec<(u64, U256)> = ParentKeys::::get(hotkey, netuid); + let _: u16 = ChildkeyTake::::get(hotkey, netuid); + }); +} + +#[test] +fn indexer_subnet_hyperparams() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u16 = Tempo::::get(netuid); + let _: u16 = MaxAllowedUids::::get(netuid); + }); +} + +#[test] +fn indexer_validator_dividends() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + + let _: AlphaBalance = AlphaDividendsPerSubnet::::get(netuid, hotkey); + }); +} + +#[test] +fn indexer_network_economics() { + new_test_ext(1).execute_with(|| { + let _: u64 = TaoWeight::::get(); + }); +} + +#[test] +fn indexer_swap_fee_rate() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: u16 = FeeRate::::get(netuid); + }); +} + +#[test] +fn indexer_mechanism_emission() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + + let _: MechId = MechanismCountCurrent::::get(netuid); + let _: Option> = MechanismEmissionSplit::::get(netuid); + }); +} + +#[test] +fn indexer_root_claim_type() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + let _: RootClaimTypeEnum = RootClaimType::::get(coldkey); + }); +} + +#[test] +fn indexer_pending_childkey_cooldown() { + new_test_ext(1).execute_with(|| { + let _: u64 = PendingChildKeyCooldown::::get(); + }); +} + +#[test] +fn indexer_root_alpha_dividends_per_subnet() { + new_test_ext(1).execute_with(|| { + let netuid = NetUid::from(1u16); + let hotkey = U256::from(1); + + let _: AlphaBalance = RootAlphaDividendsPerSubnet::::get(netuid, hotkey); + }); +} + +// --------------------------------------------------------------------------- +// Storage queries — proxy pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_proxy_proxies() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + let _ = Proxies::::get(coldkey); + }); +} + +#[test] +fn indexer_proxy_real_pays_fee() { + new_test_ext(1).execute_with(|| { + let real = U256::from(1); + let delegate = U256::from(2); + + let _: Option<()> = RealPaysFee::::get(real, delegate); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — SubtensorModule (call signatures) +// +// These run inside externalities and the Result is intentionally ignored: +// the call will typically return Err (no funds / no network), which is fine. +// We only lock that the dispatchable's argument list and types are unchanged. +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_add_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1u16); + let amount = TaoBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount, + ); + }); +} + +#[test] +fn indexer_extrinsic_remove_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1u16); + let amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::remove_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount, + ); + }); +} + +#[test] +fn indexer_extrinsic_add_stake_limit() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1u16); + let amount = TaoBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount, + TaoBalance::from(0u64), + false, + ); + }); +} + +#[test] +fn indexer_extrinsic_remove_stake_limit() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid = NetUid::from(1u16); + let alpha_amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::remove_stake_limit( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + alpha_amount, + TaoBalance::from(0u64), + false, + ); + }); +} + +#[test] +fn indexer_extrinsic_swap_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid_origin = NetUid::from(1u16); + let netuid_dest = NetUid::from(2u16); + let alpha_amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::swap_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid_origin, + netuid_dest, + alpha_amount, + ); + }); +} + +#[test] +fn indexer_extrinsic_swap_stake_limit() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let netuid_origin = NetUid::from(1u16); + let netuid_dest = NetUid::from(2u16); + let alpha_amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::swap_stake_limit( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid_origin, + netuid_dest, + alpha_amount, + TaoBalance::from(0u64), + false, + ); + }); +} + +#[test] +fn indexer_extrinsic_move_stake() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey_origin = U256::from(2); + let hotkey_dest = U256::from(3); + let netuid = NetUid::from(1u16); + let alpha_amount = AlphaBalance::from(1_000_000_000u64); + + let _ = SubtensorModule::move_stake( + RuntimeOrigin::signed(coldkey), + hotkey_origin, + hotkey_dest, + netuid, + netuid, + alpha_amount, + ); + }); +} + +#[test] +fn indexer_extrinsic_set_children() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let child = U256::from(3); + let netuid = NetUid::from(1u16); + let children: Vec<(u64, U256)> = vec![(u64::MAX, child)]; + + let _ = SubtensorModule::set_children( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + children, + ); + }); +} + +#[test] +fn indexer_extrinsic_decrease_take() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let hotkey = U256::from(2); + let take_u16: u16 = 1000; + + let _ = SubtensorModule::decrease_take( + RuntimeOrigin::signed(coldkey), + hotkey, + take_u16, + ); + }); +} + +#[test] +fn indexer_extrinsic_set_root_claim_type() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + + let _ = SubtensorModule::set_root_claim_type( + RuntimeOrigin::signed(coldkey), + RootClaimTypeEnum::Swap, + ); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — proxy pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_proxy_add_proxy() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let delegate = U256::from(2); + + let _ = pallet_subtensor_proxy::Pallet::::add_proxy( + RuntimeOrigin::signed(coldkey), + delegate, + subtensor_runtime_common::ProxyType::Any, + 0u64.into(), + ); + }); +} + +#[test] +fn indexer_extrinsic_proxy_proxy() { + new_test_ext(1).execute_with(|| { + let delegate = U256::from(1); + let real = U256::from(2); + let call = Box::new(RuntimeCall::System(system::Call::remark { remark: vec![] })); + + let _ = pallet_subtensor_proxy::Pallet::::proxy( + RuntimeOrigin::signed(delegate), + real, + Some(subtensor_runtime_common::ProxyType::Any), + call, + ); + }); +} + +#[test] +fn indexer_extrinsic_proxy_remove_proxy() { + new_test_ext(1).execute_with(|| { + let coldkey = U256::from(1); + let delegate = U256::from(2); + + let _ = pallet_subtensor_proxy::Pallet::::remove_proxy( + RuntimeOrigin::signed(coldkey), + delegate, + subtensor_runtime_common::ProxyType::Any, + 0u64.into(), + ); + }); +} + +#[test] +fn indexer_extrinsic_proxy_set_real_pays_fee() { + new_test_ext(1).execute_with(|| { + let real = U256::from(1); + let delegate = U256::from(2); + + let _ = pallet_subtensor_proxy::Pallet::::set_real_pays_fee( + RuntimeOrigin::signed(real), + delegate, + true, + ); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — balances pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_balances_transfer_keep_alive() { + new_test_ext(1).execute_with(|| { + let from = U256::from(1); + let dest = U256::from(2); + let value = TaoBalance::from(1_000_000_000u64); + + let _ = Balances::transfer_keep_alive( + RuntimeOrigin::signed(from), + dest, + value, + ); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — system pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_system_remark() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let remark = vec![0u8; 32]; + + let _ = system::Pallet::::remark( + RuntimeOrigin::signed(who), + remark, + ); + }); +} + +// --------------------------------------------------------------------------- +// Extrinsics — utility pallet +// --------------------------------------------------------------------------- + +#[test] +fn indexer_extrinsic_utility_batch() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let calls: Vec = vec![ + RuntimeCall::System(system::Call::remark { remark: vec![] }), + ]; + + let _ = pallet_subtensor_utility::Pallet::::batch( + RuntimeOrigin::signed(who), + calls, + ); + }); +} + +#[test] +fn indexer_extrinsic_utility_batch_all() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let calls: Vec = vec![ + RuntimeCall::System(system::Call::remark { remark: vec![] }), + ]; + + let _ = pallet_subtensor_utility::Pallet::::batch_all( + RuntimeOrigin::signed(who), + calls, + ); + }); +} + +#[test] +fn indexer_extrinsic_utility_force_batch() { + new_test_ext(1).execute_with(|| { + let who = U256::from(1); + let calls: Vec = vec![ + RuntimeCall::System(system::Call::remark { remark: vec![] }), + ]; + + let _ = pallet_subtensor_utility::Pallet::::force_batch( + RuntimeOrigin::signed(who), + calls, + ); + }); +} + +// --------------------------------------------------------------------------- +// Runtime API signatures +// --------------------------------------------------------------------------- + +#[test] +fn indexer_runtime_api_current_alpha_price() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + + let _: u64 = SwapRuntimeApi::current_alpha_price(&MockApi, at, netuid).unwrap(); +} + +#[test] +fn indexer_runtime_api_sim_swap() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + let tao = TaoBalance::from(1_000_000_000u64); + let alpha = AlphaBalance::from(1_000_000_000u64); + + let _: pallet_subtensor_swap_runtime_api::SimSwapResult = + SwapRuntimeApi::sim_swap_tao_for_alpha(&MockApi, at, netuid, tao).unwrap(); + let _: pallet_subtensor_swap_runtime_api::SimSwapResult = + SwapRuntimeApi::sim_swap_alpha_for_tao(&MockApi, at, netuid, alpha).unwrap(); +} + +#[test] +fn indexer_runtime_api_get_metagraph() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + + let _: Option> = + SubnetInfoRuntimeApi::get_metagraph(&MockApi, at, netuid).unwrap(); +} + +#[test] +fn indexer_runtime_api_get_mechagraph() { + let at = ::Hash::default(); + let netuid = NetUid::from(1u16); + let mecid = MechId::from(0u8); + + let _: Option> = + SubnetInfoRuntimeApi::get_mechagraph(&MockApi, at, netuid, mecid).unwrap(); +} + +#[test] +fn indexer_runtime_api_stake_info_for_coldkey() { + let at = ::Hash::default(); + let acct = sp_runtime::AccountId32::new([0u8; 32]); + + let _: Vec> = + StakeInfoRuntimeApi::get_stake_info_for_coldkey(&MockApi, at, acct).unwrap(); +} + +#[test] +fn indexer_runtime_api_stake_info_for_hotkey_coldkey_netuid() { + let at = ::Hash::default(); + let hotkey = sp_runtime::AccountId32::new([1u8; 32]); + let coldkey = sp_runtime::AccountId32::new([2u8; 32]); + let netuid = NetUid::from(1u16); + + let _: Option> = + StakeInfoRuntimeApi::get_stake_info_for_hotkey_coldkey_netuid( + &MockApi, at, hotkey, coldkey, netuid, + ) + .unwrap(); +} diff --git a/pallets/subtensor/src/coinbase/root.rs b/pallets/subtensor/src/coinbase/root.rs index b4db388621..0e6bf7269a 100644 --- a/pallets/subtensor/src/coinbase/root.rs +++ b/pallets/subtensor/src/coinbase/root.rs @@ -210,11 +210,22 @@ impl Pallet { Error::::SubnetNotExists ); + let reservoir_tao = T::SwapInterface::protocol_tao_reservoir(netuid); + let reservoir_alpha = T::SwapInterface::protocol_alpha_reservoir(netuid); + T::SwapInterface::clear_protocol_liquidity_reservoirs(netuid); + Self::increase_provided_tao_reserve(netuid, reservoir_tao); + Self::increase_provided_alpha_reserve(netuid, reservoir_alpha); + if !reservoir_tao.is_zero() { + TotalStake::::mutate(|total| { + *total = total.saturating_add(reservoir_tao); + }); + } + Self::finalize_all_subnet_root_dividends(netuid); // --- Perform the cleanup before removing the network. Self::destroy_alpha_in_out_stakes(netuid)?; - T::SwapInterface::clear_protocol_liquidity(netuid)?; + T::SwapInterface::clear_protocol_liquidity(netuid); T::CommitmentsInterface::purge_netuid(netuid); // --- Remove the network diff --git a/pallets/subtensor/src/coinbase/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index d7c75964b2..66d488317c 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -85,11 +85,6 @@ impl Pallet { let tao_to_swap_with: TaoBalance = tou64!(excess_tao.get(netuid_i).unwrap_or(&asfloat!(0))).into(); - // Inject tao and alpha into protocol liquidity. In theorry, it may not always - // be a success (returned values are 0s) in case of high liquidity disbalance - let (actual_injected_tao, actual_injected_alpha) = - T::SwapInterface::adjust_protocol_liquidity(*netuid_i, tao_in_i, alpha_in_i); - // Clear per-block pool-side emission counters up front so a subnet // disabled this block does not display stale values from an earlier block. SubnetExcessTao::::insert(*netuid_i, TaoBalance::ZERO); @@ -107,17 +102,36 @@ impl Pallet { T::SwapInterface::max_price(), true, ); - if let Ok(buy_swap_result_ok) = buy_swap_result { - let bought_alpha: AlphaBalance = - buy_swap_result_ok.amount_paid_out.into(); - SubnetProtocolAlpha::::mutate(*netuid_i, |total| { - *total = total.saturating_add(bought_alpha); - }); - - // Record actual excess TAO that entered pool. - let actual_excess: TaoBalance = buy_swap_result_ok.amount_paid_in; - SubnetExcessTao::::insert(*netuid_i, actual_excess); - Self::record_protocol_inflow(*netuid_i, actual_excess); + match buy_swap_result { + Ok(buy_swap_result_ok) => { + let bought_alpha: AlphaBalance = + buy_swap_result_ok.amount_paid_out.into(); + SubnetProtocolAlpha::::mutate(*netuid_i, |total| { + *total = total.saturating_add(bought_alpha); + }); + + // Record actual excess TAO that entered pool. + let actual_excess: TaoBalance = + buy_swap_result_ok.amount_paid_in; + SubnetExcessTao::::insert(*netuid_i, actual_excess); + Self::record_protocol_inflow(*netuid_i, actual_excess); + } + Err(error) => { + match Self::withdraw_tao_as_credit( + &subnet_account_id, + tao_to_swap_with, + ) { + Ok(refund_credit) => { + remaining_credit = + remaining_credit.merge(refund_credit); + } + Err(withdraw_error) => { + log::error!( + "Failed to revert excess TAO deposit after swap failure: netuid_i = {netuid_i:?}, tao_to_swap_with = {tao_to_swap_with:?}, swap_error = {error:?}, withdraw_error = {withdraw_error:?}" + ); + } + } + } } } Err(remainder) => { @@ -130,38 +144,56 @@ impl Pallet { } } - // Inject Alpha in. - SubnetAlphaInEmission::::insert(*netuid_i, actual_injected_alpha); - - // Mint alpha and resolve to alpha reserve - Self::resolve_to_alpha_in(Self::mint_alpha(*netuid_i, actual_injected_alpha)); - - // Inject TAO in. - if !actual_injected_tao.is_zero() { - match Self::spend_tao(&subnet_account_id, remaining_credit, actual_injected_tao) - { + // Materialize this block's TAO before updating balancer reservoir + // state. If spending fails, do not let the swap pallet consume + // reservoir state as if this block's TAO arrived. + let materialized_tao_delta = if tao_in_i.is_zero() { + TaoBalance::ZERO + } else { + match Self::spend_tao(&subnet_account_id, remaining_credit, tao_in_i) { Ok(remainder) => { remaining_credit = remainder; - - SubnetTaoInEmission::::insert(*netuid_i, actual_injected_tao); - SubnetTAO::::mutate(*netuid_i, |total| { - *total = total.saturating_add(actual_injected_tao); - }); - TotalStake::::mutate(|total| { - *total = total.saturating_add(actual_injected_tao); - }); - - // Record emission injection as protocol inflow. - Self::record_protocol_inflow(*netuid_i, actual_injected_tao); + tao_in_i } Err(remainder) => { remaining_credit = remainder; let remaining_balance = remaining_credit.peek(); log::error!( - "Failed to spend credit: injected_tao = {actual_injected_tao:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" + "Failed to spend credit: tao_delta = {tao_in_i:?}, netuid_i = {netuid_i:?}, remaining_balance = {remaining_balance:?}" ); + TaoBalance::ZERO } } + }; + + // Decide which current/reservoir liquidity can become price-active + // without pushing balancer weights out of range. Only already + // materialized current TAO is offered to the swap pallet. + let (price_active_tao, price_active_alpha) = + T::SwapInterface::adjust_protocol_liquidity( + *netuid_i, + materialized_tao_delta, + alpha_in_i, + ); + + // Materialize this block's alpha emission, then add only the + // price-active portion to the pool reserve. The price-active + // portion may include alpha that was materialized in an earlier + // block and held in the reservoir. + let _ = Self::mint_alpha(*netuid_i, alpha_in_i); + SubnetAlphaInEmission::::insert(*netuid_i, price_active_alpha); + Self::increase_provided_alpha_reserve(*netuid_i, price_active_alpha); + + // Add only the price-active TAO to the pool reserve. This may + // include TAO materialized in an earlier block and held in the + // reservoir. + if !price_active_tao.is_zero() { + SubnetTaoInEmission::::insert(*netuid_i, price_active_tao); + Self::increase_provided_tao_reserve(*netuid_i, price_active_tao); + TotalStake::::mutate(|total| { + *total = total.saturating_add(price_active_tao); + }); + Self::record_protocol_inflow(*netuid_i, price_active_tao); } } } @@ -411,9 +443,8 @@ impl Pallet { ); epochs_run_this_block = epochs_run_this_block.saturating_add(1); - // Reserved for potential future enhancements. - // Ownership update logic based on conviction is currently inactive by design. - // Self::change_subnet_owner_if_needed(netuid); + // Change subnet owner based on conviction. + Self::change_subnet_owner_if_needed(netuid); } else { // Schedule advances below; execution skipped. Pending emissions accumulate // and will be drained by the next successful epoch. diff --git a/pallets/subtensor/src/coinbase/subnet_emissions.rs b/pallets/subtensor/src/coinbase/subnet_emissions.rs index f378e3e930..b82bb2b0fb 100644 --- a/pallets/subtensor/src/coinbase/subnet_emissions.rs +++ b/pallets/subtensor/src/coinbase/subnet_emissions.rs @@ -36,7 +36,7 @@ impl Pallet { block_emission: U96F32, ) -> BTreeMap { // Disabled subnets get zero TAO-side emission, redistributed to enabled subnets. - // They stay in the map so the normal alpha_out/root-prop path still runs. + // They stay in the map so the normal alpha_out path still runs. let shares = Self::get_shares(subnets_to_emit_to); log::debug!("Subnet emission shares = {shares:?}"); @@ -354,11 +354,9 @@ impl Pallet { pub(crate) fn get_shares(subnets_to_emit_to: &[NetUid]) -> BTreeMap { let price_shares = Self::get_shares_price_ema(subnets_to_emit_to); - // Weight each subnet's price share by root_proportion * (1 - miner_burned), then - // renormalize. The effective emission is therefore proportional to - // root_proportion_i * price_i * (1 - miner_burned_i). - // - root_proportion shrinks as a subnet's alpha issuance grows, so emission is - // reallocated away from older subnets toward newer ones (easier entrance). + // Weight each subnet's price share by (1 - miner_burned), then + // renormalize. The effective emission is proportional to + // price_i * (1 - miner_burned_i). // - (1 - miner_burned) reallocates away from subnets that withhold miner emission. let zero = U64F64::saturating_from_num(0); let one = U64F64::saturating_from_num(1); @@ -366,8 +364,8 @@ impl Pallet { .iter() .map(|(netuid, share)| { let burned = U64F64::saturating_from_num(MinerBurned::::get(netuid)).min(one); - let root_prop = U64F64::saturating_from_num(Self::root_proportion(*netuid)); - let factor = root_prop.saturating_mul(one.saturating_sub(burned)); + let factor = one.saturating_sub(burned); + (*netuid, share.saturating_mul(factor)) }) .collect(); diff --git a/pallets/subtensor/src/coinbase/tao.rs b/pallets/subtensor/src/coinbase/tao.rs index 0dee496c3b..d6d675c771 100644 --- a/pallets/subtensor/src/coinbase/tao.rs +++ b/pallets/subtensor/src/coinbase/tao.rs @@ -33,18 +33,14 @@ impl Pallet { SubnetTAO::::get(netuid) } - /// Internal function that transfers and updates subtensor pallet total issuance - /// in case of dust collection. + /// Internal function that transfers TAO and allows the origin account to be reaped. + /// + /// Dust collection is handled by the runtime's Balances `DustRemoval` implementation. fn transfer_allow_death_update_ti( origin_coldkey: &T::AccountId, destination_coldkey: &T::AccountId, amount: BalanceOf, ) -> DispatchResult { - // If account balance remainder drops below ED, then account is killed, balance - // is lost, and we need to reduce total issuance in subtensor pallet. Measure - // balance TI before and after to detect the dust. - let balances_ti_before = ::Currency::total_issuance(); - ::Currency::transfer( origin_coldkey, destination_coldkey, @@ -52,14 +48,6 @@ impl Pallet { Preservation::Expendable, )?; - let balances_ti_after = ::Currency::total_issuance(); - if balances_ti_after < balances_ti_before { - let burned = balances_ti_before.saturating_sub(balances_ti_after); - TotalIssuance::::mutate(|total| { - *total = total.saturating_sub(burned); - }); - } - Ok(()) } @@ -286,6 +274,25 @@ impl Pallet { } } + /// Withdraw TAO from an account into a fresh credit. + /// + /// This is useful when a previous `spend_tao` resolve must be undone without + /// changing total issuance. + pub fn withdraw_tao_as_credit( + coldkey: &T::AccountId, + amount: BalanceOf, + ) -> Result, DispatchError> { + let credit = ::Currency::withdraw( + coldkey, + amount, + Precision::Exact, + Preservation::Expendable, + Fortitude::Polite, + )?; + + Ok(credit) + } + /// Finalizes the unused part of the minted TAO. pub fn recycle_credit(credit: CreditOf) { let amount = credit.peek(); diff --git a/pallets/subtensor/src/lib.rs b/pallets/subtensor/src/lib.rs index 8896ceaf0f..cc41003e70 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -67,6 +67,21 @@ pub const MAX_SUBNET_CLAIMS: usize = 5; pub const MAX_ROOT_CLAIM_THRESHOLD: u64 = 10_000_000; +pub struct SubtensorDustRemoval(PhantomData); +impl frame_support::traits::OnUnbalanced> + for SubtensorDustRemoval +where + T: Config + pallet_balances::Config, + ::Balance: Into + Copy, +{ + fn on_nonzero_unbalanced(dust: pallet_balances::CreditOf) { + let amount: TaoBalance = frame_support::traits::Imbalance::peek(&dust).into(); + TotalIssuance::::mutate(|total| { + *total = total.saturating_sub(amount); + }); + } +} + /// Account flag bit that opts into receiving locked alpha transfers. pub const ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA: u128 = 1u128 << 0; diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index a4596c9add..763bbe2867 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -611,7 +611,7 @@ mod dispatches { /// - The delegate is setting a take which is not lower than the previous. /// #[pallet::call_index(65)] - #[pallet::weight((::WeightInfo::decrease_take(), DispatchClass::Normal, Pays::No))] + #[pallet::weight((::WeightInfo::decrease_take(), DispatchClass::Normal, Pays::Yes))] pub fn decrease_take( origin: OriginFor, hotkey: T::AccountId, @@ -651,7 +651,7 @@ mod dispatches { /// - The delegate is setting a take which is not greater than the previous. /// #[pallet::call_index(66)] - #[pallet::weight((::WeightInfo::increase_take(), DispatchClass::Normal, Pays::No))] + #[pallet::weight((::WeightInfo::increase_take(), DispatchClass::Normal, Pays::Yes))] pub fn increase_take( origin: OriginFor, hotkey: T::AccountId, diff --git a/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs b/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs index 7ff1dea7c8..93af663092 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_subnet_hotkey_lock_swaps.rs @@ -273,30 +273,51 @@ pub fn migrate_fix_subnet_hotkey_lock_swaps() -> Weight { }; let netuid = NetUid::from(fix.netuid); + let index_reads: u64; + let index_writes: u64; + let lock_take_reads: u64; + let lock_take_writes: u64; let locks_to_fix: Vec<(T::AccountId, LockState)> = if let Some(coldkey) = fix.coldkey { let Some(coldkey) = decode_account_id32::(coldkey) else { log::error!("Failed to decode coldkey: {}", coldkey); continue; }; - Lock::::take((coldkey.clone(), netuid, old_hotkey.clone())) - .map(|lock| vec![(coldkey, lock)]) - .unwrap_or_default() + index_reads = 0; + index_writes = 1; + lock_take_reads = 1; + LockingColdkeys::::remove((netuid, old_hotkey.clone(), coldkey.clone())); + if let Some(lock) = Lock::::take((coldkey.clone(), netuid, old_hotkey.clone())) { + lock_take_writes = 1; + vec![(coldkey, lock)] + } else { + lock_take_writes = 0; + Vec::new() + } } else { - let locks: Vec<(T::AccountId, LockState)> = Lock::::iter() - .filter_map(|((coldkey, lock_netuid, hotkey), lock)| { - (lock_netuid == netuid && hotkey == old_hotkey).then_some((coldkey, lock)) + let coldkeys: Vec = + LockingColdkeys::::iter_prefix((netuid, old_hotkey.clone())) + .map(|(coldkey, ())| coldkey) + .collect(); + let indexed_coldkeys = coldkeys.len() as u64; + index_reads = indexed_coldkeys; + index_writes = indexed_coldkeys; + lock_take_reads = indexed_coldkeys; + + let locks: Vec<(T::AccountId, LockState)> = coldkeys + .into_iter() + .filter_map(|coldkey| { + LockingColdkeys::::remove((netuid, old_hotkey.clone(), coldkey.clone())); + Lock::::take((coldkey.clone(), netuid, old_hotkey.clone())) + .map(|lock| (coldkey, lock)) }) .collect(); - for (coldkey, _) in &locks { - Lock::::remove((coldkey.clone(), netuid, old_hotkey.clone())); - } + lock_take_writes = locks.len() as u64; locks }; - let locks_to_fix_count = locks_to_fix.len() as u64; - weight = weight.saturating_add( - T::DbWeight::get() - .reads_writes(locks_to_fix_count.saturating_add(1), locks_to_fix_count), - ); + weight = weight.saturating_add(T::DbWeight::get().reads_writes( + index_reads.saturating_add(lock_take_reads), + index_writes.saturating_add(lock_take_writes), + )); if locks_to_fix.is_empty() { missing_locks = missing_locks.saturating_add(1); @@ -323,7 +344,8 @@ pub fn migrate_fix_subnet_hotkey_lock_swaps() -> Weight { } Lock::::insert((coldkey.clone(), netuid, new_hotkey.clone()), lock.clone()); - weight = weight.saturating_add(T::DbWeight::get().writes(1)); + LockingColdkeys::::insert((netuid, new_hotkey.clone(), coldkey.clone()), ()); + weight = weight.saturating_add(T::DbWeight::get().writes(2)); if !new_hotkey_is_owner { add_to_aggregate::(&coldkey, netuid, &new_hotkey, &lock); diff --git a/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs b/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs index b4851745cb..476a9d8b4f 100644 --- a/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs +++ b/pallets/subtensor/src/migrations/migrate_fix_total_issuance_evm_fees.rs @@ -3,24 +3,31 @@ use frame_support::traits::fungible::Inspect; use frame_support::weights::Weight; pub fn migrate_fix_total_issuance_evm_fees() -> Weight { - let migration_name = b"migrate_fix_total_issuance_evm_fees".to_vec(); - let mut weight = T::DbWeight::get().reads(1); - - if HasMigrationRun::::get(&migration_name) { - log::info!( - "Migration '{:?}' has already run. Skipping.", - String::from_utf8_lossy(&migration_name) - ); + let migration_names: [&[u8]; 2] = [ + // Fix testnet TotalIssuance after the earlier EVM fees issue caused the + // Subtensor pallet's accounting to diverge from the balances pallet. + b"migrate_fix_total_issuance_evm_fees", + // Fix Subtensor TotalIssuance after dust collection caused accounting drift. + b"migrate_fix_total_issuance_after_dust_collection", + ]; + let mut weight = T::DbWeight::get().reads(migration_names.len() as u64); + + let Some(migration_name) = migration_names + .iter() + .map(|name| name.to_vec()) + .find(|name| !HasMigrationRun::::get(name)) + else { + log::info!("All total issuance fix migrations have already run. Skipping."); return weight; - } + }; log::info!( "Running migration '{}'", String::from_utf8_lossy(&migration_name) ); - // Fix testnet TotalIssuance after the earlier EVM fees issue caused the - // Subtensor pallet's accounting to diverge from the balances pallet. + // All migration instances reset Subtensor TotalIssuance to the authoritative + // Balances pallet total issuance. let balances_total_issuance = ::Currency::total_issuance(); let subtensor_total_issuance_before = TotalIssuance::::get(); TotalIssuance::::put(balances_total_issuance); diff --git a/pallets/subtensor/src/staking/order_swap.rs b/pallets/subtensor/src/staking/order_swap.rs index fd0ae3461c..9cf428d74c 100644 --- a/pallets/subtensor/src/staking/order_swap.rs +++ b/pallets/subtensor/src/staking/order_swap.rs @@ -1,6 +1,4 @@ use super::*; -use frame_support::traits::fungible::Mutate; -use frame_support::traits::tokens::Preservation; use frame_support::transactional; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance}; @@ -103,8 +101,7 @@ impl OrderSwapInterface for Pallet { } fn transfer_tao(from: &T::AccountId, to: &T::AccountId, amount: TaoBalance) -> DispatchResult { - ::Currency::transfer(from, to, amount, Preservation::Expendable)?; - Ok(()) + Pallet::::transfer_tao(from, to, amount) } #[transactional] diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index 2826590c50..8327a9a72b 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -18,7 +18,9 @@ impl Pallet { /// # Returns /// * `u64` - The total alpha issuance for the specified subnet. pub fn get_alpha_issuance(netuid: NetUid) -> AlphaBalance { - SubnetAlphaIn::::get(netuid).saturating_add(SubnetAlphaOut::::get(netuid)) + SubnetAlphaIn::::get(netuid) + .saturating_add(SubnetAlphaOut::::get(netuid)) + .saturating_add(T::SwapInterface::protocol_alpha_reservoir(netuid)) } pub fn get_moving_alpha_price(netuid: NetUid) -> U64F64 { diff --git a/pallets/subtensor/src/subnets/leasing.rs b/pallets/subtensor/src/subnets/leasing.rs index d3e1b84df5..3ba615b4c5 100644 --- a/pallets/subtensor/src/subnets/leasing.rs +++ b/pallets/subtensor/src/subnets/leasing.rs @@ -18,7 +18,7 @@ use super::*; use frame_support::{ dispatch::RawOrigin, - traits::{Defensive, fungible::*, tokens::Preservation}, + traits::{Defensive, fungible::*}, }; use frame_system::pallet_prelude::OriginFor; use frame_system::pallet_prelude::*; @@ -92,12 +92,7 @@ impl Pallet { frame_system::Pallet::::inc_providers(&lease_coldkey); frame_system::Pallet::::inc_providers(&lease_hotkey); - ::Currency::transfer( - &crowdloan.funds_account, - &lease_coldkey, - crowdloan.raised, - Preservation::Expendable, - )?; + Self::transfer_tao(&crowdloan.funds_account, &lease_coldkey, crowdloan.raised)?; Self::do_register_network( RawOrigin::Signed(lease_coldkey.clone()).into(), @@ -151,23 +146,13 @@ impl Pallet { .saturating_mul(U64F64::from(u64::from(leftover_cap))) .floor() .saturating_to_num::(); - ::Currency::transfer( - &lease_coldkey, - &contributor, - contributor_refund.into(), - Preservation::Expendable, - )?; + Self::transfer_tao(&lease_coldkey, &contributor, contributor_refund.into())?; refunded_cap = refunded_cap.saturating_add(contributor_refund); } // Refund what's left after refunding the contributors to the beneficiary let beneficiary_refund = leftover_cap.saturating_sub(refunded_cap.into()); - ::Currency::transfer( - &lease_coldkey, - &who, - beneficiary_refund, - Preservation::Expendable, - )?; + Self::transfer_tao(&lease_coldkey, &who, beneficiary_refund)?; Self::deposit_event(Event::SubnetLeaseCreated { beneficiary: who, diff --git a/pallets/subtensor/src/swap/swap_hotkey.rs b/pallets/subtensor/src/swap/swap_hotkey.rs index 43b901927b..af1df032a4 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -45,17 +45,22 @@ impl Pallet { Error::::NonAssociatedColdKey ); - // 3. If the new hotkey already exists globally, ensure the coldkey owns it + // 3. Initialize the weight for this operation. The coldkey/old_hotkey + // ownership check above reads Owner twice. + let mut weight = T::DbWeight::get().reads(2); + + // 4. If the new hotkey already exists globally, ensure the coldkey owns it + weight.saturating_accrue(T::DbWeight::get().reads(1)); if Self::hotkey_account_exists(new_hotkey) { + weight.saturating_accrue(T::DbWeight::get().reads(2)); ensure!( Self::coldkey_owns_hotkey(&coldkey, new_hotkey), Error::::NonAssociatedColdKey ); + } else { + weight.saturating_accrue(T::DbWeight::get().reads(1)); } - // 4. Initialize the weight for this operation - let mut weight = T::DbWeight::get().reads(2); - // 5. Ensure the new hotkey is different from the old one ensure!(old_hotkey != new_hotkey, Error::::NewHotKeyIsSameWithOld); diff --git a/pallets/subtensor/src/tests/coinbase.rs b/pallets/subtensor/src/tests/coinbase.rs index 0e3e8038ff..d6fa24b444 100644 --- a/pallets/subtensor/src/tests/coinbase.rs +++ b/pallets/subtensor/src/tests/coinbase.rs @@ -1,5 +1,6 @@ #![allow( unused, + clippy::arithmetic_side_effects, clippy::indexing_slicing, clippy::panic, clippy::unwrap_used, @@ -13,12 +14,9 @@ use alloc::collections::BTreeMap; use approx::assert_abs_diff_eq; use frame_support::assert_ok; use sp_core::U256; -use substrate_fixed::{ - transcendental::sqrt, - types::{I64F64, I96F32, U64F64, U96F32}, -}; +use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; use subtensor_runtime_common::{AlphaBalance, NetUidStorageIndex}; -use subtensor_swap_interface::{SwapEngine, SwapHandler}; +use subtensor_swap_interface::SwapHandler; #[allow(clippy::arithmetic_side_effects)] fn close(value: u64, target: u64, eps: u64) { @@ -3701,6 +3699,119 @@ fn test_coinbase_inject_and_maybe_swap_does_not_skew_reserves() { }); } +#[test] +fn test_coinbase_failed_tao_materialization_does_not_activate_current_tao() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let initial_reserve = TaoBalance::from(1_000_000_u64); + let reservoir_tao = TaoBalance::from(100_u64); + let current_tao = TaoBalance::from(200_u64); + let current_alpha = AlphaBalance::from(100_u64); + + mock::setup_reserves(netuid, initial_reserve, AlphaBalance::from(1_000_000_u64)); + Swap::maybe_initialize_palswap(netuid, None); + pallet_subtensor_swap::BalancerTaoReservoir::::insert(netuid, reservoir_tao); + + let tao_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_tao))]); + let alpha_in = BTreeMap::from([(netuid, U96F32::saturating_from_num(current_alpha))]); + let excess_tao = BTreeMap::new(); + let credit = SubtensorModule::mint_tao(TaoBalance::ZERO); + + SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); + + assert_eq!( + SubnetTAO::::get(netuid), + initial_reserve.saturating_add(reservoir_tao) + ); + assert_eq!(SubnetTaoInEmission::::get(netuid), reservoir_tao); + assert_eq!( + SubnetProtocolFlow::::get(netuid), + reservoir_tao.to_u64() as i64 + ); + assert_eq!( + pallet_subtensor_swap::BalancerTaoReservoir::::get(netuid), + TaoBalance::ZERO + ); + }); +} + +#[test] +fn test_alpha_reservoir_counts_toward_subnet_issuance_across_blocks() { + new_test_ext(1).execute_with(|| { + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let alpha_in = AlphaBalance::from(10_000_u64); + let alpha_out = AlphaBalance::from(20_000_u64); + let reservoir_alpha = AlphaBalance::from(30_000_u64); + + SubnetAlphaIn::::insert(netuid, alpha_in); + SubnetAlphaOut::::insert(netuid, alpha_out); + pallet_subtensor_swap::BalancerAlphaReservoir::::insert(netuid, reservoir_alpha); + + let expected = alpha_in + .saturating_add(alpha_out) + .saturating_add(reservoir_alpha); + assert_eq!(SubtensorModule::get_alpha_issuance(netuid), expected); + + System::set_block_number(System::block_number().saturating_add(1)); + + assert_eq!(SubnetAlphaIn::::get(netuid), alpha_in); + assert_eq!( + pallet_subtensor_swap::BalancerAlphaReservoir::::get(netuid), + reservoir_alpha + ); + assert_eq!(SubtensorModule::get_alpha_issuance(netuid), expected); + }); +} + +fn test_coinbase_inject_and_maybe_swap_reverts_excess_tao_deposit_on_swap_failure() { + new_test_ext(1).execute_with(|| { + let zero = U96F32::saturating_from_num(0); + let netuid = add_dynamic_network(&U256::from(1), &U256::from(2)); + let tao_to_swap = TaoBalance::from(789_100_u64); + + mock::setup_reserves( + netuid, + TaoBalance::from(1_000_000_000_000_u64), + AlphaBalance::from(1_000_000_000_000_u64), + ); + Swap::maybe_initialize_palswap(netuid, None); + + // Force the buy swap to fail after the excess TAO credit is deposited. + SubnetAlphaIn::::set( + netuid, + AlphaBalance::from(u64::from(mock::SwapMinimumReserve::get()) - 1), + ); + assert!( + SubtensorModule::swap_tao_for_alpha( + netuid, + tao_to_swap, + ::SwapInterface::max_price(), + true, + ) + .is_err() + ); + + let subnet_account = SubtensorModule::get_subnet_account_id(netuid).unwrap(); + let chain_before = Balances::free_balance(subnet_account); + let subnet_tao_before = SubnetTAO::::get(netuid); + let total_issuance_before = TotalIssuance::::get(); + let balances_issuance_before = Balances::total_issuance(); + + let tao_in = BTreeMap::from([(netuid, zero)]); + let alpha_in = BTreeMap::from([(netuid, zero)]); + let excess_tao = BTreeMap::from([(netuid, U96F32::saturating_from_num(tao_to_swap))]); + let credit = SubtensorModule::mint_tao(tao_to_swap); + + SubtensorModule::inject_and_maybe_swap(&[netuid], &tao_in, &alpha_in, &excess_tao, credit); + + assert_eq!(Balances::free_balance(subnet_account), chain_before); + assert_eq!(SubnetTAO::::get(netuid), subnet_tao_before); + assert_eq!(SubnetExcessTao::::get(netuid), TaoBalance::ZERO); + assert_eq!(TotalIssuance::::get(), total_issuance_before); + assert_eq!(Balances::total_issuance(), balances_issuance_before); + }); +} + #[test] fn test_coinbase_drain_pending_increments_blockssincelaststep() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/locks.rs b/pallets/subtensor/src/tests/locks.rs index 91b48b7881..17e45595cd 100644 --- a/pallets/subtensor/src/tests/locks.rs +++ b/pallets/subtensor/src/tests/locks.rs @@ -2951,6 +2951,60 @@ fn test_change_subnet_owner_if_needed_reassigns_to_subnet_king() { }); } +#[test] +fn test_run_coinbase_reassigns_subnet_owner_by_conviction_on_epoch() { + new_test_ext(1).execute_with(|| { + let old_owner_coldkey = U256::from(1); + let old_owner_hotkey = U256::from(2); + let netuid = setup_subnet_with_stake(old_owner_coldkey, old_owner_hotkey, 100_000_000_000); + SubnetOwner::::insert(netuid, old_owner_coldkey); + SubnetOwnerHotkey::::insert(netuid, old_owner_hotkey); + + let new_owner_coldkey = U256::from(5); + let king_hotkey = U256::from(6); + assert_ok!(SubtensorModule::create_account_if_non_existent( + &new_owner_coldkey, + &king_hotkey + )); + + let now = crate::staking::lock::ONE_YEAR + 1; + System::set_block_number(now); + NetworkRegisteredAt::::insert(netuid, 1); + SubnetAlphaOut::::insert(netuid, AlphaBalance::from(10_000u64)); + SubtensorModule::set_tempo_unchecked(netuid, 1); + LastEpochBlock::::insert(netuid, now.saturating_sub(1)); + PendingEpochAt::::insert(netuid, 0); + + let locked_mass = AlphaBalance::from(1_000u64); + Lock::::insert( + (new_owner_coldkey, netuid, king_hotkey), + LockState { + locked_mass, + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + HotkeyLock::::insert( + netuid, + king_hotkey, + LockState { + locked_mass, + conviction: U64F64::from_num(1_000), + last_update: now, + }, + ); + + assert_eq!(SubnetOwner::::get(netuid), old_owner_coldkey); + assert_eq!(SubnetOwnerHotkey::::get(netuid), old_owner_hotkey); + + SubtensorModule::run_coinbase(SubtensorModule::mint_tao(0.into())); + + assert_eq!(SubnetOwner::::get(netuid), new_owner_coldkey); + assert_eq!(SubnetOwnerHotkey::::get(netuid), king_hotkey); + assert_eq!(LastEpochBlock::::get(netuid), now); + }); +} + #[test] fn test_change_subnet_owner_rebuilds_old_owner_hotkey_by_lock_mode() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/tests/migration.rs b/pallets/subtensor/src/tests/migration.rs index ec24697522..7b2a520014 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -123,14 +123,17 @@ fn test_migrate_fix_subnet_hotkey_lock_swaps_moves_or_discards_conflicts() { (coldkey_to_move, netuid, old_hotkey), moved_lock.clone(), ); + LockingColdkeys::::insert((netuid, old_hotkey, coldkey_to_move), ()); Lock::::insert( (coldkey_with_conflict, netuid, old_hotkey), discarded_lock.clone(), ); + LockingColdkeys::::insert((netuid, old_hotkey, coldkey_with_conflict), ()); Lock::::insert( (coldkey_with_conflict, netuid, new_hotkey), existing_destination_lock.clone(), ); + LockingColdkeys::::insert((netuid, new_hotkey, coldkey_with_conflict), ()); DecayingLock::::insert(coldkey_to_move, netuid, false); DecayingLock::::insert(coldkey_with_conflict, netuid, false); DecayingLock::::insert(chained_coldkey, chained_netuid, false); @@ -148,6 +151,10 @@ fn test_migrate_fix_subnet_hotkey_lock_swaps_moves_or_discards_conflicts() { (chained_coldkey, chained_netuid, chained_first_hotkey), chained_lock.clone(), ); + LockingColdkeys::::insert( + (chained_netuid, chained_first_hotkey, chained_coldkey), + (), + ); HotkeyLock::::insert(chained_netuid, chained_first_hotkey, chained_lock.clone()); let weight = @@ -157,14 +164,34 @@ fn test_migrate_fix_subnet_hotkey_lock_swaps_moves_or_discards_conflicts() { assert!(HasMigrationRun::::get(&migration_name)); assert!(Lock::::get((coldkey_to_move, netuid, old_hotkey)).is_none()); assert!(Lock::::get((coldkey_with_conflict, netuid, old_hotkey)).is_none()); + assert!(!LockingColdkeys::::contains_key(( + netuid, + old_hotkey, + coldkey_to_move + ))); + assert!(!LockingColdkeys::::contains_key(( + netuid, + old_hotkey, + coldkey_with_conflict + ))); assert_eq!( Lock::::get((coldkey_to_move, netuid, new_hotkey)), Some(moved_lock.clone()) ); + assert!(LockingColdkeys::::contains_key(( + netuid, + new_hotkey, + coldkey_to_move + ))); assert_eq!( Lock::::get((coldkey_with_conflict, netuid, new_hotkey)), Some(existing_destination_lock.clone()) ); + assert!(LockingColdkeys::::contains_key(( + netuid, + new_hotkey, + coldkey_with_conflict + ))); assert!(HotkeyLock::::get(netuid, old_hotkey).is_none()); let new_aggregate = HotkeyLock::::get(netuid, new_hotkey) @@ -193,10 +220,25 @@ fn test_migrate_fix_subnet_hotkey_lock_swaps_moves_or_discards_conflicts() { chained_middle_hotkey )) .is_none()); + assert!(!LockingColdkeys::::contains_key(( + chained_netuid, + chained_first_hotkey, + chained_coldkey + ))); + assert!(!LockingColdkeys::::contains_key(( + chained_netuid, + chained_middle_hotkey, + chained_coldkey + ))); assert_eq!( Lock::::get((chained_coldkey, chained_netuid, chained_final_hotkey)), Some(chained_lock.clone()) ); + assert!(LockingColdkeys::::contains_key(( + chained_netuid, + chained_final_hotkey, + chained_coldkey + ))); assert!(HotkeyLock::::get(chained_netuid, chained_first_hotkey).is_none()); assert!(HotkeyLock::::get(chained_netuid, chained_middle_hotkey).is_none()); assert_eq!( @@ -4691,6 +4733,7 @@ fn test_migrate_subnet_balances() { fn test_migrate_fix_total_issuance_evm_fees() { new_test_ext(1).execute_with(|| { const MIGRATION_NAME: &[u8] = b"migrate_fix_total_issuance_evm_fees"; + const DUST_MIGRATION_NAME: &[u8] = b"migrate_fix_total_issuance_after_dust_collection"; let account = U256::from(42); let balances_total_issuance = TaoBalance::from(123_456_789_u64); @@ -4711,16 +4754,29 @@ fn test_migrate_fix_total_issuance_evm_fees() { assert!(!weight.is_zero(), "weight must be non-zero"); assert_eq!(TotalIssuance::::get(), balances_total_issuance); assert!(HasMigrationRun::::get(MIGRATION_NAME.to_vec())); + assert!(!HasMigrationRun::::get( + DUST_MIGRATION_NAME.to_vec() + )); let second_wrong_value = TaoBalance::from(555_u64); TotalIssuance::::put(second_wrong_value); crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); + assert_eq!(TotalIssuance::::get(), balances_total_issuance); + assert!(HasMigrationRun::::get( + DUST_MIGRATION_NAME.to_vec() + )); + + let third_wrong_value = TaoBalance::from(777_u64); + TotalIssuance::::put(third_wrong_value); + + crate::migrations::migrate_fix_total_issuance_evm_fees::migrate_fix_total_issuance_evm_fees::(); + assert_eq!( TotalIssuance::::get(), - second_wrong_value, - "migration must not run more than once" + third_wrong_value, + "migration must not run after all known migration keys have run" ); }); } diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 0e3347ec89..879b4de474 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -88,7 +88,7 @@ pub type BlockNumber = u64; impl pallet_balances::Config for Test { type Balance = Balance; type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); + type DustRemoval = crate::SubtensorDustRemoval; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type MaxLocks = (); diff --git a/pallets/subtensor/src/tests/mock_high_ed.rs b/pallets/subtensor/src/tests/mock_high_ed.rs index 6f5c69a89c..2cca07d25b 100644 --- a/pallets/subtensor/src/tests/mock_high_ed.rs +++ b/pallets/subtensor/src/tests/mock_high_ed.rs @@ -70,7 +70,7 @@ pub type BlockNumber = u64; impl pallet_balances::Config for Test { type Balance = Balance; type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); + type DustRemoval = crate::SubtensorDustRemoval; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type MaxLocks = (); diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index a967761ef5..20531e370d 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -522,6 +522,8 @@ fn dissolve_clears_all_per_subnet_storages() { assert!(!SubnetAlphaOutEmission::::contains_key(net)); assert!(!SubnetTaoInEmission::::contains_key(net)); assert!(!SubnetVolume::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); // TAO Flow assert!(!SubnetTaoFlow::::contains_key(net)); @@ -643,6 +645,58 @@ fn dissolve_clears_all_per_subnet_storages() { }); } +#[test] +fn dissolve_materializes_nonzero_protocol_reservoirs_before_cleanup() { + new_test_ext(0).execute_with(|| { + let owner_cold = U256::from(123); + let owner_hot = U256::from(456); + let net = add_dynamic_network(&owner_hot, &owner_cold); + remove_owner_registration_stake(net); + + // Force the modern dissolve branch where pool alpha participates in + // the protocol denominator. + TaoInRefundDeploymentBlock::::put(0); + NetworkRegisteredAt::::insert(net, 1); + + let reservoir_tao = TaoBalance::from(100_u64); + let reservoir_alpha = AlphaBalance::from(100_u64); + let staker_hot = U256::from(789); + let staker_cold = U256::from(987); + + let subnet_account = SubtensorModule::get_subnet_account_id(net).unwrap(); + add_balance_to_coldkey_account(&subnet_account, reservoir_tao); + + SubnetTAO::::insert(net, TaoBalance::ZERO); + SubtensorModule::set_subnet_locked_balance(net, TaoBalance::ZERO); + SubnetAlphaIn::::insert(net, AlphaBalance::ZERO); + SubnetProtocolAlpha::::insert(net, AlphaBalance::ZERO); + AlphaV2::::insert((staker_hot, staker_cold, net), sf_from_u64(100u64)); + TotalHotkeyAlpha::::insert(staker_hot, net, AlphaBalance::from(100u64)); + pallet_subtensor_swap::BalancerTaoReservoir::::insert(net, reservoir_tao); + pallet_subtensor_swap::BalancerAlphaReservoir::::insert(net, reservoir_alpha); + + let staker_before = SubtensorModule::get_coldkey_balance(&staker_cold); + let issuance_before = TotalIssuance::::get(); + + assert_ok!(SubtensorModule::do_dissolve_network(net)); + + // Reservoir alpha is treated like materialized protocol pool alpha. + // The staker owns half the denominator, so receives half the reservoir + // TAO pot; the protocol share is recycled. + assert_eq!( + SubtensorModule::get_coldkey_balance(&staker_cold), + staker_before + TaoBalance::from(50_u64) + ); + assert!(TotalIssuance::::get() < issuance_before); + assert!(!NetworksAdded::::contains_key(net)); + assert!(!SubnetOwner::::contains_key(net)); + assert!(!SubnetAlphaIn::::contains_key(net)); + assert!(!SubnetProtocolAlpha::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); + }); +} + #[test] fn dissolve_alpha_out_but_zero_tao_no_rewards() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 3d759e200c..2c6e7b28e5 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -22,6 +22,26 @@ use crate::*; staking::add_stake() tests ************************************************************/ +#[test] +fn test_delegate_take_dispatch_info_pays_fee() { + new_test_ext(1).execute_with(|| { + let hotkey = U256::from(1); + let take = SubtensorModule::get_min_delegate_take(); + + let decrease_take_call = + RuntimeCall::SubtensorModule(SubtensorCall::decrease_take { hotkey, take }); + let decrease_take_dispatch_info = decrease_take_call.get_dispatch_info(); + assert_eq!(decrease_take_dispatch_info.class, DispatchClass::Normal); + assert_eq!(decrease_take_dispatch_info.pays_fee, Pays::Yes); + + let increase_take_call = + RuntimeCall::SubtensorModule(SubtensorCall::increase_take { hotkey, take }); + let increase_take_dispatch_info = increase_take_call.get_dispatch_info(); + assert_eq!(increase_take_dispatch_info.class, DispatchClass::Normal); + assert_eq!(increase_take_dispatch_info.pays_fee, Pays::Yes); + }); +} + #[test] fn test_add_stake_dispatch_info_ok() { new_test_ext(1).execute_with(|| { @@ -4313,12 +4333,8 @@ fn test_move_stake_limit_partial() { // Registration now goes through the burn/swap path, which initializes swap V3 state. // Clear that state first so the manual reserve fixture below actually controls price. - assert_ok!( - ::SwapInterface::clear_protocol_liquidity(origin_netuid) - ); - assert_ok!( - ::SwapInterface::clear_protocol_liquidity(destination_netuid) - ); + ::SwapInterface::clear_protocol_liquidity(origin_netuid); + ::SwapInterface::clear_protocol_liquidity(destination_netuid); // Force-set alpha in and tao reserve to make price equal 1.5 on both origin and destination, // but there's much more liquidity on destination, so its price wouldn't go up when restaked. diff --git a/pallets/subtensor/src/tests/subnet_emissions.rs b/pallets/subtensor/src/tests/subnet_emissions.rs index 9218eabe76..ff56be851c 100644 --- a/pallets/subtensor/src/tests/subnet_emissions.rs +++ b/pallets/subtensor/src/tests/subnet_emissions.rs @@ -151,6 +151,47 @@ fn inplace_pow_normalize_fractional_exponent() { }) } +#[test] +fn get_shares_ignores_root_prop_storage_when_prices_and_burns_match() { + new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(70); + let owner_coldkey = U256::from(71); + let n1 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + let n2 = add_dynamic_network(&owner_hotkey, &owner_coldkey); + + // Equal prices and equal miner-burn values should produce equal shares, + // regardless of the stored root proportion. + SubnetMovingPrice::::insert(n1, i96f32(1.0)); + SubnetMovingPrice::::insert(n2, i96f32(1.0)); + MinerBurned::::insert(n1, U96F32::saturating_from_num(0.0)); + MinerBurned::::insert(n2, U96F32::saturating_from_num(0.0)); + + // Deliberately make root-prop unequal. The old formula would weight + // these equal-price subnets as 1.0 : 0.1 and fail the equality checks. + RootProp::::insert(n1, U96F32::saturating_from_num(1.0)); + RootProp::::insert(n2, U96F32::saturating_from_num(0.1)); + + assert_abs_diff_eq!( + RootProp::::get(n1).to_num::(), + 1.0_f64, + epsilon = 1e-9 + ); + assert_abs_diff_eq!( + RootProp::::get(n2).to_num::(), + 0.1_f64, + epsilon = 1e-9 + ); + + let shares = SubtensorModule::get_shares(&[n1, n2]); + let s1 = shares.get(&n1).copied().unwrap().to_num::(); + let s2 = shares.get(&n2).copied().unwrap().to_num::(); + + assert_abs_diff_eq!(s1, 0.5_f64, epsilon = 1e-9); + assert_abs_diff_eq!(s2, 0.5_f64, epsilon = 1e-9); + assert_abs_diff_eq!(s1 + s2, 1.0_f64, epsilon = 1e-9); + }); +} + // /// Normal (moderate, non-zero) EMA flows across 3 subnets. // /// Expect: shares sum to ~1 and are monotonic with flows. // #[test] diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index e8d9b50a16..e60b85fab0 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -837,22 +837,23 @@ fn test_swap_owner_old_hotkey_not_exist() { }); } -// SKIP_WASM_BUILD=1 RUST_LOG=debug cargo test --test swap_hotkey_with_subnet -- test_swap_owner_new_hotkey_already_exists --exact --nocapture +// SKIP_WASM_BUILD=1 cargo test --package pallet-subtensor --lib -- tests::swap_hotkey_with_subnet::test_swap_owner_new_hotkey_already_exists --exact --nocapture #[test] fn test_swap_owner_new_hotkey_already_exists() { new_test_ext(1).execute_with(|| { let old_hotkey = U256::from(1); let new_hotkey = U256::from(2); let coldkey = U256::from(3); + let another_coldkey = U256::from(4); - let netuid = add_dynamic_network(&new_hotkey, &coldkey); + let netuid = add_dynamic_network(&old_hotkey, &coldkey); add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); // old_hotkey is owned by coldkey; new_hotkey was already registered on `netuid` // by add_dynamic_network (the condition under test). Do NOT reassign new_hotkey to // a foreign coldkey — the new_hotkey-ownership check (NonAssociatedColdKey) would // then fire before the already-registered-in-subnet check this test targets. - Owner::::insert(old_hotkey, coldkey); + Owner::::insert(new_hotkey, another_coldkey); // Perform the swap System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); @@ -864,7 +865,7 @@ fn test_swap_owner_new_hotkey_already_exists() { Some(netuid), false ), - Error::::HotKeyAlreadyRegisteredInSubNet + Error::::NonAssociatedColdKey ); // Verify the swap diff --git a/pallets/subtensor/src/tests/tao.rs b/pallets/subtensor/src/tests/tao.rs index b79b80e3f3..0220bf258d 100644 --- a/pallets/subtensor/src/tests/tao.rs +++ b/pallets/subtensor/src/tests/tao.rs @@ -19,6 +19,7 @@ use frame_support::{ use sp_core::U256; use sp_runtime::traits::{AccountIdConversion, Zero}; use subtensor_runtime_common::TaoBalance; +use subtensor_swap_interface::OrderSwapInterface; const MAX_TAO_ISSUANCE: u64 = 21_000_000_000_000_000_u64; @@ -500,6 +501,62 @@ fn test_transfer_tao_reaps_origin() { }); } +#[test] +fn test_withdraw_tao_as_credit_reaps_origin_and_updates_subtensor_total_issuance() { + new_test_ext(1).execute_with(|| { + let origin = U256::from(1); + + let ed = ExistentialDeposit::get(); + let dust = ed - 1u64.into(); + let amount = TaoBalance::from(1_000); + let balance = amount + dust; + add_balance_to_coldkey_account(&origin, balance); + + let subtensor_ti_before = subtensor_total_issuance(); + let balances_ti_before = balances_total_issuance(); + + let credit = SubtensorModule::withdraw_tao_as_credit(&origin, amount).unwrap(); + + let subtensor_ti_after = subtensor_total_issuance(); + let balances_ti_after = balances_total_issuance(); + + assert_eq!(credit.peek(), amount); + assert_eq!(Balances::total_balance(&origin), 0.into()); + assert_eq!(balances_ti_before - balances_ti_after, dust); + assert_eq!(subtensor_ti_before - subtensor_ti_after, dust); + assert_eq!(balances_ti_after, subtensor_ti_after); + }); +} + +#[test] +fn test_order_swap_transfer_tao_reaps_origin_and_updates_subtensor_total_issuance() { + new_test_ext(1).execute_with(|| { + let origin = U256::from(1); + let dest = U256::from(2); + + let ed = ExistentialDeposit::get(); + let dust = ed - 1u64.into(); + let amount = TaoBalance::from(1_000); + add_balance_to_coldkey_account(&origin, amount + dust); + + let subtensor_ti_before = subtensor_total_issuance(); + let balances_ti_before = balances_total_issuance(); + + assert_ok!(>::transfer_tao( + &origin, &dest, amount + )); + + let subtensor_ti_after = subtensor_total_issuance(); + let balances_ti_after = balances_total_issuance(); + + assert_eq!(Balances::total_balance(&origin), 0.into()); + assert_eq!(Balances::total_balance(&dest), amount); + assert_eq!(balances_ti_before - balances_ti_after, dust); + assert_eq!(subtensor_ti_before - subtensor_ti_after, dust); + assert_eq!(balances_ti_after, subtensor_ti_after); + }); +} + #[test] fn test_recycle_tao_cannot_cross_preserve_threshold_in_high_ed_runtime() { new_test_ext(1).execute_with(|| { diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index c5b9b11a29..a792447c1d 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -6,7 +6,7 @@ use frame_support::{ }; use safe_math::*; use sp_arithmetic::Perquintill; -use sp_runtime::{DispatchResult, traits::AccountIdConversion}; +use sp_runtime::traits::AccountIdConversion; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaBalance, NetUid, SubnetInfo, TaoBalance, Token, TokenReserve}; use subtensor_swap_interface::{ @@ -79,8 +79,22 @@ impl Pallet { Ok(()) } - /// Returns actually added Tao and Alpha, which may be zero in case - /// of a high disbalance + /// Adjusts balancer weights with minted TAO and alpha liquidity to + /// maintain price. + /// + /// If weights cannot be adjusted (get pushed out of range), the excess TAO + /// and/or Alpha are added to reservoirs and an attempt to use them will be made + /// later. + /// + /// Returns: + /// 1. price-active TAO delta to add to `SubnetTAO` + /// 2. price-active Alpha delta to add to `SubnetAlphaIn` + /// + /// Amounts that would push weights out of range are materialized but left in + /// per-subnet reservoirs for a later balancer update. + /// + /// The caller is responsible for materializing the current `tao_delta` and + /// `alpha_delta`; reservoir amounts were materialized when first stored. pub(super) fn adjust_protocol_liquidity( netuid: NetUid, tao_delta: TaoBalance, @@ -89,31 +103,85 @@ impl Pallet { // Get reserves let alpha_reserve = T::AlphaReserve::reserve(netuid.into()); let tao_reserve = T::TaoReserve::reserve(netuid.into()); - let mut balancer = SwapBalancer::::get(netuid); + let balancer = SwapBalancer::::get(netuid); + + let pending_tao = BalancerTaoReservoir::::get(netuid).saturating_add(tao_delta); + let pending_alpha = BalancerAlphaReservoir::::get(netuid).saturating_add(alpha_delta); + + if let Some(new_balancer) = Self::try_update_balancer( + &balancer, + tao_reserve, + alpha_reserve, + pending_tao, + pending_alpha, + ) { + BalancerTaoReservoir::::insert(netuid, TaoBalance::ZERO); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::ZERO); + SwapBalancer::::insert(netuid, new_balancer); + return (pending_tao, pending_alpha); + } - // Update weights and log errors if they go out of range - if balancer - .update_weights_for_added_liquidity( - u64::from(tao_reserve), - u64::from(alpha_reserve), - u64::from(tao_delta), - u64::from(alpha_delta), - ) - .is_err() - { + if let Some(new_balancer) = Self::try_update_balancer( + &balancer, + tao_reserve, + alpha_reserve, + TaoBalance::ZERO, + pending_alpha, + ) { + BalancerTaoReservoir::::insert(netuid, pending_tao); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::ZERO); + SwapBalancer::::insert(netuid, new_balancer); + return (TaoBalance::ZERO, pending_alpha); + } + + if let Some(new_balancer) = Self::try_update_balancer( + &balancer, + tao_reserve, + alpha_reserve, + pending_tao, + AlphaBalance::ZERO, + ) { + BalancerTaoReservoir::::insert(netuid, TaoBalance::ZERO); + BalancerAlphaReservoir::::insert(netuid, pending_alpha); + SwapBalancer::::insert(netuid, new_balancer); + return (pending_tao, AlphaBalance::ZERO); + } + + BalancerTaoReservoir::::insert(netuid, pending_tao); + BalancerAlphaReservoir::::insert(netuid, pending_alpha); + if pending_tao > TaoBalance::ZERO || pending_alpha > AlphaBalance::ZERO { log::warn!( - "Reserves are out of range for emission: netuid = {}, tao = {}, alpha = {}, tao_delta = {}, alpha_delta = {}", + "Reserves are out of range for emission: netuid = {}, tao = {}, alpha = {}, tao_delta = {}, alpha_delta = {}, tao_reservoir = {}, alpha_reservoir = {}", netuid, tao_reserve, alpha_reserve, tao_delta, - alpha_delta + alpha_delta, + pending_tao, + pending_alpha ); - (TaoBalance::ZERO, AlphaBalance::ZERO) - } else { - SwapBalancer::::insert(netuid, balancer); - (tao_delta, alpha_delta) } + + (TaoBalance::ZERO, AlphaBalance::ZERO) + } + + fn try_update_balancer( + balancer: &Balancer, + tao_reserve: TaoBalance, + alpha_reserve: AlphaBalance, + tao_delta: TaoBalance, + alpha_delta: AlphaBalance, + ) -> Option { + let mut new_balancer = balancer.clone(); + new_balancer + .update_weights_for_added_liquidity( + u64::from(tao_reserve), + u64::from(alpha_reserve), + u64::from(tao_delta), + u64::from(alpha_delta), + ) + .ok()?; + Some(new_balancer) } /// Executes a token swap on the specified subnet. @@ -286,7 +354,15 @@ impl Pallet { } /// Clear **protocol-owned** liquidity and wipe all swap state for `netuid`. - pub fn do_clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { + pub fn do_clear_protocol_liquidity(netuid: NetUid) { + // Reservoir balances are materialized protocol liquidity that never became + // price-active. Fold them into the reserve abstraction first so cleanup + // clears them through the same path as active protocol liquidity. + let reservoir_tao = BalancerTaoReservoir::::take(netuid); + let reservoir_alpha = BalancerAlphaReservoir::::take(netuid); + T::TaoReserve::increase_provided(netuid.into(), reservoir_tao); + T::AlphaReserve::increase_provided(netuid.into(), reservoir_alpha); + // 1) Force-close protocol liquidity, burning proceeds. let burned_tao = T::TaoReserve::reserve(netuid.into()); let burned_alpha = T::AlphaReserve::reserve(netuid.into()); @@ -302,8 +378,6 @@ impl Pallet { log::debug!( "clear_protocol_liquidity: netuid={netuid:?}, protocol_burned: τ={burned_tao:?}, α={burned_alpha:?}; state cleared" ); - - Ok(()) } } @@ -416,8 +490,21 @@ impl SwapHandler for Pallet { Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta) } - fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult { - Self::do_clear_protocol_liquidity(netuid) + fn protocol_alpha_reservoir(netuid: NetUid) -> AlphaBalance { + BalancerAlphaReservoir::::get(netuid) + } + + fn protocol_tao_reservoir(netuid: NetUid) -> TaoBalance { + BalancerTaoReservoir::::get(netuid) + } + + fn clear_protocol_liquidity_reservoirs(netuid: NetUid) { + BalancerTaoReservoir::::remove(netuid); + BalancerAlphaReservoir::::remove(netuid); + } + + fn clear_protocol_liquidity(netuid: NetUid) { + Self::do_clear_protocol_liquidity(netuid); } fn init_swap(netuid: NetUid, maybe_price: Option) { diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index b9044d4e82..9cb5e9c4e9 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -113,6 +113,15 @@ mod pallet { #[pallet::storage] pub type PalSwapInitialized = StorageMap<_, Twox64Concat, NetUid, bool, ValueQuery>; + /// TAO protocol liquidity that could not be injected without exceeding balancer weight bounds. + #[pallet::storage] + pub type BalancerTaoReservoir = StorageMap<_, Twox64Concat, NetUid, TaoBalance, ValueQuery>; + + /// Alpha protocol liquidity that could not be injected without exceeding balancer weight bounds. + #[pallet::storage] + pub type BalancerAlphaReservoir = + StorageMap<_, Twox64Concat, NetUid, AlphaBalance, ValueQuery>; + /// --- Storage for migration run status #[pallet::storage] pub type HasMigrationRun = diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index 5e9712d63d..fc807d05d5 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -155,6 +155,124 @@ mod dispatchables { }); } + #[test] + fn test_adjust_protocol_liquidity_materializes_tao_when_reservoiring_tao() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao = TaoBalance::from(1_000_u64); + let alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(200_000_u64), + AlphaBalance::from(1_000_u64), + ); + + assert_eq!(price_active_tao, TaoBalance::ZERO); + assert_eq!(price_active_alpha, AlphaBalance::from(1_000_u64)); + assert_eq!( + BalancerTaoReservoir::::get(netuid), + TaoBalance::from(200_000_u64) + ); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); + } + + #[test] + fn test_adjust_protocol_liquidity_materializes_alpha_when_reservoiring_alpha() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let tao = TaoBalance::from(1_000_u64); + let alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(1_000_u64), + AlphaBalance::from(200_000_u64), + ); + + assert_eq!(price_active_tao, TaoBalance::from(1_000_u64)); + assert_eq!(price_active_alpha, AlphaBalance::ZERO); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::from(200_000_u64) + ); + }); + } + + #[test] + fn test_adjust_protocol_liquidity_retries_reservoir_with_new_injection() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + let mut tao = TaoBalance::from(1_000_u64); + let mut alpha = AlphaBalance::from(1_000_u64); + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(200_000_u64), + AlphaBalance::from(1_000_u64), + ); + assert_eq!(price_active_tao, TaoBalance::ZERO); + assert_eq!(price_active_alpha, AlphaBalance::from(1_000_u64)); + tao += price_active_tao; + alpha += price_active_alpha; + TaoReserve::set_mock_reserve(netuid, tao); + AlphaReserve::set_mock_reserve(netuid, alpha); + + let (price_active_tao, price_active_alpha) = Swap::adjust_protocol_liquidity( + netuid, + TaoBalance::from(1_000_u64), + AlphaBalance::from(200_000_u64), + ); + + assert!(price_active_tao >= TaoBalance::from(1_000_u64)); + assert!(price_active_alpha >= AlphaBalance::from(200_000_u64)); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); + } + + #[test] + fn test_adjust_protocol_liquidity_activates_reservoir_amounts() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000_000_u64)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000_000_u64)); + BalancerTaoReservoir::::insert(netuid, TaoBalance::from(10_000_u64)); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::from(20_000_u64)); + + let tao_delta = TaoBalance::from(300_u64); + let alpha_delta = AlphaBalance::from(400_u64); + let (price_active_tao, price_active_alpha) = + Swap::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta); + + assert_eq!(price_active_tao, TaoBalance::from(10_300_u64)); + assert_eq!(price_active_alpha, AlphaBalance::from(20_400_u64)); + assert_eq!(BalancerTaoReservoir::::get(netuid), TaoBalance::ZERO); + assert_eq!( + BalancerAlphaReservoir::::get(netuid), + AlphaBalance::ZERO + ); + }); + } + /// This test case verifies that small gradual injections (like emissions in every block) /// in the worst case /// - Do not cause price to change @@ -810,17 +928,18 @@ fn print_current_price(netuid: NetUid) { log::trace!("Current price: {current_price:.6}"); } -/// Simple palswap path: PalSwap is initialized, but no positions, only protocol; function -/// must still clear any residual storages and succeed. -/// TODO: Revise when user liquidity is available +/// Reservoir liquidity is already materialized but not price-active; direct +/// cleanup materializes it into the reserve abstraction before clearing. #[test] -fn test_liquidate_pal_simple_ok_and_clears() { +fn test_clear_protocol_liquidity_clears_nonzero_reservoirs() { new_test_ext().execute_with(|| { let netuid = NetUid::from(202); // Insert map values FeeRate::::insert(netuid, 1_000); PalSwapInitialized::::insert(netuid, true); + BalancerTaoReservoir::::insert(netuid, TaoBalance::from(12_345_u64)); + BalancerAlphaReservoir::::insert(netuid, AlphaBalance::from(67_890_u64)); let w_quote_pt = Perquintill::from_rational(1u128, 2u128); let bal = Balancer::new(w_quote_pt).unwrap(); SwapBalancer::::insert(netuid, bal); @@ -828,13 +947,13 @@ fn test_liquidate_pal_simple_ok_and_clears() { // Sanity: PalSwap is not initialized assert!(PalSwapInitialized::::get(netuid)); - // ACT - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + Pallet::::do_clear_protocol_liquidity(netuid); - // All single-key maps should not have the key after liquidation assert!(!FeeRate::::contains_key(netuid)); assert!(!PalSwapInitialized::::contains_key(netuid)); assert!(!SwapBalancer::::contains_key(netuid)); + assert!(!BalancerTaoReservoir::::contains_key(netuid)); + assert!(!BalancerAlphaReservoir::::contains_key(netuid)); }); } @@ -853,7 +972,7 @@ fn test_clear_protocol_liquidity_green_path() { // --- Act --- // Green path: just clear protocol liquidity and wipe all V3 state. - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + Pallet::::do_clear_protocol_liquidity(netuid); // Flags assert!(!PalSwapInitialized::::contains_key(netuid)); @@ -862,7 +981,7 @@ fn test_clear_protocol_liquidity_green_path() { assert!(!FeeRate::::contains_key(netuid)); // --- And it's idempotent --- - assert_ok!(Pallet::::do_clear_protocol_liquidity(netuid)); + Pallet::::do_clear_protocol_liquidity(netuid); assert!(!PalSwapInitialized::::contains_key(netuid)); }); } diff --git a/primitives/swap-interface/src/lib.rs b/primitives/swap-interface/src/lib.rs index 9980604707..5b202a8bae 100644 --- a/primitives/swap-interface/src/lib.rs +++ b/primitives/swap-interface/src/lib.rs @@ -47,7 +47,10 @@ pub trait SwapHandler { tao_delta: TaoBalance, alpha_delta: AlphaBalance, ) -> (TaoBalance, AlphaBalance); - fn clear_protocol_liquidity(netuid: NetUid) -> DispatchResult; + fn protocol_alpha_reservoir(netuid: NetUid) -> AlphaBalance; + fn protocol_tao_reservoir(netuid: NetUid) -> TaoBalance; + fn clear_protocol_liquidity_reservoirs(netuid: NetUid); + fn clear_protocol_liquidity(netuid: NetUid); fn init_swap(netuid: NetUid, maybe_price: Option); fn get_alpha_amount_for_tao(netuid: NetUid, tao_amount: TaoBalance) -> AlphaBalance; } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 05a15e8ff9..71a091b150 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -234,7 +234,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 424, + spec_version: 426, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -483,7 +483,7 @@ impl pallet_balances::Config for Runtime { type Balance = Balance; // The ubiquitous event type. type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); + type DustRemoval = pallet_subtensor::SubtensorDustRemoval; type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; type WeightInfo = pallet_balances::weights::SubstrateWeight; diff --git a/runtime/tests/balances_dust.rs b/runtime/tests/balances_dust.rs new file mode 100644 index 0000000000..b56ada26bc --- /dev/null +++ b/runtime/tests/balances_dust.rs @@ -0,0 +1,60 @@ +#![allow(clippy::unwrap_used)] + +use frame_support::traits::fungible::{Inspect, Mutate}; +use frame_support::traits::tokens::Preservation; +use node_subtensor_runtime::{Balances, BuildStorage, Runtime, RuntimeGenesisConfig}; +use sp_core::crypto::AccountId32; +use subtensor_runtime_common::TaoBalance; + +fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = RuntimeGenesisConfig { + ..Default::default() + } + .build_storage() + .unwrap() + .into(); + ext.execute_with(|| frame_system::Pallet::::set_block_number(1)); + ext +} + +fn add_balance_to_account(account: &AccountId32, tao: TaoBalance) { + let credit = pallet_subtensor::Pallet::::mint_tao(tao); + let _ = pallet_subtensor::Pallet::::spend_tao(account, credit, tao).unwrap(); +} + +#[test] +fn balances_dust_removal_updates_subtensor_total_issuance() { + new_test_ext().execute_with(|| { + let origin = AccountId32::new([1u8; 32]); + let destination = AccountId32::new([2u8; 32]); + let existential_deposit = TaoBalance::from(500u64); + let dust = TaoBalance::from(1u64); + let transfer_amount = existential_deposit; + + add_balance_to_account(&origin, transfer_amount + dust); + + let balances_issuance_before = Balances::total_issuance(); + let subtensor_issuance_before = pallet_subtensor::Pallet::::get_total_issuance(); + assert_eq!(balances_issuance_before, subtensor_issuance_before); + + >::transfer( + &origin, + &destination, + transfer_amount, + Preservation::Expendable, + ) + .unwrap(); + + assert_eq!(Balances::total_balance(&origin), 0u64.into()); + assert_eq!(Balances::total_balance(&destination), transfer_amount); + assert_eq!(balances_issuance_before - Balances::total_issuance(), dust); + assert_eq!( + subtensor_issuance_before - pallet_subtensor::Pallet::::get_total_issuance(), + dust + ); + assert_eq!( + Balances::total_issuance(), + pallet_subtensor::Pallet::::get_total_issuance() + ); + }); +}