#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(test)]
pub(crate) mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use cfg_traits::TransferAllowance;
pub use pallet::*;
pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
use core::fmt::Debug;
use frame_support::{
pallet_prelude::{DispatchResult, Member, OptionQuery, StorageDoubleMap, StorageNMap, *},
traits::{
fungible,
fungible::MutateHold,
tokens::{AssetId, Precision},
},
Twox64Concat,
};
use frame_system::pallet_prelude::{OriginFor, *};
use parity_scale_codec::{Decode, Encode, EncodeLike, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{AtLeast32BitUnsigned, EnsureAdd, EnsureSub},
Saturating,
};
use super::*;
pub type DepositBalanceOf<T> = <<T as Config>::ReserveCurrency as fungible::Inspect<
<T as frame_system::Config>::AccountId,
>>::Balance;
pub type AllowanceDetailsOf<T> = AllowanceDetails<BlockNumberFor<T>>;
pub type ReasonOf<T> = <<T as Config>::ReserveCurrency as fungible::hold::Inspect<
<T as frame_system::Config>::AccountId,
>>::Reason;
pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::composite_enum]
pub enum HoldReason {
TransferAllowance,
}
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type CurrencyId: AssetId + Parameter + Member + Copy;
type ReserveCurrency: fungible::hold::Mutate<
Self::AccountId,
Reason = Self::RuntimeHoldReason,
>;
type RuntimeHoldReason: From<HoldReason>;
type Deposit: Get<DepositBalanceOf<Self>>;
type Location: Member + TypeInfo + Encode + EncodeLike + Decode + MaxEncodedLen;
type WeightInfo: WeightInfo;
}
#[derive(Clone, Debug, Encode, Decode, Eq, PartialEq, MaxEncodedLen, TypeInfo)]
pub struct AllowanceDetails<BlockNumber> {
pub allowed_at: BlockNumber,
pub blocked_at: BlockNumber,
}
impl<BlockNumber> Default for AllowanceDetails<BlockNumber>
where
BlockNumber: AtLeast32BitUnsigned,
{
fn default() -> Self {
Self {
allowed_at: BlockNumber::zero(),
blocked_at: BlockNumber::max_value(),
}
}
}
#[derive(Clone, Copy, Debug, Encode, Decode, Eq, PartialEq, MaxEncodedLen, TypeInfo)]
pub struct AllowanceMetadata<BlockNumber> {
pub(super) allowance_count: u64,
pub(super) current_delay: Option<BlockNumber>,
pub(super) once_modifiable_after: Option<BlockNumber>,
}
impl<BlockNumber> Default for AllowanceMetadata<BlockNumber>
where
BlockNumber: AtLeast32BitUnsigned,
{
fn default() -> Self {
Self {
allowance_count: 1u64,
current_delay: None,
once_modifiable_after: None,
}
}
}
#[pallet::storage]
#[pallet::getter(fn get_account_currency_restriction_count_delay)]
pub type AccountCurrencyTransferCountDelay<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
T::AccountId,
Twox64Concat,
T::CurrencyId,
AllowanceMetadata<BlockNumberFor<T>>,
OptionQuery,
>;
#[pallet::storage]
#[pallet::getter(fn get_account_currency_transfer_allowance)]
pub type AccountCurrencyTransferAllowance<T: Config> = StorageNMap<
_,
(
NMapKey<Twox64Concat, T::AccountId>,
NMapKey<Twox64Concat, T::CurrencyId>,
NMapKey<Blake2_128Concat, T::Location>,
),
AllowanceDetails<BlockNumberFor<T>>,
OptionQuery,
>;
#[pallet::error]
pub enum Error<T> {
NoAllowancesSet,
DuplicateAllowance,
NoMatchingAllowance,
NoMatchingDelay,
DuplicateDelay,
DelayUnmodifiable,
AllowanceHasNotExpired,
NoAllowanceForDestination,
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
TransferAllowanceCreated {
sender_account_id: T::AccountId,
currency_id: T::CurrencyId,
receiver: T::Location,
allowed_at: BlockNumberFor<T>,
blocked_at: BlockNumberFor<T>,
},
TransferAllowanceRemoved {
sender_account_id: T::AccountId,
currency_id: T::CurrencyId,
receiver: T::Location,
allowed_at: BlockNumberFor<T>,
blocked_at: BlockNumberFor<T>,
},
TransferAllowancePurged {
sender_account_id: T::AccountId,
currency_id: T::CurrencyId,
receiver: T::Location,
},
TransferAllowanceDelayAdd {
sender_account_id: T::AccountId,
currency_id: T::CurrencyId,
delay: BlockNumberFor<T>,
},
TransferAllowanceDelayUpdate {
sender_account_id: T::AccountId,
currency_id: T::CurrencyId,
delay: BlockNumberFor<T>,
},
ToggleTransferAllowanceDelayFutureModifiable {
sender_account_id: T::AccountId,
currency_id: T::CurrencyId,
modifiable_once_after: Option<BlockNumberFor<T>>,
},
TransferAllowanceDelayPurge {
sender_account_id: T::AccountId,
currency_id: T::CurrencyId,
},
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::add_transfer_allowance_no_existing_metadata().max(T::WeightInfo::add_transfer_allowance_existing_metadata()))]
pub fn add_transfer_allowance(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
receiver: T::Location,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let allowance_details = match Self::get_account_currency_restriction_count_delay(
&account_id,
currency_id,
) {
Some(AllowanceMetadata {
current_delay: Some(delay),
..
}) => AllowanceDetails {
allowed_at: <frame_system::Pallet<T>>::block_number().saturating_add(delay),
..AllowanceDetails::default()
},
_ => AllowanceDetails::default(),
};
if !<AccountCurrencyTransferAllowance<T>>::contains_key((
&account_id,
¤cy_id,
&receiver,
)) {
Self::increment_or_create_allowance_count(&account_id, ¤cy_id)?;
T::ReserveCurrency::hold(
&HoldReason::TransferAllowance.into(),
&account_id,
T::Deposit::get(),
)?;
};
<AccountCurrencyTransferAllowance<T>>::insert(
(&account_id, ¤cy_id, &receiver),
&allowance_details,
);
Self::deposit_event(Event::TransferAllowanceCreated {
sender_account_id: account_id,
currency_id,
receiver,
allowed_at: allowance_details.allowed_at,
blocked_at: allowance_details.blocked_at,
});
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::remove_transfer_allowance_delay_present().max(T::WeightInfo::remove_transfer_allowance_no_delay()))]
pub fn remove_transfer_allowance(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
receiver: T::Location,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let blocked_at = match Self::get_account_currency_restriction_count_delay(
&account_id,
currency_id,
) {
Some(AllowanceMetadata {
current_delay: Some(delay),
..
}) => <frame_system::Pallet<T>>::block_number().saturating_add(delay),
_ => <frame_system::Pallet<T>>::block_number(),
};
match <AccountCurrencyTransferAllowance<T>>::get((&account_id, ¤cy_id, &receiver))
{
Some(existing_allowance) => {
let allowance_details = AllowanceDetails {
blocked_at,
..existing_allowance
};
<AccountCurrencyTransferAllowance<T>>::insert(
(&account_id, ¤cy_id, &receiver),
&allowance_details,
);
Self::deposit_event(Event::TransferAllowanceRemoved {
sender_account_id: account_id,
currency_id,
receiver,
allowed_at: allowance_details.allowed_at,
blocked_at: allowance_details.blocked_at,
});
Ok(())
}
None => Err(DispatchError::from(Error::<T>::NoMatchingAllowance)),
}
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::purge_transfer_allowance_no_remaining_metadata().max(T::WeightInfo::purge_allowance_delay_remaining_metadata()))]
pub fn purge_transfer_allowance(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
receiver: T::Location,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let current_block = <frame_system::Pallet<T>>::block_number();
match <AccountCurrencyTransferAllowance<T>>::get((&account_id, ¤cy_id, &receiver))
{
Some(AllowanceDetails { blocked_at, .. }) if blocked_at < current_block => {
T::ReserveCurrency::release(
&HoldReason::TransferAllowance.into(),
&account_id,
T::Deposit::get(),
Precision::BestEffort,
)?;
<AccountCurrencyTransferAllowance<T>>::remove((
&account_id,
¤cy_id,
&receiver,
));
Self::decrement_or_remove_allowance_count(&account_id, ¤cy_id)?;
Self::deposit_event(Event::TransferAllowancePurged {
sender_account_id: account_id,
currency_id,
receiver,
});
Ok(())
}
Some(_) => Err(DispatchError::from(Error::<T>::AllowanceHasNotExpired)),
None => Err(DispatchError::from(Error::<T>::NoMatchingAllowance)),
}
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::add_allowance_delay_existing_metadata().max(T::WeightInfo::add_allowance_delay_no_existing_metadata()))]
pub fn add_allowance_delay(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
delay: BlockNumberFor<T>,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let count_delay = match Self::get_account_currency_restriction_count_delay(
&account_id,
currency_id,
) {
None => Ok(AllowanceMetadata {
allowance_count: 0,
current_delay: Some(delay),
once_modifiable_after: None,
}),
Some(
metadata @ AllowanceMetadata {
current_delay: None,
..
},
) => Ok(AllowanceMetadata {
current_delay: Some(delay),
..metadata
}),
Some(AllowanceMetadata {
current_delay: Some(_),
..
}) => Err(DispatchError::from(Error::<T>::DuplicateDelay)),
}?;
<AccountCurrencyTransferCountDelay<T>>::insert(&account_id, currency_id, count_delay);
Self::deposit_event(Event::TransferAllowanceDelayAdd {
sender_account_id: account_id,
currency_id,
delay,
});
Ok(())
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::update_allowance_delay())]
pub fn update_allowance_delay(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
delay: BlockNumberFor<T>,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let current_block = <frame_system::Pallet<T>>::block_number();
match Self::get_account_currency_restriction_count_delay(&account_id, currency_id) {
None => Err(DispatchError::from(Error::<T>::NoMatchingDelay)),
Some(AllowanceMetadata {
current_delay: None,
..
}) => Err(DispatchError::from(Error::<T>::NoMatchingDelay)),
Some(AllowanceMetadata {
once_modifiable_after: None,
..
}) => Err(DispatchError::from(Error::<T>::DelayUnmodifiable)),
Some(AllowanceMetadata {
once_modifiable_after: Some(modifiable_at),
..
}) if current_block < modifiable_at => Err(DispatchError::from(Error::<T>::DelayUnmodifiable)),
Some(metadata) => {
<AccountCurrencyTransferCountDelay<T>>::insert(
&account_id,
currency_id,
AllowanceMetadata {
current_delay: Some(delay),
once_modifiable_after: None,
..metadata
},
);
Self::deposit_event(Event::TransferAllowanceDelayUpdate {
sender_account_id: account_id,
currency_id,
delay,
});
Ok(())
}
}
}
#[pallet::call_index(5)]
#[pallet::weight(T::WeightInfo::toggle_allowance_delay_once_future_modifiable())]
pub fn toggle_allowance_delay_once_future_modifiable(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let current_block = <frame_system::Pallet<T>>::block_number();
let metadata = match Self::get_account_currency_restriction_count_delay(
&account_id,
currency_id,
) {
None => Err(DispatchError::from(Error::<T>::NoMatchingDelay)),
Some(AllowanceMetadata {
current_delay: None,
..
}) => Err(DispatchError::from(Error::<T>::NoMatchingDelay)),
Some(AllowanceMetadata {
once_modifiable_after: Some(modifiable_at),
..
}) if modifiable_at > current_block => Err(DispatchError::from(Error::<T>::DelayUnmodifiable)),
Some(
metadata @ AllowanceMetadata {
once_modifiable_after: Some(_),
..
},
) => Ok(AllowanceMetadata {
once_modifiable_after: None,
..metadata
}),
Some(
metadata @ AllowanceMetadata {
current_delay: Some(current_delay),
..
},
) => Ok(AllowanceMetadata {
once_modifiable_after: Some(current_block.ensure_add(current_delay)?),
..metadata
}),
}?;
<AccountCurrencyTransferCountDelay<T>>::insert(&account_id, currency_id, metadata);
Self::deposit_event(Event::ToggleTransferAllowanceDelayFutureModifiable {
sender_account_id: account_id,
currency_id,
modifiable_once_after: metadata.once_modifiable_after,
});
Ok(())
}
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::purge_allowance_delay_remaining_metadata().max(T::WeightInfo::purge_allowance_delay_no_remaining_metadata()))]
pub fn purge_allowance_delay(
origin: OriginFor<T>,
currency_id: T::CurrencyId,
) -> DispatchResult {
let account_id = ensure_signed(origin)?;
let current_block = <frame_system::Pallet<T>>::block_number();
match Self::get_account_currency_restriction_count_delay(&account_id, currency_id) {
Some(AllowanceMetadata {
allowance_count: 0,
once_modifiable_after: Some(modifiable_at),
..
}) if modifiable_at < current_block => {
<AccountCurrencyTransferCountDelay<T>>::remove(&account_id, currency_id);
Self::deposit_event(Event::TransferAllowanceDelayPurge {
sender_account_id: account_id,
currency_id,
});
Ok(())
}
Some(
metadata @ AllowanceMetadata {
once_modifiable_after: Some(modifiable_at),
..
},
) if modifiable_at <= current_block => {
<AccountCurrencyTransferCountDelay<T>>::insert(
&account_id,
currency_id,
AllowanceMetadata {
current_delay: None,
once_modifiable_after: None,
..metadata
},
);
Self::deposit_event(Event::TransferAllowanceDelayPurge {
sender_account_id: account_id,
currency_id,
});
Ok(())
}
None => Err(DispatchError::from(Error::<T>::NoMatchingDelay)),
_ => Err(DispatchError::from(Error::<T>::DelayUnmodifiable)),
}
}
}
impl<T: Config> Pallet<T> {
pub fn increment_or_create_allowance_count(
account_id: &T::AccountId,
currency_id: &T::CurrencyId,
) -> DispatchResult {
match Self::get_account_currency_restriction_count_delay(account_id, currency_id) {
Some(
metadata @ AllowanceMetadata {
allowance_count, ..
},
) => {
let new_allowance_count = allowance_count.ensure_add(1)?;
<AccountCurrencyTransferCountDelay<T>>::insert(
account_id,
currency_id,
AllowanceMetadata {
allowance_count: new_allowance_count,
..metadata
},
);
Ok(())
}
_ => {
<AccountCurrencyTransferCountDelay<T>>::insert(
account_id,
currency_id,
AllowanceMetadata::default(),
);
Ok(())
}
}
}
pub fn decrement_or_remove_allowance_count(
account_id: &T::AccountId,
currency_id: &T::CurrencyId,
) -> DispatchResult {
match Self::get_account_currency_restriction_count_delay(account_id, currency_id) {
Some(AllowanceMetadata {
allowance_count,
current_delay: None,
once_modifiable_after: None,
}) if allowance_count <= 1 => {
<AccountCurrencyTransferCountDelay<T>>::remove(account_id, currency_id);
Ok(())
}
Some(
metadata @ AllowanceMetadata {
allowance_count, ..
},
) if allowance_count <= 1 => {
<AccountCurrencyTransferCountDelay<T>>::insert(
account_id,
currency_id,
AllowanceMetadata {
allowance_count: 0,
..metadata
},
);
Ok(())
}
Some(
metadata @ AllowanceMetadata {
allowance_count, ..
},
) => {
let new_allowance_count = allowance_count.ensure_sub(1)?;
<AccountCurrencyTransferCountDelay<T>>::insert(
account_id,
currency_id,
AllowanceMetadata {
allowance_count: new_allowance_count,
..metadata
},
);
Ok(())
}
_ => Err(DispatchError::from(Error::<T>::NoAllowancesSet)),
}
}
}
impl<T: Config> TransferAllowance<T::AccountId> for Pallet<T> {
type CurrencyId = T::CurrencyId;
type Location = T::Location;
fn allowance(
send: T::AccountId,
receive: Self::Location,
currency: T::CurrencyId,
) -> Result<Option<Self::Location>, DispatchError> {
match Self::get_account_currency_restriction_count_delay(&send, currency) {
Some(AllowanceMetadata {
allowance_count: count,
..
}) if count > 0 => {
let current_block = <frame_system::Pallet<T>>::block_number();
match <AccountCurrencyTransferAllowance<T>>::get((
&send,
¤cy,
receive.clone(),
)) {
Some(AllowanceDetails {
allowed_at,
blocked_at,
}) if current_block >= allowed_at && current_block < blocked_at => Ok(Some(receive)),
_ => Err(DispatchError::from(Error::<T>::NoAllowanceForDestination)),
}
}
_ => Ok(None),
}
}
}
}