// 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. //! # Proxy Pallet //! A pallet allowing accounts to give permission to other accounts to dispatch types of calls from //! their signed origin. //! //! The accounts to which permission is delegated may be required to announce the action that they //! wish to execute some duration prior to execution happens. In this case, the target account may //! reject the announcement and in doing so, veto the execution. //! //! - [`Config`] //! - [`Call`] // Ensure we're `no_std` when compiling for Wasm. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; mod tests; pub mod weights; extern crate alloc; use alloc::{boxed::Box, vec}; use frame::{ prelude::*, traits::{Currency, InstanceFilter, ReservableCurrency}, }; use frame_system::pallet_prelude::BlockNumberFor as SystemBlockNumberFor; pub use pallet::*; use subtensor_macros::freeze_struct; pub use weights::WeightInfo; type CallHashOf = <::CallHasher as Hash>::Output; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; pub type BlockNumberFor = <::BlockNumberProvider as BlockNumberProvider>::BlockNumber; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; /// The parameters under which a particular account has a proxy relationship with some other /// account. #[derive( Encode, Decode, DecodeWithMemTracking, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, MaxEncodedLen, TypeInfo, )] #[freeze_struct("a37bb67fe5520678")] pub struct ProxyDefinition { /// The account which may act on behalf of another. pub delegate: AccountId, /// A value defining the subset of calls that it is allowed to make. pub proxy_type: ProxyType, /// The number of blocks that an announcement must be in place for before the corresponding /// call may be dispatched. If zero, then no announcement is needed. pub delay: BlockNumber, } /// Details surrounding a specific instance of an announcement to make a call. #[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] #[freeze_struct("4c1b5c8c3bc489ad")] pub struct Announcement { /// The account which made the announcement. real: AccountId, /// The hash of the call to be made. call_hash: Hash, /// The height at which the announcement was made. height: BlockNumber, } /// The type of deposit #[derive( Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo, DecodeWithMemTracking, )] pub enum DepositKind { /// Proxy registration deposit Proxies, /// Announcement deposit Announcements, } #[frame::pallet] #[allow(clippy::expect_used)] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet(_); /// Configuration trait. #[pallet::config] pub trait Config: frame_system::Config { /// The overarching call type. type RuntimeCall: Parameter + Dispatchable + GetDispatchInfo + From> + IsSubType> + IsType<::RuntimeCall>; /// The currency mechanism. type Currency: ReservableCurrency; /// A kind of proxy; specified with the proxy and passed in to the `IsProxyable` filter. /// The instance filter determines whether a given call may be proxied under this type. /// /// IMPORTANT: `Default` must be provided and MUST BE the the *most permissive* value. type ProxyType: Parameter + Member + Ord + PartialOrd + frame::traits::InstanceFilter<::RuntimeCall> + Default + MaxEncodedLen; /// The base amount of currency needed to reserve for creating a proxy. /// /// This is held for an additional storage item whose value size is /// `sizeof(Balance)` bytes and whose key size is `sizeof(AccountId)` bytes. #[pallet::constant] type ProxyDepositBase: Get>; /// The amount of currency needed per proxy added. /// /// This is held for adding 32 bytes plus an instance of `ProxyType` more into a /// pre-existing storage value. Thus, when configuring `ProxyDepositFactor` one should take /// into account `32 + proxy_type.encode().len()` bytes of data. #[pallet::constant] type ProxyDepositFactor: Get>; /// The maximum amount of proxies allowed for a single account. #[pallet::constant] type MaxProxies: Get; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; /// The maximum amount of time-delayed announcements that are allowed to be pending. #[pallet::constant] type MaxPending: Get; /// The type of hash used for hashing the call. type CallHasher: Hash; /// The base amount of currency needed to reserve for creating an announcement. /// /// This is held when a new storage item holding a `Balance` is created (typically 16 /// bytes). #[pallet::constant] type AnnouncementDepositBase: Get>; /// The amount of currency needed per announcement made. /// /// This is held for adding an `AccountId`, `Hash` and `BlockNumber` (typically 68 bytes) /// into a pre-existing storage value. #[pallet::constant] type AnnouncementDepositFactor: Get>; /// Query the current block number. /// /// Must return monotonically increasing values when called from consecutive blocks. /// Can be configured to return either: /// - the local block number of the runtime via `frame_system::Pallet` /// - a remote block number, eg from the relay chain through `RelaychainDataProvider` /// - an arbitrary value through a custom implementation of the trait /// /// There is currently no migration provided to "hot-swap" block number providers and it may /// result in undefined behavior when doing so. Parachains are therefore best off setting /// this to their local block number provider if they have the pallet already deployed. /// /// Suggested values: /// - Solo- and Relay-chains: `frame_system::Pallet` /// - Parachains that may produce blocks sparingly or only when needed (on-demand): /// - already have the pallet deployed: `frame_system::Pallet` /// - are freshly deploying this pallet: `RelaychainDataProvider` /// - Parachains with a reliably block production rate (PLO or bulk-coretime): /// - already have the pallet deployed: `frame_system::Pallet` /// - are freshly deploying this pallet: no strong recommendation. Both local and remote /// providers can be used. Relay provider can be a bit better in cases where the /// parachain is lagging its block production to avoid clock skew. type BlockNumberProvider: BlockNumberProvider; } #[pallet::call] impl Pallet { #![deny(clippy::expect_used)] /// Dispatch the given `call` from an account that the sender is authorised for through /// `add_proxy`. /// /// The dispatch origin for this call must be _Signed_. /// /// Parameters: /// - `real`: The account that the proxy will make a call on behalf of. /// - `force_proxy_type`: Specify the exact proxy type to be used and checked for this call. /// - `call`: The call to be made by the `real` account. #[pallet::call_index(0)] #[pallet::weight({ let di = call.get_dispatch_info(); (T::WeightInfo::proxy(T::MaxProxies::get()) // AccountData for inner call origin accountdata. .saturating_add(T::DbWeight::get().reads_writes(1, 1)) .saturating_add(di.call_weight), DispatchClass::Normal, di.pays_fee) })] pub fn proxy( origin: OriginFor, real: AccountIdLookupOf, force_proxy_type: Option, call: Box<::RuntimeCall>, ) -> DispatchResult { let who = ensure_signed(origin)?; let real = T::Lookup::lookup(real)?; let def = Self::find_proxy(&real, &who, force_proxy_type)?; ensure!(def.delay.is_zero(), Error::::Unannounced); Self::do_proxy(def, real, *call); Ok(()) } /// Register a proxy account for the sender that is able to make calls on its behalf. /// /// The dispatch origin for this call must be _Signed_. /// /// Parameters: /// - `proxy`: The account that the `caller` would like to make a proxy. /// - `proxy_type`: The permissions allowed for this proxy account. /// - `delay`: The announcement period required of the initial proxy. Will generally be /// zero. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::add_proxy(T::MaxProxies::get()))] pub fn add_proxy( origin: OriginFor, delegate: AccountIdLookupOf, proxy_type: T::ProxyType, delay: BlockNumberFor, ) -> DispatchResult { let who = ensure_signed(origin)?; let delegate = T::Lookup::lookup(delegate)?; Self::add_proxy_delegate(&who, delegate, proxy_type, delay) } /// Unregister a proxy account for the sender. /// /// The dispatch origin for this call must be _Signed_. /// /// Parameters: /// - `proxy`: The account that the `caller` would like to remove as a proxy. /// - `proxy_type`: The permissions currently enabled for the removed proxy account. #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::remove_proxy(T::MaxProxies::get()))] pub fn remove_proxy( origin: OriginFor, delegate: AccountIdLookupOf, proxy_type: T::ProxyType, delay: BlockNumberFor, ) -> DispatchResult { let who = ensure_signed(origin)?; let delegate = T::Lookup::lookup(delegate)?; Self::remove_proxy_delegate(&who, delegate, proxy_type, delay) } /// Unregister all proxy accounts for the sender. /// /// The dispatch origin for this call must be _Signed_. /// /// WARNING: This may be called on accounts created by `create_pure`, however if done, then /// the unreserved fees will be inaccessible. **All access to this account will be lost.** #[pallet::call_index(3)] #[pallet::weight(T::WeightInfo::remove_proxies(T::MaxProxies::get()))] pub fn remove_proxies(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; Self::remove_all_proxy_delegates(&who); Ok(()) } /// Spawn a fresh new account that is guaranteed to be otherwise inaccessible, and /// initialize it with a proxy of `proxy_type` for `origin` sender. /// /// Requires a `Signed` origin. /// /// - `proxy_type`: The type of the proxy that the sender will be registered as over the /// new account. This will almost always be the most permissive `ProxyType` possible to /// allow for maximum flexibility. /// - `index`: A disambiguation index, in case this is called multiple times in the same /// transaction (e.g. with `utility::batch`). Unless you're using `batch` you probably just /// want to use `0`. /// - `delay`: The announcement period required of the initial proxy. Will generally be /// zero. /// /// Fails with `Duplicate` if this has already been called in this transaction, from the /// same sender, with the same parameters. /// /// Fails if there are insufficient funds to pay for deposit. #[pallet::call_index(4)] #[pallet::weight(T::WeightInfo::create_pure(T::MaxProxies::get()))] pub fn create_pure( origin: OriginFor, proxy_type: T::ProxyType, delay: BlockNumberFor, index: u16, ) -> DispatchResult { let who = ensure_signed(origin)?; let pure = Self::pure_account(&who, &proxy_type, index, None)?; ensure!(!Proxies::::contains_key(&pure), Error::::Duplicate); let proxy_def = ProxyDefinition { delegate: who.clone(), proxy_type: proxy_type.clone(), delay, }; let bounded_proxies: BoundedVec<_, T::MaxProxies> = vec![proxy_def] .try_into() .map_err(|_| Error::::TooMany)?; let deposit = T::ProxyDepositBase::get().saturating_add(T::ProxyDepositFactor::get()); T::Currency::reserve(&who, deposit)?; Proxies::::insert(&pure, (bounded_proxies, deposit)); Self::deposit_event(Event::PureCreated { pure, who, proxy_type, disambiguation_index: index, }); Ok(()) } /// Removes a previously spawned pure proxy. /// /// WARNING: **All access to this account will be lost.** Any funds held in it will be /// inaccessible. /// /// Requires a `Signed` origin, and the sender account must have been created by a call to /// `create_pure` with corresponding parameters. /// /// - `spawner`: The account that originally called `create_pure` to create this account. /// - `index`: The disambiguation index originally passed to `create_pure`. Probably `0`. /// - `proxy_type`: The proxy type originally passed to `create_pure`. /// - `height`: The height of the chain when the call to `create_pure` was processed. /// - `ext_index`: The extrinsic index in which the call to `create_pure` was processed. /// /// Fails with `NoPermission` in case the caller is not a previously created pure /// account whose `create_pure` call has corresponding parameters. #[pallet::call_index(5)] #[pallet::weight(T::WeightInfo::kill_pure(T::MaxProxies::get()))] pub fn kill_pure( origin: OriginFor, spawner: AccountIdLookupOf, proxy_type: T::ProxyType, index: u16, #[pallet::compact] height: BlockNumberFor, #[pallet::compact] ext_index: u32, ) -> DispatchResult { let who = ensure_signed(origin)?; let spawner = T::Lookup::lookup(spawner)?; let when = (height, ext_index); let proxy = Self::pure_account(&spawner, &proxy_type, index, Some(when))?; ensure!(proxy == who, Error::::NoPermission); let (_, deposit) = Proxies::::take(&who); T::Currency::unreserve(&spawner, deposit); Self::deposit_event(Event::PureKilled { pure: who, spawner, proxy_type, disambiguation_index: index, }); Ok(()) } /// Publish the hash of a proxy-call that will be made in the future. /// /// This must be called some number of blocks before the corresponding `proxy` is attempted /// if the delay associated with the proxy relationship is greater than zero. /// /// No more than `MaxPending` announcements may be made at any one time. /// /// This will take a deposit of `AnnouncementDepositFactor` as well as /// `AnnouncementDepositBase` if there are no other pending announcements. /// /// The dispatch origin for this call must be _Signed_ and a proxy of `real`. /// /// Parameters: /// - `real`: The account that the proxy will make a call on behalf of. /// - `call_hash`: The hash of the call to be made by the `real` account. #[pallet::call_index(6)] #[pallet::weight(T::WeightInfo::announce(T::MaxPending::get(), T::MaxProxies::get()))] pub fn announce( origin: OriginFor, real: AccountIdLookupOf, call_hash: CallHashOf, ) -> DispatchResult { let who = ensure_signed(origin)?; let real = T::Lookup::lookup(real)?; Proxies::::get(&real) .0 .into_iter() .find(|x| x.delegate == who) .ok_or(Error::::NotProxy)?; let announcement = Announcement { real: real.clone(), call_hash, height: T::BlockNumberProvider::current_block_number(), }; Announcements::::try_mutate(&who, |(pending, deposit)| { pending .try_push(announcement) .map_err(|_| Error::::TooMany)?; let new_deposit = Self::rejig_deposit( &who, *deposit, T::AnnouncementDepositBase::get(), T::AnnouncementDepositFactor::get(), pending.len(), )? .ok_or(Error::::AnnouncementDepositInvariantViolated)?; *deposit = new_deposit; Ok::<(), DispatchError>(()) })?; Self::deposit_event(Event::Announced { real, proxy: who, call_hash, }); Ok(()) } /// Remove a given announcement. /// /// May be called by a proxy account to remove a call they previously announced and return /// the deposit. /// /// The dispatch origin for this call must be _Signed_. /// /// Parameters: /// - `real`: The account that the proxy will make a call on behalf of. /// - `call_hash`: The hash of the call to be made by the `real` account. #[pallet::call_index(7)] #[pallet::weight(T::WeightInfo::remove_announcement( T::MaxPending::get(), T::MaxProxies::get() ))] pub fn remove_announcement( origin: OriginFor, real: AccountIdLookupOf, call_hash: CallHashOf, ) -> DispatchResult { let who = ensure_signed(origin)?; let real = T::Lookup::lookup(real)?; Self::edit_announcements(&who, |ann| ann.real != real || ann.call_hash != call_hash)?; Ok(()) } /// Remove the given announcement of a delegate. /// /// May be called by a target (proxied) account to remove a call that one of their delegates /// (`delegate`) has announced they want to execute. The deposit is returned. /// /// The dispatch origin for this call must be _Signed_. /// /// Parameters: /// - `delegate`: The account that previously announced the call. /// - `call_hash`: The hash of the call to be made. #[pallet::call_index(8)] #[pallet::weight(T::WeightInfo::reject_announcement( T::MaxPending::get(), T::MaxProxies::get() ))] pub fn reject_announcement( origin: OriginFor, delegate: AccountIdLookupOf, call_hash: CallHashOf, ) -> DispatchResult { let who = ensure_signed(origin)?; let delegate = T::Lookup::lookup(delegate)?; Self::edit_announcements(&delegate, |ann| { ann.real != who || ann.call_hash != call_hash })?; Ok(()) } /// Dispatch the given `call` from an account that the sender is authorized for through /// `add_proxy`. /// /// Removes any corresponding announcement(s). /// /// The dispatch origin for this call must be _Signed_. /// /// Parameters: /// - `real`: The account that the proxy will make a call on behalf of. /// - `force_proxy_type`: Specify the exact proxy type to be used and checked for this call. /// - `call`: The call to be made by the `real` account. #[pallet::call_index(9)] #[pallet::weight({ let di = call.get_dispatch_info(); (T::WeightInfo::proxy_announced(T::MaxPending::get(), T::MaxProxies::get()) // AccountData for inner call origin accountdata. .saturating_add(T::DbWeight::get().reads_writes(1, 1)) .saturating_add(di.call_weight), DispatchClass::Normal) })] pub fn proxy_announced( origin: OriginFor, delegate: AccountIdLookupOf, real: AccountIdLookupOf, force_proxy_type: Option, call: Box<::RuntimeCall>, ) -> DispatchResult { ensure_signed(origin)?; let delegate = T::Lookup::lookup(delegate)?; let real = T::Lookup::lookup(real)?; let def = Self::find_proxy(&real, &delegate, force_proxy_type)?; let call_hash = T::CallHasher::hash_of(&call); let now = T::BlockNumberProvider::current_block_number(); Self::edit_announcements(&delegate, |ann| { ann.real != real || ann.call_hash != call_hash || now.saturating_sub(ann.height) < def.delay }) .map_err(|_| Error::::Unannounced)?; Self::do_proxy(def, real, *call); Ok(()) } /// Poke / Adjust deposits made for proxies and announcements based on current values. /// This can be used by accounts to possibly lower their locked amount. /// /// The dispatch origin for this call must be _Signed_. /// /// The transaction fee is waived if the deposit amount has changed. /// /// Emits `DepositPoked` if successful. #[pallet::call_index(10)] #[pallet::weight(T::WeightInfo::poke_deposit())] pub fn poke_deposit(origin: OriginFor) -> DispatchResultWithPostInfo { let who = ensure_signed(origin)?; let mut deposit_updated = false; // Check and update proxy deposits Proxies::::try_mutate_exists(&who, |maybe_proxies| -> DispatchResult { let (proxies, old_deposit) = maybe_proxies.take().unwrap_or_default(); let maybe_new_deposit = Self::rejig_deposit( &who, old_deposit, T::ProxyDepositBase::get(), T::ProxyDepositFactor::get(), proxies.len(), )?; match maybe_new_deposit { Some(new_deposit) if new_deposit != old_deposit => { *maybe_proxies = Some((proxies, new_deposit)); deposit_updated = true; Self::deposit_event(Event::DepositPoked { who: who.clone(), kind: DepositKind::Proxies, old_deposit, new_deposit, }); } Some(_) => { *maybe_proxies = Some((proxies, old_deposit)); } None => { *maybe_proxies = None; if !old_deposit.is_zero() { deposit_updated = true; Self::deposit_event(Event::DepositPoked { who: who.clone(), kind: DepositKind::Proxies, old_deposit, new_deposit: BalanceOf::::zero(), }); } } } Ok(()) })?; // Check and update announcement deposits Announcements::::try_mutate_exists(&who, |maybe_announcements| -> DispatchResult { let (announcements, old_deposit) = maybe_announcements.take().unwrap_or_default(); let maybe_new_deposit = Self::rejig_deposit( &who, old_deposit, T::AnnouncementDepositBase::get(), T::AnnouncementDepositFactor::get(), announcements.len(), )?; match maybe_new_deposit { Some(new_deposit) if new_deposit != old_deposit => { *maybe_announcements = Some((announcements, new_deposit)); deposit_updated = true; Self::deposit_event(Event::DepositPoked { who: who.clone(), kind: DepositKind::Announcements, old_deposit, new_deposit, }); } Some(_) => { *maybe_announcements = Some((announcements, old_deposit)); } None => { *maybe_announcements = None; if !old_deposit.is_zero() { deposit_updated = true; Self::deposit_event(Event::DepositPoked { who: who.clone(), kind: DepositKind::Announcements, old_deposit, new_deposit: BalanceOf::::zero(), }); } } } Ok(()) })?; Ok(if deposit_updated { Pays::No.into() } else { Pays::Yes.into() }) } /// Set whether the real account pays transaction fees for proxy calls made by a /// specific delegate. /// /// The dispatch origin for this call must be _Signed_ and must be the real (delegator) /// account that has an existing proxy relationship with the delegate. /// /// Parameters: /// - `delegate`: The proxy account for which to set the fee payment preference. /// - `pays_fee`: If `true`, the real account will pay fees for proxy calls made by /// this delegate. If `false`, the delegate pays (default behavior). #[pallet::call_index(11)] #[pallet::weight(T::WeightInfo::set_real_pays_fee(T::MaxProxies::get()))] pub fn set_real_pays_fee( origin: OriginFor, delegate: AccountIdLookupOf, pays_fee: bool, ) -> DispatchResult { let real = ensure_signed(origin)?; let delegate = T::Lookup::lookup(delegate)?; // Verify proxy relationship exists Self::find_proxy(&real, &delegate, None)?; if pays_fee { RealPaysFee::::insert(&real, &delegate, ()); } else { RealPaysFee::::remove(&real, &delegate); } Self::deposit_event(Event::RealPaysFeeSet { real, delegate, pays_fee, }); Ok(()) } } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// A proxy was executed correctly, with the given. ProxyExecuted { result: DispatchResult }, /// A pure account has been created by new proxy with given /// disambiguation index and proxy type. PureCreated { pure: T::AccountId, who: T::AccountId, proxy_type: T::ProxyType, disambiguation_index: u16, }, /// A pure proxy was killed by its spawner. PureKilled { // The pure proxy account that was destroyed. pure: T::AccountId, // The account that created the pure proxy. spawner: T::AccountId, // The proxy type of the pure proxy that was destroyed. proxy_type: T::ProxyType, // The index originally passed to `create_pure` when this pure proxy was created. disambiguation_index: u16, }, /// An announcement was placed to make a call in the future. Announced { real: T::AccountId, proxy: T::AccountId, call_hash: CallHashOf, }, /// A proxy was added. ProxyAdded { delegator: T::AccountId, delegatee: T::AccountId, proxy_type: T::ProxyType, delay: BlockNumberFor, }, /// A proxy was removed. ProxyRemoved { delegator: T::AccountId, delegatee: T::AccountId, proxy_type: T::ProxyType, delay: BlockNumberFor, }, /// A deposit stored for proxies or announcements was poked / updated. DepositPoked { who: T::AccountId, kind: DepositKind, old_deposit: BalanceOf, new_deposit: BalanceOf, }, /// The real-pays-fee setting was updated for a proxy relationship. RealPaysFeeSet { real: T::AccountId, delegate: T::AccountId, pays_fee: bool, }, } #[pallet::error] pub enum Error { /// There are too many proxies registered or too many announcements pending. TooMany, /// Proxy registration not found. NotFound, /// Sender is not a proxy of the account to be proxied. NotProxy, /// A call which is incompatible with the proxy type's filter was attempted. Unproxyable, /// Account is already a proxy. Duplicate, /// Call may not be made by proxy because it may escalate its privileges. NoPermission, /// Announcement, if made at all, was made too recently. Unannounced, /// Cannot add self as proxy. NoSelfProxy, /// Invariant violated: deposit recomputation returned None after updating announcements. AnnouncementDepositInvariantViolated, /// Failed to derive a valid account id from the provided entropy. InvalidDerivedAccountId, } #[pallet::hooks] impl Hooks> for Pallet { fn on_finalize(_n: SystemBlockNumberFor) { // clear this map on end of each block let _ = LastCallResult::::clear(u32::MAX, None); } } /// The set of account proxies. Maps the account which has delegated to the accounts /// which are being delegated to, together with the amount held on deposit. #[pallet::storage] pub type Proxies = StorageMap< _, Twox64Concat, T::AccountId, ( BoundedVec< ProxyDefinition>, T::MaxProxies, >, BalanceOf, ), ValueQuery, >; /// The announcements made by the proxy (key). #[pallet::storage] pub type Announcements = StorageMap< _, Twox64Concat, T::AccountId, ( BoundedVec, BlockNumberFor>, T::MaxPending>, BalanceOf, ), ValueQuery, >; /// The result of the last call made by the proxy (key). #[pallet::storage] pub type LastCallResult = StorageMap<_, Twox64Concat, T::AccountId, DispatchResult, OptionQuery>; /// Tracks which (real, delegate) pairs have opted in to the real account paying /// transaction fees for proxy calls made by the delegate. /// Existence of an entry means the real account pays; absence means the delegate pays /// (default). #[pallet::storage] pub type RealPaysFee = StorageDoubleMap< _, Twox64Concat, T::AccountId, // real Twox64Concat, T::AccountId, // delegate (), OptionQuery, >; #[pallet::view_functions] impl Pallet { /// Check if a `RuntimeCall` is allowed for a given `ProxyType`. pub fn check_permissions( call: ::RuntimeCall, proxy_type: T::ProxyType, ) -> bool { proxy_type.filter(&call) } /// Check if one `ProxyType` is a subset of another `ProxyType`. pub fn is_superset(to_check: T::ProxyType, against: T::ProxyType) -> bool { to_check.is_superset(&against) } } } impl Pallet { /// Public function to proxies storage. pub fn proxies( account: T::AccountId, ) -> ( BoundedVec>, T::MaxProxies>, BalanceOf, ) { Proxies::::get(account) } /// Public function to announcements storage. pub fn announcements( account: T::AccountId, ) -> ( BoundedVec, BlockNumberFor>, T::MaxPending>, BalanceOf, ) { Announcements::::get(account) } /// Calculate the address of an pure account. /// /// - `who`: The spawner account. /// - `proxy_type`: The type of the proxy that the sender will be registered as over the /// new account. This will almost always be the most permissive `ProxyType` possible to /// allow for maximum flexibility. /// - `index`: A disambiguation index, in case this is called multiple times in the same /// transaction (e.g. with `utility::batch`). Unless you're using `batch` you probably just /// want to use `0`. /// - `maybe_when`: The block height and extrinsic index of when the pure account was /// created. None to use current block height and extrinsic index. pub fn pure_account( who: &T::AccountId, proxy_type: &T::ProxyType, index: u16, maybe_when: Option<(BlockNumberFor, u32)>, ) -> Result { let (height, ext_index) = maybe_when.unwrap_or_else(|| { ( T::BlockNumberProvider::current_block_number(), frame_system::Pallet::::extrinsic_index().unwrap_or_default(), ) }); let entropy = ( b"modlpy/proxy____", who, height, ext_index, proxy_type, index, ) .using_encoded(blake2_256); T::AccountId::decode(&mut TrailingZeroInput::new(entropy.as_ref())) .map_err(|_| Error::::InvalidDerivedAccountId.into()) } /// Register a proxy account for the delegator that is able to make calls on its behalf. /// /// Parameters: /// - `delegator`: The delegator account. /// - `delegatee`: The account that the `delegator` would like to make a proxy. /// - `proxy_type`: The permissions allowed for this proxy account. /// - `delay`: The announcement period required of the initial proxy. Will generally be /// zero. pub fn add_proxy_delegate( delegator: &T::AccountId, delegatee: T::AccountId, proxy_type: T::ProxyType, delay: BlockNumberFor, ) -> DispatchResult { ensure!(delegator != &delegatee, Error::::NoSelfProxy); Proxies::::try_mutate(delegator, |(proxies, deposit)| { let proxy_def = ProxyDefinition { delegate: delegatee.clone(), proxy_type: proxy_type.clone(), delay, }; let i = proxies .binary_search(&proxy_def) .err() .ok_or(Error::::Duplicate)?; proxies .try_insert(i, proxy_def) .map_err(|_| Error::::TooMany)?; let new_deposit = Self::deposit(proxies.len() as u32); if new_deposit > *deposit { T::Currency::reserve(delegator, new_deposit.saturating_sub(*deposit))?; } else if new_deposit < *deposit { T::Currency::unreserve(delegator, (*deposit).saturating_sub(new_deposit)); } *deposit = new_deposit; Self::deposit_event(Event::::ProxyAdded { delegator: delegator.clone(), delegatee, proxy_type, delay, }); Ok(()) }) } /// Unregister a proxy account for the delegator. /// /// Parameters: /// - `delegator`: The delegator account. /// - `delegatee`: The account that the `delegator` would like to make a proxy. /// - `proxy_type`: The permissions allowed for this proxy account. /// - `delay`: The announcement period required of the initial proxy. Will generally be /// zero. pub fn remove_proxy_delegate( delegator: &T::AccountId, delegatee: T::AccountId, proxy_type: T::ProxyType, delay: BlockNumberFor, ) -> DispatchResult { Proxies::::try_mutate_exists(delegator, |x| { let (mut proxies, old_deposit) = x.take().ok_or(Error::::NotFound)?; let proxy_def = ProxyDefinition { delegate: delegatee.clone(), proxy_type: proxy_type.clone(), delay, }; let i = proxies .binary_search(&proxy_def) .ok() .ok_or(Error::::NotFound)?; proxies.remove(i); let new_deposit = Self::deposit(proxies.len() as u32); if new_deposit > old_deposit { T::Currency::reserve(delegator, new_deposit.saturating_sub(old_deposit))?; } else if new_deposit < old_deposit { T::Currency::unreserve(delegator, old_deposit.saturating_sub(new_deposit)); } if !proxies.is_empty() { *x = Some((proxies, new_deposit)) } // Clean up real-pays-fee flag for this specific proxy relationship RealPaysFee::::remove(delegator, &delegatee); Self::deposit_event(Event::::ProxyRemoved { delegator: delegator.clone(), delegatee, proxy_type, delay, }); Ok(()) }) } pub fn deposit(num_proxies: u32) -> BalanceOf { if num_proxies == 0 { Zero::zero() } else { T::ProxyDepositBase::get() .saturating_add(T::ProxyDepositFactor::get().saturating_mul(num_proxies.into())) } } fn rejig_deposit( who: &T::AccountId, old_deposit: BalanceOf, base: BalanceOf, factor: BalanceOf, len: usize, ) -> Result>, DispatchError> { let new_deposit = if len == 0 { BalanceOf::::zero() } else { base.saturating_add(factor.saturating_mul((len as u32).into())) }; if new_deposit > old_deposit { T::Currency::reserve(who, new_deposit.saturating_sub(old_deposit))?; } else if new_deposit < old_deposit { let excess = old_deposit.saturating_sub(new_deposit); let remaining_unreserved = T::Currency::unreserve(who, excess); if !remaining_unreserved.is_zero() { defensive!( "Failed to unreserve full amount. (Requested, Actual)", (excess, excess.saturating_sub(remaining_unreserved)) ); } } Ok(if len == 0 { None } else { Some(new_deposit) }) } fn edit_announcements< F: FnMut(&Announcement, BlockNumberFor>) -> bool, >( delegate: &T::AccountId, f: F, ) -> DispatchResult { Announcements::::try_mutate_exists(delegate, |x| { let (mut pending, old_deposit) = x.take().ok_or(Error::::NotFound)?; let orig_pending_len = pending.len(); pending.retain(f); ensure!(orig_pending_len > pending.len(), Error::::NotFound); *x = Self::rejig_deposit( delegate, old_deposit, T::AnnouncementDepositBase::get(), T::AnnouncementDepositFactor::get(), pending.len(), )? .map(|deposit| (pending, deposit)); Ok(()) }) } pub fn find_proxy( real: &T::AccountId, delegate: &T::AccountId, force_proxy_type: Option, ) -> Result>, DispatchError> { let f = |x: &ProxyDefinition>| -> bool { &x.delegate == delegate && force_proxy_type.as_ref().is_none_or(|y| &x.proxy_type == y) }; Ok(Proxies::::get(real) .0 .into_iter() .find(f) .ok_or(Error::::NotProxy)?) } fn do_proxy( def: ProxyDefinition>, real: T::AccountId, call: ::RuntimeCall, ) { use frame::traits::{InstanceFilter as _, OriginTrait as _}; // This is a freshly authenticated new account, the origin restrictions doesn't apply. let mut origin: T::RuntimeOrigin = frame_system::RawOrigin::Signed(real.clone()).into(); origin.add_filter(move |c: &::RuntimeCall| { let c = ::RuntimeCall::from_ref(c); // We make sure the proxy call does access this pallet to change modify proxies. match c.is_sub_type() { // Proxy call cannot add or remove a proxy with more permissions than it already // has. Some(Call::add_proxy { proxy_type, .. }) | Some(Call::remove_proxy { proxy_type, .. }) if !def.proxy_type.is_superset(proxy_type) => { false } // Proxy call cannot remove all proxies or kill pure proxies unless it has full // permissions. Some(Call::remove_proxies { .. }) | Some(Call::kill_pure { .. }) if def.proxy_type != T::ProxyType::default() => { false } _ => def.proxy_type.filter(c), } }); let e = call.dispatch(origin); LastCallResult::::insert(real, e.map(|_| ()).map_err(|e| e.error)); Self::deposit_event(Event::ProxyExecuted { result: e.map(|_| ()).map_err(|e| e.error), }); } /// Removes all proxy delegates for a given delegator. /// /// Parameters: /// - `delegator`: The delegator account. pub fn remove_all_proxy_delegates(delegator: &T::AccountId) { let (_, old_deposit) = Proxies::::take(delegator); T::Currency::unreserve(delegator, old_deposit); // Clean up all real-pays-fee flags for this delegator let _ = RealPaysFee::::clear_prefix(delegator, u32::MAX, None); } /// Check if the real account has opted in to paying fees for a specific delegate. pub fn is_real_pays_fee(real: &T::AccountId, delegate: &T::AccountId) -> bool { RealPaysFee::::contains_key(real, delegate) } }