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>: super::Balanced<AccountId> + Unbalanced<AccountId> {
218 /// Reduce the balance of some funds on hold in an account.
219 ///
220 /// The resulting imbalance is the first item of the tuple returned.
221 ///
222 /// As much funds that are on hold up to `amount` will be deducted as possible. If this is less
223 /// than `amount`, then a non-zero second item will be returned.
224 fn slash(
225 asset: Self::AssetId,
226 reason: &Self::Reason,
227 who: &AccountId,
228 amount: Self::Balance,
229 ) -> (Credit<AccountId, Self>, Self::Balance) {
230 let decrease =
231 Self::decrease_balance_on_hold(asset.clone(), reason, who, amount, BestEffort)
232 .unwrap_or(Default::default());
233 let credit =
234 Imbalance::<Self::AssetId, Self::Balance, Self::OnDropCredit, Self::OnDropDebt>::new(
235 asset.clone(),
236 decrease,
237 );
238 Self::done_slash(asset, reason, who, decrease);
239 (credit, amount.saturating_sub(decrease))
240 }
241
242 fn done_slash(
243 _asset: Self::AssetId,
244 _reason: &Self::Reason,
245 _who: &AccountId,
246 _amount: Self::Balance,
247 ) {
248 }
249}
250
251/// Trait for mutating a fungible asset which can be placed on hold.
252pub trait Mutate<AccountId>:
253 Inspect<AccountId> + super::Unbalanced<AccountId> + Unbalanced<AccountId>
254{
255 /// Hold some funds in an account. If a hold for `reason` is already in place, then this
256 /// will increase it.
257 fn hold(
258 asset: Self::AssetId,
259 reason: &Self::Reason,
260 who: &AccountId,
261 amount: Self::Balance,
262 ) -> DispatchResult {
263 // NOTE: This doesn't change the total balance of the account so there's no need to
264 // check liquidity.
265
266 Self::ensure_can_hold(asset.clone(), reason, who, amount)?;
267 // Should be infallible now, but we proceed softly anyway.
268 Self::decrease_balance(asset.clone(), who, amount, Exact, Protect, Force)?;
269 Self::increase_balance_on_hold(asset.clone(), reason, who, amount, BestEffort)?;
270 Self::done_hold(asset, reason, who, amount);
271 Ok(())
272 }
273
274 /// Release up to `amount` held funds in an account.
275 ///
276 /// The actual amount released is returned with `Ok`.
277 ///
278 /// If `precision` is `BestEffort`, then the amount actually unreserved and returned as the
279 /// inner value of `Ok` may be smaller than the `amount` passed.
280 fn release(
281 asset: Self::AssetId,
282 reason: &Self::Reason,
283 who: &AccountId,
284 amount: Self::Balance,
285 precision: Precision,
286 ) -> Result<Self::Balance, DispatchError> {
287 // NOTE: This doesn't change the total balance of the account so there's no need to
288 // check liquidity.
289
290 // We want to make sure we can deposit the amount in advance. If we can't then something is
291 // very wrong.
292 ensure!(
293 Self::can_deposit(asset.clone(), who, amount, Extant) == Success,
294 TokenError::CannotCreate
295 );
296 // Get the amount we can actually take from the hold. This might be less than what we want
297 // if we're only doing a best-effort.
298 let amount = Self::decrease_balance_on_hold(asset.clone(), reason, who, amount, precision)?;
299 // Increase the main balance by what we took. We always do a best-effort here because we
300 // already checked that we can deposit before.
301 let actual = Self::increase_balance(asset.clone(), who, amount, BestEffort)?;
302 Self::done_release(asset, reason, who, actual);
303 Ok(actual)
304 }
305
306 /// Attempt to decrease the `asset` balance of `who` which is held for the given `reason` by
307 /// `amount`.
308 ///
309 /// If `precision` is true, then as much as possible is reduced, up to `amount`, and the
310 /// amount of tokens reduced is returned. Otherwise, if the total amount can be reduced, then it
311 /// is and the amount returned, and if not, then nothing changes and `Err` is returned.
312 ///
313 /// If `force` is `Force`, then locks/freezes will be ignored. This should only be used when
314 /// conducting slashing or other activity which materially disadvantages the account holder
315 /// since it could provide a means of circumventing freezes.
316 fn burn_held(
317 asset: Self::AssetId,
318 reason: &Self::Reason,
319 who: &AccountId,
320 mut amount: Self::Balance,
321 precision: Precision,
322 force: Fortitude,
323 ) -> Result<Self::Balance, DispatchError> {
324 // We must check total-balance requirements if `!force`.
325 let liquid = Self::reducible_total_balance_on_hold(asset.clone(), who, force);
326 if let BestEffort = precision {
327 amount = amount.min(liquid);
328 } else {
329 ensure!(amount <= liquid, TokenError::Frozen);
330 }
331 let amount = Self::decrease_balance_on_hold(asset.clone(), reason, who, amount, precision)?;
332 Self::set_total_issuance(
333 asset.clone(),
334 Self::total_issuance(asset.clone()).saturating_sub(amount),
335 );
336 Self::done_burn_held(asset, reason, who, amount);
337 Ok(amount)
338 }
339
340 /// Attempt to decrease the `asset` balance of `who` which is held for the given `reason` to
341 /// zero.
342 ///
343 /// If `precision` is `BestEffort`, then as much as possible is reduced, up to `amount`, and the
344 /// amount of tokens reduced is returned. Otherwise, if the total amount can be reduced, then it
345 /// is and the amount returned, and if not, then nothing changes and `Err` is returned.
346 ///
347 /// If `force` is `Force`, then locks/freezes will be ignored. This should only be used when
348 /// conducting slashing or other activity which materially disadvantages the account holder
349 /// since it could provide a means of circumventing freezes.
350 fn burn_all_held(
351 asset: Self::AssetId,
352 reason: &Self::Reason,
353 who: &AccountId,
354 precision: Precision,
355 force: Fortitude,
356 ) -> Result<Self::Balance, DispatchError> {
357 let amount = Self::balance_on_hold(asset.clone(), reason, who);
358 Self::burn_held(asset, reason, who, amount, precision, force)
359 }
360
361 /// Transfer held funds into a destination account.
362 ///
363 /// If `mode` is `OnHold`, then the destination account must already exist and the assets
364 /// transferred will still be on hold in the destination account. If not, then the destination
365 /// account need not already exist, but must be creatable.
366 ///
367 /// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without
368 /// error.
369 ///
370 /// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be
371 /// left as `Regular` in most circumstances, but when you want the same power as a `slash`, it
372 /// may be `Force`.
373 ///
374 /// The actual amount transferred is returned, or `Err` in the case of error and nothing is
375 /// changed.
376 fn transfer_on_hold(
377 asset: Self::AssetId,
378 reason: &Self::Reason,
379 source: &AccountId,
380 dest: &AccountId,
381 mut amount: Self::Balance,
382 precision: Precision,
383 mode: Restriction,
384 force: Fortitude,
385 ) -> Result<Self::Balance, DispatchError> {
386 // We must check total-balance requirements if `!force`.
387 let have = Self::balance_on_hold(asset.clone(), reason, source);
388 let liquid = Self::reducible_total_balance_on_hold(asset.clone(), source, force);
389 if let BestEffort = precision {
390 amount = amount.min(liquid).min(have);
391 } else {
392 ensure!(amount <= liquid, TokenError::Frozen);
393 ensure!(amount <= have, TokenError::FundsUnavailable);
394 }
395
396 // We want to make sure we can deposit the amount in advance. If we can't then something is
397 // very wrong.
398 ensure!(
399 Self::can_deposit(asset.clone(), dest, amount, Extant) == Success,
400 TokenError::CannotCreate
401 );
402 ensure!(
403 mode == Free || Self::hold_available(asset.clone(), reason, dest),
404 TokenError::CannotCreateHold
405 );
406
407 let amount =
408 Self::decrease_balance_on_hold(asset.clone(), reason, source, amount, precision)?;
409 let actual = if mode == OnHold {
410 Self::increase_balance_on_hold(asset.clone(), reason, dest, amount, precision)?
411 } else {
412 Self::increase_balance(asset.clone(), dest, amount, precision)?
413 };
414 Self::done_transfer_on_hold(asset, reason, source, dest, actual);
415 Ok(actual)
416 }
417
418 /// Transfer some `amount` of free balance from `source` to become owned by `dest` but on hold
419 /// for `reason`.
420 /// for `reason`.
421 ///
422 /// If `precision` is `BestEffort`, then an amount less than `amount` may be transferred without
423 /// error.
424 ///
425 /// `source` must obey the requirements of `keep_alive`.
426 ///
427 /// If `force` is `Force`, then other fund-locking mechanisms may be disregarded. It should be
428 /// left as `Regular` in most circumstances, but when you want the same power as a `slash`, it
429 /// may be `Force`.
430 ///
431 /// The amount placed on hold is returned or `Err` in the case of error and nothing is changed.
432 ///
433 /// WARNING: This may return an error after a partial storage mutation. It should be used only
434 /// inside a transactional storage context and an `Err` result must imply a storage rollback.
435 fn transfer_and_hold(
436 asset: Self::AssetId,
437 reason: &Self::Reason,
438 source: &AccountId,
439 dest: &AccountId,
440 amount: Self::Balance,
441 precision: Precision,
442 expendability: Preservation,
443 force: Fortitude,
444 ) -> Result<Self::Balance, DispatchError> {
445 ensure!(Self::hold_available(asset.clone(), reason, dest), TokenError::CannotCreateHold);
446 ensure!(
447 Self::can_deposit(asset.clone(), dest, amount, Extant) == Success,
448 TokenError::CannotCreate
449 );
450 let actual =
451 Self::decrease_balance(asset.clone(), source, amount, precision, expendability, force)?;
452 Self::increase_balance_on_hold(asset.clone(), reason, dest, actual, precision)?;
453 Self::done_transfer_on_hold(asset, reason, source, dest, actual);
454 Ok(actual)
455 }
456
457 fn done_hold(
458 _asset: Self::AssetId,
459 _reason: &Self::Reason,
460 _who: &AccountId,
461 _amount: Self::Balance,
462 ) {
463 }
464 fn done_release(
465 _asset: Self::AssetId,
466 _reason: &Self::Reason,
467 _who: &AccountId,
468 _amount: Self::Balance,
469 ) {
470 }
471 fn done_burn_held(
472 _asset: Self::AssetId,
473 _reason: &Self::Reason,
474 _who: &AccountId,
475 _amount: Self::Balance,
476 ) {
477 }
478 fn done_transfer_on_hold(
479 _asset: Self::AssetId,
480 _reason: &Self::Reason,
481 _source: &AccountId,
482 _dest: &AccountId,
483 _amount: Self::Balance,
484 ) {
485 }
486 fn done_transfer_and_hold(
487 _asset: Self::AssetId,
488 _reason: &Self::Reason,
489 _source: &AccountId,
490 _dest: &AccountId,
491 _transferred: Self::Balance,
492 ) {
493 }
494}