frame_support/traits/tokens/fungibles/hold.rs
1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! The traits for putting holds within a single fungible token class.
19//!
20//! See the [`crate::traits::fungibles`] doc for more information about fungibles traits.
21
22use crate::{
23 ensure,
24 traits::tokens::{
25 DepositConsequence::Success,
26 Fortitude::{self, Force},
27 Precision::{self, BestEffort, Exact},
28 Preservation::{self, Protect},
29 Provenance::Extant,
30 Restriction::{self, Free, OnHold},
31 },
32};
33use scale_info::TypeInfo;
34use sp_arithmetic::{
35 traits::{CheckedAdd, CheckedSub, Zero},
36 ArithmeticError,
37};
38use sp_runtime::{DispatchError, DispatchResult, Saturating, TokenError};
39
40use super::*;
41
42/// Trait for inspecting a fungible asset whose accounts support partitioning and slashing.
43pub trait Inspect<AccountId>: super::Inspect<AccountId> {
44 /// An identifier for a hold. Used for disambiguating different holds so that
45 /// they can be individually replaced or removed and funds from one hold don't accidentally
46 /// become unreserved or slashed for another.
47 type Reason: codec::Encode + TypeInfo + 'static;
48
49 /// Amount of funds on hold (for all hold reasons) of `who`.
50 fn total_balance_on_hold(asset: Self::AssetId, who: &AccountId) -> Self::Balance;
51
52 /// Get the maximum amount that the `total_balance_on_hold` of `who` can be reduced successfully
53 /// based on whether we are willing to force the reduction and potentially go below user-level
54 /// restrictions on the minimum amount of the account. Note: This cannot bring the account into
55 /// an inconsistent state with regards any required existential deposit.
56 ///
57 /// Never more than `total_balance_on_hold()`.
58 fn reducible_total_balance_on_hold(
59 asset: Self::AssetId,
60 who: &AccountId,
61 _force: Fortitude,
62 ) -> Self::Balance {
63 Self::total_balance_on_hold(asset, who)
64 }
65
66 /// Amount of funds on hold (for the given reason) of `who`.
67 fn balance_on_hold(
68 asset: Self::AssetId,
69 reason: &Self::Reason,
70 who: &AccountId,
71 ) -> Self::Balance;
72
73 /// Returns `true` if it's possible to place (additional) funds under a hold of a given
74 /// `reason`. This may fail if the account has exhausted a limited number of concurrent
75 /// holds or if it cannot be made to exist (e.g. there is no provider reference).
76 ///
77 /// NOTE: This does not take into account changes which could be made to the account of `who`
78 /// (such as removing a provider reference) after this call is made. Any usage of this should
79 /// therefore ensure the account is already in the appropriate state prior to calling it.
80 fn hold_available(_asset: Self::AssetId, _reason: &Self::Reason, _who: &AccountId) -> bool {
81 true
82 }
83
84 /// Check to see if some `amount` of funds of `who` may be placed on hold with the given
85 /// `reason`. Reasons why this may not be true:
86 ///
87 /// - The implementor supports only a limited number of concurrent holds on an account which is
88 /// the possible values of `reason`;
89 /// - The total balance of the account is less than `amount`;
90 /// - Removing `amount` from the total balance would kill the account and remove the only
91 /// provider reference.
92 ///
93 /// Note: we pass `Fortitude::Force` as the last argument to `reducible_balance` since we assume
94 /// that if needed the balance can slashed. If we are using a simple non-forcing
95 /// reserve-transfer, then we really ought to check that we are not reducing the funds below the
96 /// freeze-limit (if any).
97 ///
98 /// NOTE: This does not take into account changes which could be made to the account of `who`
99 /// (such as removing a provider reference) after this call is made. Any usage of this should
100 /// therefore ensure the account is already in the appropriate state prior to calling it.
101 fn ensure_can_hold(
102 asset: Self::AssetId,
103 reason: &Self::Reason,
104 who: &AccountId,
105 amount: Self::Balance,
106 ) -> DispatchResult {
107 ensure!(Self::hold_available(asset.clone(), reason, who), TokenError::CannotCreateHold);
108 ensure!(
109 amount <= Self::reducible_balance(asset, who, Protect, Force),
110 TokenError::FundsUnavailable
111 );
112 Ok(())
113 }
114
115 /// Check to see if some `amount` of funds of `who` may be placed on hold for the given
116 /// `reason`. Reasons why this may not be true:
117 ///
118 /// - The implementor supports only a limited number of concurrent holds on an account which is
119 /// the possible values of `reason`;
120 /// - The main balance of the account is less than `amount`;
121 /// - Removing `amount` from the main balance would kill the account and remove the only
122 /// provider reference.
123 ///
124 /// NOTE: This does not take into account changes which could be made to the account of `who`
125 /// (such as removing a provider reference) after this call is made. Any usage of this should
126 /// therefore ensure the account is already in the appropriate state prior to calling it.
127 fn can_hold(
128 asset: Self::AssetId,
129 reason: &Self::Reason,
130 who: &AccountId,
131 amount: Self::Balance,
132 ) -> bool {
133 Self::ensure_can_hold(asset, reason, who, amount).is_ok()
134 }
135}
136
137/// A fungible, holdable token class where the balance on hold can be set arbitrarily.
138///
139/// **WARNING**
140/// Do not use this directly unless you want trouble, since it allows you to alter account balances
141/// without keeping the issuance up to date. It has no safeguards against accidentally creating
142/// token imbalances in your system leading to accidental inflation or deflation. It's really just
143/// for the underlying datatype to implement so the user gets the much safer `Balanced` trait to
144/// use.
145pub trait Unbalanced<AccountId>: Inspect<AccountId> {
146 /// Forcefully set the balance on hold of `who` to `amount`. This is independent of any other
147 /// balances on hold or the main ("free") balance.
148 ///
149 /// If this call executes successfully, you can `assert_eq!(Self::balance_on_hold(), amount);`.
150 ///
151 /// This function does its best to force the balance change through, but will not break system
152 /// invariants such as any Existential Deposits needed or overflows/underflows.
153 /// If this cannot be done for some reason (e.g. because the account doesn't exist) then an
154 /// `Err` is returned.
155 // Implementation note: This should increment the consumer refs if it moves total on hold from
156 // zero to non-zero and decrement in the opposite direction.
157 //
158 // Since this was not done in the previous logic, this will need either a migration or a
159 // state item which tracks whether the account is on the old logic or new.
160 fn set_balance_on_hold(
161 asset: Self::AssetId,
162 reason: &Self::Reason,
163 who: &AccountId,
164 amount: Self::Balance,
165 ) -> DispatchResult;
166
167 /// Reduce the balance on hold of `who` by `amount`.
168 ///
169 /// If `precision` is `Precision::Exact` and it cannot be reduced by that amount for
170 /// some reason, return `Err` and don't reduce it at all. If `precision` is
171 /// `Precision::BestEffort`, then reduce the balance of `who` by the most that is possible, up
172 /// to `amount`.
173 ///
174 /// In either case, if `Ok` is returned then the inner is the amount by which is was reduced.
175 fn decrease_balance_on_hold(
176 asset: Self::AssetId,
177 reason: &Self::Reason,
178 who: &AccountId,
179 mut amount: Self::Balance,
180 precision: Precision,
181 ) -> Result<Self::Balance, DispatchError> {
182 let old_balance = Self::balance_on_hold(asset.clone(), reason, who);
183 if let BestEffort = precision {
184 amount = amount.min(old_balance);
185 }
186 let new_balance = old_balance.checked_sub(&amount).ok_or(TokenError::FundsUnavailable)?;
187 Self::set_balance_on_hold(asset, reason, who, new_balance)?;
188 Ok(amount)
189 }
190
191 /// Increase the balance on hold of `who` by `amount`.
192 ///
193 /// If it cannot be increased by that amount for some reason, return `Err` and don't increase
194 /// it at all. If Ok, return the imbalance.
195 fn increase_balance_on_hold(
196 asset: Self::AssetId,
197 reason: &Self::Reason,
198 who: &AccountId,
199 amount: Self::Balance,
200 precision: Precision,
201 ) -> Result<Self::Balance, DispatchError> {
202 let old_balance = Self::balance_on_hold(asset.clone(), reason, who);
203 let new_balance = if let BestEffort = precision {
204 old_balance.saturating_add(amount)
205 } else {
206 old_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)?
207 };
208 let amount = new_balance.saturating_sub(old_balance);
209 if !amount.is_zero() {
210 Self::set_balance_on_hold(asset, reason, who, new_balance)?;
211 }
212 Ok(amount)
213 }
214}
215
216/// Trait for slashing a fungible asset which can be place on hold.
217pub trait Balanced<AccountId>:
218 super::Balanced<AccountId>
219 + Unbalanced<AccountId>
220 + DoneSlash<Self::AssetId, Self::Reason, AccountId, Self::Balance>
221{
222 /// Reduce the balance of some funds on hold in an account.
223 ///
224 /// The resulting imbalance is the first item of the tuple returned.
225 ///
226 /// As much funds that are on hold up to `amount` will be deducted as possible. If this is less
227 /// than `amount`, then a non-zero second item will be returned.
228 fn slash(
229 asset: Self::AssetId,
230 reason: &Self::Reason,
231 who: &AccountId,
232 amount: Self::Balance,
233 ) -> (Credit<AccountId, Self>, Self::Balance) {
234 let decrease =
235 Self::decrease_balance_on_hold(asset.clone(), reason, who, amount, BestEffort)
236 .unwrap_or(Default::default());
237 let credit =
238 Imbalance::<Self::AssetId, Self::Balance, Self::OnDropCredit, Self::OnDropDebt>::new(
239 asset.clone(),
240 decrease,
241 );
242 Self::done_slash(asset, reason, who, decrease);
243 (credit, amount.saturating_sub(decrease))
244 }
245}
246
247/// Trait for optional bookkeeping callbacks after a slash
248pub trait DoneSlash<AssetId, Reason, AccountId, Balance> {
249 fn done_slash(_asset: AssetId, _reason: &Reason, _who: &AccountId, _amount: Balance) {}
250}
251
252#[impl_trait_for_tuples::impl_for_tuples(30)]
253impl<AssetId: Copy, Reason, AccountId, Balance: Copy> DoneSlash<AssetId, Reason, AccountId, Balance>
254 for Tuple
255{
256 fn done_slash(asset_id: AssetId, reason: &Reason, who: &AccountId, amount: Balance) {
257 for_tuples!( #( Tuple::done_slash(asset_id, reason, who, amount); )* );
258 }
259}
260
261/// Trait for mutating a fungible asset which can be placed on hold.
262pub trait Mutate<AccountId>:
263 Inspect<AccountId> + super::Unbalanced<AccountId> + Unbalanced<AccountId>
264{
265 /// Hold some funds in an account. If a hold for `reason` is already in place, then this
266 /// will increase it.
267 fn hold(
268 asset: Self::AssetId,
269 reason: &Self::Reason,
270 who: &AccountId,
271 amount: Self::Balance,
272 ) -> DispatchResult {
273 // NOTE: This doesn't change the total balance of the account so there's no need to
274 // check liquidity.
275
276 Self::ensure_can_hold(asset.clone(), reason, who, amount)?;
277 // Should be infallible now, but we proceed softly anyway.
278 Self::decrease_balance(asset.clone(), who, amount, Exact, Protect, Force)?;
279 Self::increase_balance_on_hold(asset.clone(), reason, who, amount, BestEffort)?;
280 Self::done_hold(asset, reason, who, amount);
281 Ok(())
282 }
283
284 /// Release up to `amount` held funds in an account.
285 ///
286 /// The actual amount released is returned with `Ok`.
287 ///
288 /// If `precision` is `BestEffort`, then the amount actually unreserved and returned as the
289 /// inner value of `Ok` may be smaller than the `amount` passed.
290 fn release(
291 asset: Self::AssetId,
292 reason: &Self::Reason,
293 who: &AccountId,
294 amount: Self::Balance,
295 precision: Precision,
296 ) -> Result<Self::Balance, DispatchError> {
297 // NOTE: This doesn't change the total balance of the account so there's no need to
298 // check liquidity.
299
300 // We want to make sure we can deposit the amount in advance. If we can't then something is
301 // very wrong.
302 ensure!(
303 Self::can_deposit(asset.clone(), who, amount, Extant) == Success,
304 TokenError::CannotCreate
305 );
306 // Get the amount we can actually take from the hold. This might be less than what we want
307 // if we're only doing a best-effort.
308 let amount = Self::decrease_balance_on_hold(asset.clone(), reason, who, amount, precision)?;
309 // Increase the main balance by what we took. We always do a best-effort here because we
310 // already checked that we can deposit before.
311 let actual = Self::increase_balance(asset.clone(), who, amount, BestEffort)?;
312 Self::done_release(asset, reason, who, actual);
313 Ok(actual)
314 }
315
316 /// Attempt to decrease the `asset` balance of `who` which is held for the given `reason` by
317 /// `amount`.
318 ///
319 /// If `precision` is true, then as much as possible is reduced, up to `amount`, and the
320 /// amount of tokens reduced is returned. Otherwise, if the total amount can be reduced, then it
321 /// is and the amount returned, and if not, then nothing changes and `Err` is returned.
322 ///
323 /// If `force` is `Force`, then locks/freezes will be ignored. This should only be used when
324 /// conducting slashing or other activity which materially disadvantages the account holder
325 /// since it could provide a means of circumventing freezes.
326 fn burn_held(
327 asset: Self::AssetId,
328 reason: &Self::Reason,
329 who: &AccountId,
330 mut amount: Self::Balance,
331 precision: Precision,
332 force: Fortitude,
333 ) -> Result<Self::Balance, DispatchError> {
334 // We must check total-balance requirements if `!force`.
335 let liquid = Self::reducible_total_balance_on_hold(asset.clone(), who, force);
336 if let BestEffort = precision {
337 amount = amount.min(liquid);
338 } else {
339 ensure!(amount <= liquid, TokenError::Frozen);
340 }
341 let amount = Self::decrease_balance_on_hold(asset.clone(), reason, who, amount, precision)?;
342 Self::set_total_issuance(
343 asset.clone(),
344 Self::total_issuance(asset.clone()).saturating_sub(amount),
345 );
346 Self::done_burn_held(asset, reason, who, amount);
347 Ok(amount)
348 }
349
350 /// Attempt to decrease the `asset` balance of `who` which is held for the given `reason` to
351 /// zero.
352 ///
353 /// If `precision` is `BestEffort`, then as much as possible is reduced, up to `amount`, and the
354 /// amount of tokens reduced is returned. Otherwise, if the total amount can be reduced, then it
355 /// is and the amount returned, and if not, then nothing changes and `Err` is returned.
356 ///
357 /// If `force` is `Force`, then locks/freezes will be ignored. This should only be used when
358 /// conducting slashing or other activity which materially disadvantages the account holder
359 /// since it could provide a means of circumventing freezes.
360 fn burn_all_held(
361 asset: Self::AssetId,
362 reason: &Self::Reason,
363 who: &AccountId,
364 precision: Precision,
365 force: Fortitude,
366 ) -> Result<Self::Balance, DispatchError> {
367 let amount = Self::balance_on_hold(asset.clone(), reason, who);
368 Self::burn_held(asset, reason, who, amount, precision, force)
369 }
370
371 /// Transfer held funds into a destination account.
372 ///
373 /// If `mode` is `OnHold`, then the destination account must already exist and the assets
374 /// transferred will still be on hold in the destination account. If not, then the destination
375 /// account need not already exist, but must be creatable.
376 ///
377 /// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without
378 /// error.
379 ///
380 /// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be
381 /// left as `Regular` in most circumstances, but when you want the same power as a `slash`, it
382 /// may be `Force`.
383 ///
384 /// The actual amount transferred is returned, or `Err` in the case of error and nothing is
385 /// changed.
386 fn transfer_on_hold(
387 asset: Self::AssetId,
388 reason: &Self::Reason,
389 source: &AccountId,
390 dest: &AccountId,
391 mut amount: Self::Balance,
392 precision: Precision,
393 mode: Restriction,
394 force: Fortitude,
395 ) -> Result<Self::Balance, DispatchError> {
396 // We must check total-balance requirements if `!force`.
397 let have = Self::balance_on_hold(asset.clone(), reason, source);
398 let liquid = Self::reducible_total_balance_on_hold(asset.clone(), source, force);
399 if let BestEffort = precision {
400 amount = amount.min(liquid).min(have);
401 } else {
402 ensure!(amount <= liquid, TokenError::Frozen);
403 ensure!(amount <= have, TokenError::FundsUnavailable);
404 }
405
406 // We want to make sure we can deposit the amount in advance. If we can't then something is
407 // very wrong.
408 ensure!(
409 Self::can_deposit(asset.clone(), dest, amount, Extant) == Success,
410 TokenError::CannotCreate
411 );
412 ensure!(
413 mode == Free || Self::hold_available(asset.clone(), reason, dest),
414 TokenError::CannotCreateHold
415 );
416
417 let amount =
418 Self::decrease_balance_on_hold(asset.clone(), reason, source, amount, precision)?;
419 let actual = if mode == OnHold {
420 Self::increase_balance_on_hold(asset.clone(), reason, dest, amount, precision)?
421 } else {
422 Self::increase_balance(asset.clone(), dest, amount, precision)?
423 };
424 Self::done_transfer_on_hold(asset, reason, source, dest, actual);
425 Ok(actual)
426 }
427
428 /// Transfer some `amount` of free balance from `source` to become owned by `dest` but on hold
429 /// for `reason`.
430 /// for `reason`.
431 ///
432 /// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without
433 /// error.
434 ///
435 /// `source` must obey the requirements of `keep_alive`.
436 ///
437 /// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be
438 /// left as `Regular` in most circumstances, but when you want the same power as a `slash`, it
439 /// may be `Force`.
440 ///
441 /// The amount placed on hold is returned or `Err` in the case of error and nothing is changed.
442 ///
443 /// WARNING: This may return an error after a partial storage mutation. It should be used only
444 /// inside a transactional storage context and an `Err` result must imply a storage rollback.
445 fn transfer_and_hold(
446 asset: Self::AssetId,
447 reason: &Self::Reason,
448 source: &AccountId,
449 dest: &AccountId,
450 amount: Self::Balance,
451 precision: Precision,
452 expendability: Preservation,
453 force: Fortitude,
454 ) -> Result<Self::Balance, DispatchError> {
455 ensure!(Self::hold_available(asset.clone(), reason, dest), TokenError::CannotCreateHold);
456 ensure!(
457 Self::can_deposit(asset.clone(), dest, amount, Extant) == Success,
458 TokenError::CannotCreate
459 );
460 let actual =
461 Self::decrease_balance(asset.clone(), source, amount, precision, expendability, force)?;
462 Self::increase_balance_on_hold(asset.clone(), reason, dest, actual, precision)?;
463 Self::done_transfer_on_hold(asset, reason, source, dest, actual);
464 Ok(actual)
465 }
466
467 fn done_hold(
468 _asset: Self::AssetId,
469 _reason: &Self::Reason,
470 _who: &AccountId,
471 _amount: Self::Balance,
472 ) {
473 }
474 fn done_release(
475 _asset: Self::AssetId,
476 _reason: &Self::Reason,
477 _who: &AccountId,
478 _amount: Self::Balance,
479 ) {
480 }
481 fn done_burn_held(
482 _asset: Self::AssetId,
483 _reason: &Self::Reason,
484 _who: &AccountId,
485 _amount: Self::Balance,
486 ) {
487 }
488 fn done_transfer_on_hold(
489 _asset: Self::AssetId,
490 _reason: &Self::Reason,
491 _source: &AccountId,
492 _dest: &AccountId,
493 _amount: Self::Balance,
494 ) {
495 }
496 fn done_transfer_and_hold(
497 _asset: Self::AssetId,
498 _reason: &Self::Reason,
499 _source: &AccountId,
500 _dest: &AccountId,
501 _transferred: Self::Balance,
502 ) {
503 }
504}