diff --git a/Cargo.lock b/Cargo.lock index cf445cd3195b..8eb191dec0d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10177,6 +10177,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "pallet-account-sponsorship", "pallet-balances", "pallet-transaction-payment", "parity-scale-codec", diff --git a/substrate/frame/account-sponsorship/src/mock.rs b/substrate/frame/account-sponsorship/src/mock.rs index 6438e1c74d91..0c332c0103e1 100644 --- a/substrate/frame/account-sponsorship/src/mock.rs +++ b/substrate/frame/account-sponsorship/src/mock.rs @@ -51,6 +51,7 @@ impl pallet_meta_tx::Config for Runtime { type PublicKey = ::Signer; type Context = (); type Extension = MetaTxExtension; + type ExistenceProvider = AccountSponsorship; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] diff --git a/substrate/frame/meta-tx/Cargo.toml b/substrate/frame/meta-tx/Cargo.toml index 4a5a0cf2e6c4..cbe7b436b609 100644 --- a/substrate/frame/meta-tx/Cargo.toml +++ b/substrate/frame/meta-tx/Cargo.toml @@ -22,6 +22,7 @@ frame-benchmarking = { path = "../benchmarking", default-features = false, optio [dev-dependencies] pallet-balances = { path = "../balances", features = ["std"] } +pallet-account-sponsorship = { path = "../account-sponsorship", features = ["std"] } sp-io = { path = "../../primitives/io", features = ["std"] } keyring = { package = "sp-keyring", path = "../../primitives/keyring" } pallet-transaction-payment = { path = "../../frame/transaction-payment" } diff --git a/substrate/frame/meta-tx/src/lib.rs b/substrate/frame/meta-tx/src/lib.rs index dea857284f56..55c77ea4526d 100644 --- a/substrate/frame/meta-tx/src/lib.rs +++ b/substrate/frame/meta-tx/src/lib.rs @@ -69,7 +69,8 @@ use frame_support::{ }; use frame_system::pallet_prelude::*; use sp_runtime::traits::{ - Dispatchable, IdentifyAccount, TransactionExtension, TransactionExtensionBase, Verify, + AccountExistenceProvider, Dispatchable, IdentifyAccount, TransactionExtension, + TransactionExtensionBase, Verify, }; use sp_std::prelude::*; @@ -150,6 +151,8 @@ pub mod pallet { /// [frame_system::CheckTxVersion], [frame_system::CheckGenesis], /// [frame_system::CheckMortality], [frame_system::CheckNonce], etc. type Extension: TransactionExtension<::RuntimeCall, Self::Context>; + /// Type to provide for new, nonexistent accounts. + type ExistenceProvider: AccountExistenceProvider; } #[pallet::error] @@ -240,6 +243,74 @@ pub mod pallet { res } + + /// Dispatch a given meta transaction. + /// + /// - `origin`: Can be any kind of origin. + /// - `meta_tx`: Meta Transaction with a target call to be dispatched. + #[pallet::call_index(1)] + #[pallet::weight({ + let dispatch_info = meta_tx.call.get_dispatch_info(); + // TODO: plus T::WeightInfo::dispatch() which must include the weight of T::Extension + ( + dispatch_info.weight, + dispatch_info.class, + ) + })] + pub fn dispatch_creating( + origin: OriginFor, + meta_tx: MetaTxFor, + ) -> DispatchResultWithPostInfo { + let sponsor = ensure_signed(origin)?; + let meta_tx_size = meta_tx.encoded_size(); + + let (signer, signature) = match meta_tx.proof { + Proof::Signed(signer, signature) => (signer, signature), + }; + + let signed_payload = SignedPayloadFor::::new(*meta_tx.call, meta_tx.extension) + .map_err(|_| Error::::Invalid)?; + + if !signed_payload.using_encoded(|payload| signature.verify(payload, &signer)) { + return Err(Error::::BadProof.into()); + } + + if !>::account_exists(&signer) { + T::ExistenceProvider::provide(&sponsor, &signer)?; + } + + let origin = T::RuntimeOrigin::signed(signer); + let (call, extension, _) = signed_payload.deconstruct(); + let info = call.get_dispatch_info(); + let mut ctx = T::Context::default(); + + let (_, val, origin) = T::Extension::validate( + &extension, + origin, + &call, + &info, + meta_tx_size, + &mut ctx, + extension.implicit().map_err(|_| Error::::Invalid)?, + &call, + ) + .map_err(Error::::from)?; + + let pre = + T::Extension::prepare(extension, val, &origin, &call, &info, meta_tx_size, &ctx) + .map_err(Error::::from)?; + + let res = call.dispatch(origin); + let post_info = res.unwrap_or_else(|err| err.post_info); + let pd_res = res.map(|_| ()).map_err(|e| e.error); + + T::Extension::post_dispatch(pre, &info, &post_info, meta_tx_size, &pd_res, &ctx) + .map_err(Error::::from)?; + + Self::deposit_event(Event::Dispatched { result: res }); + + res + } } /// Implements [`From`] for [`Error`] by mapping the relevant error diff --git a/substrate/frame/meta-tx/src/mock.rs b/substrate/frame/meta-tx/src/mock.rs index 70e8d9d220a4..838009daeaf4 100644 --- a/substrate/frame/meta-tx/src/mock.rs +++ b/substrate/frame/meta-tx/src/mock.rs @@ -24,7 +24,7 @@ use frame_support::{ construct_runtime, derive_impl, weights::{FixedFee, NoFee}, }; -use sp_core::ConstU8; +use sp_core::{ConstU64, ConstU8}; use sp_runtime::{traits::IdentityLookup, MultiSignature}; pub type Balance = u64; @@ -62,6 +62,7 @@ impl Config for Runtime { type PublicKey = ::Signer; type Context = (); type Extension = MetaTxExtension; + type ExistenceProvider = AccountSponsorship; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] @@ -90,12 +91,22 @@ impl pallet_transaction_payment::Config for Runtime { type FeeMultiplierUpdate = (); } +impl pallet_account_sponsorship::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type RuntimeHoldReason = RuntimeHoldReason; + type BaseDeposit = ConstU64<5>; + type BeneficiaryDeposit = ConstU64<1>; + type GracePeriod = ConstU64<10>; +} + construct_runtime!( pub enum Runtime { System: frame_system, Balances: pallet_balances, MetaTx: pallet_meta_tx, TxPayment: pallet_transaction_payment, + AccountSponsorship: pallet_account_sponsorship, } ); diff --git a/substrate/frame/meta-tx/src/tests.rs b/substrate/frame/meta-tx/src/tests.rs index b3292eda5df1..0547a252f0c1 100644 --- a/substrate/frame/meta-tx/src/tests.rs +++ b/substrate/frame/meta-tx/src/tests.rs @@ -16,7 +16,7 @@ // limitations under the License. use crate::*; -use frame_support::traits::tokens::fungible::Inspect; +use frame_support::traits::tokens::fungible::{Inspect, InspectHold}; use keyring::AccountKeyring; use mock::*; use sp_io::hashing::blake2_256; @@ -141,3 +141,148 @@ fn sign_and_execute_meta_tx() { assert_eq!(bob_balance - tx_fee, Balances::free_balance(bob_account)); }); } + +#[test] +fn nonexistent_account_meta_tx() { + new_test_ext().execute_with(|| { + // meta tx signer + let alice_keyring = AccountKeyring::Alice; + // meta tx relayer + let bob_keyring = AccountKeyring::Bob; + + let alice_account = AccountId::from(alice_keyring.public()); + let bob_account = AccountId::from(bob_keyring.public()); + + let ed = Balances::minimum_balance(); + let tx_fee: Balance = (2 * TX_FEE).into(); // base tx fee + weight fee + let bob_balance = ed * 100; + + { + // setup initial balance only for bob + Balances::force_set_balance( + RuntimeOrigin::root(), + bob_account.clone().into(), + bob_balance, + ) + .unwrap(); + } + + // Alice builds a meta transaction. + + let remark_call = + RuntimeCall::System(frame_system::Call::remark_with_event { remark: vec![1] }); + let meta_tx_ext: MetaTxExtension = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(sp_runtime::generic::Era::immortal()), + frame_system::CheckNonce::::from( + frame_system::Pallet::::account(&alice_account).nonce, + ), + ); + + let meta_tx_sig = MultiSignature::Sr25519( + (remark_call.clone(), meta_tx_ext.clone(), meta_tx_ext.implicit().unwrap()) + .using_encoded(|e| alice_keyring.sign(&blake2_256(e))), + ); + + let meta_tx = MetaTxFor::::new_signed( + alice_account.clone(), + meta_tx_sig, + remark_call.clone(), + meta_tx_ext.clone(), + ); + + // Encode and share with the world. + let meta_tx_encoded = meta_tx.encode(); + + // Bob acts as meta transaction relayer and as the sponsor for Alice's account existence. + + let meta_tx = MetaTxFor::::decode(&mut &meta_tx_encoded[..]).unwrap(); + // Use meta dispatch which also creates Alice's account. + let call = RuntimeCall::MetaTx(Call::dispatch_creating { meta_tx: meta_tx.clone() }); + let tx_ext: Extension = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckMortality::::from(sp_runtime::generic::Era::immortal()), + frame_system::CheckNonce::::from( + frame_system::Pallet::::account(&bob_account).nonce, + ), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + ); + + let tx_sig = MultiSignature::Sr25519( + (call.clone(), tx_ext.clone(), tx_ext.implicit().unwrap()) + .using_encoded(|e| bob_keyring.sign(&blake2_256(e))), + ); + + let uxt = UncheckedExtrinsic::new_signed(call, bob_account.clone(), tx_sig, tx_ext); + + // Alice's account doesn't exist yet. + assert!(!System::account_exists(&alice_account)); + + // Check Extrinsic validity and apply it. + + let uxt_info = uxt.get_dispatch_info(); + let uxt_len = uxt.using_encoded(|e| e.len()); + + let xt = >>::check( + uxt, + &Default::default(), + ) + .unwrap(); + + let res = xt.apply::(&uxt_info, uxt_len).unwrap(); + + // Asserting the results. + + assert!(res.is_ok()); + + System::assert_has_event(RuntimeEvent::MetaTx(crate::Event::Dispatched { result: res })); + + System::assert_has_event(RuntimeEvent::System(frame_system::Event::Remarked { + sender: alice_account.clone(), + hash: ::Hashing::hash(&[1]), + })); + + // Alice's account has been created and ran the transaction, and Bob paid the transaction + // fee. + assert!(System::account_exists(&alice_account)); + // Nonce is stored and updated. + assert_eq!(System::account_nonce(&alice_account), 1); + assert_eq!(0, Balances::free_balance(&alice_account)); + let provider_deposit = + <::BaseDeposit as sp_core::Get>::get() + + <::BeneficiaryDeposit as sp_core::Get>::get() + + pallet_account_sponsorship::AccountDeposit::::get(); + assert_eq!(bob_balance - tx_fee - provider_deposit, Balances::free_balance(&bob_account)); + assert_eq!(provider_deposit, Balances::total_balance_on_hold(&bob_account)); + + // Alice has just been sponsored, the sponsorship cannot be withdrawn yet. + assert_eq!( + AccountSponsorship::withdraw_sponsorship( + Some(bob_account.clone()).into(), + alice_account.clone() + ) + .unwrap_err(), + pallet_account_sponsorship::Error::::EarlyWithdrawal.into() + ); + + // Let the grace period pass. + let grace_period = + <::GracePeriod as sp_core::Get>::get(); + System::set_block_number(System::block_number() + grace_period); + + // Bob can now withdraw his sponsorship and release the deposit. + frame_support::assert_ok!(AccountSponsorship::withdraw_sponsorship( + Some(bob_account.clone()).into(), + alice_account.clone() + )); + assert!(!System::account_exists(&alice_account)); + assert_eq!(Balances::total_balance_on_hold(&bob_account), 0); + }); +}