Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Add Blocked Account Status to Assets Pallet #14070

Merged
merged 5 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions frame/assets/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -532,5 +532,12 @@ benchmarks_instance_pallet! {
assert!(T::Currency::reserved_balance(&asset_owner).is_zero());
}

block {
let (asset_id, caller, caller_lookup) = create_default_minted_asset::<T, I>(true, 100u32.into());
}: _(SystemOrigin::Signed(caller.clone()), asset_id, caller_lookup)
verify {
assert_last_event::<T, I>(Event::Blocked { asset_id: asset_id.into(), who: caller }.into());
}

impl_benchmark_test_suite!(Assets, crate::mock::new_test_ext(), crate::mock::Test)
}
19 changes: 11 additions & 8 deletions frame/assets/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,11 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
if increase_supply && details.supply.checked_add(&amount).is_none() {
return DepositConsequence::Overflow
}
if let Some(balance) = Self::maybe_balance(id, who) {
if balance.checked_add(&amount).is_none() {
if let Some(account) = Account::<T, I>::get(id, who) {
if account.status.is_blocked() {
return DepositConsequence::Blocked
}
if account.balance.checked_add(&amount).is_none() {
return DepositConsequence::Overflow
}
} else {
Expand Down Expand Up @@ -179,7 +182,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
Some(a) => a,
None => return BalanceLow,
};
if account.is_frozen {
if account.status.is_frozen() {
return Frozen
}
if let Some(rest) = account.balance.checked_sub(&amount) {
Expand Down Expand Up @@ -220,7 +223,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);

let account = Account::<T, I>::get(id, who).ok_or(Error::<T, I>::NoAccount)?;
ensure!(!account.is_frozen, Error::<T, I>::Frozen);
ensure!(!account.status.is_frozen(), Error::<T, I>::Frozen);

let amount = if let Some(frozen) = T::Freezer::frozen_balance(id, who) {
// Frozen balance: account CANNOT be deleted
Expand Down Expand Up @@ -329,7 +332,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
&who,
AssetAccountOf::<T, I> {
balance: Zero::zero(),
is_frozen: false,
status: AccountStatus::Liquid,
reason,
extra: T::Extra::default(),
},
Expand Down Expand Up @@ -378,7 +381,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
account.reason.take_deposit_from().ok_or(Error::<T, I>::NoDeposit)?;
let mut details = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
ensure!(details.status == AssetStatus::Live, Error::<T, I>::AssetNotLive);
ensure!(!account.is_frozen, Error::<T, I>::Frozen);
ensure!(!account.status.is_frozen(), Error::<T, I>::Frozen);
ensure!(caller == &depositor || caller == &details.admin, Error::<T, I>::NoPermission);
ensure!(account.balance.is_zero(), Error::<T, I>::WouldBurn);

Expand Down Expand Up @@ -460,7 +463,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
*maybe_account = Some(AssetAccountOf::<T, I> {
balance: amount,
reason: Self::new_account(beneficiary, details, None)?,
is_frozen: false,
status: AccountStatus::Liquid,
extra: T::Extra::default(),
});
},
Expand Down Expand Up @@ -654,7 +657,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
maybe_account @ None => {
*maybe_account = Some(AssetAccountOf::<T, I> {
balance: credit,
is_frozen: false,
status: AccountStatus::Liquid,
reason: Self::new_account(dest, details, None)?,
extra: T::Extra::default(),
});
Expand Down
51 changes: 47 additions & 4 deletions frame/assets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,16 @@
//! * `burn`: Decreases the asset balance of an account; called by the asset class's Admin.
//! * `force_transfer`: Transfers between arbitrary accounts; called by the asset class's Admin.
//! * `freeze`: Disallows further `transfer`s from an account; called by the asset class's Freezer.
//! * `thaw`: Allows further `transfer`s from an account; called by the asset class's Admin.
//! * `thaw`: Allows further `transfer`s to and from an account; called by the asset class's Admin.
//! * `transfer_ownership`: Changes an asset class's Owner; called by the asset class's Owner.
//! * `set_team`: Changes an asset class's Admin, Freezer and Issuer; called by the asset class's
//! Owner.
//! * `set_metadata`: Set the metadata of an asset class; called by the asset class's Owner.
//! * `clear_metadata`: Remove the metadata of an asset class; called by the asset class's Owner.
//! * `touch_other`: Create an asset account for specified account. Caller must place a deposit;
//! called by the asset class's Freezer or Admin.
//! * `block`: Disallows further `transfer`s to and from an account; called by the asset class's
//! Freezer.
//!
//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
Expand Down Expand Up @@ -527,6 +529,8 @@ pub mod pallet {
AssetMinBalanceChanged { asset_id: T::AssetId, new_min_balance: T::Balance },
/// Some account `who` was created with a deposit from `depositor`.
Touched { asset_id: T::AssetId, who: T::AccountId, depositor: T::AccountId },
/// Some account `who` was blocked.
Blocked { asset_id: T::AssetId, who: T::AccountId },
}

#[pallet::error]
Expand Down Expand Up @@ -949,15 +953,16 @@ pub mod pallet {
let who = T::Lookup::lookup(who)?;

Account::<T, I>::try_mutate(id, &who, |maybe_account| -> DispatchResult {
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.is_frozen = true;
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.status =
AccountStatus::Frozen;
Ok(())
})?;

Self::deposit_event(Event::<T, I>::Frozen { asset_id: id, who });
Ok(())
}

/// Allow unprivileged transfers from an account again.
/// Allow unprivileged transfers to and from an account again.
///
/// Origin must be Signed and the sender should be the Admin of the asset `id`.
///
Expand Down Expand Up @@ -985,7 +990,8 @@ pub mod pallet {
let who = T::Lookup::lookup(who)?;

Account::<T, I>::try_mutate(id, &who, |maybe_account| -> DispatchResult {
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.is_frozen = false;
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.status =
AccountStatus::Liquid;
Ok(())
})?;

Expand Down Expand Up @@ -1600,6 +1606,43 @@ pub mod pallet {
let id: T::AssetId = id.into();
Self::do_refund_other(id, &who, &origin)
}

/// Disallow further unprivileged transfers of an asset `id` to and from an account `who`.
///
/// Origin must be Signed and the sender should be the Freezer of the asset `id`.
///
/// - `id`: The identifier of the account's asset.
/// - `who`: The account to be unblocked.
///
/// Emits `Blocked`.
///
/// Weight: `O(1)`
#[pallet::call_index(31)]
pub fn block(
origin: OriginFor<T>,
id: T::AssetIdParameter,
who: AccountIdLookupOf<T>,
) -> DispatchResult {
let origin = ensure_signed(origin)?;
let id: T::AssetId = id.into();

let d = Asset::<T, I>::get(id).ok_or(Error::<T, I>::Unknown)?;
ensure!(
d.status == AssetStatus::Live || d.status == AssetStatus::Frozen,
Error::<T, I>::AssetNotLive
);
ensure!(origin == d.freezer, Error::<T, I>::NoPermission);
let who = T::Lookup::lookup(who)?;

Account::<T, I>::try_mutate(id, &who, |maybe_account| -> DispatchResult {
maybe_account.as_mut().ok_or(Error::<T, I>::NoAccount)?.status =
AccountStatus::Blocked;
Ok(())
})?;

Self::deposit_event(Event::<T, I>::Blocked { asset_id: id, who });
Ok(())
}
}
}

Expand Down
31 changes: 31 additions & 0 deletions frame/assets/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,37 @@ fn approve_transfer_frozen_asset_should_not_work() {
});
}

#[test]
fn transferring_from_blocked_account_should_not_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1));
assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_ok!(Assets::block(RuntimeOrigin::signed(1), 0, 1));
// behaves as frozen when transferring from blocked
assert_noop!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50), Error::<Test>::Frozen);
assert_ok!(Assets::thaw(RuntimeOrigin::signed(1), 0, 1));
assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50));
assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 50));
});
}

