diff --git a/Cargo.lock b/Cargo.lock index 39ec3e8c26b18..c7eaec4e5fffa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4637,6 +4637,23 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-assets-freezer" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-assets", + "pallet-balances", + "parity-scale-codec", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-atomic-swap" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index 9d7017be1d0de..ddd338fec1d15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ members = [ "client/transaction-pool", "client/transaction-pool/graph", "frame/assets", + "frame/assets-freezer", "frame/atomic-swap", "frame/aura", "frame/authority-discovery", diff --git a/frame/assets-freezer/Cargo.toml b/frame/assets-freezer/Cargo.toml new file mode 100644 index 0000000000000..0d60553e64570 --- /dev/null +++ b/frame/assets-freezer/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "pallet-assets-freezer" +version = "3.0.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/substrate/" +description = "Extension pallet for managing frozen assets" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { version = "1.0.101", optional = true } +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +sp-std = { version = "3.0.0", default-features = false, path = "../../primitives/std" } +sp-runtime = { version = "3.0.0", default-features = false, path = "../../primitives/runtime" } +frame-support = { version = "3.0.0", default-features = false, path = "../support" } +frame-system = { version = "3.0.0", default-features = false, path = "../system" } +frame-benchmarking = { version = "3.1.0", default-features = false, path = "../benchmarking", optional = true } + +[dev-dependencies] +sp-core = { version = "3.0.0", path = "../../primitives/core" } +sp-std = { version = "3.0.0", path = "../../primitives/std" } +sp-io = { version = "3.0.0", path = "../../primitives/io" } +pallet-balances = { version = "3.0.0", path = "../balances" } +pallet-assets = { version = "3.0.0", default-features = false, path = "../assets" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sp-std/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + "frame-benchmarking/std", + "pallet-assets/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "sp-runtime/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/frame/assets-freezer/README.md b/frame/assets-freezer/README.md new file mode 100644 index 0000000000000..94b70026970ca --- /dev/null +++ b/frame/assets-freezer/README.md @@ -0,0 +1,21 @@ +# Assets Freezer Pallet + +## Overview +### Terminology +### Goals + +## Interface +### Dispatchable Functions +### Public Functions + +## Usage +### Prerequisites +### Simple Code Snippet + +## Assumptions + +## Related Modules + +* [`Assets`](https://docs.rs/frame-support/latest/pallet_assets/) + +License: Apache-2.0 diff --git a/frame/assets-freezer/src/lib.rs b/frame/assets-freezer/src/lib.rs new file mode 100644 index 0000000000000..cc6c385de4446 --- /dev/null +++ b/frame/assets-freezer/src/lib.rs @@ -0,0 +1,361 @@ +// This file is part of Substrate. + +// Copyright (C) 2017-2021 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. + +//! # Assets Freezer Pallet +//! +//! An extension pallet for use with the Assets pallet for allowing funds to be locked and reserved. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +pub mod mock; +#[cfg(test)] +mod tests; + +use sp_std::prelude::*; +use sp_runtime::{TokenError, ArithmeticError, traits::{Zero, Saturating, CheckedAdd, CheckedSub}}; +use frame_support::{ensure, dispatch::{DispatchError, DispatchResult}}; +use frame_support::traits::{ + StoredMap, tokens::{ + WithdrawConsequence, DepositConsequence, fungibles, fungibles::InspectHold, FrozenBalance, + WhenDust + } +}; +use frame_system::Config as SystemConfig; + +pub use pallet::*; + +type BalanceOf = <::Assets as fungibles::Inspect<::AccountId>>::Balance; +type AssetIdOf = <::Assets as fungibles::Inspect<::AccountId>>::AssetId; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use super::*; + + /// The information concerning our freezing. + #[derive(Eq, PartialEq, Clone, Encode, Decode, RuntimeDebug, Default)] + pub struct FreezeData { + /// The amount of funds that have been reserved. The actual amount of funds held in reserve + /// (and thus guaranteed of being unreserved) is this amount less `melted`. + /// + /// If this `is_zero`, then the account may be deleted. If it is non-zero, then the assets + /// pallet will attempt to keep the account alive by retaining the `minimum_balance` *plus* + /// this number of funds in it. + pub(super) reserved: Balance, + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + /// The module configuration trait. + pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + IsType<::Event>; + + /// The fungibles trait impl whose assets this reserves. + type Assets: fungibles::InspectWithoutFreezer; + + /// Place to store the fast-access freeze data for the given asset/account. + type Store: StoredMap<(AssetIdOf, Self::AccountId), FreezeData>>; + } + + // + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + #[pallet::metadata(T::AccountId = "AccountId", BalanceOf = "Balance", AssetIdOf = "AssetId")] + pub enum Event { + /// An asset has been reserved. + /// \[asset, who, amount\] + Held(AssetIdOf, T::AccountId, BalanceOf), + /// An asset has been unreserved. + /// \[asset, who, amount\] + Released(AssetIdOf, T::AccountId, BalanceOf), + } + + // No new errors + #[pallet::error] + pub enum Error {} + + // No hooks. + #[pallet::hooks] + impl Hooks> for Pallet {} + + // No calls. + #[pallet::call] + impl Pallet {} +} + +impl FrozenBalance, T::AccountId, BalanceOf> for Pallet { + fn frozen_balance(id: AssetIdOf, who: &T::AccountId) -> Option> { + let f = T::Store::get(&(id, who.clone())); + if f.reserved.is_zero() { None } else { Some(f.reserved) } + } +} + +impl fungibles::Inspect<::AccountId> for Pallet { + type AssetId = AssetIdOf; + type Balance = BalanceOf; + fn total_issuance(asset: AssetIdOf) -> BalanceOf { + T::Assets::total_issuance(asset) + } + fn minimum_balance(asset: AssetIdOf) -> BalanceOf { + T::Assets::minimum_balance(asset) + } + fn balance(asset: AssetIdOf, who: &T::AccountId) -> BalanceOf { + T::Assets::balance(asset, who) + } + fn reducible_balance(asset: AssetIdOf, who: &T::AccountId, keep_alive: bool) -> BalanceOf { + T::Assets::reducible_balance(asset, who, keep_alive) + } + fn can_deposit(asset: AssetIdOf, who: &T::AccountId, amount: BalanceOf) + -> DepositConsequence + { + T::Assets::can_deposit(asset, who, amount) + } + fn can_withdraw( + asset: AssetIdOf, + who: &T::AccountId, + amount: BalanceOf, + ) -> WithdrawConsequence> { + T::Assets::can_withdraw(asset, who, amount) + } +} + +impl fungibles::Transfer<::AccountId> for Pallet where + T::Assets: fungibles::Transfer +{ + fn transfer( + asset: Self::AssetId, + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + death: WhenDust, + ) -> Result { + T::Assets::transfer(asset, source, dest, amount, death) + } + fn transfer_best_effort( + asset: Self::AssetId, + source: &T::AccountId, + dest: &T::AccountId, + amount: Self::Balance, + death: WhenDust, + ) -> Result { + T::Assets::transfer_best_effort(asset, source, dest, amount, death) + } +} + +impl fungibles::Unbalanced for Pallet where + T::Assets: fungibles::Unbalanced +{ + fn set_balance(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) + -> DispatchResult + { + T::Assets::set_balance(asset, who, amount) + } + + fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) { + T::Assets::set_total_issuance(asset, amount) + } + + fn decrease_balance( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + keep_alive: bool, + ) -> Result { + T::Assets::decrease_balance(asset, who, amount, keep_alive) + } + + fn increase_balance(asset: Self::AssetId, who: &T::AccountId, amount: Self::Balance) + -> Result<(), DispatchError> + { + T::Assets::increase_balance(asset, who, amount) + } + + fn decrease_balance_at_most( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + keep_alive: bool, + ) -> Self::Balance { + T::Assets::decrease_balance_at_most(asset, who, amount, keep_alive) + } + + fn increase_balance_at_most( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Self::Balance { + T::Assets::increase_balance_at_most(asset, who, amount) + } +} + +impl fungibles::InspectHold<::AccountId> for Pallet { + fn balance_on_hold(asset: AssetIdOf, who: &T::AccountId) -> BalanceOf { + T::Store::get(&(asset, who.clone())).reserved + } + fn can_hold(asset: AssetIdOf, who: &T::AccountId, amount: BalanceOf) -> bool { + // If we can withdraw without destroying the account, then we're good. + >::can_withdraw(asset, who, amount) == WithdrawConsequence::Success + } + fn reducible_balance_on_hold(asset: AssetIdOf, who: &T::AccountId) -> BalanceOf { + // Figure out the most we can transfer from the balance on hold. This is basically the same + // as the balance on hold, but also ensures that the account actually has enough funds to + // be reduced and that no freezes have been placed on the asset/account. + let amount = T::Store::get(&(asset, who.clone())).reserved; + use fungibles::InspectWithoutFreezer; + let backing = T::Assets::reducible_balance(asset, who, true); + amount.min(backing) + } +} + +impl fungibles::MutateHold<::AccountId> for Pallet where + T::Assets: fungibles::Transfer + fungibles::InspectWithoutFreezer +{ + fn hold(asset: AssetIdOf, who: &T::AccountId, amount: BalanceOf) -> DispatchResult { + if !Self::can_hold(asset, who, amount) { + Err(TokenError::NoFunds)? + } + T::Store::mutate( + &(asset, who.clone()), + |extra| extra.reserved = extra.reserved.saturating_add(amount), + )?; + + Self::deposit_event(Event::Held(asset, who.clone(), amount)); + Ok(()) + } + + fn release(asset: AssetIdOf, who: &T::AccountId, amount: BalanceOf, best_effort: bool) + -> Result, DispatchError> + { + T::Store::try_mutate_exists( + &(asset, who.clone()), + |maybe_extra| if let Some(ref mut extra) = maybe_extra { + let old = extra.reserved; + extra.reserved = extra.reserved.saturating_sub(amount); + let actual = old - extra.reserved; + ensure!(best_effort || actual == amount, TokenError::NoFunds); + + Self::deposit_event(Event::Released(asset, who.clone(), actual)); + + Ok(actual) + } else { + Err(TokenError::NoFunds)? + }, + ) + } + + fn transfer_held( + asset: AssetIdOf, + source: &T::AccountId, + dest: &T::AccountId, + amount: BalanceOf, + on_hold: bool, + ) -> Result, DispatchError> { + // Can't create the account with just a chunk of held balance - there needs to already be + // the minimum deposit. + let min_balance = >::minimum_balance(asset); + let dest_balance = >::balance(asset, dest); + ensure!(!on_hold || dest_balance >= min_balance, TokenError::CannotCreate); + Self::balance_on_hold(asset, dest).checked_add(&amount).ok_or(ArithmeticError::Overflow)?; + + Self::decrease_on_hold_ensuring_backed(asset, source, amount)?; + + // `death` is `KeepAlive` here since we're only transferring funds that were on hold, for + // which there must be an additional min_balance, it should be impossible for the transfer + // to cause the account to be deleted. + use fungibles::Transfer; + let result = Self::transfer(asset, source, dest, amount, WhenDust::KeepAlive); + if result.is_ok() { + if on_hold { + let r = Self::increase_on_hold(asset, dest, amount); + debug_assert!(r.is_ok(), "account exists and funds transferred in; qed"); + r? + } + } else { + debug_assert!(false, "can_withdraw was successful; qed"); + } + result + } +} + +impl fungibles::UnbalancedHold<::AccountId> for Pallet where + T::Assets: fungibles::Unbalanced +{ + fn decrease_balance_on_hold( + asset: AssetIdOf, + source: &T::AccountId, + amount: BalanceOf, + ) -> Result, DispatchError> { + Self::decrease_on_hold_ensuring_backed(asset, source, amount)?; + // The previous call's success guarantees the next will succeed. + >::decrease_balance(asset, source, amount, true) + } +} + +impl Pallet { + /// Reduce the amount we have on hold of an account in such a way to ensure that the balance + /// should be decreasable by the amount reduced. + /// + /// NOTE: This won't alter the balance of the account. + fn decrease_on_hold( + asset: AssetIdOf, + source: &T::AccountId, + amount: BalanceOf, + ) -> Result<(), DispatchError> { + T::Store::try_mutate_exists(&(asset, source.clone()), |maybe_extra| { + if let Some(ref mut extra) = maybe_extra { + // Figure out the most we can unreserve and transfer. + extra.reserved = extra.reserved.checked_sub(&amount).ok_or(TokenError::NoFunds)?; + Ok(()) + } else { + Err(TokenError::NoFunds)? + } + }) + } + + fn increase_on_hold( + asset: AssetIdOf, + source: &T::AccountId, + amount: BalanceOf, + ) -> Result<(), DispatchError> { + T::Store::mutate( + &(asset, source.clone()), + |extra| extra.reserved = extra.reserved.saturating_add(amount) + )?; + Ok(()) + } + + /// Same as decrease_on_hold, except that we guarantee that `amount` balance will be definitely + /// be reducible immediately afterwards. + fn decrease_on_hold_ensuring_backed( + asset: AssetIdOf, + source: &T::AccountId, + amount: BalanceOf, + ) -> Result<(), DispatchError> { + // Just make sure that we can actually draw the amount of asset out of source once it + // becomes unfrozen first. + >::can_withdraw(asset, source, amount) + .into_result(true)?; + Self::decrease_on_hold(asset, source, amount) + } +} diff --git a/frame/assets-freezer/src/mock.rs b/frame/assets-freezer/src/mock.rs new file mode 100644 index 0000000000000..83e8b4a67433e --- /dev/null +++ b/frame/assets-freezer/src/mock.rs @@ -0,0 +1,121 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 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. + +//! Test environment for Assets pallet. + +use super::*; +use crate as pallet_assets_freezer; + +use sp_core::H256; +use sp_runtime::{traits::{BlakeTwo256, IdentityLookup}, testing::Header}; +use frame_support::{parameter_types, construct_runtime}; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Assets: pallet_assets::{Pallet, Call, Storage, Event}, + AssetsFreezer: pallet_assets_freezer::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} +impl frame_system::Config for Test { + type BaseCallFilter = (); + type BlockWeights = (); + type BlockLength = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +parameter_types! { + pub const ExistentialDeposit: u64 = 1; +} + +impl pallet_balances::Config for Test { + type Balance = u64; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); +} + +parameter_types! { + pub const AssetDeposit: u64 = 1; + pub const ApprovalDeposit: u64 = 1; + pub const StringLimit: u32 = 50; + pub const MetadataDepositBase: u64 = 1; + pub const MetadataDepositPerByte: u64 = 1; +} + +impl pallet_assets::Config for Test { + type Event = Event; + type Balance = u64; + type AssetId = u32; + type Currency = Balances; + type ForceOrigin = frame_system::EnsureRoot; + type AssetDeposit = AssetDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = StringLimit; + type Freezer = AssetsFreezer; + type WeightInfo = (); + type Extra = super::FreezeData; +} + +impl Config for Test { + type Event = Event; + type Assets = Assets; + type Store = Assets; +} + +pub(crate) fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/frame/assets-freezer/src/tests.rs b/frame/assets-freezer/src/tests.rs new file mode 100644 index 0000000000000..324a35699e466 --- /dev/null +++ b/frame/assets-freezer/src/tests.rs @@ -0,0 +1,182 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2021 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. + +//! Tests for Assets Freezer pallet. + +use super::*; +use crate::mock::*; +use sp_runtime::TokenError; +use frame_support::{ + assert_ok, + assert_noop, + traits::{ + fungibles::{ + Inspect, + MutateHold, + UnbalancedHold, + Transfer, + }, + }, +}; +use pallet_assets::Error as AssetsError; + +fn last_event() -> mock::Event { + frame_system::Pallet::::events().pop().expect("Event expected").event +} + +#[test] +fn basic_minting_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100)); + assert_eq!(AssetsFreezer::balance(0, &1), 100); + assert_eq!(AssetsFreezer::total_issuance(0), 100); + }); +} + +#[test] +fn hold_asset_balance_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 200)); + assert_eq!(AssetsFreezer::can_hold(0, &1, 100), true); + assert_ok!(AssetsFreezer::hold(0, &1, 100)); + assert_eq!( + last_event(), + mock::Event::pallet_assets_freezer(crate::Event::Held(0, 1, 100)), + ); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 100); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + }); +} + +#[test] +fn decrease_and_remove_asset_on_hold_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 200)); + assert_eq!(AssetsFreezer::can_hold(0, &1, 100), true); + assert_ok!(AssetsFreezer::hold(0, &1, 100)); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 100); + assert_ok!(AssetsFreezer::decrease_balance_on_hold(0, &1, 50)); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 50); + assert_eq!(AssetsFreezer::balance(0, &1), 150); + }); +} + +#[test] +fn decrease_asset_on_hold_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 200)); + assert_eq!(AssetsFreezer::can_hold(0, &1, 100), true); + assert_ok!(AssetsFreezer::hold(0, &1, 100)); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 100); + assert_ok!(AssetsFreezer::decrease_on_hold(0, &1, 50)); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 50); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + }); +} + +#[test] +fn decrease_reducible_asset_on_hold_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 200)); + assert_ok!(AssetsFreezer::hold(0, &1, 100)); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 100); + assert_eq!(AssetsFreezer::reducible_balance_on_hold(0, &1), 100); + assert_noop!(AssetsFreezer::decrease_on_hold(0, &1, 150), TokenError::NoFunds); + assert_ok!(AssetsFreezer::decrease_on_hold(0, &1, 50)); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 50); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + }); +} + +#[test] +fn increase_asset_on_hold_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 200)); + assert_eq!(AssetsFreezer::can_hold(0, &1, 100), true); + assert_ok!(AssetsFreezer::hold(0, &1, 100)); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 100); + assert_ok!(AssetsFreezer::increase_on_hold(0, &1, 50)); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 150); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + }); +} + +#[test] +fn release_asset_on_hold_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 200)); + assert_eq!(AssetsFreezer::can_hold(0, &1, 100), true); + assert_ok!(AssetsFreezer::hold(0, &1, 100)); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 100); + assert_ok!(AssetsFreezer::release(0, &1, 30, true)); + assert_eq!( + last_event(), + mock::Event::pallet_assets_freezer(crate::Event::Released(0, 1, 30)), + ); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 70); + assert_ok!(AssetsFreezer::release(0, &1, 70, true)); + assert_eq!( + last_event(), + mock::Event::pallet_assets_freezer(crate::Event::Released(0, 1, 70)), + ); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 0); + assert_eq!(AssetsFreezer::balance(0, &1), 200); + }); +} + +#[test] +fn transfer_asset_on_hold_should_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 200)); + assert_ok!(AssetsFreezer::hold(0, &1, 100)); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 100); + assert_ok!(Assets::mint(Origin::signed(1), 0, 2, 1)); + assert_eq!(AssetsFreezer::transfer_held(0, &1, &2, 100, true), Ok(100)); + assert_eq!(AssetsFreezer::balance(0, &1), 100); + assert_eq!(AssetsFreezer::balance(0, &2), 101); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 0); + assert_eq!(AssetsFreezer::balance_on_hold(0, &2), 100); + }); +} + +#[test] +fn transfer_low_asset_on_hold_should_fail() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 200)); + assert_ok!(AssetsFreezer::hold(0, &1, 100)); + assert_eq!(AssetsFreezer::balance_on_hold(0, &1), 100); + assert_noop!(AssetsFreezer::transfer(0, &1, &2, 150, WhenDust::Dispose), AssetsError::::BalanceLow); + // Can't create the account with just a chunk of held balance - there needs to already be + // the minimum deposit. + assert_noop!(AssetsFreezer::transfer_held(0, &1, &2, 150, true), TokenError::CannotCreate); + assert_ok!(Assets::mint(Origin::signed(1), 0, 2, 1)); + assert_noop!(AssetsFreezer::transfer_held(0, &1, &2, 150, true), TokenError::NoFunds); + }); +} diff --git a/frame/assets/src/benchmarking.rs b/frame/assets/src/benchmarking.rs index c6925df9ad88f..120ac7a03210a 100644 --- a/frame/assets/src/benchmarking.rs +++ b/frame/assets/src/benchmarking.rs @@ -167,7 +167,7 @@ benchmarks_instance_pallet! { assert_last_event::(Event::Issued(Default::default(), caller, amount).into()); } - burn { + slash { let amount = T::Balance::from(100u32); let (caller, caller_lookup) = create_default_minted_asset::(true, amount); }: _(SystemOrigin::Signed(caller.clone()), Default::default(), caller_lookup, amount) diff --git a/frame/assets/src/functions.rs b/frame/assets/src/functions.rs index c6b5391cff860..cd5830e5923ef 100644 --- a/frame/assets/src/functions.rs +++ b/frame/assets/src/functions.rs @@ -61,7 +61,7 @@ impl, I: 'static> Pallet { } pub(super) fn dead_account( - what: T::AssetId, + _what: T::AssetId, who: &T::AccountId, d: &mut AssetDetails>, sufficient: bool, @@ -73,7 +73,6 @@ impl, I: 'static> Pallet { frame_system::Pallet::::dec_consumers(who); } d.accounts = d.accounts.saturating_sub(1); - T::Freezer::died(what, who) } pub(super) fn can_increase( @@ -112,7 +111,7 @@ impl, I: 'static> Pallet { id: T::AssetId, who: &T::AccountId, amount: T::Balance, - keep_alive: bool, + f: DebitFlags, ) -> WithdrawConsequence { use WithdrawConsequence::*; let details = match Asset::::get(id) { @@ -130,7 +129,7 @@ impl, I: 'static> Pallet { return Frozen } if let Some(rest) = account.balance.checked_sub(&amount) { - if let Some(frozen) = T::Freezer::frozen_balance(id, who) { + if let (Some(frozen), false) = (T::Freezer::frozen_balance(id, who), f.ignore_freezer) { match frozen.checked_add(&details.min_balance) { Some(required) if rest < required => return Frozen, None => return Overflow, @@ -140,7 +139,7 @@ impl, I: 'static> Pallet { let is_provider = false; let is_required = is_provider && !frame_system::Pallet::::can_dec_provider(who); - let must_keep_alive = keep_alive || is_required; + let must_keep_alive = f.keep_alive || is_required; if rest < details.min_balance { if must_keep_alive { @@ -161,7 +160,7 @@ impl, I: 'static> Pallet { pub(super) fn reducible_balance( id: T::AssetId, who: &T::AccountId, - keep_alive: bool, + f: DebitFlags, ) -> Result { let details = Asset::::get(id).ok_or_else(|| Error::::Unknown)?; ensure!(!details.is_frozen, Error::::Frozen); @@ -169,7 +168,7 @@ impl, I: 'static> Pallet { let account = Account::::get(id, who); ensure!(!account.is_frozen, Error::::Frozen); - let amount = if let Some(frozen) = T::Freezer::frozen_balance(id, who) { + let amount = if let (Some(frozen), false) = (T::Freezer::frozen_balance(id, who), f.ignore_freezer) { // Frozen balance: account CANNOT be deleted let required = frozen .checked_add(&details.min_balance) @@ -178,7 +177,7 @@ impl, I: 'static> Pallet { } else { let is_provider = false; let is_required = is_provider && !frame_system::Pallet::::can_dec_provider(who); - if keep_alive || is_required { + if f.keep_alive || is_required { // We want to keep the account around. account.balance.saturating_sub(details.min_balance) } else { @@ -210,11 +209,12 @@ impl, I: 'static> Pallet { amount: T::Balance, f: DebitFlags, ) -> Result { - let actual = Self::reducible_balance(id, target, f.keep_alive)?.min(amount); - ensure!(f.best_effort || actual >= amount, Error::::BalanceLow); + let actual = Self::reducible_balance(id, target, f.into())? + .min(amount); + ensure!(actual >= amount, Error::::BalanceLow); - let conseq = Self::can_decrease(id, target, actual, f.keep_alive); - let actual = match conseq.into_result() { + let conseq = Self::can_decrease(id, target, actual, f.into()); + let actual = match conseq.into_result(false) { Ok(dust) => actual.saturating_add(dust), //< guaranteed by reducible_balance Err(e) => { debug_assert!(false, "passed from reducible_balance; qed"); @@ -409,7 +409,7 @@ impl, I: 'static> Pallet { dest: &T::AccountId, amount: T::Balance, maybe_need_admin: Option, - f: TransferFlags, + death: WhenDust, ) -> Result { // Early exist if no-op. if amount.is_zero() { @@ -418,8 +418,8 @@ impl, I: 'static> Pallet { } // Figure out the debit and credit, together with side-effects. - let debit = Self::prep_debit(id, &source, amount, f.into())?; - let (credit, maybe_burn) = Self::prep_credit(id, &dest, amount, debit, f.burn_dust)?; + let debit = Self::prep_debit(id, &source, amount, death.into())?; + let (credit, maybe_burn) = Self::prep_credit(id, &dest, amount, debit, death.dispose())?; let mut source_account = Account::::get(id, &source); diff --git a/frame/assets/src/impl_fungibles.rs b/frame/assets/src/impl_fungibles.rs index d0ab13072a88d..294a23ae4c2a1 100644 --- a/frame/assets/src/impl_fungibles.rs +++ b/frame/assets/src/impl_fungibles.rs @@ -44,7 +44,8 @@ impl, I: 'static> fungibles::Inspect<::AccountId who: &::AccountId, keep_alive: bool, ) -> Self::Balance { - Pallet::::reducible_balance(asset, who, keep_alive).unwrap_or(Zero::zero()) + let f = DebitFlags { keep_alive, ignore_freezer: false }; + Pallet::::reducible_balance(asset, who, f).unwrap_or(Zero::zero()) } fn can_deposit( @@ -60,40 +61,46 @@ impl, I: 'static> fungibles::Inspect<::AccountId who: &::AccountId, amount: Self::Balance, ) -> WithdrawConsequence { - Pallet::::can_decrease(asset, who, amount, false) + let f = DebitFlags { keep_alive: false, ignore_freezer: false }; + Pallet::::can_decrease(asset, who, amount, f) } } -impl, I: 'static> fungibles::Mutate<::AccountId> for Pallet { - fn mint_into( +impl, I: 'static> fungibles::InspectWithoutFreezer<::AccountId> for Pallet { + fn reducible_balance( + asset: Self::AssetId, + who: &::AccountId, + keep_alive: bool, + ) -> Self::Balance { + let f = DebitFlags { keep_alive, ignore_freezer: true }; + Pallet::::reducible_balance(asset, who, f).unwrap_or(Zero::zero()) + } + + fn can_withdraw( asset: Self::AssetId, who: &::AccountId, amount: Self::Balance, - ) -> DispatchResult { - Self::do_mint(asset, who, amount, None) + ) -> WithdrawConsequence { + let f = DebitFlags { keep_alive: false, ignore_freezer: true }; + Pallet::::can_decrease(asset, who, amount, f) } +} - fn burn_from( +impl, I: 'static> fungibles::Mutate<::AccountId> for Pallet { + fn mint_into( asset: Self::AssetId, who: &::AccountId, amount: Self::Balance, - ) -> Result { - let f = DebitFlags { - keep_alive: false, - best_effort: false, - }; - Self::do_burn(asset, who, amount, None, f) + ) -> DispatchResult { + Self::do_mint(asset, who, amount, None) } - fn slash( + fn burn_from( asset: Self::AssetId, who: &::AccountId, amount: Self::Balance, ) -> Result { - let f = DebitFlags { - keep_alive: false, - best_effort: true, - }; + let f = DebitFlags { keep_alive: false, ignore_freezer: false }; Self::do_burn(asset, who, amount, None, f) } } @@ -104,14 +111,9 @@ impl, I: 'static> fungibles::Transfer for Pallet Result { - let f = TransferFlags { - keep_alive, - best_effort: false, - burn_dust: false - }; - Self::do_transfer(asset, source, dest, amount, None, f) + Self::do_transfer(asset, source, dest, amount, None, death) } } @@ -126,31 +128,15 @@ impl, I: 'static> fungibles::Unbalanced for Pallet Result + fn decrease_balance(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance, keep_alive: bool) + -> Result { - let f = DebitFlags { keep_alive: false, best_effort: false }; + let f = DebitFlags { keep_alive, ignore_freezer: false }; Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())) } - fn decrease_balance_at_most(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance) - -> Self::Balance - { - let f = DebitFlags { keep_alive: false, best_effort: true }; - Self::decrease_balance(asset, who, amount, f, |_, _| Ok(())) - .unwrap_or(Zero::zero()) - } fn increase_balance(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance) - -> Result - { - Self::increase_balance(asset, who, amount, |_| Ok(()))?; - Ok(amount) - } - fn increase_balance_at_most(asset: T::AssetId, who: &T::AccountId, amount: Self::Balance) - -> Self::Balance + -> DispatchResult { - match Self::increase_balance(asset, who, amount, |_| Ok(())) { - Ok(()) => amount, - Err(_) => Zero::zero(), - } + Self::increase_balance(asset, who, amount, |_| Ok(())) } } diff --git a/frame/assets/src/lib.rs b/frame/assets/src/lib.rs index e856211289b0b..02168b7eb1a8b 100644 --- a/frame/assets/src/lib.rs +++ b/frame/assets/src/lib.rs @@ -146,9 +146,13 @@ use sp_runtime::{ } }; use codec::{Encode, Decode, HasCompact}; -use frame_support::{ensure, dispatch::{DispatchError, DispatchResult}}; -use frame_support::traits::{Currency, ReservableCurrency, BalanceStatus::Reserved, StoredMap}; -use frame_support::traits::tokens::{WithdrawConsequence, DepositConsequence, fungibles}; +use frame_support::{ + ensure, dispatch::{DispatchError, DispatchResult}, traits::{ + Currency, ReservableCurrency, BalanceStatus::Reserved, StoredMap, tokens::{ + WithdrawConsequence, DepositConsequence, fungibles, FrozenBalance, WhenDust + }, + }, +}; use frame_system::Config as SystemConfig; pub use weights::WeightInfo; @@ -418,7 +422,7 @@ pub mod pallet { /// /// Weight: `O(1)` #[pallet::weight(T::WeightInfo::force_create())] - pub(super) fn force_create( + pub fn force_create( origin: OriginFor, #[pallet::compact] id: T::AssetId, owner: ::Source, @@ -522,7 +526,7 @@ pub mod pallet { /// Weight: `O(1)` /// Modes: Pre-existing balance of `beneficiary`; Account pre-existence of `beneficiary`. #[pallet::weight(T::WeightInfo::mint())] - pub(super) fn mint( + pub fn mint( origin: OriginFor, #[pallet::compact] id: T::AssetId, beneficiary: ::Source, @@ -531,7 +535,6 @@ pub mod pallet { let origin = ensure_signed(origin)?; let beneficiary = T::Lookup::lookup(beneficiary)?; Self::do_mint(id, &beneficiary, amount, Some(origin))?; - Self::deposit_event(Event::Issued(id, beneficiary, amount)); Ok(()) } @@ -541,7 +544,7 @@ pub mod pallet { /// /// Bails with `BalanceZero` if the `who` is already dead. /// - /// - `id`: The identifier of the asset to have some amount burned. + /// - `id`: The identifier of the asset to have some amount slashed. /// - `who`: The account to be debited from. /// - `amount`: The maximum amount by which `who`'s balance should be reduced. /// @@ -550,8 +553,8 @@ pub mod pallet { /// /// Weight: `O(1)` /// Modes: Post-existence of `who`; Pre & post Zombie-status of `who`. - #[pallet::weight(T::WeightInfo::burn())] - pub(super) fn burn( + #[pallet::weight(T::WeightInfo::slash())] + pub(super) fn slash( origin: OriginFor, #[pallet::compact] id: T::AssetId, who: ::Source, @@ -560,9 +563,9 @@ pub mod pallet { let origin = ensure_signed(origin)?; let who = T::Lookup::lookup(who)?; - let f = DebitFlags { keep_alive: false, best_effort: true }; - let burned = Self::do_burn(id, &who, amount, Some(origin), f)?; - Self::deposit_event(Event::Burned(id, who, burned)); + let f = DebitFlags { keep_alive: false, ignore_freezer: false }; + let amount = amount.min(Self::reducible_balance(id, &who, f)?); + let _ = Self::do_burn(id, &who, amount, Some(origin), f)?; Ok(()) } @@ -589,17 +592,12 @@ pub mod pallet { origin: OriginFor, #[pallet::compact] id: T::AssetId, target: ::Source, - #[pallet::compact] amount: T::Balance + #[pallet::compact] amount: T::Balance, ) -> DispatchResult { let origin = ensure_signed(origin)?; let dest = T::Lookup::lookup(target)?; - let f = TransferFlags { - keep_alive: false, - best_effort: false, - burn_dust: false - }; - Self::do_transfer(id, &origin, &dest, amount, None, f).map(|_| ()) + Self::do_transfer(id, &origin, &dest, amount, None, WhenDust::Credit).map(|_| ()) } /// Move some assets from the sender account to another, keeping the sender account alive. @@ -629,13 +627,7 @@ pub mod pallet { ) -> DispatchResult { let source = ensure_signed(origin)?; let dest = T::Lookup::lookup(target)?; - - let f = TransferFlags { - keep_alive: true, - best_effort: false, - burn_dust: false - }; - Self::do_transfer(id, &source, &dest, amount, None, f).map(|_| ()) + Self::do_transfer(id, &source, &dest, amount, None, WhenDust::KeepAlive).map(|_| ()) } /// Move some assets from one account to another. @@ -668,13 +660,7 @@ pub mod pallet { let origin = ensure_signed(origin)?; let source = T::Lookup::lookup(source)?; let dest = T::Lookup::lookup(dest)?; - - let f = TransferFlags { - keep_alive: false, - best_effort: false, - burn_dust: false - }; - Self::do_transfer(id, &source, &dest, amount, Some(origin), f).map(|_| ()) + Self::do_transfer(id, &source, &dest, amount, Some(origin), WhenDust::Credit).map(|_| ()) } /// Disallow further unprivileged transfers from an account. @@ -1237,12 +1223,7 @@ pub mod pallet { .checked_sub(&amount) .ok_or(Error::::Unapproved)?; - let f = TransferFlags { - keep_alive: false, - best_effort: false, - burn_dust: false - }; - Self::do_transfer(id, &owner, &destination, amount, None, f)?; + Self::do_transfer(id, &owner, &destination, amount, None, WhenDust::Credit)?; if remaining.is_zero() { T::Currency::unreserve(&owner, approved.deposit); diff --git a/frame/assets/src/mock.rs b/frame/assets/src/mock.rs index 0b7aa339835ec..5c747ab95a5cb 100644 --- a/frame/assets/src/mock.rs +++ b/frame/assets/src/mock.rs @@ -109,13 +109,8 @@ impl Config for Test { use std::cell::RefCell; use std::collections::HashMap; -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub(crate) enum Hook { - Died(u32, u64), -} thread_local! { static FROZEN: RefCell> = RefCell::new(Default::default()); - static HOOKS: RefCell> = RefCell::new(Default::default()); } pub struct TestFreezer; @@ -123,10 +118,6 @@ impl FrozenBalance for TestFreezer { fn frozen_balance(asset: u32, who: &u64) -> Option { FROZEN.with(|f| f.borrow().get(&(asset, who.clone())).cloned()) } - - fn died(asset: u32, who: &u64) { - HOOKS.with(|h| h.borrow_mut().push(Hook::Died(asset, who.clone()))); - } } pub(crate) fn set_frozen_balance(asset: u32, who: u64, amount: u64) { @@ -135,9 +126,6 @@ pub(crate) fn set_frozen_balance(asset: u32, who: u64, amount: u64) { pub(crate) fn clear_frozen_balance(asset: u32, who: u64) { FROZEN.with(|f| f.borrow_mut().remove(&(asset, who))); } -pub(crate) fn hooks() -> Vec { - HOOKS.with(|h| h.borrow().clone()) -} pub(crate) fn new_test_ext() -> sp_io::TestExternalities { let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); diff --git a/frame/assets/src/tests.rs b/frame/assets/src/tests.rs index 3ee8f9a9cfa47..53e445e15a082 100644 --- a/frame/assets/src/tests.rs +++ b/frame/assets/src/tests.rs @@ -231,7 +231,7 @@ fn min_balance_should_work() { assert_eq!(Assets::balance(0, 1), 100); assert_eq!(Asset::::get(0).unwrap().accounts, 1); - assert_ok!(Assets::burn(Origin::signed(1), 0, 1, 91)); + assert_ok!(Assets::slash(Origin::signed(1), 0, 1, 91)); assert!(Assets::balance(0, 1).is_zero()); assert_eq!(Asset::::get(0).unwrap().accounts, 0); }); @@ -250,7 +250,7 @@ fn querying_total_supply_should_work() { assert_eq!(Assets::balance(0, 1), 50); assert_eq!(Assets::balance(0, 2), 19); assert_eq!(Assets::balance(0, 3), 31); - assert_ok!(Assets::burn(Origin::signed(1), 0, 3, u64::max_value())); + assert_ok!(Assets::slash(Origin::signed(1), 0, 3, u64::max_value())); assert_eq!(Assets::total_supply(0), 69); }); } @@ -316,7 +316,7 @@ fn origin_guards_should_work() { assert_noop!(Assets::freeze(Origin::signed(2), 0, 1), Error::::NoPermission); assert_noop!(Assets::thaw(Origin::signed(2), 0, 2), Error::::NoPermission); assert_noop!(Assets::mint(Origin::signed(2), 0, 2, 100), Error::::NoPermission); - assert_noop!(Assets::burn(Origin::signed(2), 0, 1, 100), Error::::NoPermission); + assert_noop!(Assets::slash(Origin::signed(2), 0, 1, 100), Error::::NoPermission); assert_noop!(Assets::force_transfer(Origin::signed(2), 0, 1, 2, 100), Error::::NoPermission); let w = Asset::::get(0).unwrap().destroy_witness(); assert_noop!(Assets::destroy(Origin::signed(2), 0, w), Error::::NoPermission); @@ -356,7 +356,7 @@ fn set_team_should_work() { assert_ok!(Assets::freeze(Origin::signed(4), 0, 2)); assert_ok!(Assets::thaw(Origin::signed(3), 0, 2)); assert_ok!(Assets::force_transfer(Origin::signed(3), 0, 2, 3, 100)); - assert_ok!(Assets::burn(Origin::signed(3), 0, 3, 100)); + assert_ok!(Assets::slash(Origin::signed(3), 0, 3, 100)); }); } @@ -383,7 +383,7 @@ fn transferring_amount_more_than_available_balance_should_not_work() { assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50)); assert_eq!(Assets::balance(0, 1), 50); assert_eq!(Assets::balance(0, 2), 50); - assert_ok!(Assets::burn(Origin::signed(1), 0, 1, u64::max_value())); + assert_ok!(Assets::slash(Origin::signed(1), 0, 1, u64::max_value())); assert_eq!(Assets::balance(0, 1), 0); assert_noop!(Assets::transfer(Origin::signed(1), 0, 1, 50), Error::::BalanceLow); assert_noop!(Assets::transfer(Origin::signed(2), 0, 1, 51), Error::::BalanceLow); @@ -417,7 +417,7 @@ fn burning_asset_balance_with_positive_balance_should_work() { assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100)); assert_eq!(Assets::balance(0, 1), 100); - assert_ok!(Assets::burn(Origin::signed(1), 0, 1, u64::max_value())); + assert_ok!(Assets::slash(Origin::signed(1), 0, 1, u64::max_value())); assert_eq!(Assets::balance(0, 1), 0); }); } @@ -428,7 +428,7 @@ fn burning_asset_balance_with_zero_balance_does_nothing() { assert_ok!(Assets::force_create(Origin::root(), 0, 1, true, 1)); assert_ok!(Assets::mint(Origin::signed(1), 0, 1, 100)); assert_eq!(Assets::balance(0, 2), 0); - assert_ok!(Assets::burn(Origin::signed(1), 0, 2, u64::max_value())); + assert_ok!(Assets::slash(Origin::signed(1), 0, 2, u64::max_value())); assert_eq!(Assets::balance(0, 2), 0); assert_eq!(Assets::total_supply(0), 100); }); @@ -519,7 +519,6 @@ fn freezer_should_work() { // and if we clear it, we can remove the account completely. clear_frozen_balance(0, 1); assert_ok!(Assets::transfer(Origin::signed(1), 0, 2, 50)); - assert_eq!(hooks(), vec![Hook::Died(0, 1)]); }); } diff --git a/frame/assets/src/types.rs b/frame/assets/src/types.rs index 0cfcb64e137f2..ff4ae70ea9f96 100644 --- a/frame/assets/src/types.rs +++ b/frame/assets/src/types.rs @@ -117,62 +117,20 @@ pub struct DestroyWitness { pub(super) approvals: u32, } -/// Trait for allowing a minimum balance on the account to be specified, beyond the -/// `minimum_balance` of the asset. This is additive - the `minimum_balance` of the asset must be -/// met *and then* anything here in addition. -pub trait FrozenBalance { - /// Return the frozen balance. Under normal behaviour, this amount should always be - /// withdrawable. - /// - /// In reality, the balance of every account must be at least the sum of this (if `Some`) and - /// the asset's minimum_balance, since there may be complications to destroying an asset's - /// account completely. - /// - /// If `None` is returned, then nothing special is enforced. - /// - /// If any operation ever breaks this requirement (which will only happen through some sort of - /// privileged intervention), then `melted` is called to do any cleanup. - fn frozen_balance(asset: AssetId, who: &AccountId) -> Option; - - /// Called when an account has been removed. - fn died(asset: AssetId, who: &AccountId); -} - -impl FrozenBalance for () { - fn frozen_balance(_: AssetId, _: &AccountId) -> Option { None } - fn died(_: AssetId, _: &AccountId) {} -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub(super) struct TransferFlags { - /// The debited account must stay alive at the end of the operation; an error is returned if - /// this cannot be achieved legally. - pub(super) keep_alive: bool, - /// Less than the amount specified needs be debited by the operation for it to be considered - /// successful. If `false`, then the amount debited will always be at least the amount - /// specified. - pub(super) best_effort: bool, - /// Any additional funds debited (due to minimum balance requirements) should be burned rather - /// than credited to the destination account. - pub(super) burn_dust: bool, -} - #[derive(Copy, Clone, PartialEq, Eq)] pub(super) struct DebitFlags { /// The debited account must stay alive at the end of the operation; an error is returned if /// this cannot be achieved legally. pub(super) keep_alive: bool, - /// Less than the amount specified needs be debited by the operation for it to be considered - /// successful. If `false`, then the amount debited will always be at least the amount - /// specified. - pub(super) best_effort: bool, + /// Ignore the freezer. Don't set this to true unless you actually are the underlying freezer. + pub(super) ignore_freezer: bool, } -impl From for DebitFlags { - fn from(f: TransferFlags) -> Self { +impl From for DebitFlags { + fn from(death: WhenDust) -> Self { Self { - keep_alive: f.keep_alive, - best_effort: f.best_effort, + keep_alive: death.keep_alive(), + ignore_freezer: false, } } } diff --git a/frame/assets/src/weights.rs b/frame/assets/src/weights.rs index c3c804a392dbe..bc581e3924c80 100644 --- a/frame/assets/src/weights.rs +++ b/frame/assets/src/weights.rs @@ -48,7 +48,7 @@ pub trait WeightInfo { fn force_create() -> Weight; fn destroy(c: u32, s: u32, a: u32, ) -> Weight; fn mint() -> Weight; - fn burn() -> Weight; + fn slash() -> Weight; fn transfer() -> Weight; fn transfer_keep_alive() -> Weight; fn force_transfer() -> Weight; @@ -103,7 +103,7 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) } - fn burn() -> Weight { + fn slash() -> Weight { (46_000_000 as Weight) .saturating_add(T::DbWeight::get().reads(2 as Weight)) .saturating_add(T::DbWeight::get().writes(2 as Weight)) @@ -237,7 +237,7 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) } - fn burn() -> Weight { + fn slash() -> Weight { (46_000_000 as Weight) .saturating_add(RocksDbWeight::get().reads(2 as Weight)) .saturating_add(RocksDbWeight::get().writes(2 as Weight)) diff --git a/frame/balances/src/lib.rs b/frame/balances/src/lib.rs index c0566f84a1be1..7ac08853dd06c 100644 --- a/frame/balances/src/lib.rs +++ b/frame/balances/src/lib.rs @@ -164,8 +164,9 @@ use frame_support::{ Currency, OnUnbalanced, TryDrop, StoredMap, WithdrawReasons, LockIdentifier, LockableCurrency, ExistenceRequirement, Imbalance, SignedImbalance, ReservableCurrency, Get, ExistenceRequirement::KeepAlive, - ExistenceRequirement::AllowDeath, - tokens::{fungible, DepositConsequence, WithdrawConsequence, BalanceStatus as Status} + ExistenceRequirement::AllowDeath, tokens::{ + fungible, DepositConsequence, WithdrawConsequence, BalanceStatus as Status, WhenDust + } } }; #[cfg(feature = "std")] @@ -948,7 +949,8 @@ impl, I: 'static> fungible::Inspect for Pallet fn can_deposit(who: &T::AccountId, amount: Self::Balance) -> DepositConsequence { Self::deposit_consequence(who, amount, &Self::account(who)) } - fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence { + fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence + { Self::withdraw_consequence(who, amount, &Self::account(who)) } } @@ -968,7 +970,7 @@ impl, I: 'static> fungible::Mutate for Pallet { fn burn_from(who: &T::AccountId, amount: Self::Balance) -> Result { if amount.is_zero() { return Ok(Self::Balance::zero()); } let actual = Self::try_mutate_account(who, |account, _is_new| -> Result { - let extra = Self::withdraw_consequence(who, amount, &account).into_result()?; + let extra = Self::withdraw_consequence(who, amount, &account).into_result(false)?; let actual = amount + extra; account.free -= actual; Ok(actual) @@ -983,9 +985,9 @@ impl, I: 'static> fungible::Transfer for Pallet source: &T::AccountId, dest: &T::AccountId, amount: T::Balance, - keep_alive: bool, + death: WhenDust, ) -> Result { - let er = if keep_alive { KeepAlive } else { AllowDeath }; + let er = if death.keep_alive() { KeepAlive } else { AllowDeath }; >::transfer(source, dest, amount, er) .map(|_| amount) } @@ -1018,6 +1020,9 @@ impl, I: 'static> fungible::InspectHold for Pallet= required_free } + fn reducible_balance_on_hold(who: &T::AccountId) -> Self::Balance { + Self::balance_on_hold(who) + } } impl, I: 'static> fungible::MutateHold for Pallet { fn hold(who: &T::AccountId, amount: Self::Balance) -> DispatchResult { @@ -1048,11 +1053,10 @@ impl, I: 'static> fungible::MutateHold for Pallet Result { let status = if on_hold { Status::Reserved } else { Status::Free }; - Self::do_transfer_reserved(source, dest, amount, best_effort, status) + Self::do_transfer_reserved(source, dest, amount, false, status) } } diff --git a/frame/support/src/traits/tokens.rs b/frame/support/src/traits/tokens.rs index 82af5dbade8f7..0e3bce12d0c18 100644 --- a/frame/support/src/traits/tokens.rs +++ b/frame/support/src/traits/tokens.rs @@ -24,5 +24,6 @@ pub mod imbalance; mod misc; pub use misc::{ WithdrawConsequence, DepositConsequence, ExistenceRequirement, BalanceStatus, WithdrawReasons, + FrozenBalance, WhenDust, }; pub use imbalance::Imbalance; diff --git a/frame/support/src/traits/tokens/fungible.rs b/frame/support/src/traits/tokens/fungible.rs index 5472212aaa65e..0eaf7d4e9e9d1 100644 --- a/frame/support/src/traits/tokens/fungible.rs +++ b/frame/support/src/traits/tokens/fungible.rs @@ -21,11 +21,11 @@ use super::*; use sp_runtime::traits::Saturating; use crate::traits::misc::Get; use crate::dispatch::{DispatchResult, DispatchError}; -use super::misc::{DepositConsequence, WithdrawConsequence, Balance}; +use super::misc::{DepositConsequence, WithdrawConsequence, Balance, WhenDust}; mod balanced; mod imbalance; -pub use balanced::{Balanced, Unbalanced}; +pub use balanced::{Balanced, Unbalanced, BalancedHold, UnbalancedHold}; pub use imbalance::{Imbalance, HandleImbalanceDrop, DebtOf, CreditOf}; /// Trait for providing balance-inspection access to a fungible asset. @@ -53,6 +53,16 @@ pub trait Inspect { fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence; } +/// Trait for providing balance-inspection access to a set of named fungible assets, ignoring any +/// per-balance freezing mechanism. +pub trait InspectWithoutFreezer: Inspect { + /// Get the maximum amount of `asset` that `who` can withdraw/transfer successfully. + fn reducible_balance(who: &AccountId, keep_alive: bool) -> Self::Balance; + + /// Returns `true` if the `asset` balance of `who` may be increased by `amount`. + fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence; +} + /// Trait for providing an ERC-20 style fungible asset. pub trait Mutate: Inspect { /// Increase the balance of `who` by exactly `amount`, minting new tokens. If that isn't @@ -81,7 +91,7 @@ pub trait Mutate: Inspect { dest: &AccountId, amount: Self::Balance, ) -> Result { - let extra = Self::can_withdraw(&source, amount).into_result()?; + let extra = Self::can_withdraw(&source, amount).into_result(false)?; Self::can_deposit(&dest, amount.saturating_add(extra)).into_result()?; let actual = Self::burn_from(source, amount)?; debug_assert!(actual == amount.saturating_add(extra), "can_withdraw must agree with withdraw; qed"); @@ -100,13 +110,33 @@ pub trait Mutate: Inspect { /// Trait for providing a fungible asset which can only be transferred. pub trait Transfer: Inspect { - /// Transfer funds from one account into another. + /// Transfer `amount` of funds from `source` account into `dest`, possibly transferring a + /// little more depending on the value of `death`. + /// + /// If successful, will return the amount transferred, which will never be less than `amount`. + /// On error, nothing will be done and an `Err` returned. fn transfer( source: &AccountId, dest: &AccountId, amount: Self::Balance, - keep_alive: bool, + death: WhenDust, ) -> Result; + + /// Transfer `amount` of funds, or as much of them that are available for transfer, from + /// `source` account into `dest`, possibly transferring a little more depending on the value of + /// `death`. + /// + /// If successful, will return the amount transferred. On error, nothing will be done and an + /// `Err` returned. + fn transfer_best_effort( + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + death: WhenDust, + ) -> Result { + let possible = Self::reducible_balance(source, death.keep_alive()); + Self::transfer(source, dest, amount.min(possible), death) + } } /// Trait for inspecting a fungible asset which can be reserved. @@ -116,10 +146,25 @@ pub trait InspectHold: Inspect { /// Check to see if some `amount` of funds of `who` may be placed on hold. fn can_hold(who: &AccountId, amount: Self::Balance) -> bool; + + /// Return the amount of funds which can be reduced of account `who` from the part of their + /// account balance on hold. + /// + /// Generally, this should be the same as `balance_on_hold`, but if the account is frozen or + /// has somehow had its balance reduced below that which is on hold, then it may be less. + /// + /// Assuming your type implements `InspectWithoutFreezer`, then this can generally be + /// implemented very simply with: + /// + /// ```nocompile + /// >::reducible_balance(who, true) + /// .min(Self::balance_on_hold(who)) + /// ``` + fn reducible_balance_on_hold(who: &AccountId) -> Self::Balance; } /// Trait for mutating a fungible asset which can be reserved. -pub trait MutateHold: InspectHold + Transfer { +pub trait MutateHold: InspectHold { /// Hold some funds in an account. fn hold(who: &AccountId, amount: Self::Balance) -> DispatchResult; @@ -132,50 +177,38 @@ pub trait MutateHold: InspectHold + Transfer { fn release(who: &AccountId, amount: Self::Balance, best_effort: bool) -> Result; - /// Transfer held funds into a destination account. + /// Transfer exactly `amount` of funds from `source` account into `dest`. /// /// If `on_hold` is `true`, then the destination account must already exist and the assets /// transferred will still be on hold in the destination account. If not, then the destination /// account need not already exist, but must be creatable. /// - /// If `best_effort` is `true`, then an amount less than `amount` may be transferred without - /// error. - /// /// The actual amount transferred is returned, or `Err` in the case of error and nothing is /// changed. fn transfer_held( source: &AccountId, dest: &AccountId, amount: Self::Balance, - best_effort: bool, - on_held: bool, + on_hold: bool, ) -> Result; -} -/// Trait for slashing a fungible asset which can be reserved. -pub trait BalancedHold: Balanced + MutateHold { - /// Reduce the balance of some funds on hold in an account. + /// Transfer as much as possible of funds on hold in `source` account, up to `amount`, into + /// `dest` account. /// - /// The resulting imbalance is the first item of the tuple returned. + /// If `on_hold` is `true`, then the destination account must already exist and the assets + /// transferred will still be on hold in the destination account. If not, then the destination + /// account need not already exist, but must be creatable. /// - /// As much funds that are on hold up to `amount` will be deducted as possible. If this is less - /// than `amount`, then a non-zero second item will be returned. - fn slash_held(who: &AccountId, amount: Self::Balance) - -> (CreditOf, Self::Balance); -} - -impl< - AccountId, - T: Balanced + MutateHold, -> BalancedHold for T { - fn slash_held(who: &AccountId, amount: Self::Balance) - -> (CreditOf, Self::Balance) - { - let actual = match Self::release(who, amount, true) { - Ok(x) => x, - Err(_) => return (Imbalance::default(), amount), - }; - >::slash(who, actual) + /// The actual amount transferred is returned, or `Err` in the case of error and nothing is + /// changed. + fn transfer_best_effort_held( + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + on_hold: bool, + ) -> Result { + let possible = Self::reducible_balance_on_hold(source); + Self::transfer_held(source, dest, amount.min(possible), on_hold) } } @@ -215,6 +248,19 @@ impl< } } +impl< + F: fungibles::InspectWithoutFreezer, + A: Get<>::AssetId>, + AccountId, +> InspectWithoutFreezer for ItemOf { + fn reducible_balance(who: &AccountId, keep_alive: bool) -> Self::Balance { + >::reducible_balance(A::get(), who, keep_alive) + } + fn can_withdraw(who: &AccountId, amount: Self::Balance) -> WithdrawConsequence { + >::can_withdraw(A::get(), who, amount) + } +} + impl< F: fungibles::Mutate, A: Get<>::AssetId>, @@ -233,10 +279,16 @@ impl< A: Get<>::AssetId>, AccountId, > Transfer for ItemOf { - fn transfer(source: &AccountId, dest: &AccountId, amount: Self::Balance, keep_alive: bool) + fn transfer(source: &AccountId, dest: &AccountId, amount: Self::Balance, death: WhenDust) + -> Result + { + >::transfer(A::get(), source, dest, amount, death) + } + + fn transfer_best_effort(source: &AccountId, dest: &AccountId, amount: Self::Balance, death: WhenDust) -> Result { - >::transfer(A::get(), source, dest, amount, keep_alive) + >::transfer_best_effort(A::get(), source, dest, amount, death) } } @@ -251,6 +303,9 @@ impl< fn can_hold(who: &AccountId, amount: Self::Balance) -> bool { >::can_hold(A::get(), who, amount) } + fn reducible_balance_on_hold(who: &AccountId) -> Self::Balance { + >::reducible_balance_on_hold(A::get(), who) + } } impl< @@ -270,7 +325,6 @@ impl< source: &AccountId, dest: &AccountId, amount: Self::Balance, - best_effort: bool, on_hold: bool, ) -> Result { >::transfer_held( @@ -278,10 +332,51 @@ impl< source, dest, amount, - best_effort, on_hold, ) } + fn transfer_best_effort_held( + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + on_hold: bool, + ) -> Result { + >::transfer_best_effort_held( + A::get(), + source, + dest, + amount, + on_hold, + ) + } +} + +impl< + F: fungibles::UnbalancedHold, + A: Get<>::AssetId>, + AccountId, +> UnbalancedHold for ItemOf { + fn decrease_balance_on_hold( + who: &AccountId, + amount: Self::Balance, + ) -> Result { + >::decrease_balance_on_hold( + A::get(), + who, + amount, + ) + } + + fn decrease_balance_on_hold_at_most( + who: &AccountId, + amount: Self::Balance, + ) -> Result { + >::decrease_balance_on_hold_at_most( + A::get(), + who, + amount, + ) + } } impl< @@ -295,13 +390,13 @@ impl< fn set_total_issuance(amount: Self::Balance) -> () { >::set_total_issuance(A::get(), amount) } - fn decrease_balance(who: &AccountId, amount: Self::Balance) -> Result { - >::decrease_balance(A::get(), who, amount) + fn decrease_balance(who: &AccountId, amount: Self::Balance, keep_alive: bool) -> Result { + >::decrease_balance(A::get(), who, amount, keep_alive) } - fn decrease_balance_at_most(who: &AccountId, amount: Self::Balance) -> Self::Balance { - >::decrease_balance_at_most(A::get(), who, amount) + fn decrease_balance_at_most(who: &AccountId, amount: Self::Balance, keep_alive: bool) -> Self::Balance { + >::decrease_balance_at_most(A::get(), who, amount, keep_alive) } - fn increase_balance(who: &AccountId, amount: Self::Balance) -> Result { + fn increase_balance(who: &AccountId, amount: Self::Balance) -> Result<(), DispatchError> { >::increase_balance(A::get(), who, amount) } fn increase_balance_at_most(who: &AccountId, amount: Self::Balance) -> Self::Balance { diff --git a/frame/support/src/traits/tokens/fungible/balanced.rs b/frame/support/src/traits/tokens/fungible/balanced.rs index 1cd0fcf0ca414..ec0601d010d4a 100644 --- a/frame/support/src/traits/tokens/fungible/balanced.rs +++ b/frame/support/src/traits/tokens/fungible/balanced.rs @@ -90,7 +90,7 @@ pub trait Balanced: Inspect { fn withdraw( who: &AccountId, value: Self::Balance, - //TODO: liveness: ExistenceRequirement, + keep_alive: bool, ) -> Result, DispatchError>; /// The balance of `who` is increased in order to counter `credit`. If the whole of `credit` @@ -119,10 +119,10 @@ pub trait Balanced: Inspect { fn settle( who: &AccountId, debt: DebtOf, - //TODO: liveness: ExistenceRequirement, + keep_alive: bool, ) -> Result, DebtOf> { let amount = debt.peek(); - let credit = match Self::withdraw(who, amount) { + let credit = match Self::withdraw(who, amount, keep_alive) { Err(_) => return Err(debt), Ok(d) => d, }; @@ -137,6 +137,16 @@ pub trait Balanced: Inspect { } } +/// Trait for mutating one of several types of fungible assets which can be held. +pub trait BalancedHold: Unbalanced { + /// Release and slash some as much funds on hold in an account up to `amount`. + /// + /// The resulting imbalance is the first item of the tuple returned; the second is the + /// remainder, if any, from `amount`. + fn slash_held(who: &AccountId, amount: Self::Balance) + -> (Credit, Self::Balance); +} + /// A fungible token class where the balance can be set arbitrarily. /// /// **WARNING** @@ -145,9 +155,12 @@ pub trait Balanced: Inspect { /// token imbalances in your system leading to accidental imflation or deflation. It's really just /// for the underlying datatype to implement so the user gets the much safer `Balanced` trait to /// use. -pub trait Unbalanced: Inspect { +pub trait Unbalanced: Inspect + Sized { /// Set the balance of `who` to `amount`. If this cannot be done for some reason (e.g. /// because the account cannot be created or an overflow) then an `Err` is returned. + /// + /// This only needs to be implemented if `decrease_balance` and `increase_balance` are left + /// to their default implementations. fn set_balance(who: &AccountId, amount: Self::Balance) -> DispatchResult; /// Set the total issuance to `amount`. @@ -158,9 +171,10 @@ pub trait Unbalanced: Inspect { /// /// Minimum balance will be respected and the returned imbalance may be up to /// `Self::minimum_balance() - 1` greater than `amount`. - fn decrease_balance(who: &AccountId, amount: Self::Balance) + fn decrease_balance(who: &AccountId, amount: Self::Balance, keep_alive: bool) -> Result { + Self::can_withdraw(who, amount).into_result(keep_alive)?; let old_balance = Self::balance(who); let (mut new_balance, mut amount) = if old_balance < amount { Err(TokenError::NoFunds)? @@ -176,49 +190,13 @@ pub trait Unbalanced: Inspect { Ok(amount) } - /// Reduce the balance of `who` by the most that is possible, up to `amount`. - /// - /// Minimum balance will be respected and the returned imbalance may be up to - /// `Self::minimum_balance() - 1` greater than `amount`. - /// - /// Return the imbalance by which the account was reduced. - fn decrease_balance_at_most(who: &AccountId, amount: Self::Balance) - -> Self::Balance - { - let old_balance = Self::balance(who); - let (mut new_balance, mut amount) = if old_balance < amount { - (Zero::zero(), old_balance) - } else { - (old_balance - amount, amount) - }; - let minimum_balance = Self::minimum_balance(); - if new_balance < minimum_balance { - amount = amount.saturating_add(new_balance); - new_balance = Zero::zero(); - } - let mut r = Self::set_balance(who, new_balance); - if r.is_err() { - // Some error, probably because we tried to destroy an account which cannot be destroyed. - if new_balance.is_zero() && amount >= minimum_balance { - new_balance = minimum_balance; - amount -= minimum_balance; - r = Self::set_balance(who, new_balance); - } - if r.is_err() { - // Still an error. Apparently it's not possible to reduce at all. - amount = Zero::zero(); - } - } - amount - } - /// Increase the balance of `who` by `amount`. If it cannot be increased by that amount /// for some reason, return `Err` and don't increase it at all. If Ok, return the imbalance. /// /// Minimum balance will be respected and an error will be returned if /// `amount < Self::minimum_balance()` when the account of `who` is zero. fn increase_balance(who: &AccountId, amount: Self::Balance) - -> Result + -> Result<(), DispatchError> { let old_balance = Self::balance(who); let new_balance = old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; @@ -228,30 +206,61 @@ pub trait Unbalanced: Inspect { if old_balance != new_balance { Self::set_balance(who, new_balance)?; } - Ok(amount) + Ok(()) + } + + /// Reduce the balance of `who` by the most that is possible, up to `amount`. + /// + /// Minimum balance will be respected and the returned amount may be up to + /// `Self::minimum_balance() - 1` greater than `amount`. + /// + /// Return the amount by which the account was reduced. + /// + /// NOTE: This contains a default implementation that should be sufficient in most + /// circumstances. + fn decrease_balance_at_most(who: &AccountId, amount: Self::Balance, keep_alive: bool) -> Self::Balance { + let amount = amount.min(Self::reducible_balance(who, keep_alive)); + Self::decrease_balance(who, amount, keep_alive).unwrap_or(Zero::zero()) } /// Increase the balance of `who` by the most that is possible, up to `amount`. /// - /// Minimum balance will be respected and the returned imbalance will be zero in the case that + /// Minimum balance will be respected and the returned amount will be zero in the case that /// `amount < Self::minimum_balance()`. /// - /// Return the imbalance by which the account was increased. - fn increase_balance_at_most(who: &AccountId, amount: Self::Balance) - -> Self::Balance - { - let old_balance = Self::balance(who); - let mut new_balance = old_balance.saturating_add(amount); - let mut amount = new_balance - old_balance; - if new_balance < Self::minimum_balance() { - new_balance = Zero::zero(); - amount = Zero::zero(); - } - if old_balance == new_balance || Self::set_balance(who, new_balance).is_ok() { - amount - } else { - Zero::zero() - } + /// Return the amount by which the account was increased. + /// + /// NOTE: This contains a default implementation that should be sufficient in most + /// circumstances. + fn increase_balance_at_most(who: &AccountId, amount: Self::Balance) -> Self::Balance { + Self::increase_balance(who, amount).map_or(Zero::zero(), |_| amount) + } +} + +/// A fungible token class capable of placing funds on hold where the balance can be changed +/// arbitrarily. +pub trait UnbalancedHold: Unbalanced + InspectHold { + /// Reduce the balance of `who` by `amount` from the funds on hold. + /// + /// If successful, then exactly `amount` is returned otherwise an `Err` is returned and + /// nothing is changed. + fn decrease_balance_on_hold( + who: &AccountId, + amount: Self::Balance, + ) -> Result; + + /// Reduce the balance of `who` by as much as possible up to at most `amount` from the + /// funds on hold. + /// + /// If successful, then the amount decreased is returned. + /// + /// If it cannot be validly reduced, return `Err` and do nothing. + fn decrease_balance_on_hold_at_most( + who: &AccountId, + amount: Self::Balance, + ) -> Result { + let amount = amount.min(Self::reducible_balance_on_hold(who)); + Self::decrease_balance_on_hold(who, amount) } } @@ -332,7 +341,7 @@ impl> Balanced for U { who: &AccountId, amount: Self::Balance, ) -> (Credit, Self::Balance) { - let slashed = U::decrease_balance_at_most(who, amount); + let slashed = U::decrease_balance_at_most(who, amount, false); // `slashed` could be less than, greater than or equal to `amount`. // If slashed == amount, it means the account had at least amount in it and it could all be // removed without a problem. @@ -346,15 +355,27 @@ impl> Balanced for U { who: &AccountId, amount: Self::Balance ) -> Result, DispatchError> { - let increase = U::increase_balance(who, amount)?; - Ok(debt(increase)) + U::increase_balance(who, amount)?; + Ok(debt(amount)) } fn withdraw( who: &AccountId, amount: Self::Balance, - //TODO: liveness: ExistenceRequirement, + keep_alive: bool, ) -> Result, DispatchError> { - let decrease = U::decrease_balance(who, amount)?; + Self::can_withdraw(who, amount).into_result(keep_alive)?; + let decrease = U::decrease_balance(who, amount, keep_alive)?; Ok(credit(decrease)) } } + +impl> BalancedHold for U { + fn slash_held( + who: &AccountId, + amount: Self::Balance, + ) -> (Credit, Self::Balance) { + let slashed = U::decrease_balance_on_hold_at_most(who, amount,) + .unwrap_or(Zero::zero()); + (credit(slashed), amount.saturating_sub(slashed)) + } +} diff --git a/frame/support/src/traits/tokens/fungibles.rs b/frame/support/src/traits/tokens/fungibles.rs index 490f28dfb453a..9c76e5c09219a 100644 --- a/frame/support/src/traits/tokens/fungibles.rs +++ b/frame/support/src/traits/tokens/fungibles.rs @@ -19,11 +19,11 @@ use super::*; use crate::dispatch::{DispatchError, DispatchResult}; -use super::misc::{AssetId, Balance}; +use super::misc::{AssetId, Balance, WhenDust}; use sp_runtime::traits::Saturating; mod balanced; -pub use balanced::{Balanced, Unbalanced}; +pub use balanced::{Balanced, Unbalanced, BalancedHold, UnbalancedHold}; mod imbalance; pub use imbalance::{Imbalance, HandleImbalanceDrop, DebtOf, CreditOf}; @@ -45,11 +45,18 @@ pub trait Inspect { fn balance(asset: Self::AssetId, who: &AccountId) -> Self::Balance; /// Get the maximum amount of `asset` that `who` can withdraw/transfer successfully. - fn reducible_balance(asset: Self::AssetId, who: &AccountId, keep_alive: bool) -> Self::Balance; + fn reducible_balance( + asset: Self::AssetId, + who: &AccountId, + keep_alive: bool, + ) -> Self::Balance; /// Returns `true` if the `asset` balance of `who` may be increased by `amount`. - fn can_deposit(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) - -> DepositConsequence; + fn can_deposit( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> DepositConsequence; /// Returns `Failed` if the `asset` balance of `who` may not be decreased by `amount`, otherwise /// the consequence. @@ -60,6 +67,24 @@ pub trait Inspect { ) -> WithdrawConsequence; } +/// Trait for providing balance-inspection access to a set of named fungible assets, ignoring any +/// per-balance freezing mechanism. +pub trait InspectWithoutFreezer: Inspect { + /// Get the maximum amount of `asset` that `who` can withdraw/transfer successfully. + fn reducible_balance( + asset: Self::AssetId, + who: &AccountId, + keep_alive: bool, + ) -> Self::Balance; + + /// Returns `true` if the `asset` balance of `who` may be increased by `amount`. + fn can_withdraw( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence; +} + /// Trait for providing a set of named fungible assets which can be created and destroyed. pub trait Mutate: Inspect { /// Attempt to increase the `asset` balance of `who` by `amount`. @@ -111,7 +136,7 @@ pub trait Mutate: Inspect { dest: &AccountId, amount: Self::Balance, ) -> Result { - let extra = Self::can_withdraw(asset, &source, amount).into_result()?; + let extra = Self::can_withdraw(asset, &source, amount).into_result(false)?; Self::can_deposit(asset, &dest, amount.saturating_add(extra)).into_result()?; let actual = Self::burn_from(asset, source, amount)?; debug_assert!(actual == amount.saturating_add(extra), "can_withdraw must agree with withdraw; qed"); @@ -130,14 +155,35 @@ pub trait Mutate: Inspect { /// Trait for providing a set of named fungible assets which can only be transferred. pub trait Transfer: Inspect { - /// Transfer funds from one account into another. + /// Transfer `amount` of `asset` from `source` account into `dest`, possibly transferring a + /// little more depending on the value of `death`. + /// + /// If successful, will return the amount transferred, which will never be less than `amount`. + /// On error, nothing will be done and an `Err` returned. fn transfer( asset: Self::AssetId, source: &AccountId, dest: &AccountId, amount: Self::Balance, - keep_alive: bool, + death: WhenDust, ) -> Result; + + /// Transfer `amount` of `asset` from `source` account into `dest`, or as much of it that are + /// available for transfer, possibly transferring a little more depending on the value of + /// `death`. + /// + /// If successful, will return the amount transferred. On error, nothing will be done and an + /// `Err` returned. + fn transfer_best_effort( + asset: Self::AssetId, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + death: WhenDust, + ) -> Result { + let possible = Self::reducible_balance(asset, source, death.keep_alive()); + Self::transfer(asset, source, dest, amount.min(possible), death) + } } /// Trait for inspecting a set of named fungible assets which can be placed on hold. @@ -147,10 +193,25 @@ pub trait InspectHold: Inspect { /// Check to see if some `amount` of `asset` may be held on the account of `who`. fn can_hold(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> bool; + + /// Return the amount of `asset` which can be reduced of account `who` from the part of their + /// account balance on hold. + /// + /// Generally, this should be the same as `balance_on_hold`, but if the account is frozen or + /// has somehow had its balance reduced below that which is on hold, then it may be less. + /// + /// If your type implements `InspectWithoutFreezer`, then this can generally be + /// implemented very simply with: + /// + /// ```nocompile + /// >::reducible_balance(asset, who, true) + /// .min(Self::balance_on_hold(asset, who)) + /// ``` + fn reducible_balance_on_hold(asset: Self::AssetId, who: &AccountId) -> Self::Balance; } /// Trait for mutating a set of named fungible assets which can be placed on hold. -pub trait MutateHold: InspectHold + Transfer { +pub trait MutateHold: InspectHold { /// Hold some funds in an account. fn hold(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult; @@ -161,15 +222,12 @@ pub trait MutateHold: InspectHold + Transfer { fn release(asset: Self::AssetId, who: &AccountId, amount: Self::Balance, best_effort: bool) -> Result; - /// Transfer held funds into a destination account. + /// Transfer exactly `amount` of `asset` from `source` account into `dest`. /// /// If `on_hold` is `true`, then the destination account must already exist and the assets /// transferred will still be on hold in the destination account. If not, then the destination /// account need not already exist, but must be creatable. /// - /// If `best_effort` is `true`, then an amount less than `amount` may be transferred without - /// error. - /// /// The actual amount transferred is returned, or `Err` in the case of error and nothing is /// changed. fn transfer_held( @@ -177,34 +235,26 @@ pub trait MutateHold: InspectHold + Transfer { source: &AccountId, dest: &AccountId, amount: Self::Balance, - best_effort: bool, on_hold: bool, ) -> Result; -} -/// Trait for mutating one of several types of fungible assets which can be held. -pub trait BalancedHold: Balanced + MutateHold { - /// Release and slash some funds in an account. + /// Transfer as much as possible of `asset` on hold in `source` account, up to `amount`, into + /// `dest` account. /// - /// The resulting imbalance is the first item of the tuple returned. + /// If `on_hold` is `true`, then the destination account must already exist and the assets + /// transferred will still be on hold in the destination account. If not, then the destination + /// account need not already exist, but must be creatable. /// - /// As much funds up to `amount` will be deducted as possible. If this is less than `amount`, - /// then a non-zero second item will be returned. - fn slash_held(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) - -> (CreditOf, Self::Balance); -} - -impl< - AccountId, - T: Balanced + MutateHold, -> BalancedHold for T { - fn slash_held(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) - -> (CreditOf, Self::Balance) - { - let actual = match Self::release(asset, who, amount, true) { - Ok(x) => x, - Err(_) => return (Imbalance::zero(asset), amount), - }; - >::slash(asset, who, actual) + /// The actual amount transferred is returned, or `Err` in the case of error and nothing is + /// changed. + fn transfer_best_effort_held( + asset: Self::AssetId, + source: &AccountId, + dest: &AccountId, + amount: Self::Balance, + on_hold: bool, + ) -> Result { + let possible = Self::reducible_balance_on_hold(asset, source); + Self::transfer_held(asset, source, dest, amount.min(possible), on_hold) } } diff --git a/frame/support/src/traits/tokens/fungibles/balanced.rs b/frame/support/src/traits/tokens/fungibles/balanced.rs index a1016f8c11955..22a484bdadbe5 100644 --- a/frame/support/src/traits/tokens/fungibles/balanced.rs +++ b/frame/support/src/traits/tokens/fungibles/balanced.rs @@ -56,7 +56,7 @@ pub trait Balanced: Inspect { /// This is just the same as burning and issuing the same amount and has no effect on the /// total issuance. fn pair(asset: Self::AssetId, amount: Self::Balance) - -> (DebtOf, CreditOf) + -> (DebtOf, CreditOf) { (Self::rescind(asset, amount), Self::issue(asset, amount)) } @@ -96,7 +96,7 @@ pub trait Balanced: Inspect { asset: Self::AssetId, who: &AccountId, value: Self::Balance, - //TODO: liveness: ExistenceRequirement, + keep_alive: bool, ) -> Result, DispatchError>; /// The balance of `who` is increased in order to counter `credit`. If the whole of `credit` @@ -129,11 +129,11 @@ pub trait Balanced: Inspect { fn settle( who: &AccountId, debt: DebtOf, - //TODO: liveness: ExistenceRequirement, + keep_alive: bool, ) -> Result, DebtOf> { let amount = debt.peek(); let asset = debt.asset(); - let credit = match Self::withdraw(asset, who, amount) { + let credit = match Self::withdraw(asset, who, amount, keep_alive) { Err(_) => return Err(debt), Ok(d) => d, }; @@ -152,6 +152,16 @@ pub trait Balanced: Inspect { } } +/// Trait for mutating one of several types of fungible assets which can be held. +pub trait BalancedHold: Unbalanced { + /// Release and slash some as much funds on hold in an account up to `amount`. + /// + /// The resulting imbalance is the first item of the tuple returned; the second is the + /// remainder, if any, from `amount`. + fn slash_held(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) + -> (Credit, Self::Balance); +} + /// A fungible token class where the balance can be set arbitrarily. /// /// **WARNING** @@ -160,9 +170,12 @@ pub trait Balanced: Inspect { /// token imbalances in your system leading to accidental imflation or deflation. It's really just /// for the underlying datatype to implement so the user gets the much safer `Balanced` trait to /// use. -pub trait Unbalanced: Inspect { +pub trait Unbalanced: Inspect + Sized { /// Set the `asset` balance of `who` to `amount`. If this cannot be done for some reason (e.g. /// because the account cannot be created or an overflow) then an `Err` is returned. + /// + /// This only needs to be implemented if `decrease_balance` and `increase_balance` are left + /// to their default implementations. fn set_balance(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> DispatchResult; /// Set the total issuance of `asset` to `amount`. @@ -173,9 +186,10 @@ pub trait Unbalanced: Inspect { /// /// Minimum balance will be respected and the returned imbalance may be up to /// `Self::minimum_balance() - 1` greater than `amount`. - fn decrease_balance(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) + fn decrease_balance(asset: Self::AssetId, who: &AccountId, amount: Self::Balance, keep_alive: bool) -> Result { + Self::can_withdraw(asset, who, amount).into_result(keep_alive)?; let old_balance = Self::balance(asset, who); let (mut new_balance, mut amount) = if old_balance < amount { Err(TokenError::NoFunds)? @@ -191,49 +205,13 @@ pub trait Unbalanced: Inspect { Ok(amount) } - /// Reduce the `asset` balance of `who` by the most that is possible, up to `amount`. - /// - /// Minimum balance will be respected and the returned imbalance may be up to - /// `Self::minimum_balance() - 1` greater than `amount`. - /// - /// Return the imbalance by which the account was reduced. - fn decrease_balance_at_most(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) - -> Self::Balance - { - let old_balance = Self::balance(asset, who); - let (mut new_balance, mut amount) = if old_balance < amount { - (Zero::zero(), old_balance) - } else { - (old_balance - amount, amount) - }; - let minimum_balance = Self::minimum_balance(asset); - if new_balance < minimum_balance { - amount = amount.saturating_add(new_balance); - new_balance = Zero::zero(); - } - let mut r = Self::set_balance(asset, who, new_balance); - if r.is_err() { - // Some error, probably because we tried to destroy an account which cannot be destroyed. - if new_balance.is_zero() && amount >= minimum_balance { - new_balance = minimum_balance; - amount -= minimum_balance; - r = Self::set_balance(asset, who, new_balance); - } - if r.is_err() { - // Still an error. Apparently it's not possible to reduce at all. - amount = Zero::zero(); - } - } - amount - } - /// Increase the `asset` balance of `who` by `amount`. If it cannot be increased by that amount /// for some reason, return `Err` and don't increase it at all. If Ok, return the imbalance. /// /// Minimum balance will be respected and an error will be returned if /// `amount < Self::minimum_balance()` when the account of `who` is zero. fn increase_balance(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) - -> Result + -> Result<(), DispatchError> { let old_balance = Self::balance(asset, who); let new_balance = old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; @@ -243,36 +221,74 @@ pub trait Unbalanced: Inspect { if old_balance != new_balance { Self::set_balance(asset, who, new_balance)?; } - Ok(amount) + Ok(()) + } + + /// Reduce the `asset` balance of `who` by the most that is possible, up to `amount`. + /// + /// Minimum balance will be respected and the returned amount may be up to + /// `Self::minimum_balance() - 1` greater than `amount`. + /// + /// Return the amount by which the account was reduced. + /// + /// NOTE: This contains a default implementation that should be sufficient in most + /// circumstances. + fn decrease_balance_at_most(asset: Self::AssetId, who: &AccountId, amount: Self::Balance, keep_alive: bool) + -> Self::Balance + { + let amount = amount.min(Self::reducible_balance(asset, who, keep_alive)); + Self::decrease_balance(asset, who, amount, keep_alive).unwrap_or(Zero::zero()) } /// Increase the `asset` balance of `who` by the most that is possible, up to `amount`. /// - /// Minimum balance will be respected and the returned imbalance will be zero in the case that + /// Minimum balance will be respected and the returned amount will be zero in the case that /// `amount < Self::minimum_balance()`. /// - /// Return the imbalance by which the account was increased. + /// Return the amount by which the account was increased. + /// + /// NOTE: This contains a default implementation that should be sufficient in most + /// circumstances. fn increase_balance_at_most(asset: Self::AssetId, who: &AccountId, amount: Self::Balance) -> Self::Balance { - let old_balance = Self::balance(asset, who); - let mut new_balance = old_balance.saturating_add(amount); - let mut amount = new_balance - old_balance; - if new_balance < Self::minimum_balance(asset) { - new_balance = Zero::zero(); - amount = Zero::zero(); - } - if old_balance == new_balance || Self::set_balance(asset, who, new_balance).is_ok() { - amount - } else { - Zero::zero() - } + Self::increase_balance(asset, who, amount).map_or(Zero::zero(), |_| amount) + } +} + +/// A fungible token class capable of placing funds on hold where the balance can be changed +/// arbitrarily. +pub trait UnbalancedHold: Unbalanced + InspectHold { + /// Reduce the `asset` balance of `who` by `amount` from the funds on hold. + /// + /// If successful, then the amount decreased is returned. This will be exactly `amount`. + /// + /// If it cannot be validly reduced, return `Err` and do nothing. + fn decrease_balance_on_hold( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> Result; + + /// Reduce the `asset` balance of `who` by as much as possible up to at most `amount` from the + /// funds on hold. + /// + /// If successful, then the amount decreased is returned. + /// + /// If it cannot be validly reduced, return `Err` and do nothing. + fn decrease_balance_on_hold_at_most( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> Result { + let amount = amount.min(Self::reducible_balance_on_hold(asset, who)); + Self::decrease_balance_on_hold(asset, who, amount) } } /// Simple handler for an imbalance drop which increases the total issuance of the system by the /// imbalance amount. Used for leftover debt. -pub struct IncreaseIssuance(PhantomData<(AccountId, U)>); +pub struct IncreaseIssuance(PhantomData<(AccountId, U)>); impl> HandleImbalanceDrop for IncreaseIssuance { @@ -283,7 +299,7 @@ impl> HandleImbalanceDrop(PhantomData<(AccountId, U)>); +pub struct DecreaseIssuance(PhantomData<(AccountId, U)>); impl> HandleImbalanceDrop for DecreaseIssuance { @@ -348,7 +364,7 @@ impl> Balanced for U { who: &AccountId, amount: Self::Balance, ) -> (Credit, Self::Balance) { - let slashed = U::decrease_balance_at_most(asset, who, amount); + let slashed = U::decrease_balance_at_most(asset, who, amount, false); // `slashed` could be less than, greater than or equal to `amount`. // If slashed == amount, it means the account had at least amount in it and it could all be // removed without a problem. @@ -363,16 +379,28 @@ impl> Balanced for U { who: &AccountId, amount: Self::Balance ) -> Result, DispatchError> { - let increase = U::increase_balance(asset, who, amount)?; - Ok(debt(asset, increase)) + U::increase_balance(asset, who, amount)?; + Ok(debt(asset, amount)) } fn withdraw( asset: Self::AssetId, who: &AccountId, amount: Self::Balance, - //TODO: liveness: ExistenceRequirement, + keep_alive: bool, ) -> Result, DispatchError> { - let decrease = U::decrease_balance(asset, who, amount)?; + let decrease = U::decrease_balance(asset, who, amount, keep_alive)?; Ok(credit(asset, decrease)) } } + +impl> BalancedHold for U { + fn slash_held( + asset: Self::AssetId, + who: &AccountId, + amount: Self::Balance, + ) -> (Credit, Self::Balance) { + let slashed = U::decrease_balance_on_hold_at_most(asset, who, amount) + .unwrap_or(Zero::zero()); + (credit(asset, slashed), amount.saturating_sub(slashed)) + } +} diff --git a/frame/support/src/traits/tokens/misc.rs b/frame/support/src/traits/tokens/misc.rs index d6329e585324c..40febbd3f8f3a 100644 --- a/frame/support/src/traits/tokens/misc.rs +++ b/frame/support/src/traits/tokens/misc.rs @@ -53,17 +53,17 @@ pub enum WithdrawConsequence { impl WithdrawConsequence { /// Convert the type into a `Result` with `DispatchError` as the error or the additional `Balance` /// by which the account will be reduced. - pub fn into_result(self) -> Result { + pub fn into_result(self, keep_alive: bool) -> Result { use WithdrawConsequence::*; match self { - NoFunds => Err(TokenError::NoFunds.into()), - WouldDie => Err(TokenError::WouldDie.into()), + Success => Ok(Zero::zero()), + ReducedToZero(result) if !keep_alive => Ok(result), + WouldDie | ReducedToZero(_) => Err(TokenError::WouldDie.into()), UnknownAsset => Err(TokenError::UnknownAsset.into()), Underflow => Err(ArithmeticError::Underflow.into()), Overflow => Err(ArithmeticError::Overflow.into()), Frozen => Err(TokenError::Frozen.into()), - ReducedToZero(result) => Ok(result), - Success => Ok(Zero::zero()), + NoFunds => Err(TokenError::NoFunds.into()), } } } @@ -124,6 +124,50 @@ pub enum BalanceStatus { Reserved, } +/// When happens if/when a debited account has been reduced below the dust threshold (aka "minimum +/// balance" or "existential deposit"). +#[derive(Copy, Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug)] +pub enum WhenDust { + /// Operation must not result in the account going out of existence. + KeepAlive, + /// Any dust resulting from the deletion of the debited account should be disposed of. + Dispose, + /// Any dust resulting from the deletion of the debited account should be added to the + /// resulting credit. + Credit, +} + +impl WhenDust { + /// Return `true` if the account balance should be disposed of should it fall below minimum. + pub fn dispose(self) -> bool { matches!(self, WhenDust::Dispose) } + + /// Return `true` if the account balance should neve be allowed to fall below minimum. + pub fn keep_alive(self) -> bool { matches!(self, WhenDust::KeepAlive) } +} + + +/// Trait for allowing a minimum balance on the account to be specified, beyond the +/// `minimum_balance` of the asset. This is additive - the `minimum_balance` of the asset must be +/// met *and then* anything here in addition. +pub trait FrozenBalance { + /// Return the frozen balance. Under normal behaviour, this amount should always be + /// withdrawable. + /// + /// In reality, the balance of every account must be at least the sum of this (if `Some`) and + /// the asset's minimum_balance, since there may be complications to destroying an asset's + /// account completely. + /// + /// If `None` is returned, then nothing special is enforced. + /// + /// If any operation ever breaks this requirement (which will only happen through some sort of + /// privileged intervention), then `melted` is called to do any cleanup. + fn frozen_balance(asset: AssetId, who: &AccountId) -> Option; +} + +impl FrozenBalance for () { + fn frozen_balance(_: AssetId, _: &AccountId) -> Option { None } +} + bitflags::bitflags! { /// Reasons for moving funds out of an account. #[derive(Encode, Decode)]