diff --git a/chain-extensions/src/mock.rs b/chain-extensions/src/mock.rs index 69fb9de089..a1e6334b1b 100644 --- a/chain-extensions/src/mock.rs +++ b/chain-extensions/src/mock.rs @@ -25,7 +25,7 @@ use sp_runtime::{ traits::{BlakeTwo256, Convert, IdentityLookup}, }; use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; -use subtensor_runtime_common::{AlphaCurrency, NetUid, TaoCurrency}; +use subtensor_runtime_common::{AlphaCurrency, AuthorshipInfo, NetUid, TaoCurrency}; type Block = frame_system::mocking::MockBlock; @@ -262,6 +262,14 @@ parameter_types! { pub const AnnouncementDepositFactor: Balance = 1; } +pub struct MockAuthorshipProvider; + +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(U256::from(12345u64)) + } +} + parameter_types! { pub const InitialMinAllowedWeights: u16 = 0; pub const InitialEmissionValue: u16 = 0; @@ -411,6 +419,7 @@ impl pallet_subtensor::Config for Test { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; } // Swap-related parameter types diff --git a/common/src/lib.rs b/common/src/lib.rs index 658f8b2e01..f28ec6d878 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -260,6 +260,12 @@ pub trait BalanceOps { ) -> Result; } +/// Allows to query the current block author +pub trait AuthorshipInfo { + /// Return the current block author + fn author() -> Option; +} + pub mod time { use super::*; diff --git a/pallets/admin-utils/src/tests/mock.rs b/pallets/admin-utils/src/tests/mock.rs index c28755802f..19d2e891cd 100644 --- a/pallets/admin-utils/src/tests/mock.rs +++ b/pallets/admin-utils/src/tests/mock.rs @@ -19,7 +19,7 @@ use sp_runtime::{ }; use sp_std::cmp::Ordering; use sp_weights::Weight; -use subtensor_runtime_common::{NetUid, TaoCurrency}; +use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoCurrency}; type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. @@ -74,6 +74,14 @@ pub type BlockNumber = u64; pub type TestAuthId = test_crypto::TestAuthId; pub type UncheckedExtrinsic = TestXt; +pub struct MockAuthorshipProvider; + +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(U256::from(12345u64)) + } +} + parameter_types! { pub const InitialMinAllowedWeights: u16 = 0; pub const InitialEmissionValue: u16 = 0; @@ -222,6 +230,7 @@ impl pallet_subtensor::Config for Test { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; } parameter_types! { diff --git a/pallets/subtensor/src/macros/config.rs b/pallets/subtensor/src/macros/config.rs index 15a8fe90c8..528e5c9fd5 100644 --- a/pallets/subtensor/src/macros/config.rs +++ b/pallets/subtensor/src/macros/config.rs @@ -8,6 +8,7 @@ mod config { use crate::{CommitmentsInterface, GetAlphaForTao, GetTaoForAlpha}; use pallet_commitments::GetCommitments; + use subtensor_runtime_common::AuthorshipInfo; use subtensor_swap_interface::{SwapEngine, SwapHandler}; /// Configure the pallet by specifying the parameters and types on which it depends. @@ -59,6 +60,9 @@ mod config { /// Rate limit for associating an EVM key. type EvmKeyAssociateRateLimit: Get; + /// Provider of current block author + type AuthorshipProvider: AuthorshipInfo; + /// ================================= /// ==== Initial Value Constants ==== /// ================================= diff --git a/pallets/subtensor/src/macros/dispatches.rs b/pallets/subtensor/src/macros/dispatches.rs index da77292ca7..df1e00dd9a 100644 --- a/pallets/subtensor/src/macros/dispatches.rs +++ b/pallets/subtensor/src/macros/dispatches.rs @@ -711,8 +711,8 @@ mod dispatches { /// #[pallet::call_index(2)] #[pallet::weight((Weight::from_parts(523_200_000, 0) - .saturating_add(T::DbWeight::get().reads(19_u64)) - .saturating_add(T::DbWeight::get().writes(13_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().reads(20_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)), DispatchClass::Normal, Pays::Yes))] pub fn add_stake( origin: OriginFor, hotkey: T::AccountId, @@ -1491,8 +1491,8 @@ mod dispatches { /// - Thrown if key has hit transaction rate limit #[pallet::call_index(84)] #[pallet::weight((Weight::from_parts(486_500_000, 0) - .saturating_add(T::DbWeight::get().reads(34_u64)) - .saturating_add(T::DbWeight::get().writes(22_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().reads(35_u64)) + .saturating_add(T::DbWeight::get().writes(23_u64)), DispatchClass::Normal, Pays::Yes))] pub fn unstake_all_alpha(origin: OriginFor, hotkey: T::AccountId) -> DispatchResult { Self::do_unstake_all_alpha(origin, hotkey) } @@ -1605,8 +1605,8 @@ mod dispatches { #[pallet::call_index(87)] #[pallet::weight(( Weight::from_parts(453_800_000, 0) - .saturating_add(T::DbWeight::get().reads(30_u64)) - .saturating_add(T::DbWeight::get().writes(20_u64)), + .saturating_add(T::DbWeight::get().reads(31_u64)) + .saturating_add(T::DbWeight::get().writes(21_u64)), DispatchClass::Normal, Pays::Yes ))] @@ -1670,8 +1670,8 @@ mod dispatches { /// #[pallet::call_index(88)] #[pallet::weight((Weight::from_parts(713_200_000, 0) - .saturating_add(T::DbWeight::get().reads(19_u64)) - .saturating_add(T::DbWeight::get().writes(13_u64)), DispatchClass::Normal, Pays::Yes))] + .saturating_add(T::DbWeight::get().reads(20_u64)) + .saturating_add(T::DbWeight::get().writes(14_u64)), DispatchClass::Normal, Pays::Yes))] pub fn add_stake_limit( origin: OriginFor, hotkey: T::AccountId, @@ -1779,8 +1779,8 @@ mod dispatches { #[pallet::call_index(90)] #[pallet::weight(( Weight::from_parts(661_800_000, 0) - .saturating_add(T::DbWeight::get().reads(30_u64)) - .saturating_add(T::DbWeight::get().writes(20_u64)), + .saturating_add(T::DbWeight::get().reads(31_u64)) + .saturating_add(T::DbWeight::get().writes(21_u64)), DispatchClass::Normal, Pays::Yes ))] @@ -2567,8 +2567,8 @@ mod dispatches { #[pallet::call_index(132)] #[pallet::weight(( Weight::from_parts(757_700_000, 8556) - .saturating_add(T::DbWeight::get().reads(22_u64)) - .saturating_add(T::DbWeight::get().writes(14_u64)), + .saturating_add(T::DbWeight::get().reads(23_u64)) + .saturating_add(T::DbWeight::get().writes(15_u64)), DispatchClass::Normal, Pays::Yes ))] diff --git a/pallets/subtensor/src/staking/stake_utils.rs b/pallets/subtensor/src/staking/stake_utils.rs index f7528935b4..d030ee2d76 100644 --- a/pallets/subtensor/src/staking/stake_utils.rs +++ b/pallets/subtensor/src/staking/stake_utils.rs @@ -3,7 +3,7 @@ use safe_math::*; use share_pool::{SharePool, SharePoolDataOperations}; use sp_std::ops::Neg; use substrate_fixed::types::{I64F64, I96F32, U64F64, U96F32}; -use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; +use subtensor_runtime_common::{AlphaCurrency, AuthorshipInfo, Currency, NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler, SwapResult}; impl Pallet { @@ -590,6 +590,7 @@ impl Pallet { amount_paid_in: tao, amount_paid_out: tao.to_u64().into(), fee_paid: TaoCurrency::ZERO, + fee_to_block_author: TaoCurrency::ZERO, } }; @@ -643,19 +644,17 @@ impl Pallet { amount_paid_in: alpha, amount_paid_out: alpha.to_u64().into(), fee_paid: AlphaCurrency::ZERO, + fee_to_block_author: AlphaCurrency::ZERO, } }; - // Increase only the protocol Alpha reserve. We only use the sum of - // (SubnetAlphaIn + SubnetAlphaInProvided) in alpha_reserve(), so it is irrelevant - // which one to increase. + // Increase only the protocol Alpha reserve let alpha_delta = swap_result.paid_in_reserve_delta_i64().unsigned_abs(); SubnetAlphaIn::::mutate(netuid, |total| { *total = total.saturating_add(alpha_delta.into()); }); // Decrease Alpha outstanding. - // TODO: Deprecate, not accurate in v3 anymore SubnetAlphaOut::::mutate(netuid, |total| { *total = total.saturating_sub(alpha_delta.into()); }); @@ -706,6 +705,26 @@ impl Pallet { Self::increase_stake_for_hotkey_and_coldkey_on_subnet(hotkey, coldkey, netuid, refund); } + // Swap (in a fee-less way) the block builder alpha fee + let mut fee_outflow = 0_u64; + let maybe_block_author_coldkey = T::AuthorshipProvider::author(); + if let Some(block_author_coldkey) = maybe_block_author_coldkey { + let bb_swap_result = Self::swap_alpha_for_tao( + netuid, + swap_result.fee_to_block_author, + T::SwapInterface::min_price::(), + true, + )?; + Self::add_balance_to_coldkey_account( + &block_author_coldkey, + bb_swap_result.amount_paid_out.into(), + ); + fee_outflow = bb_swap_result.amount_paid_out.into(); + } else { + // block author is not found, burn this alpha + Self::burn_subnet_alpha(netuid, swap_result.fee_to_block_author); + } + // If this is a root-stake if netuid == NetUid::ROOT { // Adjust root claimed value for this hotkey and coldkey. @@ -721,7 +740,12 @@ impl Pallet { // } // Record TAO outflow - Self::record_tao_outflow(netuid, swap_result.amount_paid_out.into()); + Self::record_tao_outflow( + netuid, + swap_result + .amount_paid_out + .saturating_add(fee_outflow.into()), + ); LastColdkeyHotkeyStakeBlock::::insert(coldkey, hotkey, Self::get_current_block_as_u64()); @@ -797,6 +821,21 @@ impl Pallet { StakingHotkeys::::insert(coldkey, staking_hotkeys.clone()); } + // Increase the balance of the block author + let maybe_block_author_coldkey = T::AuthorshipProvider::author(); + if let Some(block_author_coldkey) = maybe_block_author_coldkey { + Self::add_balance_to_coldkey_account( + &block_author_coldkey, + swap_result.fee_to_block_author.into(), + ); + } else { + // Block author is not found - burn this TAO + // Pallet balances total issuance was taken care of when balance was withdrawn for this swap + TotalIssuance::::mutate(|ti| { + *ti = ti.saturating_sub(swap_result.fee_to_block_author); + }); + } + // Record TAO inflow Self::record_tao_inflow(netuid, swap_result.amount_paid_in.into()); diff --git a/pallets/subtensor/src/tests/mock.rs b/pallets/subtensor/src/tests/mock.rs index 62f11ad4d0..94ae2d240d 100644 --- a/pallets/subtensor/src/tests/mock.rs +++ b/pallets/subtensor/src/tests/mock.rs @@ -29,7 +29,7 @@ use sp_runtime::{ }; use sp_std::{cell::RefCell, cmp::Ordering, sync::OnceLock}; use sp_tracing::tracing_subscriber; -use subtensor_runtime_common::{NetUid, TaoCurrency}; +use subtensor_runtime_common::{AuthorshipInfo, NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; type Block = frame_system::mocking::MockBlock; @@ -153,6 +153,14 @@ parameter_types! { pub const SS58Prefix: u8 = 42; } +pub struct MockAuthorshipProvider; + +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(U256::from(12345u64)) + } +} + parameter_types! { pub const InitialMinAllowedWeights: u16 = 0; pub const InitialEmissionValue: u16 = 0; @@ -302,6 +310,7 @@ impl crate::Config for Test { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; } // Swap-related parameter types diff --git a/pallets/subtensor/src/tests/move_stake.rs b/pallets/subtensor/src/tests/move_stake.rs index 77012e6817..9c9f92d8ac 100644 --- a/pallets/subtensor/src/tests/move_stake.rs +++ b/pallets/subtensor/src/tests/move_stake.rs @@ -711,7 +711,7 @@ fn test_do_move_storage_updates() { destination_netuid ), alpha2, - epsilon = 2.into() + epsilon = 50.into() ); }); } diff --git a/pallets/subtensor/src/tests/networks.rs b/pallets/subtensor/src/tests/networks.rs index 5176ae05dc..fe141c040f 100644 --- a/pallets/subtensor/src/tests/networks.rs +++ b/pallets/subtensor/src/tests/networks.rs @@ -829,7 +829,8 @@ fn destroy_alpha_out_many_stakers_complex_distribution() { netuid.into(), min_stake, ); - min_stake.saturating_add(fee) + // Double the fees because fee is calculated for min_stake, not for min_amount + min_stake + fee * 2.into() }; const N: usize = 20; diff --git a/pallets/subtensor/src/tests/staking.rs b/pallets/subtensor/src/tests/staking.rs index 0dfd687959..86ba26c2e2 100644 --- a/pallets/subtensor/src/tests/staking.rs +++ b/pallets/subtensor/src/tests/staking.rs @@ -686,6 +686,9 @@ fn test_remove_stake_total_balance_no_change() { let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Some basic assertions assert_eq!( SubtensorModule::get_total_stake(), @@ -906,6 +909,9 @@ fn test_remove_stake_total_issuance_no_change() { let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Give it some $$$ in his coldkey balance SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); @@ -971,7 +977,7 @@ fn test_remove_stake_total_issuance_no_change() { assert_abs_diff_eq!( SubtensorModule::get_total_stake(), SubtensorModule::get_network_min_lock() + total_fee.into(), - epsilon = TaoCurrency::from(fee) / 1000.into() + epsilon = TaoCurrency::from(fee) / 1000.into() + 1.into() ); // Check if total issuance is equal to the added stake, even after remove stake (no fee, @@ -1633,6 +1639,9 @@ fn test_clear_small_nominations() { let fee = DefaultMinStake::::get().to_u64(); let init_balance = amount + fee + ExistentialDeposit::get(); + // Set fee rate to 0 so that alpha fee is not moved to block producer + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Register hot1. register_ok_neuron(netuid, hot1, cold1, 0); Delegates::::insert(hot1, SubtensorModule::get_min_delegate_take()); @@ -2794,7 +2803,7 @@ fn test_max_amount_add_stable() { // cargo test --package pallet-subtensor --lib -- tests::staking::test_max_amount_add_dynamic --exact --show-output #[test] fn test_max_amount_add_dynamic() { - // tao_in, alpha_in, limit_price, expected_max_swappable + // tao_in, alpha_in, limit_price, expected_max_swappable (with 0.05% fees) [ // Zero handling (no panics) ( @@ -2808,16 +2817,16 @@ fn test_max_amount_add_dynamic() { // Low bounds (100, 100, 1_100_000_000, Ok(4)), (1_000, 1_000, 1_100_000_000, Ok(48)), - (10_000, 10_000, 1_100_000_000, Ok(489)), + (10_000, 10_000, 1_100_000_000, Ok(488)), // Basic math - (1_000_000, 1_000_000, 4_000_000_000, Ok(1_000_000)), - (1_000_000, 1_000_000, 9_000_000_000, Ok(2_000_000)), - (1_000_000, 1_000_000, 16_000_000_000, Ok(3_000_000)), + (1_000_000, 1_000_000, 4_000_000_000, Ok(1_000_500)), + (1_000_000, 1_000_000, 9_000_000_000, Ok(2_001_000)), + (1_000_000, 1_000_000, 16_000_000_000, Ok(3_001_500)), ( 1_000_000_000_000, 1_000_000_000_000, 16_000_000_000, - Ok(3_000_000_000_000), + Ok(3_001_500_000_000), ), // Normal range values with edge cases ( @@ -2865,7 +2874,7 @@ fn test_max_amount_add_dynamic() { 150_000_000_000, 100_000_000_000, 6_000_000_000, - Ok(150_000_000_000), + Ok(150_075_000_000), ), // Miscellaneous overflows and underflows (u64::MAX / 2, u64::MAX, u64::MAX, Ok(u64::MAX)), @@ -2903,7 +2912,7 @@ fn test_max_amount_add_dynamic() { Ok(v) => assert_abs_diff_eq!( SubtensorModule::get_max_amount_add(netuid, limit_price.into()).unwrap(), v, - epsilon = v / 100 + epsilon = v / 10000 ), } }); @@ -2997,7 +3006,7 @@ fn test_max_amount_remove_dynamic() { let subnet_owner_hotkey = U256::from(1002); let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); - // tao_in, alpha_in, limit_price, expected_max_swappable + // tao_in, alpha_in, limit_price, expected_max_swappable (+ 0.05% fee) [ // Zero handling (no panics) ( @@ -3021,22 +3030,22 @@ fn test_max_amount_remove_dynamic() { // 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_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)), - (1_001, 1_001, 10_000_000, Ok(9_013)), - (1_001, 1_001, 100_000_000, Ok(2_165)), + (1_001, 1_001, 1, Ok(17_646)), + (1_001, 1_001, 2, Ok(17_646)), + (1_001, 1_001, 1_001, Ok(17_646)), + (1_001, 1_001, 10_000, Ok(17_646)), + (1_001, 1_001, 100_000, Ok(17_646)), + (1_001, 1_001, 1_000_000, Ok(17_646)), + (1_001, 1_001, 10_000_000, Ok(9_103)), + (1_001, 1_001, 100_000_000, Ok(2_186)), // Basic math - (1_000_000, 1_000_000, 250_000_000, Ok(1_000_000)), - (1_000_000, 1_000_000, 62_500_000, Ok(3_000_000)), + (1_000_000, 1_000_000, 250_000_000, Ok(1_010_000)), + (1_000_000, 1_000_000, 62_500_000, Ok(3_030_000)), ( 1_000_000_000_000, 1_000_000_000_000, 62_500_000, - Ok(3_000_000_000_000), + 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)), @@ -3044,13 +3053,13 @@ fn test_max_amount_remove_dynamic() { 200_000_000_000, 100_000_000_000, 500_000_000, - Ok(100_000_000_000), + Ok(101_000_000_000), ), ( 200_000_000_000, 100_000_000_000, 125_000_000, - Ok(300_000_000_000), + Ok(303_000_000_000), ), ( 200_000_000_000, @@ -3069,15 +3078,15 @@ fn test_max_amount_remove_dynamic() { )), ), (200_000_000_000, 100_000_000_000, 1_999_999_999, Ok(24)), - (200_000_000_000, 100_000_000_000, 1_999_999_990, Ok(252)), + (200_000_000_000, 100_000_000_000, 1_999_999_990, Ok(250)), // Miscellaneous overflows and underflows ( 21_000_000_000_000_000, 1_000_000, 21_000_000_000_000_000, - Ok(17_455_533), + Ok(17_630_088), ), - (21_000_000_000_000_000, 1_000_000, u64::MAX, Ok(67_164)), + (21_000_000_000_000_000, 1_000_000, u64::MAX, Ok(67_000)), ( 21_000_000_000_000_000, 1_000_000_000_000_000_000, @@ -3090,13 +3099,13 @@ fn test_max_amount_remove_dynamic() { 21_000_000_000_000_000, 1_000_000_000_000_000_000, 20_000_000, - Ok(24_800_000_000_000_000), + Ok(24_700_000_000_000_000), ), ( 21_000_000_000_000_000, 21_000_000_000_000_000, 999_999_999, - Ok(10_500_000), + Ok(10_605_000), ), ( 21_000_000_000_000_000, @@ -3435,8 +3444,8 @@ fn test_max_amount_move_dynamic_stable() { assert_abs_diff_eq!( SubtensorModule::get_max_amount_move(dynamic_netuid, stable_netuid, 375_000_000.into()) .unwrap(), - alpha_in, - epsilon = alpha_in / 1000.into(), + alpha_in + alpha_in / 2000.into(), // + 0.05% fee + epsilon = alpha_in / 10_000.into(), ); // Precision test: @@ -4088,6 +4097,10 @@ fn test_remove_99_9991_per_cent_stake_removes_all() { let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Set fee rate to 0 so that alpha fee is not moved to block producer + // and the hotkey stake does drop to 0 + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Give it some $$$ in his coldkey balance SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); @@ -4149,6 +4162,10 @@ fn test_remove_99_9989_per_cent_stake_leaves_a_little() { let netuid = add_dynamic_network(&subnet_owner_hotkey, &subnet_owner_coldkey); register_ok_neuron(netuid, hotkey_account_id, coldkey_account_id, 192213123); + // Set fee rate to 0 so that alpha fee is not moved to block producer + // to avoid false success in this test + pallet_subtensor_swap::FeeRate::::insert(netuid, 0); + // Give it some $$$ in his coldkey balance SubtensorModule::add_balance_to_coldkey_account(&coldkey_account_id, amount); @@ -4846,6 +4863,7 @@ fn test_swap_fees_tao_correctness() { let owner_hotkey = U256::from(1); let owner_coldkey = U256::from(2); let coldkey = U256::from(4); + let block_builder = U256::from(12345u64); let amount = 1_000_000_000; let owner_balance_before = amount * 10; let user_balance_before = amount * 100; @@ -4854,8 +4872,6 @@ fn test_swap_fees_tao_correctness() { let netuid = add_dynamic_network(&owner_hotkey, &owner_coldkey); SubtensorModule::add_balance_to_coldkey_account(&owner_coldkey, owner_balance_before); SubtensorModule::add_balance_to_coldkey_account(&coldkey, user_balance_before); - let fee_rate = pallet_subtensor_swap::FeeRate::::get(NetUid::from(netuid)) as f64 - / u16::MAX as f64; // Forse-set alpha in and tao reserve to make price equal 0.25 let tao_reserve = TaoCurrency::from(100_000_000_000); @@ -4863,8 +4879,11 @@ fn test_swap_fees_tao_correctness() { mock::setup_reserves(netuid, tao_reserve, alpha_in); // Check starting "total TAO" - let total_tao_before = - user_balance_before + owner_balance_before + SubnetTAO::::get(netuid).to_u64(); + let block_builder_balance_before = SubtensorModule::get_coldkey_balance(&block_builder); + let total_tao_before = user_balance_before + + owner_balance_before + + SubnetTAO::::get(netuid).to_u64() + + block_builder_balance_before; // Get alpha for owner assert_ok!(SubtensorModule::add_stake( @@ -4873,7 +4892,6 @@ fn test_swap_fees_tao_correctness() { netuid, amount.into(), )); - let mut fees = (fee_rate * amount as f64) as u64; // Add owner coldkey Alpha as concentrated liquidity // between current price current price + 0.01 @@ -4892,7 +4910,6 @@ fn test_swap_fees_tao_correctness() { ((limit_price * u64::MAX as f64) as u64).into(), true )); - fees += (fee_rate * amount as f64) as u64; let user_alpha = SubtensorModule::get_stake_for_hotkey_and_coldkey_on_subnet( &owner_hotkey, @@ -4906,15 +4923,25 @@ fn test_swap_fees_tao_correctness() { netuid, user_alpha, )); - // Do not add fees because selling fees are in alpha + + // Cause tao fees to propagate to SubnetTAO + let (claimed_tao_fees, _) = + ::SwapInterface::adjust_protocol_liquidity( + netuid, + 0.into(), + 0.into(), + ); + SubnetTAO::::mutate(netuid, |tao| *tao += claimed_tao_fees); // Check ending "total TAO" let owner_balance_after = SubtensorModule::get_coldkey_balance(&owner_coldkey); let user_balance_after = SubtensorModule::get_coldkey_balance(&coldkey); + let block_builder_balance_after = SubtensorModule::get_coldkey_balance(&block_builder); + let total_tao_after = user_balance_after + owner_balance_after + SubnetTAO::::get(netuid).to_u64() - + fees; + + block_builder_balance_after; // Total TAO does not change, leave some epsilon for rounding assert_abs_diff_eq!(total_tao_before, total_tao_after, epsilon = 2); @@ -5012,7 +5039,7 @@ fn test_remove_stake_full_limit_ok() { ); let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert_abs_diff_eq!(new_balance, 9_086_000_000, epsilon = 1_000_000); + assert_abs_diff_eq!(new_balance, 9_086_700_000, epsilon = 1_000_000); }); } @@ -5096,9 +5123,10 @@ fn test_remove_stake_full_limit_ok_with_no_limit_price() { ); let new_balance = SubtensorModule::get_coldkey_balance(&coldkey_account_id); - assert_abs_diff_eq!(new_balance, 9_086_000_000, epsilon = 1_000_000); + assert_abs_diff_eq!(new_balance, 9_086_700_000, epsilon = 1_000_000); }); } + /// This test verifies that minimum stake amount is sufficient to move price and apply /// non-zero staking fees #[test] @@ -5418,11 +5446,15 @@ fn test_staking_records_flow() { )); // Check that outflow has been recorded (less unstaking fees) - let expected_unstake_fee = expected_flow * fee_rate; + // The block builder will receive a fraction of the fees in alpha and will be forced + // to unstake it. So, the additional out-flow is recorded for this. + let unstaked_block_builder_fraction = 1.; + let expected_unstake_fee = + expected_flow * fee_rate * (1. - unstaked_block_builder_fraction); assert_abs_diff_eq!( SubnetTaoFlow::::get(netuid), expected_unstake_fee as i64, - epsilon = (expected_unstake_fee / 100.0) as i64 + epsilon = ((expected_unstake_fee / 100.0) as i64).max(1) ); }); } diff --git a/pallets/swap-interface/src/lib.rs b/pallets/swap-interface/src/lib.rs index 10804bd2e4..6e6ad7101a 100644 --- a/pallets/swap-interface/src/lib.rs +++ b/pallets/swap-interface/src/lib.rs @@ -58,7 +58,8 @@ where fn default_price_limit() -> C; } -#[freeze_struct("d3d0b124fe5a97c8")] +/// Externally used swap result (for RPC) +#[freeze_struct("58ff42da64adce1a")] #[derive(Decode, Encode, PartialEq, Eq, Clone, Debug, TypeInfo)] pub struct SwapResult where @@ -68,6 +69,7 @@ where pub amount_paid_in: PaidIn, pub amount_paid_out: PaidOut, pub fee_paid: PaidIn, + pub fee_to_block_author: PaidIn, } impl SwapResult diff --git a/pallets/swap/src/mock.rs b/pallets/swap/src/mock.rs index 142a59b8df..3c5e424442 100644 --- a/pallets/swap/src/mock.rs +++ b/pallets/swap/src/mock.rs @@ -15,7 +15,6 @@ use sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, }; use std::{cell::RefCell, collections::HashMap}; -// use substrate_fixed::types::U64F64; use subtensor_runtime_common::{ AlphaCurrency, BalanceOps, diff --git a/pallets/swap/src/pallet/impls.rs b/pallets/swap/src/pallet/impls.rs index 8c3fd68209..54efabb19a 100644 --- a/pallets/swap/src/pallet/impls.rs +++ b/pallets/swap/src/pallet/impls.rs @@ -234,12 +234,14 @@ impl Pallet { log::trace!("Delta out: {}", swap_result.delta_out); log::trace!("Fees: {}", swap_result.fee_paid); + log::trace!("Fees for block author: {}", swap_result.fee_to_block_author); log::trace!("======== End Swap ========"); Ok(SwapResult { amount_paid_in: swap_result.delta_in, amount_paid_out: swap_result.delta_out, fee_paid: swap_result.fee_paid, + fee_to_block_author: swap_result.fee_to_block_author, }) } @@ -388,6 +390,7 @@ impl SwapHandler for Pallet { amount_paid_in: actual_amount, amount_paid_out: actual_amount.to_u64().into(), fee_paid: 0.into(), + fee_to_block_author: 0.into(), }) } } diff --git a/pallets/swap/src/pallet/mod.rs b/pallets/swap/src/pallet/mod.rs index 842ba8697c..9fc4aca769 100644 --- a/pallets/swap/src/pallet/mod.rs +++ b/pallets/swap/src/pallet/mod.rs @@ -2,7 +2,7 @@ use core::num::NonZeroU64; use frame_support::{PalletId, pallet_prelude::*, traits::Get}; use frame_system::pallet_prelude::*; -// use safe_math::SafeDiv; +use sp_arithmetic::Perbill; use subtensor_runtime_common::{ AlphaCurrency, BalanceOps, CurrencyReserve, NetUid, SubnetInfo, TaoCurrency, }; @@ -75,6 +75,13 @@ mod pallet { 33 // ~0.05 % } + /// Fee split between pool and block builder. + /// Pool receives the portion returned by this function + #[pallet::type_value] + pub fn DefaultFeeSplit() -> Perbill { + Perbill::zero() + } + /// The fee rate applied to swaps per subnet, normalized value between 0 and u16::MAX #[pallet::storage] pub type FeeRate = StorageMap<_, Twox64Concat, NetUid, u16, ValueQuery, DefaultFeeRate>; diff --git a/pallets/swap/src/pallet/swap_step.rs b/pallets/swap/src/pallet/swap_step.rs index ddb6a7bccc..a161238300 100644 --- a/pallets/swap/src/pallet/swap_step.rs +++ b/pallets/swap/src/pallet/swap_step.rs @@ -2,6 +2,7 @@ use core::marker::PhantomData; use frame_support::ensure; use safe_math::*; +use sp_core::Get; use substrate_fixed::types::U64F64; use subtensor_runtime_common::{AlphaCurrency, Currency, CurrencyReserve, NetUid, TaoCurrency}; @@ -117,17 +118,27 @@ where // Convert amounts, actual swap happens here let delta_out = Self::convert_deltas(self.netuid, self.delta_in); log::trace!("\tDelta Out : {delta_out}"); + let mut fee_to_block_author = 0.into(); if self.delta_in > 0.into() { ensure!(delta_out > 0.into(), Error::::ReservesTooLow); - // Hold the fees - Self::add_fees(self.netuid, self.fee); + // Split fees according to DefaultFeeSplit between liquidity pool and + // validators. In case we want just to forward 100% of fees to the block + // author, it can be done this way: + // ``` + // fee_to_block_author = self.fee; + // ``` + let fee_split = DefaultFeeSplit::get(); + let lp_fee = fee_split.mul_floor(self.fee.to_u64()).into(); + Self::add_fees(self.netuid, lp_fee); + fee_to_block_author = self.fee.saturating_sub(lp_fee); } Ok(SwapStepResult { fee_paid: self.fee, delta_in: self.delta_in, delta_out, + fee_to_block_author, }) } } @@ -265,4 +276,5 @@ where pub(crate) fee_paid: PaidIn, pub(crate) delta_in: PaidIn, pub(crate) delta_out: PaidOut, + pub(crate) fee_to_block_author: PaidIn, } diff --git a/pallets/transaction-fee/src/lib.rs b/pallets/transaction-fee/src/lib.rs index 013903ea8e..21e72e8853 100644 --- a/pallets/transaction-fee/src/lib.rs +++ b/pallets/transaction-fee/src/lib.rs @@ -31,7 +31,7 @@ use core::marker::PhantomData; use smallvec::smallvec; use sp_std::vec::Vec; use substrate_fixed::types::U64F64; -use subtensor_runtime_common::{Balance, Currency, NetUid}; +use subtensor_runtime_common::{AuthorshipInfo, Balance, NetUid}; // Tests #[cfg(test)] @@ -47,7 +47,7 @@ impl WeightToFeePolynomial for LinearWeightToFee { fn polynomial() -> WeightToFeeCoefficients { let coefficient = WeightToFeeCoefficient { coeff_integer: 0, - coeff_frac: Perbill::from_parts(50_000), // 0.05 unit per weight + coeff_frac: Perbill::from_parts(500_000), // 0.5 unit per weight negative: false, degree: 1, }; @@ -95,6 +95,7 @@ where T: frame_system::Config, T: pallet_subtensor::Config, T: pallet_balances::Config, + T: AuthorshipInfo>, { fn on_nonzero_unbalanced( imbalance: FungibleImbalance< @@ -103,11 +104,18 @@ where IncreaseIssuance, pallet_balances::Pallet>, >, ) { - let ti_before = pallet_subtensor::TotalIssuance::::get(); - pallet_subtensor::TotalIssuance::::put( - ti_before.saturating_sub(imbalance.peek().into()), - ); - drop(imbalance); + if let Some(author) = T::author() { + // Pay block author instead of burning. + // One of these is the right call depending on your exact fungible API: + // let _ = pallet_balances::Pallet::::resolve(&author, imbalance); + // or: let _ = pallet_balances::Pallet::::deposit(&author, imbalance.peek(), Precision::BestEffort); + // + // Prefer "resolve" (moves the actual imbalance) if available: + let _ = as Balanced<_>>::resolve(&author, imbalance); + } else { + // Fallback: if no author, burn (or just drop). + drop(imbalance); + } } } diff --git a/pallets/transaction-fee/src/tests/mock.rs b/pallets/transaction-fee/src/tests/mock.rs index 56b7b77308..e6452a0bcd 100644 --- a/pallets/transaction-fee/src/tests/mock.rs +++ b/pallets/transaction-fee/src/tests/mock.rs @@ -21,7 +21,7 @@ use sp_runtime::{ }; use sp_std::cmp::Ordering; use sp_weights::Weight; -pub use subtensor_runtime_common::{AlphaCurrency, Currency, NetUid, TaoCurrency}; +pub use subtensor_runtime_common::{AlphaCurrency, AuthorshipInfo, Currency, NetUid, TaoCurrency}; use subtensor_swap_interface::{Order, SwapHandler}; use crate::SubtensorTxFeeHandler; @@ -29,10 +29,7 @@ use pallet_transaction_payment::{ConstFeeMultiplier, Multiplier}; pub const TAO: u64 = 1_000_000_000; -pub type Block = sp_runtime::generic::Block< - sp_runtime::generic::Header, - UncheckedExtrinsic, ->; +type Block = frame_system::mocking::MockBlock; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -139,6 +136,22 @@ impl pallet_transaction_payment::Config for Test { type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight; } +pub struct MockAuthorshipProvider; + +pub const MOCK_BLOCK_BUILDER: u64 = 12345u64; + +impl AuthorshipInfo for MockAuthorshipProvider { + fn author() -> Option { + Some(U256::from(MOCK_BLOCK_BUILDER)) + } +} + +impl AuthorshipInfo for Test { + fn author() -> Option { + Some(U256::from(MOCK_BLOCK_BUILDER)) + } +} + parameter_types! { pub const InitialMinAllowedWeights: u16 = 0; pub const InitialEmissionValue: u16 = 0; @@ -287,6 +300,7 @@ impl pallet_subtensor::Config for Test { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = MockAuthorshipProvider; } parameter_types! { @@ -517,7 +531,12 @@ where pallet_transaction_payment::ChargeTransactionPayment::::from(0), ); - Some(UncheckedExtrinsic::new_signed(call, nonce, (), extra)) + Some(UncheckedExtrinsic::new_signed( + call, + nonce.into(), + (), + extra, + )) } } @@ -624,6 +643,38 @@ pub(crate) fn swap_alpha_to_tao(netuid: NetUid, alpha: AlphaCurrency) -> (u64, u swap_alpha_to_tao_ext(netuid, alpha, false) } +pub(crate) fn swap_tao_to_alpha_ext( + netuid: NetUid, + tao: TaoCurrency, + drop_fees: bool, +) -> (u64, u64) { + if netuid.is_root() { + return (tao.into(), 0); + } + + let order = GetAlphaForTao::::with_amount(tao); + let result = ::SwapInterface::swap( + netuid.into(), + order, + ::SwapInterface::max_price(), + drop_fees, + true, + ); + + assert_ok!(&result); + + let result = result.unwrap(); + + // we don't want to have silent 0 comparisons in tests + assert!(!result.amount_paid_out.is_zero()); + + (result.amount_paid_out.to_u64(), result.fee_paid.to_u64()) +} + +pub(crate) fn swap_tao_to_alpha(netuid: NetUid, tao: TaoCurrency) -> (u64, u64) { + swap_tao_to_alpha_ext(netuid, tao, false) +} + #[allow(dead_code)] pub fn add_network(netuid: NetUid, tempo: u16) { SubtensorModule::init_new_network(netuid, tempo); diff --git a/pallets/transaction-fee/src/tests/mod.rs b/pallets/transaction-fee/src/tests/mod.rs index 7212c91077..80cfab5fd3 100644 --- a/pallets/transaction-fee/src/tests/mod.rs +++ b/pallets/transaction-fee/src/tests/mod.rs @@ -1210,3 +1210,55 @@ fn test_recycle_alpha_fees_alpha() { assert!(actual_alpha_fee > 0.into()); }); } + +// cargo test --package subtensor-transaction-fee --lib -- tests::test_add_stake_fees_go_to_block_builder --exact --show-output +#[test] +fn test_add_stake_fees_go_to_block_builder() { + new_test_ext().execute_with(|| { + // Portion of swap fees that should go to the block builder + let block_builder_fee_portion = 3. / 5.; + + // Get the block builder balance + let block_builder = U256::from(MOCK_BLOCK_BUILDER); + let block_builder_balance_before = Balances::free_balance(block_builder); + + let stake_amount = TAO; + let sn = setup_subnets(1, 1); + + // Simulate add stake to get the expected TAO fee + let (_, swap_fee) = mock::swap_tao_to_alpha(sn.subnets[0].netuid, stake_amount.into()); + + SubtensorModule::add_balance_to_coldkey_account(&sn.coldkey, stake_amount * 10_u64); + remove_stake_rate_limit_for_tests(&sn.hotkeys[0], &sn.coldkey, sn.subnets[0].netuid); + + // Stake + let balance_before = Balances::free_balance(sn.coldkey); + let call = RuntimeCall::SubtensorModule(pallet_subtensor::Call::add_stake { + hotkey: sn.hotkeys[0], + netuid: sn.subnets[0].netuid, + amount_staked: stake_amount.into(), + }); + + // Dispatch the extrinsic with ChargeTransactionPayment extension + let info = call.get_dispatch_info(); + let ext = pallet_transaction_payment::ChargeTransactionPayment::::from(0); + assert_ok!(ext.dispatch_transaction( + RuntimeOrigin::signed(sn.coldkey).into(), + call, + &info, + 0, + 0, + )); + + let final_balance = Balances::free_balance(sn.coldkey); + let actual_tao_fee = balance_before - stake_amount - final_balance; + assert!(actual_tao_fee > 0); + + // Expect that block builder balance has increased by both the swap fee and the transaction fee + let expected_block_builder_swap_reward = swap_fee as f64 * block_builder_fee_portion; + let expected_tx_fee = 0.000136; // Use very low value for less test flakiness + let block_builder_balance_after = Balances::free_balance(block_builder); + let actual_reward = block_builder_balance_after - block_builder_balance_before; + assert!(actual_reward as f64 >= expected_block_builder_swap_reward + expected_tx_fee); + }); +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 082cf124f0..7359556be6 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -74,7 +74,7 @@ use sp_version::NativeVersion; use sp_version::RuntimeVersion; use substrate_fixed::types::U64F64; use subtensor_precompiles::Precompiles; -use subtensor_runtime_common::{AlphaCurrency, TaoCurrency, time::*, *}; +use subtensor_runtime_common::{AlphaCurrency, AuthorshipInfo, TaoCurrency, time::*, *}; use subtensor_swap_interface::{Order, SwapHandler}; // A few exports that help ease life for downstream crates. @@ -452,6 +452,34 @@ impl pallet_balances::Config for Runtime { type DoneSlashHandler = (); } +// Implement AuthorshipInfo trait for Runtime to satisfy pallet transaction +// fee OnUnbalanced trait bounds +pub struct BlockAuthorFromAura(core::marker::PhantomData); + +impl> BlockAuthorFromAura { + pub fn get_block_author() -> Option { + let binding = frame_system::Pallet::::digest(); + let digest_logs = binding.logs(); + let author_index = F::find_author(digest_logs.iter().filter_map(|d| d.as_pre_runtime()))?; + let authority_id = pallet_aura::Authorities::::get() + .get(author_index as usize)? + .clone(); + Some(AccountId32::new(authority_id.to_raw_vec().try_into().ok()?)) + } +} + +impl AuthorshipInfo for Runtime { + fn author() -> Option { + BlockAuthorFromAura::::get_block_author() + } +} + +impl> AuthorshipInfo for BlockAuthorFromAura { + fn author() -> Option { + Self::get_block_author() + } +} + parameter_types! { pub const OperationalFeeMultiplier: u8 = 5; pub FeeMultiplier: Multiplier = Multiplier::one(); @@ -1097,6 +1125,7 @@ impl pallet_subtensor::Config for Runtime { type MaxImmuneUidsPercentage = MaxImmuneUidsPercentage; type CommitmentsInterface = CommitmentsI; type EvmKeyAssociateRateLimit = EvmKeyAssociateRateLimit; + type AuthorshipProvider = BlockAuthorFromAura; } parameter_types! { @@ -2478,6 +2507,7 @@ impl_runtime_apis! { let price = pallet_subtensor_swap::Pallet::::current_price(netuid.into()); let no_slippage_alpha = U64F64::saturating_from_num(u64::from(tao)).safe_div(price).saturating_to_num::(); let order = pallet_subtensor::GetAlphaForTao::::with_amount(tao); + // fee_to_block_author is included in sr.fee_paid, so it is absent in this calculation pallet_subtensor_swap::Pallet::::sim_swap( netuid.into(), order, @@ -2506,6 +2536,7 @@ impl_runtime_apis! { let price = pallet_subtensor_swap::Pallet::::current_price(netuid.into()); let no_slippage_tao = U64F64::saturating_from_num(u64::from(alpha)).saturating_mul(price).saturating_to_num::(); let order = pallet_subtensor::GetTaoForAlpha::::with_amount(alpha); + // fee_to_block_author is included in sr.fee_paid, so it is absent in this calculation pallet_subtensor_swap::Pallet::::sim_swap( netuid.into(), order,