#[test]
fn transferring_to_blocked_account_should_not_work() {
new_test_ext().execute_with(|| {
assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1));
assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100));
assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100));
assert_eq!(Assets::balance(0, 1), 100);
assert_eq!(Assets::balance(0, 2), 100);
assert_ok!(Assets::block(RuntimeOrigin::signed(1), 0, 1));
assert_noop!(Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 50), TokenError::Blocked);
assert_ok!(Assets::thaw(RuntimeOrigin::signed(1), 0, 1));
assert_ok!(Assets::transfer(RuntimeOrigin::signed(2), 0, 1, 50));
assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50));
});
}

#[test]
fn origin_guards_should_work() {
new_test_ext().execute_with(|| {
Expand Down
31 changes: 29 additions & 2 deletions frame/assets/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,39 @@ where
}
}

#[test]
fn ensure_bool_decodes_to_liquid_or_frozen() {
assert_eq!(false.encode(), AccountStatus::Liquid.encode());
assert_eq!(true.encode(), AccountStatus::Frozen.encode());
}

/// The status of an asset account.
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
pub enum AccountStatus {
/// Asset account can receive and transfer the assets.
Liquid,
/// Asset account cannot transfer the assets.
Frozen,
/// Asset account cannot receive and transfer the assets.
Blocked,
}
impl AccountStatus {
/// Returns `true` if frozen or blocked.
pub(crate) fn is_frozen(&self) -> bool {
matches!(self, AccountStatus::Frozen | AccountStatus::Blocked)
}
/// Returns `true` if blocked.
pub(crate) fn is_blocked(&self) -> bool {
matches!(self, AccountStatus::Blocked)
}
}

#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
pub struct AssetAccount<Balance, DepositBalance, Extra, AccountId> {
/// The balance.
pub(super) balance: Balance,
/// Whether the account is frozen.
pub(super) is_frozen: bool,
/// The status of the account.
pub(super) status: AccountStatus,
/// The reason for the existence of the account.
pub(super) reason: ExistenceReason<DepositBalance, AccountId>,
/// Additional "sidecar" data, in case some other pallet wants to use this storage item.
Expand Down
27 changes: 27 additions & 0 deletions frame/assets/src/weights.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frame/support/src/traits/tokens/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ pub enum DepositConsequence {
Overflow,
/// Account continued in existence.
Success,
/// Account cannot receive the assets.
Blocked,
}

impl DepositConsequence {
Expand All @@ -152,6 +154,7 @@ impl DepositConsequence {
CannotCreate => TokenError::CannotCreate.into(),
UnknownAsset => TokenError::UnknownAsset.into(),
Overflow => ArithmeticError::Overflow.into(),
Blocked => TokenError::Blocked.into(),
Success => return Ok(()),
})
}
Expand Down
3 changes: 3 additions & 0 deletions primitives/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,8 @@ pub enum TokenError {
CannotCreateHold,
/// Withdrawal would cause unwanted loss of account.
NotExpendable,
/// Account cannot receive the assets.
Blocked,
}

impl From<TokenError> for &'static str {
Expand All @@ -641,6 +643,7 @@ impl From<TokenError> for &'static str {
TokenError::CannotCreateHold =>
"Account cannot be created for recording amount on hold",
TokenError::NotExpendable => "Account that is desired to remain would die",
TokenError::Blocked => "Account cannot receive the assets",
}
}
}
Expand Down