topsoil_core/traits/tokens/fungible/mod.rs
1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: Apache-2.0 OR GPL-3.0-or-later WITH Classpath-exception-2.0
6
7//! The traits for dealing with a single fungible token class and any associated types.
8//!
9//! Also see the [`frame_tokens`] reference docs for more information about the place of
10//! `fungible` traits in Substrate.
11//!
12//! # Available Traits
13//! - [`Inspect`]: Regular balance inspector functions.
14//! - [`Unbalanced`]: Low-level balance mutating functions. Does not guarantee proper book-keeping
15//! and so should not be called into directly from application code. Other traits depend on this
16//! and provide default implementations based on it.
17//! - [`UnbalancedHold`]: Low-level balance mutating functions for balances placed on hold. Does not
18//! guarantee proper book-keeping and so should not be called into directly from application code.
19//! Other traits depend on this and provide default implementations based on it.
20//! - [`Mutate`]: Regular balance mutator functions. Pre-implemented using [`Unbalanced`], though
21//! the `done_*` functions should likely be reimplemented in case you want to do something
22//! following the operation such as emit events.
23//! - [`InspectHold`]: Inspector functions for balances on hold.
24//! - [`MutateHold`]: Mutator functions for balances on hold. Mostly pre-implemented using
25//! [`UnbalancedHold`].
26//! - [`InspectFreeze`]: Inspector functions for frozen balance.
27//! - [`MutateFreeze`]: Mutator functions for frozen balance.
28//! - [`Balanced`]: One-sided mutator functions for regular balances, which return imbalance objects
29//! which guarantee eventual book-keeping. May be useful for some sophisticated operations where
30//! funds must be removed from an account before it is known precisely what should be done with
31//! them.
32//!
33//! ## Terminology
34//!
35//! - **Total Issuance**: The total number of units in existence in a system.
36//!
37//! - **Total Balance**: The sum of an account's free and held balances.
38//!
39//! - **Free Balance**: A portion of an account's total balance that is not held. Note this is
40//! distinct from the Spendable Balance, which represents how much Balance the user can actually
41//! transfer.
42//!
43//! - **Held Balance**: Held balance still belongs to the account holder, but is suspended — it
44//! cannot be transferred or used for most operations. It may be slashed by the pallet that placed
45//! the hold.
46//!
47//! Multiple holds stack rather than overlay. This means that if an account has
48//! 3 holds for 100 units, the account can spend its funds for any reason down to 300 units, at
49//! which point the holds will start to come into play.
50//!
51//! - **Frozen Balance**: A freeze on a specified amount of an account's balance. Tokens that are
52//! frozen cannot be transferred.
53//!
54//! Multiple freezes always operate over the same funds, so they "overlay" rather than
55//! "stack". This means that if an account has 3 freezes for 100 units, the account can spend its
56//! funds for any reason down to 100 units, at which point the freezes will start to come into
57//! play.
58//!
59//! It's important to note that the frozen balance can exceed the total balance of the account.
60//! This is useful, eg, in cases where you want to prevent a user from transferring any fund. In
61//! such a case, setting the frozen balance to `Balance::MAX` would serve that purpose
62//! effectively.
63//!
64//! - **Minimum Balance (a.k.a. Existential Deposit, a.k.a. ED)**: The minimum balance required to
65//! create or keep an account open. This is to prevent "dust accounts" from filling storage. When
66//! the free plus the held balance (i.e. the total balance) falls below this, then the account is
67//! said to be dead. It loses its functionality as well as any prior history and all information
68//! on it is removed from the chain's state. No account should ever have a total balance that is
69//! strictly between 0 and the existential deposit (exclusive). If this ever happens, it indicates
70//! either a bug in the implementation of this trait or an erroneous raw mutation of storage.
71//!
72//! - **Untouchable Balance**: The part of a user's free balance they cannot spend, due to ED or
73//! Freeze(s).
74//!
75//! - **Spendable Balance**: The part of a user's free balance they can actually transfer, after
76//! accounting for Holds and Freezes.
77//!
78//! - **Imbalance**: A condition when some funds were credited or debited without equal and opposite
79//! accounting (i.e. a difference between total issuance and account balances). Functions that
80//! result in an imbalance will return an object of the [`imbalance::Credit`] or
81//! [`imbalance::Debt`] traits that can be managed within your runtime logic.
82//!
83//! If an imbalance is simply dropped, it should automatically maintain any book-keeping such as
84//! total issuance.
85//!
86//! ## Visualising Balance Components Together 💫
87//!
88//! ```ignore
89//! |__total__________________________________|
90//! |__on_hold__|_____________free____________|
91//! |__________frozen___________|
92//! |__on_hold__|__ed__|
93//! |__untouchable__|__spendable__|
94//! ```
95//!
96//! ## Holds and Freezes
97//!
98//! Both holds and freezes are used to prevent an account from using some of its balance.
99//!
100//! The primary distinction between the two are that:
101//! - Holds are cumulative (do not overlap) and are distinct from the free balance
102//! - Freezes are not cumulative, and can overlap with each other or with holds
103//!
104//! ```ignore
105//! |__total_____________________________|
106//! |__hold_a__|__hold_b__|_____free_____|
107//! |__on_hold____________| // <- the sum of all holds
108//! |__freeze_a_______________|
109//! |__freeze_b____|
110//! |__freeze_c________|
111//! |__frozen_________________| // <- the max of all freezes
112//! ```
113//!
114//! Holds are designed to be infallibly slashed, meaning that any logic using a `Freeze`
115//! must handle the possibility of the frozen amount being reduced, potentially to zero. A
116//! permissionless function should be provided in order to allow bookkeeping to be updated in this
117//! instance. E.g. some balance is frozen when it is used for voting, one could use held balance for
118//! voting, but nothing prevents this frozen balance from being reduced if the overlapping hold is
119//! slashed.
120//!
121//! Every Hold and Freeze is accompanied by a unique `Reason`, making it clear for each instance
122//! what the originating pallet and purpose is. These reasons are amalgomated into a single enum
123//! `RuntimeHoldReason` and `RuntimeFreezeReason` respectively, when the runtime is compiled.
124//!
125//! Note that `Hold` and `Freeze` reasons should remain in your runtime for as long as storage
126//! could exist in your runtime with those reasons, otherwise your runtime state could become
127//! undecodable.
128//!
129//! ### Should I use a Hold or Freeze?
130//!
131//! If you require a balance to be infaillibly slashed, then you should use Holds.
132//!
133//! If you require setting a minimum account balance amount, then you should use a Freezes. Note
134//! Freezes do not carry the same guarantees as Holds. Although the account cannot voluntarily
135//! reduce their balance below the largest freeze, if Holds on the account are slashed then the
136//! balance could drop below the freeze amount.
137//!
138//! ## Sets of Tokens
139//!
140//! For managing sets of tokens, see the [`fungibles`](`topsoil_core::traits::fungibles`) trait
141//! which is a wrapper around this trait but supporting multiple asset instances.
142//!
143//! [`frame_tokens`]: ../../../../polkadot_sdk_docs/reference_docs/frame_tokens/index.html
144
145pub mod conformance_tests;
146pub mod freeze;
147pub mod hold;
148pub(crate) mod imbalance;
149mod item_of;
150mod regular;
151mod union_of;
152
153use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
154use core::marker::PhantomData;
155use scale_info::TypeInfo;
156#[cfg(feature = "runtime-benchmarks")]
157use subsoil::runtime::Saturating;
158use topsoil_core_procedural::{CloneNoBound, DebugNoBound, EqNoBound, PartialEqNoBound};
159
160use super::{
161 Fortitude::{Force, Polite},
162 Precision::BestEffort,
163};
164pub use freeze::{Inspect as InspectFreeze, Mutate as MutateFreeze};
165pub use hold::{
166 Balanced as BalancedHold, Inspect as InspectHold, Mutate as MutateHold,
167 Unbalanced as UnbalancedHold,
168};
169pub use imbalance::{Credit, Debt, HandleImbalanceDrop, Imbalance};
170pub use item_of::ItemOf;
171pub use regular::{
172 Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, Unbalanced,
173};
174use subsoil::arithmetic::traits::Zero;
175use subsoil::core::Get;
176use subsoil::runtime::{traits::Convert, DispatchError};
177pub use union_of::{NativeFromLeft, NativeOrWithId, UnionOf};
178
179#[cfg(feature = "experimental")]
180use crate::traits::MaybeConsideration;
181use crate::{
182 ensure,
183 traits::{Consideration, Footprint},
184};
185
186/// Consideration method using a `fungible` balance frozen as the cost exacted for the footprint.
187///
188/// The aggregate amount frozen under `R::get()` for any account which has multiple tickets,
189/// is the *cumulative* amounts of each ticket's footprint (each individually determined by `D`).
190#[derive(
191 CloneNoBound, EqNoBound, PartialEqNoBound, Encode, Decode, TypeInfo, MaxEncodedLen, DebugNoBound,
192)]
193#[scale_info(skip_type_params(A, F, R, D, Fp))]
194#[codec(mel_bound())]
195pub struct FreezeConsideration<A, F, R, D, Fp>(F::Balance, PhantomData<fn() -> (A, R, D, Fp)>)
196where
197 F: MutateFreeze<A>;
198impl<
199 A: 'static + Eq,
200 #[cfg(not(feature = "runtime-benchmarks"))] F: 'static + MutateFreeze<A>,
201 #[cfg(feature = "runtime-benchmarks")] F: 'static + MutateFreeze<A> + Mutate<A>,
202 R: 'static + Get<F::Id>,
203 D: 'static + Convert<Fp, F::Balance>,
204 Fp: 'static,
205 > Consideration<A, Fp> for FreezeConsideration<A, F, R, D, Fp>
206{
207 fn new(who: &A, footprint: Fp) -> Result<Self, DispatchError> {
208 let new = D::convert(footprint);
209 F::increase_frozen(&R::get(), who, new)?;
210 Ok(Self(new, PhantomData))
211 }
212 fn update(self, who: &A, footprint: Fp) -> Result<Self, DispatchError> {
213 let new = D::convert(footprint);
214 if self.0 > new {
215 F::decrease_frozen(&R::get(), who, self.0 - new)?;
216 } else if new > self.0 {
217 F::increase_frozen(&R::get(), who, new - self.0)?;
218 }
219 Ok(Self(new, PhantomData))
220 }
221 fn drop(self, who: &A) -> Result<(), DispatchError> {
222 F::decrease_frozen(&R::get(), who, self.0).map(|_| ())
223 }
224 #[cfg(feature = "runtime-benchmarks")]
225 fn ensure_successful(who: &A, fp: Fp) {
226 let _ = F::mint_into(who, F::minimum_balance().saturating_add(D::convert(fp)));
227 }
228}
229#[cfg(feature = "experimental")]
230impl<
231 A: 'static + Eq,
232 #[cfg(not(feature = "runtime-benchmarks"))] F: 'static + MutateFreeze<A>,
233 #[cfg(feature = "runtime-benchmarks")] F: 'static + MutateFreeze<A> + Mutate<A>,
234 R: 'static + Get<F::Id>,
235 D: 'static + Convert<Fp, F::Balance>,
236 Fp: 'static,
237 > MaybeConsideration<A, Fp> for FreezeConsideration<A, F, R, D, Fp>
238{
239 fn is_none(&self) -> bool {
240 self.0.is_zero()
241 }
242}
243
244/// Consideration method using a `fungible` balance frozen as the cost exacted for the footprint.
245#[derive(
246 CloneNoBound,
247 EqNoBound,
248 PartialEqNoBound,
249 Encode,
250 Decode,
251 DecodeWithMemTracking,
252 TypeInfo,
253 MaxEncodedLen,
254 DebugNoBound,
255)]
256#[scale_info(skip_type_params(A, F, R, D, Fp))]
257#[codec(mel_bound())]
258pub struct HoldConsideration<A, F, R, D, Fp = Footprint>(
259 F::Balance,
260 PhantomData<fn() -> (A, R, D, Fp)>,
261)
262where
263 F: MutateHold<A>;
264impl<
265 A: 'static + Eq,
266 #[cfg(not(feature = "runtime-benchmarks"))] F: 'static + MutateHold<A>,
267 #[cfg(feature = "runtime-benchmarks")] F: 'static + MutateHold<A> + Mutate<A>,
268 R: 'static + Get<F::Reason>,
269 D: 'static + Convert<Fp, F::Balance>,
270 Fp: 'static,
271 > Consideration<A, Fp> for HoldConsideration<A, F, R, D, Fp>
272{
273 fn new(who: &A, footprint: Fp) -> Result<Self, DispatchError> {
274 let new = D::convert(footprint);
275 F::hold(&R::get(), who, new)?;
276 Ok(Self(new, PhantomData))
277 }
278 fn update(self, who: &A, footprint: Fp) -> Result<Self, DispatchError> {
279 let new = D::convert(footprint);
280 if self.0 > new {
281 F::release(&R::get(), who, self.0 - new, BestEffort)?;
282 } else if new > self.0 {
283 F::hold(&R::get(), who, new - self.0)?;
284 }
285 Ok(Self(new, PhantomData))
286 }
287 fn drop(self, who: &A) -> Result<(), DispatchError> {
288 F::release(&R::get(), who, self.0, BestEffort).map(|_| ())
289 }
290 fn burn(self, who: &A) {
291 let _ = F::burn_held(&R::get(), who, self.0, BestEffort, Force);
292 }
293 #[cfg(feature = "runtime-benchmarks")]
294 fn ensure_successful(who: &A, fp: Fp) {
295 let _ = F::mint_into(who, F::minimum_balance().saturating_add(D::convert(fp)));
296 }
297}
298#[cfg(feature = "experimental")]
299impl<
300 A: 'static + Eq,
301 #[cfg(not(feature = "runtime-benchmarks"))] F: 'static + MutateHold<A>,
302 #[cfg(feature = "runtime-benchmarks")] F: 'static + MutateHold<A> + Mutate<A>,
303 R: 'static + Get<F::Reason>,
304 D: 'static + Convert<Fp, F::Balance>,
305 Fp: 'static,
306 > MaybeConsideration<A, Fp> for HoldConsideration<A, F, R, D, Fp>
307{
308 fn is_none(&self) -> bool {
309 self.0.is_zero()
310 }
311}
312
313/// Basic consideration method using a `fungible` balance frozen as the cost exacted for the
314/// footprint.
315///
316/// NOTE: This is an optimized implementation, which can only be used for systems where each
317/// account has only a single active ticket associated with it since individual tickets do not
318/// track the specific balance which is frozen. If you are uncertain then use `FreezeConsideration`
319/// instead, since this works in all circumstances.
320#[derive(
321 CloneNoBound, EqNoBound, PartialEqNoBound, Encode, Decode, TypeInfo, MaxEncodedLen, DebugNoBound,
322)]
323#[scale_info(skip_type_params(A, Fx, Rx, D, Fp))]
324#[codec(mel_bound())]
325pub struct LoneFreezeConsideration<A, Fx, Rx, D, Fp>(PhantomData<fn() -> (A, Fx, Rx, D, Fp)>);
326impl<
327 A: 'static + Eq,
328 #[cfg(not(feature = "runtime-benchmarks"))] Fx: 'static + MutateFreeze<A>,
329 #[cfg(feature = "runtime-benchmarks")] Fx: 'static + MutateFreeze<A> + Mutate<A>,
330 Rx: 'static + Get<Fx::Id>,
331 D: 'static + Convert<Fp, Fx::Balance>,
332 Fp: 'static,
333 > Consideration<A, Fp> for LoneFreezeConsideration<A, Fx, Rx, D, Fp>
334{
335 fn new(who: &A, footprint: Fp) -> Result<Self, DispatchError> {
336 ensure!(Fx::balance_frozen(&Rx::get(), who).is_zero(), DispatchError::Unavailable);
337 Fx::set_frozen(&Rx::get(), who, D::convert(footprint), Polite).map(|_| Self(PhantomData))
338 }
339 fn update(self, who: &A, footprint: Fp) -> Result<Self, DispatchError> {
340 Fx::set_frozen(&Rx::get(), who, D::convert(footprint), Polite).map(|_| Self(PhantomData))
341 }
342 fn drop(self, who: &A) -> Result<(), DispatchError> {
343 Fx::thaw(&Rx::get(), who).map(|_| ())
344 }
345 #[cfg(feature = "runtime-benchmarks")]
346 fn ensure_successful(who: &A, fp: Fp) {
347 let _ = Fx::mint_into(who, Fx::minimum_balance().saturating_add(D::convert(fp)));
348 }
349}
350
351/// Basic consideration method using a `fungible` balance placed on hold as the cost exacted for the
352/// footprint.
353///
354/// NOTE: This is an optimized implementation, which can only be used for systems where each
355/// account has only a single active ticket associated with it since individual tickets do not
356/// track the specific balance which is frozen. If you are uncertain then use `FreezeConsideration`
357/// instead, since this works in all circumstances.
358#[derive(
359 CloneNoBound, EqNoBound, PartialEqNoBound, Encode, Decode, TypeInfo, MaxEncodedLen, DebugNoBound,
360)]
361#[scale_info(skip_type_params(A, Fx, Rx, D, Fp))]
362#[codec(mel_bound())]
363pub struct LoneHoldConsideration<A, Fx, Rx, D, Fp>(PhantomData<fn() -> (A, Fx, Rx, D, Fp)>);
364impl<
365 A: 'static + Eq,
366 #[cfg(not(feature = "runtime-benchmarks"))] F: 'static + MutateHold<A>,
367 #[cfg(feature = "runtime-benchmarks")] F: 'static + MutateHold<A> + Mutate<A>,
368 R: 'static + Get<F::Reason>,
369 D: 'static + Convert<Fp, F::Balance>,
370 Fp: 'static,
371 > Consideration<A, Fp> for LoneHoldConsideration<A, F, R, D, Fp>
372{
373 fn new(who: &A, footprint: Fp) -> Result<Self, DispatchError> {
374 ensure!(F::balance_on_hold(&R::get(), who).is_zero(), DispatchError::Unavailable);
375 F::set_on_hold(&R::get(), who, D::convert(footprint)).map(|_| Self(PhantomData))
376 }
377 fn update(self, who: &A, footprint: Fp) -> Result<Self, DispatchError> {
378 F::set_on_hold(&R::get(), who, D::convert(footprint)).map(|_| Self(PhantomData))
379 }
380 fn drop(self, who: &A) -> Result<(), DispatchError> {
381 F::release_all(&R::get(), who, BestEffort).map(|_| ())
382 }
383 fn burn(self, who: &A) {
384 let _ = F::burn_all_held(&R::get(), who, BestEffort, Force);
385 }
386 #[cfg(feature = "runtime-benchmarks")]
387 fn ensure_successful(who: &A, fp: Fp) {
388 let _ = F::mint_into(who, F::minimum_balance().saturating_add(D::convert(fp)));
389 }
390}