referrerpolicy=no-referrer-when-downgrade

frame_support/traits/tokens/fungible/conformance_tests/regular/
balanced.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
18use crate::traits::{
19	fungible::{Balanced, Inspect},
20	tokens::{imbalance::Imbalance as ImbalanceT, Fortitude, Precision, Preservation},
21};
22use core::fmt::Debug;
23use frame_support::traits::tokens::fungible::imbalance::{Credit, Debt};
24use sp_arithmetic::{traits::AtLeast8BitUnsigned, ArithmeticError};
25use sp_runtime::{traits::Bounded, TokenError};
26
27/// Tests issuing and resolving [`Credit`] imbalances with [`Balanced::issue`] and
28/// [`Balanced::resolve`].
29pub fn issue_and_resolve_credit<T, AccountId>()
30where
31	T: Balanced<AccountId>,
32	<T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug,
33	AccountId: AtLeast8BitUnsigned,
34{
35	let account = AccountId::from(0);
36	assert_eq!(T::total_issuance(), 0.into());
37	assert_eq!(T::balance(&account), 0.into());
38
39	// Account that doesn't exist yet can't be credited below the minimum balance
40	let credit: Credit<AccountId, T> = T::issue(T::minimum_balance() - 1.into());
41	// issue temporarily increases total issuance
42	assert_eq!(T::total_issuance(), credit.peek());
43	match T::resolve(&account, credit) {
44		Ok(_) => panic!("Balanced::resolve should have failed"),
45		Err(c) => assert_eq!(c.peek(), T::minimum_balance() - 1.into()),
46	};
47	// Credit was unused and dropped from total issuance
48	assert_eq!(T::total_issuance(), 0.into());
49	assert_eq!(T::balance(&account), 0.into());
50
51	// Credit account with minimum balance
52	let credit: Credit<AccountId, T> = T::issue(T::minimum_balance());
53	match T::resolve(&account, credit) {
54		Ok(()) => {},
55		Err(_) => panic!("resolve failed"),
56	};
57	assert_eq!(T::total_issuance(), T::minimum_balance());
58	assert_eq!(T::balance(&account), T::minimum_balance());
59
60	// Now that account has been created, it can be credited with an amount below the minimum
61	// balance.
62	let total_issuance_before = T::total_issuance();
63	let balance_before = T::balance(&account);
64	let amount = T::minimum_balance() - 1.into();
65	let credit: Credit<AccountId, T> = T::issue(amount);
66	match T::resolve(&account, credit) {
67		Ok(()) => {},
68		Err(_) => panic!("resolve failed"),
69	};
70	assert_eq!(T::total_issuance(), total_issuance_before + amount);
71	assert_eq!(T::balance(&account), balance_before + amount);
72
73	// Unhandled issuance is dropped from total issuance
74	// `let _ = ...` immediately drops the issuance, so everything should be unchanged when
75	// logic gets to the assertions.
76	let total_issuance_before = T::total_issuance();
77	let balance_before = T::balance(&account);
78	let _ = T::issue(5.into());
79	assert_eq!(T::total_issuance(), total_issuance_before);
80	assert_eq!(T::balance(&account), balance_before);
81}
82
83/// Tests issuing and resolving [`Debt`] imbalances with [`Balanced::rescind`] and
84/// [`Balanced::settle`].
85pub fn rescind_and_settle_debt<T, AccountId>()
86where
87	T: Balanced<AccountId>,
88	<T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug,
89	AccountId: AtLeast8BitUnsigned,
90{
91	// Credit account with some balance
92	let account = AccountId::from(0);
93	let initial_bal = T::minimum_balance() + 10.into();
94	let credit = T::issue(initial_bal);
95	match T::resolve(&account, credit) {
96		Ok(()) => {},
97		Err(_) => panic!("resolve failed"),
98	};
99	assert_eq!(T::total_issuance(), initial_bal);
100	assert_eq!(T::balance(&account), initial_bal);
101
102	// Rescind some balance
103	let rescind_amount = 2.into();
104	let debt: Debt<AccountId, T> = T::rescind(rescind_amount);
105	assert_eq!(debt.peek(), rescind_amount);
106	match T::settle(&account, debt, Preservation::Expendable) {
107		Ok(c) => {
108			// We settled the full debt and account was not dusted, so there is no left over
109			// credit.
110			assert_eq!(c.peek(), 0.into());
111		},
112		Err(_) => panic!("settle failed"),
113	};
114	assert_eq!(T::total_issuance(), initial_bal - rescind_amount);
115	assert_eq!(T::balance(&account), initial_bal - rescind_amount);
116
117	// Unhandled debt is added from total issuance
118	// `let _ = ...` immediately drops the debt, so everything should be unchanged when
119	// logic gets to the assertions.
120	let _ = T::rescind(T::minimum_balance());
121	assert_eq!(T::total_issuance(), initial_bal - rescind_amount);
122	assert_eq!(T::balance(&account), initial_bal - rescind_amount);
123
124	// Preservation::Preserve will not allow the account to be dusted on settle
125	let balance_before = T::balance(&account);
126	let total_issuance_before = T::total_issuance();
127	let rescind_amount = balance_before - T::minimum_balance() + 1.into();
128	let debt: Debt<AccountId, T> = T::rescind(rescind_amount);
129	assert_eq!(debt.peek(), rescind_amount);
130	// The new debt is temporarily removed from total_issuance
131	assert_eq!(T::total_issuance(), total_issuance_before - debt.peek().into());
132	match T::settle(&account, debt, Preservation::Preserve) {
133		Ok(_) => panic!("Balanced::settle should have failed"),
134		Err(d) => assert_eq!(d.peek(), rescind_amount),
135	};
136	// The debt is added back to total_issuance because it was dropped, leaving the operation a
137	// noop.
138	assert_eq!(T::total_issuance(), total_issuance_before);
139	assert_eq!(T::balance(&account), balance_before);
140
141	// Preservation::Expendable allows the account to be dusted on settle
142	let debt: Debt<AccountId, T> = T::rescind(rescind_amount);
143	match T::settle(&account, debt, Preservation::Expendable) {
144		Ok(c) => {
145			// Dusting happens internally, there is no left over credit.
146			assert_eq!(c.peek(), 0.into());
147		},
148		Err(_) => panic!("settle failed"),
149	};
150	// The account is dusted and debt dropped from total_issuance
151	assert_eq!(T::total_issuance(), 0.into());
152	assert_eq!(T::balance(&account), 0.into());
153}
154
155/// Tests [`Balanced::deposit`].
156pub fn deposit<T, AccountId>()
157where
158	T: Balanced<AccountId>,
159	<T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug,
160	AccountId: AtLeast8BitUnsigned,
161{
162	// Cannot deposit < minimum balance into non-existent account
163	let account = AccountId::from(0);
164	let amount = T::minimum_balance() - 1.into();
165	match T::deposit(&account, amount, Precision::Exact) {
166		Ok(_) => panic!("Balanced::deposit should have failed"),
167		Err(e) => assert_eq!(e, TokenError::BelowMinimum.into()),
168	};
169	assert_eq!(T::total_issuance(), 0.into());
170	assert_eq!(T::balance(&account), 0.into());
171
172	// Can deposit minimum balance into non-existent account
173	let amount = T::minimum_balance();
174	match T::deposit(&account, amount, Precision::Exact) {
175		Ok(d) => assert_eq!(d.peek(), amount),
176		Err(_) => panic!("Balanced::deposit failed"),
177	};
178	assert_eq!(T::total_issuance(), amount);
179	assert_eq!(T::balance(&account), amount);
180
181	// Depositing amount that would overflow when Precision::Exact fails and is a noop
182	let amount = T::Balance::max_value();
183	let balance_before = T::balance(&account);
184	let total_issuance_before = T::total_issuance();
185	match T::deposit(&account, amount, Precision::Exact) {
186		Ok(_) => panic!("Balanced::deposit should have failed"),
187		Err(e) => assert_eq!(e, ArithmeticError::Overflow.into()),
188	};
189	assert_eq!(T::total_issuance(), total_issuance_before);
190	assert_eq!(T::balance(&account), balance_before);
191
192	// Depositing amount that would overflow when Precision::BestEffort saturates
193	match T::deposit(&account, amount, Precision::BestEffort) {
194		Ok(d) => assert_eq!(d.peek(), T::Balance::max_value() - balance_before),
195		Err(_) => panic!("Balanced::deposit failed"),
196	};
197	assert_eq!(T::total_issuance(), T::Balance::max_value());
198	assert_eq!(T::balance(&account), T::Balance::max_value());
199}
200
201/// Tests [`Balanced::withdraw`].
202pub fn withdraw<T, AccountId>()
203where
204	T: Balanced<AccountId>,
205	<T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug,
206	AccountId: AtLeast8BitUnsigned,
207{
208	let account = AccountId::from(0);
209
210	// Init an account with some balance
211	let initial_balance = T::minimum_balance() + 10.into();
212	match T::deposit(&account, initial_balance, Precision::Exact) {
213		Ok(_) => {},
214		Err(_) => panic!("Balanced::deposit failed"),
215	};
216	assert_eq!(T::total_issuance(), initial_balance);
217	assert_eq!(T::balance(&account), initial_balance);
218
219	// Withdrawing an amount smaller than the balance works when Precision::Exact
220	let amount = 1.into();
221	match T::withdraw(
222		&account,
223		amount,
224		Precision::Exact,
225		Preservation::Expendable,
226		Fortitude::Polite,
227	) {
228		Ok(c) => assert_eq!(c.peek(), amount),
229		Err(_) => panic!("withdraw failed"),
230	};
231	assert_eq!(T::total_issuance(), initial_balance - amount);
232	assert_eq!(T::balance(&account), initial_balance - amount);
233
234	// Withdrawing an amount greater than the balance fails when Precision::Exact
235	let balance_before = T::balance(&account);
236	let amount = balance_before + 1.into();
237	match T::withdraw(
238		&account,
239		amount,
240		Precision::Exact,
241		Preservation::Expendable,
242		Fortitude::Polite,
243	) {
244		Ok(_) => panic!("should have failed"),
245		Err(e) => assert_eq!(e, TokenError::FundsUnavailable.into()),
246	};
247	assert_eq!(T::total_issuance(), balance_before);
248	assert_eq!(T::balance(&account), balance_before);
249
250	// Withdrawing an amount greater than the balance works when Precision::BestEffort
251	let balance_before = T::balance(&account);
252	let amount = balance_before + 1.into();
253	match T::withdraw(
254		&account,
255		amount,
256		Precision::BestEffort,
257		Preservation::Expendable,
258		Fortitude::Polite,
259	) {
260		Ok(c) => assert_eq!(c.peek(), balance_before),
261		Err(_) => panic!("withdraw failed"),
262	};
263	assert_eq!(T::total_issuance(), 0.into());
264	assert_eq!(T::balance(&account), 0.into());
265}
266
267/// Tests [`Balanced::pair`].
268pub fn pair<T, AccountId>()
269where
270	T: Balanced<AccountId>,
271	<T as Inspect<AccountId>>::Balance: AtLeast8BitUnsigned + Debug,
272	AccountId: AtLeast8BitUnsigned,
273{
274	T::set_total_issuance(50.into());
275
276	// Pair zero balance works
277	let (credit, debt) = T::pair(0.into()).unwrap();
278	assert_eq!(debt.peek(), 0.into());
279	assert_eq!(credit.peek(), 0.into());
280
281	// Pair with non-zero balance: the credit and debt cancel each other out
282	let balance = 10.into();
283	let (credit, debt) = T::pair(balance).unwrap();
284	assert_eq!(credit.peek(), balance);
285	assert_eq!(debt.peek(), balance);
286
287	// Creating a pair that could increase total_issuance beyond the max value returns an error
288	let max_value = T::Balance::max_value();
289	let distance_from_max_value = 5.into();
290	T::set_total_issuance(max_value - distance_from_max_value);
291	T::pair(distance_from_max_value + 5.into()).unwrap_err();
292}