-
Notifications
You must be signed in to change notification settings - Fork 36
The account abstraction pallet #130
base: polkadot-v0.9.39
Are you sure you want to change the base?
Changes from all commits
9856d9e
8e049e6
a909f8e
3619ac4
2cec263
d810947
6e71c60
c1a740e
6f5aa27
ee7e727
0973c5a
98700fa
f8149af
b0c4bb0
11ec601
eb56aa2
b494247
0307dcd
34b2d24
c829786
564a2e9
83fa23b
ca3bddc
3a1ae8b
7aca6a1
81f533d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| [package] | ||
| name = "pallet-account" | ||
| version = "0.1.0" | ||
| authors.workspace = true | ||
| edition.workspace = true | ||
| homepage.workspace = true | ||
| repository.workspace = true | ||
|
|
||
| [dependencies] | ||
| log = { workspace = true } | ||
| serde = { workspace = true, optional = true } | ||
|
|
||
| # Substrate | ||
| frame-support = { workspace = true } | ||
| frame-system = { workspace = true } | ||
| parity-scale-codec = { workspace = true } | ||
| scale-info = { workspace = true } | ||
| sp-core = { workspace = true } | ||
| sp-io = { workspace = true } | ||
| sp-runtime = { workspace = true } | ||
| sp-std = { workspace = true } | ||
|
|
||
| # Benchmarks | ||
| frame-benchmarking = { workspace = true, optional = true } | ||
| hex-literal = { workspace = true, optional = true } | ||
|
|
||
| [dev-dependencies] | ||
| assert_matches = { workspace = true } | ||
| hex-literal = { workspace = true } | ||
| pallet-balances = { workspace = true, features = ["std"] } | ||
|
|
||
| [features] | ||
| default = ["std"] | ||
| std = [ | ||
| "parity-scale-codec/std", | ||
| "frame-benchmarking/std", | ||
| "frame-support/std", | ||
| "frame-system/std", | ||
| "scale-info/std", | ||
| "serde", | ||
| "sp-core/std", | ||
| "sp-runtime/std", | ||
| "sp-std/std", | ||
| "sp-io/std", | ||
| ] | ||
| runtime-benchmarks = [ | ||
| "hex-literal", | ||
| "frame-benchmarking", | ||
| "frame-support/runtime-benchmarks", | ||
| "frame-system/runtime-benchmarks", | ||
| ] | ||
| try-runtime = ["frame-support/try-runtime"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // This file is part of Astar. | ||
|
|
||
| // Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| // Astar is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
|
|
||
| // Astar is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU General Public License for more details. | ||
|
|
||
| // You should have received a copy of the GNU General Public License | ||
| // along with Astar. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| use super::*; | ||
|
|
||
| use hex_literal::hex; | ||
| use parity_scale_codec::{Decode, Encode}; | ||
|
|
||
| use frame_benchmarking::{benchmarks, whitelisted_caller}; | ||
| use frame_support::assert_ok; | ||
| use frame_system::{Pallet as System, RawOrigin}; | ||
|
|
||
| const WHITELISTED_D1_NATIVE: [u8; 32] = | ||
| hex!["53ebafb5200b910e56654c867e08747f7dbd3695d6c133af24084b23c3253a67"]; | ||
|
|
||
| /// Assert that the last event equals the provided one. | ||
| fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) { | ||
| System::<T>::assert_last_event(generic_event.into()); | ||
| } | ||
|
|
||
| benchmarks! { | ||
| new_origin { | ||
| let caller = whitelisted_caller::<T::AccountId>(); | ||
| let origin_kind = NativeAndEVMKind::Native.encode(); | ||
| let origin = NativeAndEVM::Native(WHITELISTED_D1_NATIVE.into()).encode(); | ||
| }: _(RawOrigin::Signed(caller.clone()), Decode::decode(&mut &origin_kind[..]).unwrap()) | ||
| verify { | ||
| assert_last_event::<T>( | ||
| Event::<T>::NewOrigin { | ||
| account: caller, | ||
| origin: Decode::decode(&mut &origin[..]).unwrap(), | ||
| }.into() | ||
| ); | ||
| } | ||
|
|
||
| proxy_call { | ||
| let caller = whitelisted_caller::<T::AccountId>(); | ||
| let origin_kind = NativeAndEVMKind::Native.encode(); | ||
| assert_ok!(Pallet::<T>::new_origin( | ||
| RawOrigin::Signed(caller.clone()).into(), | ||
| Decode::decode(&mut &origin_kind[..]).unwrap() | ||
| )); | ||
|
|
||
| let call = Box::new(frame_system::Call::remark { remark: vec![42u8] }.into()); | ||
| let origin = NativeAndEVM::Native(WHITELISTED_D1_NATIVE.into()).encode(); | ||
| }: _(RawOrigin::Signed(caller.clone()), 0, call) | ||
| verify { | ||
| assert_last_event::<T>( | ||
| Event::<T>::ProxyCall { | ||
| origin: Decode::decode(&mut &origin[..]).unwrap(), | ||
| result: Ok(()), | ||
| }.into() | ||
| ); | ||
| } | ||
|
|
||
| impl_benchmark_test_suite!( | ||
| Pallet, | ||
| crate::mock::ExternalityBuilder::build(), | ||
| crate::mock::TestRuntime, | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| // This file is part of Astar. | ||
|
|
||
| // Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. | ||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||
|
|
||
| // Astar is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
|
|
||
| // Astar is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU General Public License for more details. | ||
|
|
||
| // You should have received a copy of the GNU General Public License | ||
| // along with Astar. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| //! # Account abstraction pallet | ||
| //! | ||
| //! ## Overview | ||
| //! | ||
| //! An accout abstraction pallet makes it possible to derive new blockchain based | ||
| //! account for an existing external owned account (seed phrase based). The onchain | ||
| //! account could be derived to multiple address spaces: H160 and SS58. For example, | ||
| //! it makes possible to predictably interact between substrate native account and | ||
| //! EVM smart contracts. | ||
| //! | ||
| //! ## Interface | ||
| //! | ||
| //! ### Dispatchable Function | ||
| //! | ||
| //! * new_origin() - create new origin for account | ||
| //! * proxy_call() - make proxy call with derived account as origin | ||
| //! | ||
|
|
||
| #![cfg_attr(not(feature = "std"), no_std)] | ||
|
|
||
| pub mod origins; | ||
| pub use origins::*; | ||
|
|
||
| pub mod weights; | ||
| pub use weights::*; | ||
|
|
||
| pub use pallet::*; | ||
|
|
||
| #[cfg(feature = "runtime-benchmarks")] | ||
| pub mod benchmarking; | ||
| #[cfg(test)] | ||
| mod mock; | ||
| #[cfg(test)] | ||
| mod tests; | ||
|
|
||
| #[frame_support::pallet] | ||
| #[allow(clippy::module_inception)] | ||
| pub mod pallet { | ||
| use crate::*; | ||
|
|
||
| use frame_support::pallet_prelude::*; | ||
| use frame_support::{ | ||
| dispatch::{Dispatchable, GetDispatchInfo}, | ||
| traits::{Currency, IsSubType, OnKilledAccount, ReservableCurrency}, | ||
| }; | ||
| use frame_system::pallet_prelude::*; | ||
| use sp_std::prelude::*; | ||
|
|
||
| /// The current storage version. | ||
| const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); | ||
|
|
||
| /// Maximum origins that account can hold. | ||
| const MAX_ORIGINS: u32 = 50; | ||
|
|
||
| #[pallet::pallet] | ||
| #[pallet::generate_store(pub(super) trait Store)] | ||
| #[pallet::storage_version(STORAGE_VERSION)] | ||
| pub struct Pallet<T>(PhantomData<T>); | ||
|
|
||
| #[pallet::config] | ||
| pub trait Config: frame_system::Config { | ||
| /// Reservable currency to take origin creation deposit. | ||
| type Currency: ReservableCurrency<Self::AccountId>; | ||
| /// Origin creation security deposit. | ||
| #[pallet::constant] | ||
| type SecurityDeposit: Get<<Self::Currency as Currency<Self::AccountId>>::Balance>; | ||
| /// Custom origin type. | ||
| type CustomOrigin: Parameter + TryInto<Self::AccountId> + MaxEncodedLen; | ||
| /// Parameter that defines different origin options and how to create it. | ||
| type CustomOriginKind: Parameter + OriginDeriving<Self::AccountId, Self::CustomOrigin>; | ||
| /// The runtime origin type. | ||
| type RuntimeOrigin: From<Self::CustomOrigin> | ||
| + From<frame_system::RawOrigin<Self::AccountId>>; | ||
| /// The overarching call type. | ||
| type RuntimeCall: Parameter | ||
| + Dispatchable<RuntimeOrigin = <Self as Config>::RuntimeOrigin> | ||
| + GetDispatchInfo | ||
| + From<frame_system::Call<Self>> | ||
| + IsSubType<Call<Self>> | ||
| + IsType<<Self as frame_system::Config>::RuntimeCall>; | ||
| /// General event type. | ||
| type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; | ||
| /// Weight information for extrinsics in this pallet. | ||
| type WeightInfo: WeightInfo; | ||
| } | ||
|
|
||
| #[pallet::error] | ||
| pub enum Error<T> { | ||
| /// Origin with given index not registered. | ||
| UnregisteredOrigin, | ||
| } | ||
|
|
||
| #[pallet::event] | ||
| #[pallet::generate_deposit(pub(crate) fn deposit_event)] | ||
| pub enum Event<T: Config> { | ||
| NewOrigin { | ||
| account: T::AccountId, | ||
| origin: T::CustomOrigin, | ||
| }, | ||
| ProxyCall { | ||
| origin: T::CustomOrigin, | ||
| result: DispatchResult, | ||
| }, | ||
| } | ||
|
|
||
| #[pallet::origin] | ||
| pub type Origin<T> = <T as Config>::CustomOrigin; | ||
|
|
||
| /// Account origins | ||
| #[pallet::storage] | ||
| pub type AccountOrigin<T: Config> = | ||
| StorageDoubleMap<_, Blake2_128Concat, T::AccountId, Twox64Concat, u32, T::CustomOrigin>; | ||
|
|
||
| /// Account last origin index | ||
| #[pallet::storage] | ||
| pub type AccountLastOrigin<T: Config> = | ||
| StorageMap<_, Twox64Concat, T::AccountId, u32, ValueQuery>; | ||
|
|
||
| impl<T: Config> OnKilledAccount<T::AccountId> for Pallet<T> { | ||
| fn on_killed_account(who: &T::AccountId) { | ||
| let _ = <AccountOrigin<T>>::clear_prefix(who, MAX_ORIGINS, None); | ||
| <AccountLastOrigin<T>>::remove(who); | ||
| } | ||
| } | ||
|
|
||
| #[pallet::call] | ||
| impl<T: Config> Pallet<T> { | ||
| /// Derive new origin for account. | ||
| /// | ||
| /// The dispatch origin for this call must be _Signed_. | ||
| #[pallet::weight( | ||
| T::WeightInfo::new_origin() | ||
| .saturating_add(T::DbWeight::get().reads_writes(1, 2)) | ||
| )] | ||
| #[pallet::call_index(0)] | ||
| pub fn new_origin( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As the origin derivation algorithm is deterministic, could we skip the origin registering? The derived origin could be derived just in time in a Account::proxy_call(alice, derive_kind, index, call);If we do need to cache the derived origin to reduce gas costs, the cache should be cleaned on kill account.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe derivation algorithm is just a sample, it not restricted to be deterministic. Account registration helps to prevent spamming and account address manipulation.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @akru what is the usecase behind allowing single account to have multiple derived address of the same type? The way I see it, it just adds unnecessary complexity.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shaun also commented above(here) about what happens when account is killed. Without any locked or reserved funds, these storage items will remain in the DB even without an owner. |
||
| origin: OriginFor<T>, | ||
| origin_kind: T::CustomOriginKind, | ||
| ) -> DispatchResult { | ||
| let who = ensure_signed(origin)?; | ||
|
|
||
| // reserve security deposit | ||
| T::Currency::reserve(&who, T::SecurityDeposit::get())?; | ||
|
|
||
| let next_index = AccountLastOrigin::<T>::get(&who); | ||
| ensure!( | ||
| next_index < MAX_ORIGINS, | ||
| "account has maximum origins count" | ||
| ); | ||
|
|
||
| let new_origin = origin_kind.derive(&who, next_index); | ||
| AccountOrigin::<T>::insert(&who, next_index, new_origin.clone()); | ||
| AccountLastOrigin::<T>::insert(&who, next_index + 1); | ||
|
Comment on lines
+170
to
+171
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still free - so my comment from 2 months ago is still valid. Check how
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, you pay transaction fee to cover it (prevent spam), it’s not required at all, just one of ways to do it. I’m not sure it’s required for the prototype.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My personal opinion - better charge fee than just lock token forever. Charged fee much more controllable, it could be burn, redistributed or whatever you want. But locked deposit is just locked.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The way it's implemented now - sure, that's viable option. It still needs to be added though since it's missing now.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure something is missing now, what precisely you are talking about?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm talking about payment for the created storage.
Given that our handling of fees is copy/paste of Polkadot, it is how we do things, isn't it? It's been on review for months, and better to do it right immediately then have someone else fix it later, add storage migration logic, etc. Doesn't matter if it's prototype or not, IMO. |
||
|
|
||
| Self::deposit_event(Event::NewOrigin { | ||
| account: who, | ||
| origin: new_origin, | ||
| }); | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Dispatch the given `call` from an account that the sender is authorised. | ||
| /// | ||
| /// The dispatch origin for this call must be _Signed_. | ||
| /// | ||
| /// Parameters: | ||
| /// - `origin_index`: Account origin index for using. | ||
| /// - `call`: The call to be made by the `derived` account. | ||
| #[pallet::weight({ | ||
| let di = call.get_dispatch_info(); | ||
| (T::WeightInfo::proxy_call() | ||
| .saturating_add(T::DbWeight::get().reads(1)) | ||
| .saturating_add(di.weight), | ||
| di.class) | ||
| })] | ||
| #[pallet::call_index(1)] | ||
| pub fn proxy_call( | ||
| origin: OriginFor<T>, | ||
| #[pallet::compact] origin_index: u32, | ||
| call: Box<<T as Config>::RuntimeCall>, | ||
| ) -> DispatchResult { | ||
| let who = ensure_signed(origin)?; | ||
| ensure!( | ||
| origin_index < AccountLastOrigin::<T>::get(&who), | ||
| Error::<T>::UnregisteredOrigin | ||
| ); | ||
|
|
||
| let custom_origin = AccountOrigin::<T>::get(&who, origin_index) | ||
| .ok_or(Error::<T>::UnregisteredOrigin)?; | ||
|
|
||
| let e = if let Ok(id) = custom_origin.clone().try_into() { | ||
| // in case of native dispatch with system signed origin | ||
| call.dispatch(frame_system::RawOrigin::Signed(id).into()) | ||
| } else { | ||
| // in other case dispatch with custom origin | ||
| call.dispatch(custom_origin.clone().into()) | ||
| }; | ||
|
|
||
| Self::deposit_event(Event::ProxyCall { | ||
| origin: custom_origin, | ||
| result: e.map(|_| ()).map_err(|e| e.error), | ||
| }); | ||
|
|
||
| Ok(()) | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.