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 d1afc76c55..d61a5d7287 100644 --- a/eco-tests/src/mock.rs +++ b/eco-tests/src/mock.rs @@ -617,11 +617,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; @@ -665,6 +673,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/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index 84fdac3012..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); } } } diff --git a/pallets/subtensor/src/coinbase/tao.rs b/pallets/subtensor/src/coinbase/tao.rs index 833cc493fb..bd35c6a607 100644 --- a/pallets/subtensor/src/coinbase/tao.rs +++ b/pallets/subtensor/src/coinbase/tao.rs @@ -36,18 +36,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, @@ -55,14 +51,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(()) } @@ -289,6 +277,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 f19b8086a3..df84b27eda 100644 --- a/pallets/subtensor/src/lib.rs +++ b/pallets/subtensor/src/lib.rs @@ -68,6 +68,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); + }); + } +} + /// Maximum number of UIDs (per subnet) that may be associated with a single EVM address. /// /// This bounds the size of the `AssociatedUidsByEvmAddress` reverse-index value, keeping diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index f06e42201e..d5dbbcbb26 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/dissolution.rs b/pallets/subtensor/src/subnets/dissolution.rs index c5885c167d..09e03430ce 100644 --- a/pallets/subtensor/src/subnets/dissolution.rs +++ b/pallets/subtensor/src/subnets/dissolution.rs @@ -106,6 +106,18 @@ impl Pallet { Error::::SubnetNotExists ); + // Since TotalStake is updated on this level, purge reservoirs here into reserves and TotalStake + 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); + }); + } + let mut dissolved_networks = DissolveCleanupQueue::::get(); ensure!( !dissolved_networks.contains(&netuid), 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 ca55340554..49734637e1 100644 --- a/pallets/subtensor/src/swap/swap_hotkey.rs +++ b/pallets/subtensor/src/swap/swap_hotkey.rs @@ -49,17 +49,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..240cf4b096 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,120 @@ 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); + }); +} + +#[test] +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/migration.rs b/pallets/subtensor/src/tests/migration.rs index d087df6058..133934c93f 100644 --- a/pallets/subtensor/src/tests/migration.rs +++ b/pallets/subtensor/src/tests/migration.rs @@ -200,14 +200,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); @@ -225,6 +228,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 = @@ -234,14 +241,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) @@ -270,10 +297,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!( @@ -4768,6 +4810,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); @@ -4788,16 +4831,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 4af03c19fe..a0368483ef 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -126,7 +126,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 fbfe2667f4..84229cbca1 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 afa7fc77a8..90f150b923 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -573,6 +573,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)); @@ -695,6 +697,72 @@ 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)); + + // do_dissolve_network only queues the destructive cleanup, but it must + // materialize pending protocol reservoirs before the queued cleanup can + // compute stake payouts. + assert!(!pallet_subtensor_swap::BalancerTaoReservoir::::contains_key(net)); + assert!(!pallet_subtensor_swap::BalancerAlphaReservoir::::contains_key(net)); + assert_eq!(SubnetTAO::::get(net), reservoir_tao); + assert_eq!(SubnetAlphaIn::::get(net), reservoir_alpha); + assert_eq!( + SubtensorModule::get_coldkey_balance(&staker_cold), + staker_before + ); + + run_block_idle(); + + // 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 a7e735e8ec..d1dc012ce5 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(|| { diff --git a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs index f299dbe3ae..385c3c0e83 100644 --- a/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs +++ b/pallets/subtensor/src/tests/swap_hotkey_with_subnet.rs @@ -837,7 +837,42 @@ 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_owned_by_another_coldkey --exact --nocapture +#[test] +fn test_swap_owner_new_hotkey_owned_by_another_coldkey() { + 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(&old_hotkey, &coldkey); + add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_u64.into()); + + // new_hotkey already exists globally and is owned by a foreign coldkey, + // so the new-hotkey ownership check must reject the swap. + Owner::::insert(new_hotkey, another_coldkey); + + // Perform the swap + System::set_block_number(System::block_number() + HotkeySwapOnSubnetInterval::get()); + assert_err!( + SubtensorModule::do_swap_hotkey( + RuntimeOrigin::signed(coldkey), + &old_hotkey, + &new_hotkey, + Some(netuid), + false + ), + Error::::NonAssociatedColdKey + ); + + // Verify the swap + assert_eq!(Owner::::get(old_hotkey), coldkey); + assert!(Owner::::contains_key(old_hotkey)); + }); +} + +// 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(|| { 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 966bb4721b..3b841169ed 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -80,8 +80,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, @@ -90,31 +104,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. @@ -288,12 +356,21 @@ impl Pallet { /// Clear **protocol-owned** liquidity and wipe all swap state for `netuid`. pub fn do_clear_protocol_liquidity(netuid: NetUid, weight_meter: &mut WeightMeter) -> bool { - let clear_weight = T::DbWeight::get().reads_writes(4, 5); + let clear_weight = T::DbWeight::get().reads_writes(6, 7); if !weight_meter.can_consume(clear_weight) { return false; } weight_meter.consume(clear_weight); // / 1) Force-close protocol liquidity, burning proceeds. + + // 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); + let burned_tao = T::TaoReserve::reserve(netuid.into()); let burned_alpha = T::AlphaReserve::reserve(netuid.into()); @@ -425,6 +502,19 @@ impl SwapHandler for Pallet { Self::adjust_protocol_liquidity(netuid, tao_delta, alpha_delta) } + 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 init_swap(netuid: NetUid, maybe_price: Option) { Self::maybe_initialize_palswap(netuid, maybe_price).unwrap_or_default(); } 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 9eeb5f43a8..dd41c84269 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -156,6 +156,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 @@ -811,17 +929,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); @@ -835,10 +954,11 @@ fn test_liquidate_pal_simple_ok_and_clears() { &mut WeightMeter::with_limit(Weight::from_parts(u64::MAX, u64::MAX)) )); - // 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)); }); } diff --git a/primitives/swap-interface/src/lib.rs b/primitives/swap-interface/src/lib.rs index ed2bbf010d..ed76dcda43 100644 --- a/primitives/swap-interface/src/lib.rs +++ b/primitives/swap-interface/src/lib.rs @@ -47,6 +47,9 @@ pub trait SwapHandler { tao_delta: TaoBalance, alpha_delta: AlphaBalance, ) -> (TaoBalance, AlphaBalance); + 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, weight_meter: &mut WeightMeter) -> bool; 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 f5f5ddfcc7..48106b0fdf 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -235,7 +235,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: 427, + spec_version: 428, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -484,7 +484,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() + ); + }); +}