diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index 16619389bf..886f722aa7 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -1297,15 +1297,26 @@ fn add_stake_recycle_rollback_on_recycle_failure() { let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - // Set up very low reserves so recycle will fail with InsufficientLiquidity + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::insert_lock_state( + &coldkey, + netuid, + &hotkey, + pallet_subtensor::staking::lock::LockState { + locked_mass: AlphaBalance::from(u64::MAX / 4), + conviction: U64F64::saturating_from_num(0), + last_update: pallet_subtensor::Pallet::::get_current_block_as_u64(), + }, + ); + + // Leave enough input-side liquidity for add_stake to pass the 1000x swap input cap. + // The lock above makes the recycle leg fail, exercising atomic rollback. mock::setup_reserves( netuid, - TaoBalance::from(1_000_u64), + TaoBalance::from(tao_amount_raw / 1000 + 1), AlphaBalance::from(1_000_u64), ); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - add_balance_to_coldkey_account( &coldkey, TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), @@ -1368,15 +1379,26 @@ fn add_stake_burn_rollback_on_burn_failure() { let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); - // Set up very low reserves so burn will fail with InsufficientLiquidity + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::insert_lock_state( + &coldkey, + netuid, + &hotkey, + pallet_subtensor::staking::lock::LockState { + locked_mass: AlphaBalance::from(u64::MAX / 4), + conviction: U64F64::saturating_from_num(0), + last_update: pallet_subtensor::Pallet::::get_current_block_as_u64(), + }, + ); + + // Leave enough input-side liquidity for add_stake to pass the 1000x swap input cap. + // The lock above makes the burn leg fail, exercising atomic rollback. mock::setup_reserves( netuid, - TaoBalance::from(1_000_u64), + TaoBalance::from(tao_amount_raw / 1000 + 1), AlphaBalance::from(1_000_u64), ); - mock::register_ok_neuron(netuid, hotkey, coldkey, 0); - add_balance_to_coldkey_account( &coldkey, TaoBalance::from(tao_amount_raw.saturating_add(1_000_000_000)), 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/run_coinbase.rs b/pallets/subtensor/src/coinbase/run_coinbase.rs index d7c75964b2..84fdac3012 100644 --- a/pallets/subtensor/src/coinbase/run_coinbase.rs +++ b/pallets/subtensor/src/coinbase/run_coinbase.rs @@ -411,9 +411,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/extensions/subtensor.rs b/pallets/subtensor/src/extensions/subtensor.rs index ea91c87c6e..7899ed855e 100644 --- a/pallets/subtensor/src/extensions/subtensor.rs +++ b/pallets/subtensor/src/extensions/subtensor.rs @@ -1,6 +1,6 @@ use crate::{ Call, CheckColdkeySwap, CheckDelegateTake, CheckEvmKeyAssociation, CheckRateLimits, - CheckServingEndpoints, CheckWeights, Config, Error, + CheckServingEndpoints, CheckWeights, Config, Error, guards::applicable_call, }; use codec::{Decode, DecodeWithMemTracking, Encode}; use frame_support::{ @@ -89,15 +89,23 @@ impl SubtensorTransactionExtension { CheckColdkeySwap::::check(who, call)?; - let Some(call) = call.is_sub_type() else { - return Ok(()); - }; + if let Some(call) = applicable_call(call, CheckWeights::::applies_to) { + CheckWeights::::check(who, call)?; + } + if let Some(call) = applicable_call(call, CheckRateLimits::::applies_to) { + CheckRateLimits::::check(who, call)?; + } + if let Some(call) = applicable_call(call, CheckDelegateTake::::applies_to) { + CheckDelegateTake::::check(who, call)?; + } + if let Some(call) = applicable_call(call, CheckServingEndpoints::::applies_to) { + CheckServingEndpoints::::check(who, call)?; + } + if let Some(call) = applicable_call(call, CheckEvmKeyAssociation::::applies_to) { + CheckEvmKeyAssociation::::check(who, call)?; + } - CheckWeights::::check(who, call)?; - CheckRateLimits::::check(who, call)?; - CheckDelegateTake::::check(who, call)?; - CheckServingEndpoints::::check(who, call)?; - CheckEvmKeyAssociation::::check(who, call) + Ok(()) } } diff --git a/pallets/subtensor/src/guards/check_coldkey_swap.rs b/pallets/subtensor/src/guards/check_coldkey_swap.rs index 5f124be219..907fed1d3b 100644 --- a/pallets/subtensor/src/guards/check_coldkey_swap.rs +++ b/pallets/subtensor/src/guards/check_coldkey_swap.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf}; use crate::weights::WeightInfo; use crate::{Call, ColdkeySwapAnnouncements, ColdkeySwapDisputes, Config, Error}; use frame_support::{ @@ -8,9 +9,6 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension that blocks most calls when a coldkey swap is active. /// /// When a coldkey swap has been announced for the signing account: @@ -96,9 +94,14 @@ where #[allow(clippy::expect_used, clippy::unwrap_used)] mod tests { use super::CheckColdkeySwap; - use crate::{ColdkeySwapAnnouncements, ColdkeySwapDisputes, Error, tests::mock::*}; + use crate::{ + ColdkeySwapAnnouncements, ColdkeySwapDisputes, Error, tests::mock::*, + weights::WeightInfo as _, + }; use frame_support::{ - BoundedVec, assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + BoundedVec, assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, }; use frame_system::Call as SystemCall; use pallet_subtensor_proxy::Call as ProxyCall; @@ -176,6 +179,18 @@ mod tests { ) } + #[test] + fn weight_charges_all_calls_because_swap_state_can_block_any_signed_call() { + let expected = ::WeightInfo::check_coldkey_swap_extension(); + + for call in forbidden_calls().into_iter().chain(authorized_calls()) { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn no_active_swap_allows_calls() { new_test_ext(1).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_delegate_take.rs b/pallets/subtensor/src/guards/check_delegate_take.rs index c9f54d4cb5..c80d969afc 100644 --- a/pallets/subtensor/src/guards/check_delegate_take.rs +++ b/pallets/subtensor/src/guards/check_delegate_take.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -8,9 +9,6 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension for delegate-take bounds and ownership preconditions. /// /// Signed increase/decrease take calls are checked before dispatch; unrelated @@ -18,6 +16,13 @@ type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; pub struct CheckDelegateTake(PhantomData); impl CheckDelegateTake { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!( + call, + Call::increase_take { .. } | Call::decrease_take { .. } + ) + } + pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::increase_take { hotkey, take } | Call::decrease_take { hotkey, take } => { @@ -42,8 +47,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_delegate_take_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_delegate_take_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -54,7 +61,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -68,7 +75,10 @@ mod tests { use super::*; use crate::{Error, tests::mock::*}; use frame_support::{ - assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, + weights::Weight, }; use sp_core::U256; use sp_runtime::DispatchError; @@ -91,6 +101,39 @@ mod tests { result.unwrap_err().error } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + #[test] + fn weight_only_charges_delegate_take_calls() { + let expected = ::WeightInfo::check_delegate_take_extension(); + + for call in [ + RuntimeCall::System(frame_system::Call::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + for call in [ + increase_take_call(U256::from(1), 0), + decrease_take_call(U256::from(1), 0), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn accepts_owner_with_valid_take() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_evm_key_association.rs b/pallets/subtensor/src/guards/check_evm_key_association.rs index d7e2847e99..d9b69e1a7d 100644 --- a/pallets/subtensor/src/guards/check_evm_key_association.rs +++ b/pallets/subtensor/src/guards/check_evm_key_association.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -8,9 +9,6 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension for EVM-key association preconditions. /// /// Signed EVM-key association calls are checked for subnet registration and @@ -18,6 +16,10 @@ type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; pub struct CheckEvmKeyAssociation(PhantomData); impl CheckEvmKeyAssociation { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!(call, Call::associate_evm_key { .. }) + } + pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::associate_evm_key { netuid, .. } => { @@ -40,8 +42,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_evm_key_association_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_evm_key_association_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -52,7 +56,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -64,10 +68,13 @@ where #[allow(clippy::unwrap_used, clippy::arithmetic_side_effects)] mod tests { use super::CheckEvmKeyAssociation; - use crate::{AssociatedEvmAddress, Error, tests::mock::*}; + use crate::{AssociatedEvmAddress, Error, tests::mock::*, weights::WeightInfo as _}; use codec::Encode; use frame_support::{ - assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, + weights::Weight, }; use frame_system::Call as SystemCall; use sp_core::{H160, Pair, U256, ecdsa, keccak_256}; @@ -139,6 +146,37 @@ mod tests { ) } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + #[test] + fn weight_only_charges_evm_key_association_calls() { + let netuid = NetUid::from(1); + let expected = ::WeightInfo::check_evm_key_association_extension(); + + for call in [ + RuntimeCall::System(SystemCall::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + assert_eq!( + as DispatchExtension>::weight( + &dummy_associate_call(netuid) + ), + expected + ); + } + #[test] fn unrelated_calls_pass_through() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_rate_limits.rs b/pallets/subtensor/src/guards/check_rate_limits.rs index d2c021dd5d..e12c9d064b 100644 --- a/pallets/subtensor/src/guards/check_rate_limits.rs +++ b/pallets/subtensor/src/guards/check_rate_limits.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet, TransactionType}; use frame_support::{ @@ -9,9 +10,6 @@ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension for rate-limit checks that are safe to reject before dispatch. /// /// Signed weight and network-registration calls are checked before dispatch; @@ -19,6 +17,17 @@ type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; pub struct CheckRateLimits(PhantomData); impl CheckRateLimits { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!( + call, + Call::commit_weights { .. } + | Call::commit_mechanism_weights { .. } + | Call::set_weights { .. } + | Call::set_mechanism_weights { .. } + | Call::register_network { .. } + ) + } + fn check_weights_rate_limit( who: &T::AccountId, netuid: NetUid, @@ -89,8 +98,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_rate_limits_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_rate_limits_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -101,7 +112,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -113,9 +124,12 @@ where #[allow(clippy::unwrap_used)] mod tests { use super::CheckRateLimits; - use crate::{Error, tests::mock::*}; + use crate::{Error, tests::mock::*, weights::WeightInfo as _}; use frame_support::{ - assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, + weights::Weight, }; use frame_system::Call as SystemCall; use sp_core::U256; @@ -155,6 +169,57 @@ mod tests { add_balance_to_coldkey_account(&coldkey, amount); } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + #[test] + fn weight_only_charges_rate_limited_calls() { + let netuid = NetUid::from(1); + let expected = ::WeightInfo::check_rate_limits_extension(); + let charged_calls = [ + RuntimeCall::SubtensorModule(SubtensorCall::commit_weights { + netuid, + commit_hash: sp_core::H256::zero(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_mechanism_weights { + netuid, + mecid: MechId::MAIN, + commit_hash: sp_core::H256::zero(), + }), + set_weights_call(netuid, 0), + RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights { + netuid, + mecid: MechId::MAIN, + dests: vec![0], + weights: vec![1], + version_key: 0, + }), + register_network_call(U256::from(1)), + ]; + + for call in [ + RuntimeCall::System(SystemCall::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + for call in charged_calls { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn unrelated_calls_pass_through() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_serving_endpoints.rs b/pallets/subtensor/src/guards/check_serving_endpoints.rs index 46304d337f..f8b2da64ed 100644 --- a/pallets/subtensor/src/guards/check_serving_endpoints.rs +++ b/pallets/subtensor/src/guards/check_serving_endpoints.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet}; use frame_support::{ @@ -8,9 +9,6 @@ use frame_support::{ use sp_runtime::traits::Dispatchable; use sp_std::marker::PhantomData; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; - /// Dispatch extension for axon/prometheus endpoint validation. /// /// Signed serving calls are checked before dispatch; unrelated calls and @@ -18,6 +16,13 @@ type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; pub struct CheckServingEndpoints(PhantomData); impl CheckServingEndpoints { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!( + call, + Call::serve_axon { .. } | Call::serve_axon_tls { .. } | Call::serve_prometheus { .. } + ) + } + pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { match call { Call::serve_axon { @@ -74,8 +79,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_serving_endpoints_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_serving_endpoints_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -86,7 +93,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -98,9 +105,12 @@ where #[allow(clippy::unwrap_used)] mod tests { use super::CheckServingEndpoints; - use crate::{Error, tests::mock::*}; + use crate::{Error, tests::mock::*, weights::WeightInfo as _}; use frame_support::{ - assert_ok, dispatch::DispatchResultWithPostInfo, traits::ExtendedDispatchable, + assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ExtendedDispatchable, + weights::Weight, }; use frame_system::Call as SystemCall; use sp_core::U256; @@ -160,6 +170,41 @@ mod tests { register_ok_neuron(netuid, hotkey, coldkey, 0); } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + #[test] + fn weight_only_charges_serving_endpoint_calls() { + let netuid = NetUid::from(1); + let expected = ::WeightInfo::check_serving_endpoints_extension(); + + for call in [ + RuntimeCall::System(SystemCall::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + for call in [ + serve_axon_call(netuid), + serve_axon_tls_call(netuid), + serve_prometheus_call(netuid), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn unrelated_calls_pass_through() { new_test_ext(0).execute_with(|| { diff --git a/pallets/subtensor/src/guards/check_weights.rs b/pallets/subtensor/src/guards/check_weights.rs index d10e7b8b8c..c116071ba6 100644 --- a/pallets/subtensor/src/guards/check_weights.rs +++ b/pallets/subtensor/src/guards/check_weights.rs @@ -1,3 +1,4 @@ +use super::{CallOf, DispatchableOriginOf, applicable_call}; use crate::weights::WeightInfo; use crate::{Call, Config, Error, Pallet, WeightCommits}; use frame_support::{ @@ -10,8 +11,6 @@ use sp_runtime::traits::Dispatchable; use sp_std::{collections::vec_deque::VecDeque, marker::PhantomData, vec::Vec}; use subtensor_runtime_common::{NetUid, NetUidStorageIndex}; -type CallOf = ::RuntimeCall; -type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; type WeightCommitQueue = VecDeque<(H256, u64, u64, u64)>; /// Dispatch extension for weight-setting preconditions. @@ -21,6 +20,24 @@ type WeightCommitQueue = VecDeque<(H256, u64, u64, u64)>; pub struct CheckWeights(PhantomData); impl CheckWeights { + pub(crate) fn applies_to(call: &Call) -> bool { + matches!( + call, + Call::batch_commit_weights { .. } + | Call::batch_reveal_weights { .. } + | Call::batch_set_weights { .. } + | Call::commit_weights { .. } + | Call::commit_mechanism_weights { .. } + | Call::reveal_weights { .. } + | Call::reveal_mechanism_weights { .. } + | Call::set_weights { .. } + | Call::set_mechanism_weights { .. } + | Call::commit_timelocked_weights { .. } + | Call::commit_timelocked_mechanism_weights { .. } + | Call::commit_crv3_mechanism_weights { .. } + ) + } + pub fn check(who: &T::AccountId, call: &Call) -> Result<(), Error> { Self::check_input_lengths(call)?; Self::check_min_stake(who, call)?; @@ -227,8 +244,10 @@ where { type Pre = (); - fn weight(_call: &CallOf) -> Weight { - ::WeightInfo::check_weights_extension() + fn weight(call: &CallOf) -> Weight { + applicable_call(call, Self::applies_to) + .map(|_| ::WeightInfo::check_weights_extension()) + .unwrap_or(Weight::zero()) } fn pre_dispatch( @@ -239,7 +258,7 @@ where return Ok(()); }; - let Some(call) = call.is_sub_type() else { + let Some(call) = applicable_call(call, Self::applies_to) else { return Ok(()); }; @@ -251,11 +270,14 @@ where #[allow(clippy::unwrap_used)] mod tests { use super::CheckWeights; - use crate::{Error, MAX_CRV3_COMMIT_SIZE_BYTES, tests::mock::*}; + use crate::{Error, MAX_CRV3_COMMIT_SIZE_BYTES, tests::mock::*, weights::WeightInfo as _}; use codec::Compact; use frame_support::{ - BoundedVec, assert_ok, dispatch::DispatchResultWithPostInfo, traits::ConstU32, + BoundedVec, assert_ok, + dispatch::{DispatchExtension, DispatchResultWithPostInfo}, + traits::ConstU32, traits::ExtendedDispatchable, + weights::Weight, }; use frame_system::Call as SystemCall; use pallet_drand::LastStoredRound; @@ -309,6 +331,99 @@ mod tests { }) } + fn add_stake_call() -> RuntimeCall { + RuntimeCall::SubtensorModule(SubtensorCall::add_stake { + hotkey: U256::from(1), + netuid: 1u16.into(), + amount_staked: 1_000u64.into(), + }) + } + + fn checked_weight_calls(netuid: NetUid) -> Vec { + let bounded_commit = + BoundedVec::>::try_from(vec![0]).unwrap(); + + vec![ + set_weights_call(netuid, 0), + RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights { + netuid, + mecid: MechId::MAIN, + dests: vec![0], + weights: vec![1], + version_key: 0, + }), + RuntimeCall::SubtensorModule(SubtensorCall::batch_set_weights { + netuids: vec![Compact(netuid)], + weights: vec![vec![(Compact(0_u16), Compact(1_u16))]], + version_keys: vec![Compact(0_u64)], + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_weights { + netuid, + commit_hash: H256::zero(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_mechanism_weights { + netuid, + mecid: MechId::MAIN, + commit_hash: H256::zero(), + }), + RuntimeCall::SubtensorModule(SubtensorCall::batch_commit_weights { + netuids: vec![Compact(netuid)], + commit_hashes: vec![H256::zero()], + }), + reveal_weights_call(netuid), + reveal_mechanism_weights_call(netuid, MechId::MAIN), + RuntimeCall::SubtensorModule(SubtensorCall::batch_reveal_weights { + netuid, + uids_list: vec![vec![0]], + values_list: vec![vec![1]], + salts_list: vec![vec![1]], + version_keys: vec![0], + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_weights { + netuid, + commit: bounded_commit.clone(), + reveal_round: 0, + commit_reveal_version: 0, + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_mechanism_weights { + netuid, + mecid: MechId::MAIN, + commit: bounded_commit.clone(), + reveal_round: 0, + commit_reveal_version: 0, + }), + RuntimeCall::SubtensorModule(SubtensorCall::commit_crv3_mechanism_weights { + netuid, + mecid: MechId::MAIN, + commit: bounded_commit, + reveal_round: 0, + }), + ] + } + + #[test] + fn weight_only_charges_weight_related_calls() { + let netuid = NetUid::from(1); + let expected = ::WeightInfo::check_weights_extension(); + + for call in [ + RuntimeCall::System(SystemCall::remark { remark: vec![] }), + add_stake_call(), + ] { + assert_eq!( + as DispatchExtension>::weight(&call), + Weight::zero() + ); + } + + for call in checked_weight_calls(netuid) { + assert_eq!( + as DispatchExtension>::weight(&call), + expected + ); + } + } + #[test] fn unrelated_calls_pass_through() { new_test_ext(0).execute_with(|| { @@ -360,72 +475,13 @@ mod tests { let netuid = NetUid::from(1); let hotkey = U256::from(1); let coldkey = U256::from(2); - let bounded_commit = - BoundedVec::>::try_from(vec![0]).unwrap(); add_network_disable_commit_reveal(netuid, 1, 0); setup_reserves(netuid, DEFAULT_RESERVE.into(), DEFAULT_RESERVE.into()); SubtensorModule::append_neuron(netuid, &hotkey, 0); crate::Owner::::insert(hotkey, coldkey); SubtensorModule::set_stake_threshold(1_000_000_000_000_u64); - let calls = [ - set_weights_call(netuid, 0), - RuntimeCall::SubtensorModule(SubtensorCall::set_mechanism_weights { - netuid, - mecid: MechId::MAIN, - dests: vec![0], - weights: vec![1], - version_key: 0, - }), - RuntimeCall::SubtensorModule(SubtensorCall::batch_set_weights { - netuids: vec![Compact(netuid)], - weights: vec![vec![(Compact(0_u16), Compact(1_u16))]], - version_keys: vec![Compact(0_u64)], - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_weights { - netuid, - commit_hash: H256::zero(), - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_mechanism_weights { - netuid, - mecid: MechId::MAIN, - commit_hash: H256::zero(), - }), - RuntimeCall::SubtensorModule(SubtensorCall::batch_commit_weights { - netuids: vec![Compact(netuid)], - commit_hashes: vec![H256::zero()], - }), - reveal_weights_call(netuid), - reveal_mechanism_weights_call(netuid, MechId::MAIN), - RuntimeCall::SubtensorModule(SubtensorCall::batch_reveal_weights { - netuid, - uids_list: vec![vec![0]], - values_list: vec![vec![1]], - salts_list: vec![vec![1]], - version_keys: vec![0], - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_weights { - netuid, - commit: bounded_commit.clone(), - reveal_round: 0, - commit_reveal_version: 0, - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_timelocked_mechanism_weights { - netuid, - mecid: MechId::MAIN, - commit: bounded_commit.clone(), - reveal_round: 0, - commit_reveal_version: 0, - }), - RuntimeCall::SubtensorModule(SubtensorCall::commit_crv3_mechanism_weights { - netuid, - mecid: MechId::MAIN, - commit: bounded_commit, - reveal_round: 0, - }), - ]; - - for call in calls { + for call in checked_weight_calls(netuid) { assert_eq!( err(dispatch_with_ext(call, RuntimeOrigin::signed(hotkey))), Error::::NotEnoughStakeToSetWeights.into() diff --git a/pallets/subtensor/src/guards/mod.rs b/pallets/subtensor/src/guards/mod.rs index 485fc65a04..3865352858 100644 --- a/pallets/subtensor/src/guards/mod.rs +++ b/pallets/subtensor/src/guards/mod.rs @@ -5,9 +5,28 @@ mod check_rate_limits; mod check_serving_endpoints; mod check_weights; +use crate::{Call, Config}; +use frame_support::traits::IsSubType; +use sp_runtime::traits::Dispatchable; + pub use check_coldkey_swap::*; pub use check_delegate_take::*; pub use check_evm_key_association::*; pub use check_rate_limits::*; pub use check_serving_endpoints::*; pub use check_weights::*; + +pub(crate) type CallOf = ::RuntimeCall; +pub(crate) type DispatchableOriginOf = as Dispatchable>::RuntimeOrigin; + +pub(crate) fn applicable_call( + call: &CallOf, + applies_to: impl FnOnce(&Call) -> bool, +) -> Option<&Call> +where + T: Config, + CallOf: IsSubType>, +{ + let call = call.is_sub_type()?; + applies_to(call).then_some(call) +} diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index 08e5bb8fdf..a4596c9add 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -2639,15 +2639,7 @@ mod dispatches { ))] pub fn set_reject_locked_alpha(origin: OriginFor, enabled: bool) -> DispatchResult { let coldkey = ensure_signed(origin)?; - AccountFlags::::mutate_exists(&coldkey, |maybe_flags| { - let mut flags = maybe_flags.unwrap_or_default(); - if enabled { - flags &= !crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; - } else { - flags |= crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; - } - *maybe_flags = if flags == 0 { None } else { Some(flags) }; - }); + Self::set_accept_locked_alpha(&coldkey, !enabled); Self::deposit_event(Event::RejectLockedAlphaUpdated { coldkey, enabled }); Ok(()) } diff --git a/pallets/subtensor/src/staking/add_stake.rs b/pallets/subtensor/src/staking/add_stake.rs index 33cadf241b..d2bad04787 100644 --- a/pallets/subtensor/src/staking/add_stake.rs +++ b/pallets/subtensor/src/staking/add_stake.rs @@ -48,6 +48,8 @@ impl Pallet { "do_add_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid:{netuid:?}, stake_to_be_added:{stake_to_be_added:?} )" ); + Self::ensure_add_stake_input_within_swap_limit(netuid, stake_to_be_added)?; + // 2. Validate user input Self::validate_add_stake( &coldkey, @@ -124,6 +126,8 @@ impl Pallet { "do_add_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid:{netuid:?}, stake_to_be_added:{stake_to_be_added:?} )" ); + Self::ensure_add_stake_input_within_swap_limit(netuid, stake_to_be_added)?; + // 2. Calculate the maximum amount that can be executed with price limit let max_amount: TaoBalance = Self::get_max_amount_add(netuid, limit_price)?.into(); let mut possible_stake = stake_to_be_added; @@ -175,11 +179,27 @@ impl Pallet { } } - // Use reverting swap to estimate max limit amount - let order = GetAlphaForTao::::with_amount(u64::MAX); + // Use the largest supported input instead of probing the swap path with u64::MAX. + let max_supported_input = SubnetTAO::::get(netuid).saturating_mul(1_000.into()); + let order = GetAlphaForTao::::with_amount(max_supported_input); let result = T::SwapInterface::swap(netuid.into(), order, limit_price, false, true) .map(|r| r.amount_paid_in.saturating_add(r.fee_paid))?; Ok(result.into()) } + + fn ensure_add_stake_input_within_swap_limit( + netuid: NetUid, + amount: TaoBalance, + ) -> Result<(), Error> { + if !netuid.is_root() && SubnetMechanism::::get(netuid) == 1 { + let max_supported_input = SubnetTAO::::get(netuid).saturating_mul(1_000.into()); + ensure!( + amount <= max_supported_input, + Error::::InsufficientLiquidity + ); + } + + Ok(()) + } } diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index fcf699619d..3e1b2ec8a7 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -471,6 +471,18 @@ impl Pallet { AccountFlags::::get(coldkey) & crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA != 1 } + pub fn set_accept_locked_alpha(coldkey: &T::AccountId, enabled: bool) { + AccountFlags::::mutate_exists(coldkey, |maybe_flags| { + let mut flags = maybe_flags.unwrap_or_default(); + if enabled { + flags |= crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; + } else { + flags &= !crate::ACCOUNT_FLAGS_ACCEPT_LOCKED_ALPHA; + } + *maybe_flags = if flags == 0 { None } else { Some(flags) }; + }); + } + pub fn ensure_can_receive_locked_alpha( coldkey: &T::AccountId, amount: AlphaBalance, @@ -1434,6 +1446,14 @@ impl Pallet { } } + let flags = AccountFlags::::get(old_coldkey); + AccountFlags::::remove(old_coldkey); + if flags != 0 { + AccountFlags::::insert(new_coldkey, flags); + } else { + AccountFlags::::remove(new_coldkey); + } + // Insert locks for the new coldkey and add to the destination aggregate // buckets after the flags have moved. for (netuid, hotkey, old_lock, perpetual_lock) in rolled_locks_to_transfer { diff --git a/pallets/subtensor/src/staking/recycle_alpha.rs b/pallets/subtensor/src/staking/recycle_alpha.rs index 7152c48cdb..aa0eced405 100644 --- a/pallets/subtensor/src/staking/recycle_alpha.rs +++ b/pallets/subtensor/src/staking/recycle_alpha.rs @@ -1,5 +1,6 @@ use super::*; use crate::{Error, system::ensure_signed}; +use frame_support::storage::{TransactionOutcome, with_transaction}; use subtensor_runtime_common::{AlphaBalance, NetUid}; impl Pallet { @@ -132,22 +133,38 @@ impl Pallet { amount: TaoBalance, limit: Option, ) -> DispatchResult { - let alpha = if let Some(limit) = limit { - Self::do_add_stake_limit(origin.clone(), hotkey.clone(), netuid, amount, limit, false)? - } else { - Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)? - }; - - Self::do_burn_alpha(origin, hotkey.clone(), alpha, netuid)?; - - Self::deposit_event(Event::AddStakeBurn { - netuid, - hotkey, - amount, - alpha, - }); - - Ok(()) + with_transaction(|| { + let result = (|| { + let alpha = if let Some(limit) = limit { + Self::do_add_stake_limit( + origin.clone(), + hotkey.clone(), + netuid, + amount, + limit, + false, + )? + } else { + Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)? + }; + + Self::do_burn_alpha(origin, hotkey.clone(), alpha, netuid)?; + + Self::deposit_event(Event::AddStakeBurn { + netuid, + hotkey, + amount, + alpha, + }); + + Ok(()) + })(); + + match result { + Ok(()) => TransactionOutcome::Commit(Ok(())), + Err(err) => TransactionOutcome::Rollback(Err(err)), + } + }) } /// Atomically stakes TAO and recycles the resulting alpha. @@ -160,8 +177,17 @@ impl Pallet { netuid: NetUid, amount: TaoBalance, ) -> Result { - let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; - Self::do_recycle_alpha(origin, hotkey, alpha, netuid) + with_transaction(|| { + let result = (|| { + let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; + Self::do_recycle_alpha(origin, hotkey, alpha, netuid) + })(); + + match result { + Ok(alpha) => TransactionOutcome::Commit(Ok(alpha)), + Err(err) => TransactionOutcome::Rollback(Err(err)), + } + }) } /// Atomically stakes TAO and burns the resulting alpha. Permissionless @@ -173,7 +199,16 @@ impl Pallet { netuid: NetUid, amount: TaoBalance, ) -> Result { - let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; - Self::do_burn_alpha(origin, hotkey, alpha, netuid) + with_transaction(|| { + let result = (|| { + let alpha = Self::do_add_stake(origin.clone(), hotkey.clone(), netuid, amount)?; + Self::do_burn_alpha(origin, hotkey, alpha, netuid) + })(); + + match result { + Ok(alpha) => TransactionOutcome::Commit(Ok(alpha)), + Err(err) => TransactionOutcome::Rollback(Err(err)), + } + }) } } diff --git a/pallets/subtensor/src/staking/remove_stake.rs b/pallets/subtensor/src/staking/remove_stake.rs index cf640dc661..0b460612b9 100644 --- a/pallets/subtensor/src/staking/remove_stake.rs +++ b/pallets/subtensor/src/staking/remove_stake.rs @@ -55,6 +55,7 @@ impl Pallet { let alpha_available = Self::get_stake_for_hotkey_and_coldkey_on_subnet(&hotkey, &coldkey, netuid); let alpha_unstaked = alpha_unstaked.min(alpha_available); + Self::ensure_remove_stake_input_within_swap_limit(netuid, alpha_unstaked)?; // 2. Validate the user input Self::validate_remove_stake( @@ -336,6 +337,8 @@ impl Pallet { "do_remove_stake( origin:{coldkey:?} hotkey:{hotkey:?}, netuid: {netuid:?}, alpha_unstaked:{alpha_unstaked:?} )" ); + Self::ensure_remove_stake_input_within_swap_limit(netuid, alpha_unstaked)?; + // 2. Calculate the maximum amount that can be executed with price limit let max_amount = Self::get_max_amount_remove(netuid, limit_price)?; let mut possible_alpha = alpha_unstaked; @@ -394,14 +397,30 @@ impl Pallet { } } - // Use reverting swap to estimate max limit amount - let order = GetTaoForAlpha::::with_amount(u64::MAX); + // Use the largest supported input instead of probing the swap path with u64::MAX. + let max_supported_input = SubnetAlphaIn::::get(netuid).saturating_mul(1_000.into()); + let order = GetTaoForAlpha::::with_amount(max_supported_input); let result = T::SwapInterface::swap(netuid.into(), order, limit_price.into(), false, true) .map(|r| r.amount_paid_in.saturating_add(r.fee_paid))?; Ok(result) } + fn ensure_remove_stake_input_within_swap_limit( + netuid: NetUid, + amount: AlphaBalance, + ) -> Result<(), Error> { + if !netuid.is_root() && SubnetMechanism::::get(netuid) == 1 { + let max_supported_input = SubnetAlphaIn::::get(netuid).saturating_mul(1_000.into()); + ensure!( + amount <= max_supported_input, + Error::::InsufficientLiquidity + ); + } + + Ok(()) + } + pub fn do_remove_stake_full_limit( origin: OriginFor, hotkey: T::AccountId, diff --git a/pallets/subtensor/src/swap/swap_coldkey.rs b/pallets/subtensor/src/swap/swap_coldkey.rs index 608c61fd57..7b1b4d838c 100644 --- a/pallets/subtensor/src/swap/swap_coldkey.rs +++ b/pallets/subtensor/src/swap/swap_coldkey.rs @@ -23,6 +23,10 @@ impl Pallet { IdentitiesV2::::insert(new_coldkey.clone(), identity); } + // Temporarily allow the destination coldkey to receive this stake even if some of it is + // locked; swap_coldkey_locks will copy the source AccountFlags over afterward. + Self::set_accept_locked_alpha(new_coldkey, true); + for netuid in Self::get_all_subnet_netuids() { Self::transfer_subnet_ownership(netuid, old_coldkey, new_coldkey); Self::transfer_auto_stake_destination(netuid, old_coldkey, new_coldkey); 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/staking.rs b/pallets/subtensor/src/tests/staking.rs index 660b6957d7..3d759e200c 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -776,9 +776,9 @@ fn test_add_stake_insufficient_liquidity() { }); } -/// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_insufficient_liquidity_one_side_ok --exact --show-output +/// cargo test --package pallet-subtensor --lib -- tests::staking::test_add_stake_input_reserve_too_low_fails --exact --show-output #[test] -fn test_add_stake_insufficient_liquidity_one_side_ok() { +fn test_add_stake_input_reserve_too_low_fails() { new_test_ext(1).execute_with(|| { let subnet_owner_coldkey = U256::from(1001); let subnet_owner_hotkey = U256::from(1002); @@ -795,13 +795,17 @@ fn test_add_stake_insufficient_liquidity_one_side_ok() { let reserve_tao = u64::from(mock::SwapMinimumReserve::get()) - 1; mock::setup_reserves(netuid, reserve_tao.into(), reserve_alpha.into()); - // Check the error - assert_ok!(SubtensorModule::add_stake( - RuntimeOrigin::signed(coldkey), - hotkey, - netuid, - amount_staked.into() - )); + // The output-side reserve is sufficient, but the input-side reserve is too small for the + // requested swap under the 1000x input-reserve cap. + assert_noop!( + SubtensorModule::add_stake( + RuntimeOrigin::signed(coldkey), + hotkey, + netuid, + amount_staked.into() + ), + Error::::InsufficientLiquidity + ); }); } @@ -876,7 +880,7 @@ fn test_remove_stake_insufficient_liquidity() { // Mock more liquidity - remove becomes successful SubnetTAO::::insert(netuid, TaoBalance::from(amount_staked + 1)); - SubnetAlphaIn::::insert(netuid, AlphaBalance::from(1)); + SubnetAlphaIn::::insert(netuid, AlphaBalance::from(alpha.to_u64() / 1000 + 1)); assert_ok!(SubtensorModule::remove_stake( RuntimeOrigin::signed(coldkey), hotkey, @@ -3042,14 +3046,14 @@ fn test_max_amount_remove_dynamic() { pallet_subtensor_swap::Error::::PriceLimitExceeded, )), ), - (10_000_000_000, 10_000_000_000, 0, Ok(u64::MAX)), + (10_000_000_000, 10_000_000_000, 0, Ok(10_000_000_000_000)), // Low bounds (numbers are empirical, it is only important that result // is sharply decreasing when limit price increases) - (1_000, 1_000, 0, Ok(u64::MAX)), - (1_001, 1_001, 0, Ok(u64::MAX)), - (1_001, 1_001, 1, Ok(17_472)), - (1_001, 1_001, 2, Ok(17_472)), - (1_001, 1_001, 1_001, Ok(17_472)), + (1_000, 1_000, 0, Ok(1_000_000)), + (1_001, 1_001, 0, Ok(1_001_000)), + (1_001, 1_001, 1, Ok(1_001_000)), + (1_001, 1_001, 2, Ok(1_001_000)), + (1_001, 1_001, 1_001, Ok(1_001_000)), (1_001, 1_001, 10_000, Ok(17_472)), (1_001, 1_001, 100_000, Ok(17_472)), (1_001, 1_001, 1_000_000, Ok(17_472)), @@ -3065,7 +3069,7 @@ fn test_max_amount_remove_dynamic() { Ok(3_030_000_000_000), ), // Normal range values with edge cases and sanity checks - (200_000_000_000, 100_000_000_000, 0, Ok(u64::MAX)), + (200_000_000_000, 100_000_000_000, 0, Ok(100_000_000_000_000)), ( 200_000_000_000, 100_000_000_000, @@ -3153,10 +3157,13 @@ fn test_max_amount_remove_dynamic() { ), Ok(v) => { let v = AlphaBalance::from(v); - assert_abs_diff_eq!( - SubtensorModule::get_max_amount_remove(netuid, limit_price.into()).unwrap(), - v, - epsilon = v / 100.into() + let actual = + SubtensorModule::get_max_amount_remove(netuid, limit_price.into()).unwrap(); + let epsilon = v / 100.into(); + let diff = actual.max(v).saturating_sub(actual.min(v)); + assert!( + diff <= epsilon, + "max remove mismatch: tao_in={tao_in}, alpha_in={alpha_in:?}, limit_price={limit_price}, actual={actual:?}, expected={v:?}, epsilon={epsilon:?}", ); } } @@ -3413,10 +3420,10 @@ fn test_max_amount_move_dynamic_stable() { // The tests below just mimic the remove_stake_limit tests - // 0 price => max is u64::MAX + // 0 price => max is capped at 1000x input reserve assert_eq!( SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, TaoBalance::ZERO), - Ok(AlphaBalance::MAX) + Ok(alpha_in.saturating_mul(1_000.into())) ); // Low price values don't blow things up @@ -3872,6 +3879,33 @@ fn test_add_stake_limit_fill_or_kill() { }); } +#[test] +fn test_add_stake_limit_rejects_input_over_swap_reserve_cap() { + new_test_ext(1).execute_with(|| { + let hotkey_account_id = U256::from(533454); + let coldkey_account_id = U256::from(55454); + + let netuid = add_dynamic_network(&hotkey_account_id, &coldkey_account_id); + let tao_reserve = TaoBalance::from(1_000_u64); + mock::setup_reserves(netuid, tao_reserve, AlphaBalance::from(1_000_000_000_u64)); + + let amount = tao_reserve.saturating_mul(1_000.into()) + TaoBalance::from(1_u64); + add_balance_to_coldkey_account(&coldkey_account_id, amount); + + assert_noop!( + SubtensorModule::add_stake_limit( + RuntimeOrigin::signed(coldkey_account_id), + hotkey_account_id, + netuid, + amount, + ::SwapInterface::max_price(), + true + ), + Error::::InsufficientLiquidity + ); + }); +} + #[test] fn test_add_stake_limit_partial_zero_max_stake_amount_error() { new_test_ext(1).execute_with(|| { @@ -5248,7 +5282,8 @@ fn test_large_swap() { // add network let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); add_balance_to_coldkey_account(&coldkey, 1_000_000_000_000_000_u64.into()); - let tao = TaoBalance::from(100_000_000u64); + let swap_amount = TaoBalance::from(100_000_000_000_000_u64); + let tao = TaoBalance::from(swap_amount.to_u64() / 1000); let alpha = AlphaBalance::from(1_000_000_000_000_000_u64); SubnetTAO::::insert(netuid, tao); SubnetAlphaIn::::insert(netuid, alpha); @@ -5256,7 +5291,6 @@ fn test_large_swap() { // Force the swap to initialize ::SwapInterface::init_swap(netuid, None); - let swap_amount = TaoBalance::from(100_000_000_000_000_u64); assert_ok!(SubtensorModule::add_stake( RuntimeOrigin::signed(coldkey), owner_hotkey, 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_coldkey.rs b/pallets/subtensor/src/tests/swap_coldkey.rs index fd0281ad35..ca269fe3d1 100644 --- a/pallets/subtensor/src/tests/swap_coldkey.rs +++ b/pallets/subtensor/src/tests/swap_coldkey.rs @@ -499,6 +499,105 @@ fn test_swap_coldkey_works() { }); } +#[test] +fn test_swap_coldkey_announced_transfers_locked_alpha() { + new_test_ext(1000).execute_with(|| { + let old_coldkey = U256::from(1); + let new_coldkey = U256::from(2); + let new_coldkey_hash = ::Hashing::hash_of(&new_coldkey); + let hotkey1 = U256::from(1001); + let hotkey2 = U256::from(1002); + let hotkey3 = U256::from(1003); + let ed = ExistentialDeposit::get(); + let swap_cost = SubtensorModule::get_key_swap_cost(); + let min_stake = DefaultMinStake::::get(); + let stake1 = min_stake * 10.into(); + let stake2 = min_stake * 20.into(); + let stake3 = min_stake * 30.into(); + + add_balance_to_coldkey_account(&old_coldkey, swap_cost + stake1 + stake2 + stake3 + ed); + + let ( + netuid1, + _netuid2, + _hotkeys, + hk1_alpha, + _hk2_alpha, + _hk3_alpha, + _total_ck_stake, + _identity, + _balance_before, + _total_stake_before, + ) = comprehensive_setup!( + old_coldkey, + new_coldkey, + new_coldkey_hash, + stake1, + stake2, + stake3, + hotkey1, + hotkey2, + hotkey3, + swap_cost + ed + ); + + let lock_amount = hk1_alpha / 2.into(); + assert!(!lock_amount.is_zero()); + assert_ok!(SubtensorModule::do_lock_stake( + &old_coldkey, + netuid1, + &hotkey1, + lock_amount, + )); + + let old_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &old_coldkey, + netuid1, + ); + let new_stake_before = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &new_coldkey, + netuid1, + ); + let old_lock_before = + Lock::::get((old_coldkey, netuid1, hotkey1)).expect("lock should exist"); + + ColdkeySwapAnnouncements::::insert( + old_coldkey, + (System::block_number(), new_coldkey_hash), + ); + + assert_ok!(SubtensorModule::swap_coldkey_announced( + ::RuntimeOrigin::signed(old_coldkey), + new_coldkey, + )); + + assert!(ColdkeySwapAnnouncements::::get(old_coldkey).is_none()); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &old_coldkey, + netuid1, + ), + AlphaBalance::ZERO + ); + assert_eq!( + SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey1, + &new_coldkey, + netuid1, + ), + old_stake_before + new_stake_before + ); + assert!(Lock::::get((old_coldkey, netuid1, hotkey1)).is_none()); + assert_eq!( + Lock::::get((new_coldkey, netuid1, hotkey1)), + Some(old_lock_before) + ); + }); +} + // cargo test --package pallet-subtensor --lib -- tests::swap_coldkey::test_swap_coldkey_works_with_zero_cost --exact --nocapture #[test] fn test_swap_coldkey_works_with_zero_cost() { diff --git a/pallets/swap/src/pallet/balancer.rs b/pallets/swap/src/pallet/balancer.rs index 2ffd04fdba..244e4ff9eb 100644 --- a/pallets/swap/src/pallet/balancer.rs +++ b/pallets/swap/src/pallet/balancer.rs @@ -149,7 +149,7 @@ impl Balancer { let w1: u128 = self.get_base_weight().deconstruct() as u128; let w2: u128 = self.get_quote_weight().deconstruct() as u128; - let precision = 1024; + let precision = 256; let x_safe = SafeInt::from(x); let w1_safe = SafeInt::from(w1); let w2_safe = SafeInt::from(w2); @@ -187,8 +187,13 @@ impl Balancer { if let Some(result_safe_int) = maybe_result_safe_int && let Some(result_u64) = result_safe_int.to_u64() { - return U64F64::saturating_from_num(result_u64) + let result = U64F64::saturating_from_num(result_u64) .safe_div(U64F64::saturating_from_num(ACCURACY)); + return if dx >= 0 { + result.min(U64F64::from_num(1)) + } else { + result + }; } U64F64::saturating_from_num(0) } @@ -791,6 +796,12 @@ mod tests { let dy1 = y_fixed * (one - e1); let dy2 = y_fixed * (one - e2); + if dx > x.saturating_mul(1_000) { + assert!(e1 <= one); + assert!(e2 <= one); + return; + } + let w1 = perquintill_to_f64(bal.get_base_weight()); let w2 = perquintill_to_f64(bal.get_quote_weight()); let e1_expected = (x as f64 / (x as f64 + dx as f64)).powf(w1 / w2); @@ -928,6 +939,7 @@ mod tests { } } + // cargo test --package pallet-subtensor-swap --lib -- pallet::balancer::tests::test_exp_quote_fuzzy --include-ignored --exact --nocapture #[ignore] #[test] fn test_exp_quote_fuzzy() { @@ -993,7 +1005,7 @@ mod tests { // Print progress let done = counter.fetch_add(1, Ordering::Relaxed) + 1; - if done % 100_000_000 == 0 { + if done % 10_000_000 == 0 { let progress = done as f64 / ITERATIONS as f64 * 100.0; // Replace with println for real-time progress log::debug!("progress = {progress:.4}%"); diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index c3e0b2f1d3..c5b9b11a29 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -14,7 +14,7 @@ use subtensor_swap_interface::{ }; use super::pallet::*; -use super::swap_step::{BasicSwapStep, SwapStep}; +use super::swap_step::{BasicSwapStep, MAX_SWAP_INPUT_RESERVE_MULTIPLIER, SwapStep}; use crate::{pallet::Balancer, pallet::balancer::BalancerError}; impl Pallet { @@ -154,8 +154,13 @@ impl Pallet { transactional::with_transaction(|| { let reserve = Order::ReserveOut::reserve(netuid.into()); - let result = Self::swap_inner::(netuid, order, limit_price, drop_fees) - .map_err(Into::into); + let result = Self::ensure_swap_input_within_reserve_limit::( + netuid, + order.amount(), + drop_fees, + ) + .and_then(|_| Self::swap_inner::(netuid, order, limit_price, drop_fees)) + .map_err(Into::into); if simulate || result.is_err() { // Simulation only @@ -177,6 +182,23 @@ impl Pallet { }) } + fn ensure_swap_input_within_reserve_limit( + netuid: NetUid, + amount: Order::PaidIn, + drop_fees: bool, + ) -> Result<(), Error> + where + Order: OrderT, + { + let fee = Self::calculate_fee_amount(netuid, amount, drop_fees); + let net_amount = amount.saturating_sub(fee); + let input_reserve = Order::ReserveIn::reserve(netuid); + let max_amount = input_reserve.saturating_mul(MAX_SWAP_INPUT_RESERVE_MULTIPLIER.into()); + + ensure!(net_amount <= max_amount, Error::::SwapInputTooLarge); + Ok(()) + } + fn swap_inner( netuid: NetUid, order: Order, diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 1d2fd07c59..b9044d4e82 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -165,6 +165,9 @@ mod pallet { /// Swap reserves are too imbalanced ReservesOutOfBalance, + /// Swap input is too large relative to input-side liquidity + SwapInputTooLarge, + /// The extrinsic is deprecated Deprecated, } diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index 7f10bff65a..3d4d516d1f 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -7,6 +7,8 @@ use subtensor_runtime_common::{AlphaBalance, NetUid, TaoBalance, Token, TokenRes use super::pallet::*; +pub(crate) const MAX_SWAP_INPUT_RESERVE_MULTIPLIER: u64 = 1_000; + /// A struct representing a single swap step with all its parameters and state pub(crate) struct BasicSwapStep where diff --git a/pallets/swap/src/pallet/tests.rs b/pallets/swap/src/pallet/tests.rs index b1071294d3..5e9712d63d 100644 --- a/pallets/swap/src/pallet/tests.rs +++ b/pallets/swap/src/pallet/tests.rs @@ -11,7 +11,7 @@ use sp_arithmetic::Perquintill; use sp_runtime::DispatchError; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{NetUid, Token}; -use subtensor_swap_interface::Order as OrderT; +use subtensor_swap_interface::{Order as OrderT, SwapHandler}; use super::*; use crate::mock::*; @@ -721,6 +721,78 @@ fn test_rollback_works() { }) } +#[test] +fn test_swap_rejects_input_over_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_noop!( + Pallet::::do_swap( + netuid, + GetTaoForAlpha::with_amount(1_000_001), + get_min_price(), + true, + false, + ), + Error::::SwapInputTooLarge + ); + assert_noop!( + Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(1_000_001), + get_max_price(), + true, + false, + ), + Error::::SwapInputTooLarge + ); + }); +} + +#[test] +fn test_sim_swap_rejects_input_over_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_noop!( + Pallet::::sim_swap(netuid, GetTaoForAlpha::with_amount(1_001_000)), + Error::::SwapInputTooLarge + ); + assert_noop!( + Pallet::::sim_swap(netuid, GetAlphaForTao::with_amount(1_001_000)), + Error::::SwapInputTooLarge + ); + }); +} + +#[test] +fn test_swap_allows_input_at_1000x_input_reserve() { + new_test_ext().execute_with(|| { + let netuid = NetUid::from(1); + TaoReserve::set_mock_reserve(netuid, TaoBalance::from(1_000)); + AlphaReserve::set_mock_reserve(netuid, AlphaBalance::from(1_000)); + + assert_ok!(Pallet::::do_swap( + netuid, + GetTaoForAlpha::with_amount(1_000_000), + get_min_price(), + true, + true, + )); + assert_ok!(Pallet::::do_swap( + netuid, + GetAlphaForTao::with_amount(1_000_000), + get_max_price(), + true, + true, + )); + }); +} + #[allow(dead_code)] fn bbox(t: U64F64, a: U64F64, b: U64F64) -> U64F64 { if t < a { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 2c4fa3d686..bed8e8bf00 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -233,7 +233,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: 425, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1,