diff --git a/Cargo.lock b/Cargo.lock index 30fedaa13317..8eb191dec0d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9005,6 +9005,27 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "pallet-account-sponsorship" +version = "0.0.1" +dependencies = [ + "docify 0.2.7", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-meta-tx", + "pallet-transaction-payment", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std 14.0.0", +] + [[package]] name = "pallet-alliance" version = "27.0.0" @@ -10156,6 +10177,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "pallet-account-sponsorship", "pallet-balances", "pallet-transaction-payment", "parity-scale-codec", diff --git a/Cargo.toml b/Cargo.toml index 59f431079bbd..36095dd8002e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -288,6 +288,7 @@ members = [ "substrate/deprecated/hashing", "substrate/deprecated/hashing/proc-macro", "substrate/frame", + "substrate/frame/account-sponsorship", "substrate/frame/alliance", "substrate/frame/asset-conversion", "substrate/frame/asset-rate", diff --git a/substrate/frame/account-sponsorship/Cargo.toml b/substrate/frame/account-sponsorship/Cargo.toml new file mode 100644 index 000000000000..5c3e3816f777 --- /dev/null +++ b/substrate/frame/account-sponsorship/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "pallet-account-sponsorship" +description = "Sponsor accounts' existence until they are independent" +license = "Apache-2.0" +version = "0.0.1" +edition.workspace = true +authors.workspace = true +repository.workspace = true + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["max-encoded-len"] } +docify = "0.2.7" +scale-info = { version = "2.1.2", default-features = false, features = ["derive"] } +serde = { features = ["derive"], optional = true, workspace = true, default-features = true } + +frame-support = { path = "../support", default-features = false } +frame-system = { path = "../system", default-features = false } +sp-core = { path = "../../primitives/core", default-features = false } +sp-runtime = { path = "../../primitives/runtime", default-features = false } +sp-std = { path = "../../primitives/std", default-features = false } +frame-benchmarking = { path = "../benchmarking", default-features = false, optional = true } + +[dev-dependencies] +keyring = { package = "sp-keyring", path = "../../primitives/keyring" } +pallet-balances = { path = "../balances", features = ["std"] } +pallet-meta-tx = { path = "../../frame/meta-tx" } +pallet-transaction-payment = { path = "../../frame/transaction-payment" } +sp-io = { path = "../../primitives/io", features = ["std"] } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "pallet-meta-tx/std", + "pallet-transaction-payment/std", + "scale-info/std", + "serde", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "pallet-balances/runtime-benchmarks", + "pallet-meta-tx/runtime-benchmarks", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "pallet-balances/try-runtime", + "pallet-balances/try-runtime", + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/account-sponsorship/src/lib.rs b/substrate/frame/account-sponsorship/src/lib.rs new file mode 100644 index 000000000000..90036dbe88c5 --- /dev/null +++ b/substrate/frame/account-sponsorship/src/lib.rs @@ -0,0 +1,388 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Account sponsorship pallet. +//! +//! This pallet provides a way to allow nonexistent, virtual accounts to exist before they acquire +//! ED or other means of providing for themselves and storing their nonce in +//! [`frame_system`](frame_system::Account). +//! +//! ## Pallet API +//! +//! The pallet exposes 3 main entry points: +//! - [`sponsor`](Pallet::sponsor) which allows a `sponsor` origin to put up a deposit in order to +//! sponsor the existence of another beneficiary account; +//! - [`withdraw_sponsorship`](Pallet::withdraw_sponsorship), the inverse of +//! [`sponsor`](`Pallet::sponsor`), which enables a `sponsor` origin to release a sponsorship +//! deposit and stop providing for a beneficiary account; +//! - [`become_independent`](Pallet::become_independent) which enables a `beneficiary` origin to +//! renounce the sponsorship provided by a sponsor and release the associated deposit as long as +//! the account will be able to provide for itself without it. +//! +//! Additionally, the pallet implements the [`AccountExistenceProvider`](AccountExistenceProvider) +//! interface, where [`provide`](AccountExistenceProvider::provide) mirrors +//! [`sponsor`](Pallet::sponsor) and provides for accounts using the same deposit mechanics. +//! +//! See the [`pallet`] module for more information about the interfaces this pallet exposes, +//! including its configuration trait, dispatchables, storage items, events and errors. +//! +//! ## Overview +//! +//! The pallet's main function is to allow nonexistent accounts to safely store a nonce. To do this, +//! other accounts, referred to as sponsors from this point onwards, put up funds as deposits. As +//! long as the funds are held, the nonce storage is paid for and the beneficiary accounts can +//! safely exist. +//! +//! The deposit held for a given sponsor is made up of 3 components: +//! - [`T::BaseDeposit`](pallet::Config::BaseDeposit) which any sponsor has to put up if they +//! sponsor one or more accounts. This accounts for the storage used in +//! [`Sponsors`](pallet::Sponsors). +//! - [`T::BeneficiaryDeposit`](pallet::Config::BeneficiaryDeposit) which is held per beneficiary +//! sponsored. This accounts for the storage used in [`Beneficiaries`](pallet::Beneficiaries). +//! - [`AccountDeposit`](pallet::AccountDeposit) which is held per beneficiary sponsored. This +//! accounts for the storage used by the beneficiaries nonce and other account data. Unless set +//! (still to be implemented in a separate extrinsic), this defaults to the existential deposit of +//! the underlying currency. +//! +//! For example, an account which sponsors 3 other accounts would have to deposit an amount equal to +//! `BaseDeposit + 3 * BeneficiaryDeposit + 3 * AccountDeposit`. +//! +//! Sponsorships come with a grace period specified by +//! [`T::GracePeriod`](pallet::Config::GracePeriod). The grace period starts when the sponsorship +//! takes effect and during this time, the sponsor cannot reclaim their sponsorship and associated +//! deposit from the beneficiary. This is a protection mechanism put in place for beneficiaries to +//! give them time to acquire ED or other means of providing for their account. A beneficiary can +//! renounce their sponsorship using the [`become_independent`](Pallet::become_independent) +//! extrinsic before the grace period ends. A [`T::GracePeriod`](pallet::Config::GracePeriod) value +//! of `0` will effectively disable this mechanism. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; +pub use pallet::*; + +use frame_support::{ + pallet_prelude::*, + traits::{ + fungible::{hold::Balanced, Inspect, InspectHold, Mutate, MutateHold}, + tokens::Precision, + Get, + }, +}; +use frame_system::{pallet_prelude::*, DecRefStatus}; +use sp_runtime::traits::{AccountExistenceProvider, Saturating}; +use sp_std::prelude::*; + +pub(crate) type BalanceOf = + <::Currency as Inspect<::AccountId>>::Balance; + +#[frame_support::pallet(dev_mode)] +pub mod pallet { + use super::*; + + #[pallet::config(with_default)] + pub trait Config: frame_system::Config { + /// The overarching event type. + #[pallet::no_default_bounds] + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The currency provider type. + #[pallet::no_default] + type Currency: InspectHold + + Mutate + + MutateHold + + Balanced; + + /// The overarching runtime hold reason. + #[pallet::no_default_bounds] + type RuntimeHoldReason: From; + + /// The amount to be deposited for registering as an account sponsor. + #[pallet::no_default] + type BaseDeposit: Get>; + + /// The amount to be deposited for each sponsored account. + #[pallet::no_default] + type BeneficiaryDeposit: Get>; + + /// Period of time in blocks for which a beneficiary's sponsorship cannot be withdrawn by + /// the sponsor. The beneficiary can renounce the sponsorship before this period ends. A + /// value of `0` will disable the grace period mechanism altogether. + #[pallet::no_default] + type GracePeriod: Get>; + } + + #[pallet::error] + pub enum Error { + /// Invalid. + Invalid, + /// Not enough. + NotEnoughFunds, + /// Not sponsor. + NotSponsor, + /// Not sponsored. + NotSponsored, + /// Wrong sponsor. + WrongSponsor, + // /// Account already exists. + // AlreadyExists, + /// Beneficiary account would be reaped without sponsorship. + Dependent, + /// Sponsorship can't be withdrawn during grace period. + EarlyWithdrawal, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// TODO. + Dummy, + } + + #[pallet::pallet] + pub struct Pallet(_); + + /// Default implementations of [`DefaultConfig`], which can be used to implement [`Config`]. + pub mod config_preludes { + use super::*; + use frame_support::derive_impl; + + pub struct TestDefaultConfig; + + #[derive_impl(frame_system::config_preludes::TestDefaultConfig, no_aggregated_types)] + impl frame_system::DefaultConfig for TestDefaultConfig {} + + #[frame_support::register_default_impl(TestDefaultConfig)] + impl DefaultConfig for TestDefaultConfig { + #[inject_runtime_type] + type RuntimeEvent = (); + #[inject_runtime_type] + type RuntimeHoldReason = (); + } + } + + #[pallet::type_value] + pub fn DepositOnEmpty() -> BalanceOf { + T::Currency::minimum_balance() + } + + /// The amount to be held to provide for an accounts existence. Defaults to the existential + /// deposit of the underlying currency type. + #[pallet::storage] + pub type AccountDeposit = + StorageValue<_, BalanceOf, ValueQuery, DepositOnEmpty>; + + /// Map of sponsors and their respective beneficiary count. + #[pallet::storage] + pub type Sponsors = StorageMap<_, Twox64Concat, T::AccountId, u16>; + + /// Map of the beneficiaries and their respective sponsors and last block of the grace period. + #[pallet::storage] + pub type Beneficiaries = + StorageMap<_, Blake2_128, T::AccountId, (T::AccountId, BlockNumberFor)>; + + /// The reason for this pallet placing a hold on funds. + #[pallet::composite_enum] + pub enum HoldReason { + /// The funds are held as a deposit for registering as an account sponsor. + #[codec(index = 0)] + SponsorshipDeposit, + /// The funds are held as a deposit for sponsoring a beneficiary account. + #[codec(index = 1)] + BeneficiaryDeposit, + /// The funds are held as an existential deposit for a beneficiary account. + #[codec(index = 2)] + ExistentialDeposit, + } + + #[pallet::call] + impl Pallet { + /// Sponsor an account's existence by placing a deposit in this pallet. + /// + /// The deposit is calculated as follows: + /// - if there is at least one beneficiary associated with a sponsor, then `T::BaseDeposit` + /// is held to account for the entry in `Sponsors`; + /// - for each beneficiary, `T::BeneficiaryDeposit` is held to account for the entry in + /// `Beneficiaries`; + /// - for each beneficiary, `ExistentialDeposit` is held to provide for the account's + /// existence and storing its nonce. + /// + /// Accounts can only have one sponsor at a time. Also, sponsored accounts cannot themselves + /// sponsor other accounts. + #[pallet::call_index(0)] + #[pallet::weight(Weight::zero())] + pub fn sponsor(origin: OriginFor, target: T::AccountId) -> DispatchResult { + let who = ensure_signed(origin)?; + // ensure!(!::account_exists(&target), + // Error::::AlreadyExists); + ensure!(!Sponsors::::contains_key(&target), Error::::Invalid); + ensure!(!Beneficiaries::::contains_key(&target), Error::::Invalid); + + Self::add_beneficiary(&who, &target)?; + + Ok(()) + } + + /// Withdraw sponsorship for an account's existence, releasing the associated deposit. + /// + /// The sponsorship can be withdrawn only after `T::GracePeriod` blocks have passed since + /// the sponsorship came into effect. This is to protect the beneficiary and give them a + /// chance to acquire ED or other means of providing for their account, other than the + /// sponsor's deposit. + /// + /// The beneficiary's account might be reaped if the sponsorship is its only provider. + #[pallet::call_index(1)] + #[pallet::weight(Weight::zero())] + pub fn withdraw_sponsorship(origin: OriginFor, target: T::AccountId) -> DispatchResult { + let who = ensure_signed(origin)?; + ensure!(>::account_exists(&target), Error::::Invalid); + let (sponsor, grace_period_end) = + Beneficiaries::::get(&target).ok_or(Error::::NotSponsored)?; + ensure!(sponsor == who, Error::::WrongSponsor); + let now = frame_system::Pallet::::block_number(); + ensure!(now >= grace_period_end, Error::::EarlyWithdrawal); + + Self::remove_beneficiary(&who, &target, true)?; + + Ok(()) + } + + /// Remove an account as a beneficiary of an account existence sponsorship. + /// + /// This will fail if the account cannot exist independently after the sponsorship is + /// removed. + #[pallet::call_index(2)] + #[pallet::weight(Weight::zero())] + pub fn become_independent(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + let (sponsor, _) = Beneficiaries::::get(&who).ok_or(Error::::NotSponsored)?; + + Self::remove_beneficiary(&sponsor, &who, false)?; + + Ok(()) + } + } + + impl Pallet { + // Convenience function to hold both the beneficiary and existential deposits for an + // account. + fn hold_deposit(who: &T::AccountId) -> Result<(), Error> { + T::Currency::hold( + &HoldReason::BeneficiaryDeposit.into(), + who, + T::BeneficiaryDeposit::get(), + ) + .map_err(|_| >::NotEnoughFunds)?; + T::Currency::hold( + &HoldReason::ExistentialDeposit.into(), + who, + AccountDeposit::::get(), + ) + .map_err(|_| >::NotEnoughFunds)?; + Ok(()) + } + + // Convenience function to release both the beneficiary and existential deposits for an + // account. + fn release_deposit(who: &T::AccountId) -> Result<(), Error> { + T::Currency::release( + &HoldReason::BeneficiaryDeposit.into(), + who, + T::BeneficiaryDeposit::get(), + Precision::Exact, + ) + .map_err(|_| >::NotEnoughFunds)?; + T::Currency::release( + &HoldReason::ExistentialDeposit.into(), + who, + AccountDeposit::::get(), + Precision::Exact, + ) + .map_err(|_| >::NotEnoughFunds)?; + Ok(()) + } + + fn add_beneficiary(sponsor: &T::AccountId, beneficiary: &T::AccountId) -> DispatchResult { + Sponsors::::try_mutate(&sponsor, |maybe_beneficiary_count| { + let mut beneficiary_count = match maybe_beneficiary_count { + Some(count) => *count, + None => { + T::Currency::hold( + &HoldReason::SponsorshipDeposit.into(), + &sponsor, + T::BaseDeposit::get(), + ) + .map_err(|_| >::NotEnoughFunds)?; + 0 + }, + }; + beneficiary_count.saturating_inc(); + *maybe_beneficiary_count = Some(beneficiary_count); + Ok::<(), Error>(()) + })?; + Self::hold_deposit(&sponsor)?; + frame_system::Pallet::::inc_providers(&beneficiary); + let grace_period_end = + frame_system::Pallet::::block_number().saturating_add(T::GracePeriod::get()); + Beneficiaries::::insert(beneficiary, (sponsor, grace_period_end)); + Ok(()) + } + + fn remove_beneficiary( + sponsor: &T::AccountId, + beneficiary: &T::AccountId, + expendable: bool, + ) -> DispatchResult { + Sponsors::::try_mutate(&sponsor, |maybe_beneficiary_count| { + let mut beneficiary_count = + maybe_beneficiary_count.ok_or(Error::::NotSponsor)?; + beneficiary_count = beneficiary_count.checked_sub(1).ok_or(Error::::Invalid)?; + *maybe_beneficiary_count = if beneficiary_count == 0 { + T::Currency::release( + &HoldReason::SponsorshipDeposit.into(), + &sponsor, + T::BaseDeposit::get(), + Precision::Exact, + ) + .map_err(|_| >::Invalid)?; + None + } else { + Some(beneficiary_count) + }; + Ok::<(), Error>(()) + })?; + + match frame_system::Pallet::::dec_providers(&beneficiary)? { + DecRefStatus::Reaped if !expendable => return Err(Error::::Dependent.into()), + _ => (), + } + Self::release_deposit(sponsor)?; + Beneficiaries::::remove(beneficiary); + Ok(()) + } + } + + impl AccountExistenceProvider for Pallet { + fn provide(provider: &T::AccountId, beneficiary: &T::AccountId) -> DispatchResult { + ensure!(!Sponsors::::contains_key(beneficiary), Error::::Invalid); + ensure!(!Beneficiaries::::contains_key(beneficiary), Error::::Invalid); + Self::add_beneficiary(provider, beneficiary) + } + } +} diff --git a/substrate/frame/account-sponsorship/src/mock.rs b/substrate/frame/account-sponsorship/src/mock.rs new file mode 100644 index 000000000000..0c332c0103e1 --- /dev/null +++ b/substrate/frame/account-sponsorship/src/mock.rs @@ -0,0 +1,111 @@ +// This file is part of Substrate. +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Mock setup for tests. + +#![cfg(any(test, feature = "runtime-benchmarks"))] + +use crate as pallet_account_sponsorship; +use crate::*; +use frame_support::{ + construct_runtime, derive_impl, + weights::{FixedFee, NoFee}, +}; +use sp_core::ConstU8; +use sp_runtime::{ + traits::{ConstU64, IdentifyAccount, IdentityLookup, Verify}, + MultiSignature, +}; + +pub type Balance = u64; + +pub type Signature = MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; + +pub type MetaTxExtension = ( + frame_system::CheckNonZeroSender, + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckMortality, + frame_system::CheckNonce, +); + +impl pallet_meta_tx::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type Signature = Signature; + type PublicKey = ::Signer; + type Context = (); + type Extension = MetaTxExtension; + type ExistenceProvider = AccountSponsorship; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Runtime { + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = frame_system::mocking::MockBlock; + type AccountData = pallet_balances::AccountData<::Balance>; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Runtime { + type ReserveIdentifier = [u8; 8]; + type AccountStore = System; + type RuntimeHoldReason = RuntimeHoldReason; + type ExistentialDeposit = ConstU64<5>; +} + +pub const TX_FEE: u32 = 10; + +impl pallet_transaction_payment::Config for Runtime { + type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = pallet_transaction_payment::CurrencyAdapter; + type OperationalFeeMultiplier = ConstU8<1>; + type WeightToFee = FixedFee; + type LengthToFee = NoFee; + type FeeMultiplierUpdate = (); +} + +impl 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, + } +); + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let mut ext = sp_io::TestExternalities::new(Default::default()); + ext.execute_with(|| { + frame_system::GenesisConfig::::default().build(); + System::set_block_number(1); + }); + ext +} diff --git a/substrate/frame/account-sponsorship/src/tests.rs b/substrate/frame/account-sponsorship/src/tests.rs new file mode 100644 index 000000000000..4699e11b7dad --- /dev/null +++ b/substrate/frame/account-sponsorship/src/tests.rs @@ -0,0 +1,26 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::*; +use mock::*; + +#[test] +fn todo_test() { + new_test_ext().execute_with(|| { + todo!(); + }); +} 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); + }); +} diff --git a/substrate/primitives/runtime/src/traits/mod.rs b/substrate/primitives/runtime/src/traits/mod.rs index 44860b620891..635639a0eebe 100644 --- a/substrate/primitives/runtime/src/traits/mod.rs +++ b/substrate/primitives/runtime/src/traits/mod.rs @@ -2294,6 +2294,18 @@ impl BlockNumberProvider for () { } } +/// Interface for sponsoring account existence. +pub trait AccountExistenceProvider { + /// Allow a `provider` to provide for a `beneficiary` account. + fn provide(provider: &AccountId, beneficiary: &AccountId) -> DispatchResult; +} + +impl AccountExistenceProvider for () { + fn provide(_: &AccountId, _: &AccountId) -> DispatchResult { + Err(crate::DispatchError::Unavailable) + } +} + #[cfg(test)] mod tests { use super::*;