#![cfg_attr(not(feature = "std"), no_std)] // FRAME use frame_support::{ pallet_prelude::*, storage::{TransactionOutcome, with_transaction}, traits::{ Imbalance, IsSubType, OnUnbalanced, fungible::{ Balanced, Credit, Debt, DecreaseIssuance, Imbalance as FungibleImbalance, IncreaseIssuance, Inspect, }, tokens::{Precision, WithdrawConsequence}, }, weights::{WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial}, }; use pallet_evm::{ AddressMapping, BalanceConverter, Config as EvmConfig, EvmBalance, OnChargeEVMTransaction, }; // Runtime use sp_runtime::{ DispatchError, Perbill, Saturating, traits::{DispatchInfoOf, PostDispatchInfoOf}, transaction_validity::{InvalidTransaction, TransactionValidityError}, }; // Pallets use pallet_subtensor::Call as SubtensorCall; use pallet_transaction_payment::Config as PTPConfig; use pallet_transaction_payment::OnChargeTransaction; use subtensor_swap_interface::SwapHandler; // Misc use core::marker::PhantomData; use smallvec::smallvec; use sp_core::H160; use sp_runtime::traits::SaturatedConversion; use sp_std::vec::Vec; use subtensor_runtime_common::{AlphaBalance, AuthorshipInfo, NetUid, TaoBalance}; // Tests #[cfg(test)] mod tests; type AccountIdOf = ::AccountId; type CallOf = ::RuntimeCall; pub struct LinearWeightToFee; impl WeightToFeePolynomial for LinearWeightToFee { type Balance = TaoBalance; fn polynomial() -> WeightToFeeCoefficients { let coefficient: WeightToFeeCoefficient = WeightToFeeCoefficient { coeff_integer: TaoBalance::new(0), coeff_frac: Perbill::from_parts(500_000), negative: false, degree: 1, }; smallvec![coefficient] as WeightToFeeCoefficients } } /// Trait that allows working with Alpha pub trait AlphaFeeHandler { fn can_withdraw_in_alpha( coldkey: &AccountIdOf, alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, ) -> bool; fn withdraw_in_alpha( coldkey: &AccountIdOf, alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, ) -> Result<(AlphaBalance, TaoBalance, NetUid), TransactionValidityError>; fn get_all_netuids_for_coldkey_and_hotkey( coldkey: &AccountIdOf, hotkey: &AccountIdOf, ) -> Vec; } /// Deduct the transaction fee from the Subtensor Pallet TotalIssuance when charging the transaction /// fee. pub struct TransactionFeeHandler(core::marker::PhantomData); impl Default for TransactionFeeHandler { fn default() -> Self { Self(core::marker::PhantomData) } } type BalancesImbalanceOf = FungibleImbalance< ::Balance, DecreaseIssuance, pallet_balances::Pallet>, IncreaseIssuance, pallet_balances::Pallet>, >; impl OnUnbalanced> for TransactionFeeHandler where T: frame_system::Config + pallet_balances::Config + pallet_subtensor::Config + AuthorshipInfo>, ::Balance: Into + Copy, { fn on_nonzero_unbalanced(imbalance: BalancesImbalanceOf) { if let Some(author) = T::author() { // Pay block author let _ = as Balanced<_>>::resolve(&author, imbalance); } else { // Fallback: if no author, burn (or just drop). drop(imbalance); } } } /// Handle Alpha fees impl AlphaFeeHandler for TransactionFeeHandler where T: AuthorshipInfo>, T: frame_system::Config, T: pallet_subtensor::Config, T: pallet_subtensor_swap::Config, { /// This function checks if tao_amount fee can be withdraw in Alpha currency /// by converting Alpha to TAO using the current pool conditions. /// /// If this function returns true, the transaction will be added to the mempool /// and Alpha will be withdraw from the account, no matter whether transaction /// is successful or not. /// /// If this function returns true, but at the time of execution the Alpha price /// changes and it becomes impossible to pay tx fee with the Alpha balance, /// the transaction still executes and all Alpha is withdrawn from the account. fn can_withdraw_in_alpha( coldkey: &AccountIdOf, alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, ) -> bool { if alpha_vec.len() != 1 { // Multi-subnet alpha fee deduction is prohibited. return false; } if let Some((hotkey, netuid)) = alpha_vec.first() { let alpha_balance = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, ); let alpha_fee = pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao( *netuid, tao_amount.into(), ); alpha_balance >= alpha_fee } else { false } } fn withdraw_in_alpha( coldkey: &AccountIdOf, alpha_vec: &[(AccountIdOf, NetUid)], tao_amount: TaoBalance, ) -> Result<(AlphaBalance, TaoBalance, NetUid), TransactionValidityError> { if alpha_vec.len() != 1 { return Ok((0.into(), 0.into(), NetUid::ROOT)); } if let Some((hotkey, netuid)) = alpha_vec.first() { let alpha_balance = pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, ); let mut alpha_equivalent = pallet_subtensor_swap::Pallet::::get_alpha_amount_for_tao( *netuid, tao_amount.into(), ); if alpha_equivalent.is_zero() { alpha_equivalent = alpha_balance; } let alpha_fee = alpha_equivalent.min(alpha_balance); // Sell alpha_fee and burn received tao (ignore unstake_from_subnet return). if let Some(author) = T::author() { with_transaction( || -> TransactionOutcome> { match pallet_subtensor::Pallet::::unstake_from_subnet( hotkey, coldkey, &author, *netuid, alpha_fee, 0.into(), true, ) { Ok(tao_amount) => TransactionOutcome::Commit(Ok(tao_amount)), Err(err) => TransactionOutcome::Rollback(Err(err)), } }, ) .map(|tao_amount| (alpha_fee, tao_amount, *netuid)) .map_err(|err| { log::warn!("Error withdrawing transaction fee in alpha: {err:?}"); InvalidTransaction::Payment.into() }) } else { // Fallback: no author => no fees (do nothing) Ok((0.into(), 0.into(), NetUid::ROOT)) } } else { Ok((0.into(), 0.into(), NetUid::ROOT)) } } fn get_all_netuids_for_coldkey_and_hotkey( coldkey: &AccountIdOf, hotkey: &AccountIdOf, ) -> Vec { pallet_subtensor::Pallet::::get_all_subnet_netuids() .into_iter() .filter(|netuid| pallet_subtensor::SubtokenEnabled::::get(netuid)) .filter(|netuid| { pallet_subtensor::Pallet::::get_stake_for_hotkey_and_coldkey_on_subnet( hotkey, coldkey, *netuid, ) != 0.into() }) .collect() } } /// Enum that describes either a withdrawn amount of transaction fee in TAO or /// the exact charged Alpha amount. pub enum WithdrawnFee>> { // Contains withdrawn TAO amount Tao(Credit, F>), // Contains withdrawn Alpha amount and resulting swapped TAO Alpha((AlphaBalance, TaoBalance, NetUid)), } /// Custom OnChargeTransaction implementation based on standard FungibleAdapter from transaction_payment /// FRAME pallet /// pub struct SubtensorTxFeeHandler(PhantomData<(F, OU)>); pub struct SubtensorEvmFeeHandler(PhantomData<(F, OU)>); /// This implementation contains the list of calls that require paying transaction /// fees in Alpha impl SubtensorTxFeeHandler { /// Returns Vec<(hotkey, netuid)> if the given call should pay fees in Alpha instead of TAO. /// The vector represents all subnets where this hotkey has any alpha stake. Fees will be /// distributed evenly between subnets in case of multiple subnets. pub fn fees_in_alpha(who: &AccountIdOf, call: &CallOf) -> Vec<(AccountIdOf, NetUid)> where T: frame_system::Config + pallet_subtensor::Config + AuthorshipInfo>, CallOf: IsSubType>, OU: AlphaFeeHandler, { let mut alpha_vec: Vec<(AccountIdOf, NetUid)> = Vec::new(); // Otherwise, switch to Alpha for the extrinsics that assume converting Alpha // to TAO // TODO: Populate the list match call.is_sub_type() { Some(SubtensorCall::remove_stake { hotkey, netuid, .. }) => { alpha_vec.push((hotkey.clone(), *netuid)) } Some(SubtensorCall::remove_stake_limit { hotkey, netuid, .. }) => { alpha_vec.push((hotkey.clone(), *netuid)) } Some(SubtensorCall::remove_stake_full_limit { hotkey, netuid, .. }) => { alpha_vec.push((hotkey.clone(), *netuid)) } Some(SubtensorCall::unstake_all { hotkey, .. }) => { let netuids = OU::get_all_netuids_for_coldkey_and_hotkey(who, hotkey); netuids .into_iter() .for_each(|netuid| alpha_vec.push((hotkey.clone(), netuid))); } Some(SubtensorCall::unstake_all_alpha { hotkey, .. }) => { let netuids = OU::get_all_netuids_for_coldkey_and_hotkey(who, hotkey); netuids .into_iter() .for_each(|netuid| alpha_vec.push((hotkey.clone(), netuid))); } Some(SubtensorCall::move_stake { origin_hotkey, destination_hotkey: _, origin_netuid, .. }) => alpha_vec.push((origin_hotkey.clone(), *origin_netuid)), Some(SubtensorCall::transfer_stake { destination_coldkey: _, hotkey, origin_netuid, .. }) => alpha_vec.push((hotkey.clone(), *origin_netuid)), Some(SubtensorCall::swap_stake { hotkey, origin_netuid, .. }) => alpha_vec.push((hotkey.clone(), *origin_netuid)), Some(SubtensorCall::swap_stake_limit { hotkey, origin_netuid, .. }) => alpha_vec.push((hotkey.clone(), *origin_netuid)), Some(SubtensorCall::swap_hotkey { hotkey, new_hotkey: _, netuid, }) => match netuid { Some(netuid) => alpha_vec.push((hotkey.clone(), *netuid)), None => { let netuids = OU::get_all_netuids_for_coldkey_and_hotkey(who, hotkey); netuids .into_iter() .for_each(|netuid| alpha_vec.push((hotkey.clone(), netuid))); } }, Some(SubtensorCall::swap_hotkey_v2 { hotkey, new_hotkey: _, netuid, keep_stake: _, }) => match netuid { Some(netuid) => alpha_vec.push((hotkey.clone(), *netuid)), None => { let netuids = OU::get_all_netuids_for_coldkey_and_hotkey(who, hotkey); netuids .into_iter() .for_each(|netuid| alpha_vec.push((hotkey.clone(), netuid))); } }, Some(SubtensorCall::recycle_alpha { hotkey, amount: _, netuid, }) => alpha_vec.push((hotkey.clone(), *netuid)), Some(SubtensorCall::burn_alpha { hotkey, amount: _, netuid, }) => alpha_vec.push((hotkey.clone(), *netuid)), _ => {} } alpha_vec } } impl OnChargeTransaction for SubtensorTxFeeHandler where T: PTPConfig + pallet_subtensor::Config + AuthorshipInfo>, CallOf: IsSubType>, F: Balanced, OU: OnUnbalanced> + AlphaFeeHandler, >>::Balance: Into + From, { type LiquidityInfo = Option>; type Balance = ::AccountId>>::Balance; fn withdraw_fee( who: &AccountIdOf, call: &CallOf, _dispatch_info: &DispatchInfoOf>, fee: Self::Balance, _tip: Self::Balance, ) -> Result { if fee.is_zero() { return Ok(None); } // Traditional fees in TAO match F::withdraw( who, fee, Precision::Exact, frame_support::traits::tokens::Preservation::Preserve, frame_support::traits::tokens::Fortitude::Polite, ) { Ok(imbalance) => Ok(Some(WithdrawnFee::Tao(imbalance))), Err(_) => { let alpha_vec = Self::fees_in_alpha::(who, call); if !alpha_vec.is_empty() { let fee_u64: u64 = fee.saturated_into::(); let (alpha_fee, tao_amount, netuid) = OU::withdraw_in_alpha(who, &alpha_vec, fee_u64.into())?; return Ok(Some(WithdrawnFee::Alpha((alpha_fee, tao_amount, netuid)))); } Err(InvalidTransaction::Payment.into()) } } } fn can_withdraw_fee( who: &AccountIdOf, call: &CallOf, _dispatch_info: &DispatchInfoOf>, fee: Self::Balance, _tip: Self::Balance, ) -> Result<(), TransactionValidityError> { if fee.is_zero() { return Ok(()); } // Prefer traditional fees in TAO match F::can_withdraw(who, fee) { WithdrawConsequence::Success => Ok(()), _ => { // Fallback to fees in Alpha if possible let alpha_vec = Self::fees_in_alpha::(who, call); if !alpha_vec.is_empty() { let fee_u64: u64 = fee.saturated_into::(); if OU::can_withdraw_in_alpha(who, &alpha_vec, fee_u64.into()) { return Ok(()); } } Err(InvalidTransaction::Payment.into()) } } } fn correct_and_deposit_fee( who: &AccountIdOf, _dispatch_info: &DispatchInfoOf>, _post_info: &PostDispatchInfoOf>, corrected_fee: Self::Balance, tip: Self::Balance, already_withdrawn: Self::LiquidityInfo, ) -> Result<(), TransactionValidityError> { if let Some(withdrawn) = already_withdrawn { // Fee may be paid in TAO or in Alpha. Only refund and update total issuance for // TAO fees because Alpha fees are charged precisely and do not need any adjustments match withdrawn { WithdrawnFee::Tao(paid) => { // Calculate how much refund we should return let refund_amount = paid.peek().saturating_sub(corrected_fee); // refund to the account that paid the fees if it exists. otherwise, don't refund // anything. let refund_imbalance = if F::total_balance(who) > F::Balance::zero() { F::deposit(who, refund_amount, Precision::BestEffort) .unwrap_or_else(|_| Debt::::zero()) } else { Debt::::zero() }; // merge the imbalance caused by paying the fees and refunding parts of it again. let adjusted_paid: Credit = paid.offset(refund_imbalance).same().map_err(|_| { TransactionValidityError::Invalid(InvalidTransaction::Payment) })?; // Call someone else to handle the imbalance (fee and tip separately) let (tip, fee) = adjusted_paid.split(tip); OU::on_unbalanceds(Some(fee).into_iter().chain(Some(tip))); } WithdrawnFee::Alpha((alpha_fee, tao_amount, netuid)) => { // Block author already received the fee in withdraw_in_alpha, nothing to do here. frame_system::Pallet::::deposit_event( pallet_subtensor::Event::::TransactionFeePaidWithAlpha { who: who.clone(), netuid, alpha_fee, tao_amount, }, ); // Subtensor does not refund Alpha fees, charges are final } } } Ok(()) } #[cfg(feature = "runtime-benchmarks")] fn endow_account(who: &AccountIdOf, amount: Self::Balance) { let _ = F::deposit(who, amount, Precision::BestEffort); } #[cfg(feature = "runtime-benchmarks")] fn minimum_balance() -> Self::Balance { F::minimum_balance() } } impl OnChargeEVMTransaction for SubtensorEvmFeeHandler where T: EvmConfig + pallet_subtensor::Config, F: Balanced, OU: OnUnbalanced>, T::AddressMapping: AddressMapping, >::Balance: From + Into, { type LiquidityInfo = Option>; fn withdraw_fee( who: &H160, fee: EvmBalance, ) -> Result> { if fee.into_u256().is_zero() { return Ok(None); } let account_id = >::into_account_id(*who); let fee_sub = T::BalanceConverter::into_substrate_balance(fee) .ok_or(pallet_evm::Error::::FeeOverflow)?; let imbalance = F::withdraw( &account_id, TaoBalance::from(fee_sub.into_u64_saturating()).into(), Precision::Exact, frame_support::traits::tokens::Preservation::Preserve, frame_support::traits::tokens::Fortitude::Polite, ) .map_err(|_| pallet_evm::Error::::BalanceLow)?; Ok(Some(imbalance)) } fn correct_and_deposit_fee( who: &H160, corrected_fee: EvmBalance, base_fee: EvmBalance, already_withdrawn: Self::LiquidityInfo, ) -> Self::LiquidityInfo { if let Some(paid) = already_withdrawn { let account_id = >::into_account_id(*who); let corrected_fee_sub = T::BalanceConverter::into_substrate_balance(corrected_fee) .unwrap_or_else(|| 0u64.into()); let refund_amount = paid .peek() .saturating_sub(TaoBalance::from(corrected_fee_sub.into_u64_saturating()).into()); let refund_imbalance = F::deposit(&account_id, refund_amount, Precision::BestEffort) .unwrap_or_else(|_| Debt::::zero()); let adjusted_paid = paid .offset(refund_imbalance) .same() .unwrap_or_else(|_| Credit::::zero()); let base_fee_sub = T::BalanceConverter::into_substrate_balance(base_fee) .unwrap_or_else(|| 0u64.into()); let (base_fee_credit, tip) = adjusted_paid.split(TaoBalance::from(base_fee_sub.into_u64_saturating()).into()); OU::on_unbalanced(base_fee_credit); return Some(tip); } None } fn pay_priority_fee(tip: Self::LiquidityInfo) { if let Some(tip) = tip { let author = >::into_account_id( pallet_evm::Pallet::::find_author(), ); let _ = F::resolve(&author, tip); } } }