// 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<T> = <<T as Config>::CallHasher as Hash>::Output;
type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub type BlockNumberFor<T> =
<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::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<AccountId, ProxyType, BlockNumber> {
/// 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<AccountId, Hash, BlockNumber> {
/// 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<T>(_);
/// Configuration trait.
#[pallet::config]
pub trait Config: frame_system::Config {
/// The overarching call type.
type RuntimeCall: Parameter
+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
+ GetDispatchInfo
+ From<frame_system::Call<Self>>
+ IsSubType<Call<Self>>
+ IsType<<Self as frame_system::Config>::RuntimeCall>;
/// The currency mechanism.
type Currency: ReservableCurrency<Self::AccountId>;
/// 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<<Self as Config>::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<BalanceOf<Self>>;
/// 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<BalanceOf<Self>>;
/// The maximum amount of proxies allowed for a single account.
#[pallet::constant]
type MaxProxies: Get<u32>;
/// 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<u32>;
/// 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<BalanceOf<Self>>;
/// 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<BalanceOf<Self>>;
/// 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<T: Config> Pallet<T> {
#![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<T>,
real: AccountIdLookupOf<T>,
force_proxy_type: Option<T::ProxyType>,
call: Box<<T as Config>::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::<T>::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<T>,
delegate: AccountIdLookupOf<T>,
proxy_type: T::ProxyType,
delay: BlockNumberFor<T>,
) -> 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<T>,
delegate: AccountIdLookupOf<T>,
proxy_type: T::ProxyType,
delay: BlockNumberFor<T>,
) -> 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<T>) -> 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<T>,
proxy_type: T::ProxyType,
delay: BlockNumberFor<T>,
index: u16,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let pure = Self::pure_account(&who, &proxy_type, index, None)?;
ensure!(!Proxies::<T>::contains_key(&pure), Error::<T>::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::<T>::TooMany)?;
let deposit = T::ProxyDepositBase::get().saturating_add(T::ProxyDepositFactor::get());
T::Currency::reserve(&who, deposit)?;
Proxies::<T>::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<T>,
spawner: AccountIdLookupOf<T>,
proxy_type: T::ProxyType,
index: u16,
#[pallet::compact] height: BlockNumberFor<T>,
#[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::<T>::NoPermission);
let (_, deposit) = Proxies::<T>::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<T>,
real: AccountIdLookupOf<T>,
call_hash: CallHashOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let real = T::Lookup::lookup(real)?;
Proxies::<T>::get(&real)
.0
.into_iter()
.find(|x| x.delegate == who)
.ok_or(Error::<T>::NotProxy)?;
let announcement = Announcement {
real: real.clone(),
call_hash,
height: T::BlockNumberProvider::current_block_number(),
};
Announcements::<T>::try_mutate(&who, |(pending, deposit)| {
pending
.try_push(announcement)
.map_err(|_| Error::<T>::TooMany)?;
let new_deposit = Self::rejig_deposit(
&who,
*deposit,
T::AnnouncementDepositBase::get(),
T::AnnouncementDepositFactor::get(),
pending.len(),
)?
.ok_or(Error::<T>::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<T>,
real: AccountIdLookupOf<T>,
call_hash: CallHashOf<T>,
) -> 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<T>,
delegate: AccountIdLookupOf<T>,
call_hash: CallHashOf<T>,
) -> 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<T>,
delegate: AccountIdLookupOf<T>,
real: AccountIdLookupOf<T>,
force_proxy_type: Option<T::ProxyType>,
call: Box<<T as Config>::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::<T>::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<T>) -> DispatchResultWithPostInfo {
let who = ensure_signed(origin)?;
let mut deposit_updated = false;
// Check and update proxy deposits
Proxies::<T>::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::<T>::zero(),
});
}
}
}
Ok(())
})?;
// Check and update announcement deposits
Announcements::<T>::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::<T>::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<T>,
delegate: AccountIdLookupOf<T>,
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::<T>::insert(&real, &delegate, ());
} else {
RealPaysFee::<T>::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<T: Config> {
/// 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<T>,
},
/// A proxy was added.
ProxyAdded {
delegator: T::AccountId,
delegatee: T::AccountId,
proxy_type: T::ProxyType,
delay: BlockNumberFor<T>,
},
/// A proxy was removed.
ProxyRemoved {
delegator: T::AccountId,
delegatee: T::AccountId,
proxy_type: T::ProxyType,
delay: BlockNumberFor<T>,
},
/// A deposit stored for proxies or announcements was poked / updated.
DepositPoked {
who: T::AccountId,
kind: DepositKind,
old_deposit: BalanceOf<T>,
new_deposit: BalanceOf<T>,
},
/// 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<T> {
/// 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<T: Config> Hooks<SystemBlockNumberFor<T>> for Pallet<T> {
fn on_finalize(_n: SystemBlockNumberFor<T>) {
// clear this map on end of each block
let _ = LastCallResult::<T>::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<T: Config> = StorageMap<
_,
Twox64Concat,
T::AccountId,
(
BoundedVec<
ProxyDefinition<T::AccountId, T::ProxyType, BlockNumberFor<T>>,
T::MaxProxies,
>,
BalanceOf<T>,
),
ValueQuery,
>;
/// The announcements made by the proxy (key).
#[pallet::storage]
pub type Announcements<T: Config> = StorageMap<
_,
Twox64Concat,
T::AccountId,
(
BoundedVec<Announcement<T::AccountId, CallHashOf<T>, BlockNumberFor<T>>, T::MaxPending>,
BalanceOf<T>,
),
ValueQuery,
>;
/// The result of the last call made by the proxy (key).
#[pallet::storage]
pub type LastCallResult<T: Config> =
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<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId, // real
Twox64Concat,
T::AccountId, // delegate
(),
OptionQuery,
>;
#[pallet::view_functions]
impl<T: Config> Pallet<T> {
/// Check if a `RuntimeCall` is allowed for a given `ProxyType`.
pub fn check_permissions(
call: <T as Config>::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<T: Config> Pallet<T> {
/// Public function to proxies storage.
pub fn proxies(
account: T::AccountId,
) -> (
BoundedVec<ProxyDefinition<T::AccountId, T::ProxyType, BlockNumberFor<T>>, T::MaxProxies>,
BalanceOf<T>,
) {
Proxies::<T>::get(account)
}
/// Public function to announcements storage.
pub fn announcements(
account: T::AccountId,
) -> (
BoundedVec<Announcement<T::AccountId, CallHashOf<T>, BlockNumberFor<T>>, T::MaxPending>,
BalanceOf<T>,
) {
Announcements::<T>::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<T>, u32)>,
) -> Result<T::AccountId, DispatchError> {
let (height, ext_index) = maybe_when.unwrap_or_else(|| {
(
T::BlockNumberProvider::current_block_number(),
frame_system::Pallet::<T>::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::<T>::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<T>,
) -> DispatchResult {
ensure!(delegator != &delegatee, Error::<T>::NoSelfProxy);
Proxies::<T>::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::<T>::Duplicate)?;
proxies
.try_insert(i, proxy_def)
.map_err(|_| Error::<T>::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::<T>::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<T>,
) -> DispatchResult {
Proxies::<T>::try_mutate_exists(delegator, |x| {
let (mut proxies, old_deposit) = x.take().ok_or(Error::<T>::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::<T>::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::<T>::remove(delegator, &delegatee);
Self::deposit_event(Event::<T>::ProxyRemoved {
delegator: delegator.clone(),
delegatee,
proxy_type,
delay,
});
Ok(())
})
}
pub fn deposit(num_proxies: u32) -> BalanceOf<T> {
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<T>,
base: BalanceOf<T>,
factor: BalanceOf<T>,
len: usize,
) -> Result<Option<BalanceOf<T>>, DispatchError> {
let new_deposit = if len == 0 {
BalanceOf::<T>::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<T::AccountId, CallHashOf<T>, BlockNumberFor<T>>) -> bool,
>(
delegate: &T::AccountId,
f: F,
) -> DispatchResult {
Announcements::<T>::try_mutate_exists(delegate, |x| {
let (mut pending, old_deposit) = x.take().ok_or(Error::<T>::NotFound)?;
let orig_pending_len = pending.len();
pending.retain(f);
ensure!(orig_pending_len > pending.len(), Error::<T>::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<T::ProxyType>,
) -> Result<ProxyDefinition<T::AccountId, T::ProxyType, BlockNumberFor<T>>, DispatchError> {
let f = |x: &ProxyDefinition<T::AccountId, T::ProxyType, BlockNumberFor<T>>| -> bool {
&x.delegate == delegate && force_proxy_type.as_ref().is_none_or(|y| &x.proxy_type == y)
};
Ok(Proxies::<T>::get(real)
.0
.into_iter()
.find(f)
.ok_or(Error::<T>::NotProxy)?)
}
fn do_proxy(
def: ProxyDefinition<T::AccountId, T::ProxyType, BlockNumberFor<T>>,
real: T::AccountId,
call: <T as Config>::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: &<T as frame_system::Config>::RuntimeCall| {
let c = <T as Config>::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::<T>::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::<T>::take(delegator);
T::Currency::unreserve(delegator, old_deposit);
// Clean up all real-pays-fee flags for this delegator
let _ = RealPaysFee::<T>::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::<T>::contains_key(real, delegate)
}
}