From cb8489a48a8d6eba470ff5587d5ddfde352c4ab7 Mon Sep 17 00:00:00 2001 From: Vlad Proshchavaiev <32250097+F3Joule@users.noreply.github.com> Date: Tue, 9 Apr 2024 18:34:39 +0300 Subject: [PATCH] feat: Add an ability to use real account energy for proxy (#260) * Add an ability to use real account energy for proxy * Update spec_version to 43 * Fix mocks for energy pallet * Add tests for a new feature --------- Co-authored-by: Alex Siman Co-authored-by: Oleh Mell --- Cargo.lock | 1 + pallets/energy/Cargo.toml | 2 + pallets/energy/src/lib.rs | 81 +++++++++++++++++++++++------- pallets/energy/src/mock.rs | 43 +++++++++++++--- pallets/energy/src/tests.rs | 98 ++++++++++++++++++++++++++++++++++++- runtime/src/lib.rs | 3 +- 6 files changed, 199 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5e3d046c..d7af0d2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5765,6 +5765,7 @@ dependencies = [ "frame-support", "frame-system", "pallet-balances", + "pallet-proxy", "pallet-transaction-payment", "parity-scale-codec", "scale-info", diff --git a/pallets/energy/Cargo.toml b/pallets/energy/Cargo.toml index 07a9dd99..2c3f098a 100644 --- a/pallets/energy/Cargo.toml +++ b/pallets/energy/Cargo.toml @@ -21,6 +21,7 @@ scale-info = { version = "2.2.0", default-features = false, features = ["derive" frame-benchmarking = { optional = true, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } +pallet-proxy = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.40", default-features = false } @@ -45,6 +46,7 @@ std = [ "frame-system/std", "pallet-balances/std", "pallet-transaction-payment/std", + "pallet-proxy/std", "sp-runtime/std", "sp-std/std", ] diff --git a/pallets/energy/src/lib.rs b/pallets/energy/src/lib.rs index 9a82d144..f8e5d6e8 100644 --- a/pallets/energy/src/lib.rs +++ b/pallets/energy/src/lib.rs @@ -26,14 +26,15 @@ pub mod weights; #[frame_support::pallet] pub mod pallet { use frame_support::{ + dispatch::GetDispatchInfo, pallet_prelude::*, - traits::{tokens::Balance, Currency, ExistenceRequirement, WithdrawReasons}, + traits::{tokens::Balance, Currency, ExistenceRequirement, WithdrawReasons, IsSubType}, }; use frame_system::pallet_prelude::*; use pallet_transaction_payment::OnChargeTransaction; use sp_runtime::{ traits::{ - CheckedAdd, CheckedSub, DispatchInfoOf, PostDispatchInfoOf, Saturating, StaticLookup, + CheckedAdd, CheckedSub, Dispatchable, DispatchInfoOf, PostDispatchInfoOf, Saturating, StaticLookup, Zero, }, ArithmeticError, FixedI64, FixedPointNumber, FixedPointOperand, @@ -45,10 +46,18 @@ pub mod pallet { pub(crate) type BalanceOf = ::Balance; #[pallet::config] - pub trait Config: frame_system::Config + pallet_transaction_payment::Config { + pub trait Config: frame_system::Config + pallet_transaction_payment::Config + pallet_proxy::Config { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The overarching call type. + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo + + From> + + IsSubType> + + IsType<::RuntimeCall>; + /// The currency type. type Currency: Currency; @@ -166,13 +175,13 @@ pub mod pallet { let caller = ensure_signed(origin)?; let target = T::Lookup::lookup(target)?; - let caller_balance = T::Currency::free_balance(&caller); + let caller_balance = ::Currency::free_balance(&caller); let caller_balance_after_burn = caller_balance.checked_sub(&burn_amount).ok_or(Error::::NotEnoughBalance)?; let withdraw_reason = WithdrawReasons::all(); - T::Currency::ensure_can_withdraw( + ::Currency::ensure_can_withdraw( &caller, burn_amount, withdraw_reason, @@ -192,7 +201,7 @@ pub mod pallet { Self::ensure_can_capture_energy(&target, captured_energy_amount)?; - let _ = T::Currency::withdraw( + let _ = ::Currency::withdraw( &caller, burn_amount, withdraw_reason, @@ -329,12 +338,18 @@ pub mod pallet { } } + /// Keeps track of whether transaction was paid using proxy's real account energy. + pub enum IsProxy { + Yes(AccountId), + No, + } + /// Keeps track of how the user paid for the transaction. pub enum LiquidityInfo { /// Nothing have been paid. Nothing, /// Transaction have been paid using energy. - Energy(BalanceOf), + Energy(BalanceOf, IsProxy), /// Transaction have been paid using the native method. Native(>::LiquidityInfo), } @@ -351,8 +366,8 @@ pub mod pallet { fn withdraw_fee( who: &T::AccountId, - call: &T::RuntimeCall, - dispatch_info: &DispatchInfoOf, + call: &::RuntimeCall, + dispatch_info: &DispatchInfoOf<::RuntimeCall>, fee: Self::Balance, tip: Self::Balance, ) -> Result { @@ -362,9 +377,26 @@ pub mod pallet { let fee_without_tip = fee.saturating_sub(tip); let energy_fee = Self::native_token_to_energy(fee_without_tip); + + let maybe_proxy_call = ::RuntimeCall::from_ref(call).is_sub_type(); + + let mut is_who_a_proxy = false; + let energy_provider = match maybe_proxy_call { + Some(pallet_proxy::Call::proxy { real, .. }) => { + let real_account = T::Lookup::lookup(real.clone())?; + is_who_a_proxy = pallet_proxy::Pallet::::find_proxy(&real_account, who, None).is_ok(); + + if is_who_a_proxy { + real_account + } else { + who.clone() + } + } + _ => who.clone(), + }; // if we don't have enough energy then fallback to paying with native token. - if Self::energy_balance(&who) < energy_fee { + if Self::energy_balance(&energy_provider) < energy_fee { return T::NativeOnChargeTransaction::withdraw_fee( who, call, @@ -377,7 +409,7 @@ pub mod pallet { if !tip.is_zero() { // TODO: maybe do something with tip? - let _ = T::Currency::withdraw( + let _ = ::Currency::withdraw( who, tip, WithdrawReasons::TIP, @@ -386,10 +418,17 @@ pub mod pallet { .map_err(|_| -> InvalidTransaction { InvalidTransaction::Payment })?; } - match Self::ensure_can_consume_energy(who, energy_fee) { + match Self::ensure_can_consume_energy(&energy_provider, energy_fee) { Ok(()) => { - Self::consume_energy(who, energy_fee); - Ok(LiquidityInfo::Energy(energy_fee)) + Self::consume_energy(&energy_provider, energy_fee); + Ok(LiquidityInfo::Energy( + energy_fee, + if is_who_a_proxy { + IsProxy::Yes(energy_provider) + } else { + IsProxy::No + }, + )) }, Err(_) => Err(InvalidTransaction::Payment.into()), } @@ -397,8 +436,8 @@ pub mod pallet { fn correct_and_deposit_fee( who: &T::AccountId, - dispatch_info: &DispatchInfoOf, - post_info: &PostDispatchInfoOf, + dispatch_info: &DispatchInfoOf<::RuntimeCall>, + post_info: &PostDispatchInfoOf<::RuntimeCall>, corrected_fee: Self::Balance, tip: Self::Balance, already_withdrawn: Self::LiquidityInfo, @@ -414,17 +453,21 @@ pub mod pallet { tip, fallback_info, ), - LiquidityInfo::Energy(paid) => { + LiquidityInfo::Energy(paid, maybe_proxy) => { let corrected_fee_without_tip = corrected_fee.saturating_sub(tip); let corrected_energy_fee = Self::native_token_to_energy(corrected_fee_without_tip); let refund_amount = paid.saturating_sub(corrected_energy_fee); + let refund_destination = match maybe_proxy { + IsProxy::Yes(ref energy_provider) => energy_provider, + IsProxy::No => who, + }; - Self::capture_energy(who, refund_amount); + Self::capture_energy(refund_destination, refund_amount); Ok(()) - }, + } } } } diff --git a/pallets/energy/src/mock.rs b/pallets/energy/src/mock.rs index c12641bb..11fddb01 100644 --- a/pallets/energy/src/mock.rs +++ b/pallets/energy/src/mock.rs @@ -4,7 +4,8 @@ // Full notice is available at https://github.com/dappforce/subsocial-parachain/blob/main/COPYRIGHT // Full license is available at https://github.com/dappforce/subsocial-parachain/blob/main/LICENSE -use codec::Decode; +use scale_info::TypeInfo; +use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ dispatch::{RawOrigin, DispatchInfo}, pallet_prelude::{DispatchClass, Pays, Weight}, @@ -15,17 +16,14 @@ use frame_support::{ WeightToFeePolynomial, }, }; +use frame_support::traits::InstanceFilter; use pallet_balances::NegativeImbalance; use pallet_transaction_payment::{CurrencyAdapter, OnChargeTransaction}; use smallvec::smallvec; use sp_core::H256; use sp_io::TestExternalities; -use sp_runtime::{ - testing::Header, - traits::{BlakeTwo256, DispatchInfoOf, IdentityLookup, One, PostDispatchInfoOf}, - transaction_validity::TransactionValidityError, - FixedI64, Perbill, -}; +use sp_runtime::{testing::Header, traits::{BlakeTwo256, DispatchInfoOf, IdentityLookup, One, PostDispatchInfoOf}, transaction_validity::TransactionValidityError, FixedI64, Perbill, RuntimeDebug}; +use sp_runtime::traits::{ConstU32, ConstU64}; use sp_std::{ cell::RefCell, convert::{TryFrom, TryInto}, @@ -52,6 +50,7 @@ frame_support::construct_runtime!( Balances: pallet_balances, TransactionPayment: pallet_transaction_payment, Energy: pallet_energy, + Proxy: pallet_proxy, } ); @@ -108,6 +107,35 @@ impl pallet_balances::Config for Test { type ReserveIdentifier = (); } +#[derive(Encode, Decode, Clone, Eq, PartialEq, Ord, PartialOrd, TypeInfo, MaxEncodedLen, RuntimeDebug, Default)] +pub enum MockProxyType { + #[default] + Any, +} + +impl InstanceFilter for MockProxyType { + fn filter(&self, _call: &RuntimeCall) -> bool { + match self { + Self::Any => true, + } + } +} + +impl pallet_proxy::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Currency = Balances; + type ProxyType = MockProxyType; + type ProxyDepositBase = ConstU64<0>; + type ProxyDepositFactor = ConstU64<0>; + type MaxProxies = ConstU32<10>; + type WeightInfo = (); + type MaxPending = ConstU32<0>; + type CallHasher = BlakeTwo256; + type AnnouncementDepositBase = ConstU64<0>; + type AnnouncementDepositFactor = ConstU64<0>; +} + /// It returns the input weight as the result. /// /// Equals to: f(x) = x @@ -217,6 +245,7 @@ where impl pallet_energy::Config for Test { type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; type Currency = Balances; type Balance = ::Balance; type DefaultValueCoefficient = ValueCoefficient; diff --git a/pallets/energy/src/tests.rs b/pallets/energy/src/tests.rs index 42c481cc..c8a621df 100644 --- a/pallets/energy/src/tests.rs +++ b/pallets/energy/src/tests.rs @@ -11,7 +11,7 @@ use frame_support::{ }; use pallet_transaction_payment::ChargeTransactionPayment; use sp_runtime::{ - traits::{Dispatchable, SignedExtension}, + traits::{Dispatchable, SignedExtension, Zero}, transaction_validity::{InvalidTransaction, TransactionValidityError}, DispatchError, FixedI64, FixedPointNumber, }; @@ -265,7 +265,31 @@ fn charge_transaction( tip: Balance, pre_validator: PreValidator, ) -> Result<(), ChargeTransactionError> { - let call = frame_system::Call::::remark { remark: vec![] }.into(); + do_charge_transaction(caller, fee, actual_fee, tip, pre_validator, None) +} + +fn charge_transaction_with_proxy( + caller: &AccountId, + real: AccountId, + fee: Balance, + actual_fee: Balance, + tip: Balance, + pre_validator: PreValidator, +) -> Result<(), ChargeTransactionError> { + let call = Box::new(frame_system::Call::::remark { remark: vec![] }.into()); + let proxy_call = pallet_proxy::Call::::proxy { real, force_proxy_type: None, call }; + do_charge_transaction(caller, fee, actual_fee, tip, pre_validator, Some(proxy_call.into())) +} + +fn do_charge_transaction( + caller: &AccountId, + fee: Balance, + actual_fee: Balance, + tip: Balance, + pre_validator: PreValidator, + call: Option, +) -> Result<(), ChargeTransactionError> { + let call = call.unwrap_or_else(|| frame_system::Call::::remark { remark: vec![] }.into()); let info = DispatchInfo { weight: Weight::from_parts(fee, 0), class: DispatchClass::Normal, pays_fee: Pays::Yes }; let post_info = PostDispatchInfo { actual_weight: Some(Weight::from_parts(actual_fee, 0)), pays_fee: Pays::Yes }; @@ -337,6 +361,76 @@ fn charge_transaction_should_pay_with_energy_if_enough() { }); } +#[test] +fn charge_transaction_should_pay_with_energy_if_proxy_caller() { + ExtBuilder::default().value_coefficient(2f64).build().execute_with(|| { + let real_account = account(1); + let proxy_account = account(2); + + assert_ok!(pallet_proxy::Pallet::::add_proxy_delegate( + &real_account, + proxy_account, + MockProxyType::Any, + Zero::zero(), + )); + + set_native_balance(proxy_account, 1000); + set_energy_balance(real_account, 1000); + + assert_ok!(charge_transaction_with_proxy(&proxy_account, real_account, 150, 100, 20, || { + // subtract the expected fees / coefficient from real account + assert_energy_balance!(real_account, 1000 - div_coeff!(150, 2)); + // tip subtracted from the native balance of proxy account + assert_balance!(proxy_account, 1000 - 20); + + assert!( + get_captured_withdraw_fee_args().is_none(), + "Shouldn't go through the fallback OnChargeTransaction" + ); + },),); + + assert_energy_balance!(real_account, 1000 - div_coeff!(100, 2)); + + // subtract the actual (fees + tip) / coefficient + assert_balance!(proxy_account, 1000 - 20); // tip subtracted from the native balance + assert!( + get_corrected_and_deposit_fee_args().is_none(), + "Shouldn't go through the fallback OnChargeTransaction" + ); + }); +} + +#[test] +fn charge_transaction_with_proxy_should_pay_with_native_token_of_caller_if_not_real_proxy() { + ExtBuilder::default().value_coefficient(3.36f64).build().execute_with(|| { + let real_account = account(1); + let proxy_account = account(2); + + set_native_balance(proxy_account, 1000); + set_energy_balance(real_account, 1000); + + assert_ok!(charge_transaction_with_proxy(&proxy_account, real_account, 200, 50, 13, || { + assert_energy_balance!(real_account, 1000); // no change + assert_balance!(proxy_account, 1000 - 200 - 13); // subtract the expected fees + tip + assert_eq!( + get_captured_withdraw_fee_args().unwrap(), + WithdrawFeeArgs { who: proxy_account, fee_with_tip: 200 + 13, tip: 13 } + ); + },),); + + assert_energy_balance!(real_account, 1000); // no change + assert_balance!(proxy_account, 1000 - 50 - 13); // subtract the actual fees + tip + assert!(matches!( + get_corrected_and_deposit_fee_args().unwrap(), + CorrectAndDepositFeeArgs { + who: _proxy_account, + corrected_fee_with_tip: 63, // 50 + 13 + already_withdrawn: _, // ignored + } + )); + }); +} + #[test] fn charge_transaction_should_fail_when_no_native_balance_to_pay_tip() { ExtBuilder::default().build().execute_with(|| { diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 13b8bf3a..df077b74 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -189,7 +189,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("subsocial-parachain"), impl_name: create_runtime_str!("subsocial-parachain"), authoring_version: 1, - spec_version: 42, + spec_version: 43, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 9, @@ -812,6 +812,7 @@ parameter_types! { impl pallet_energy::Config for Runtime { type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; type Currency = Balances; type Balance = Balance; type DefaultValueCoefficient = DefaultValueCoefficient;