referrerpolicy=no-referrer-when-downgrade

pallet_revive/
deposit_payment.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//! Storage deposit payment backend.
19//!
20//! Storage deposits can be backed by the native currency or by PGAS.
21//! Runtimes without PGAS leave the default `()` binding,
22//! which always uses the native currency.
23use crate::{
24	BalanceOf, Config, FreezeReason, HoldReason, LOG_TARGET, NativeDepositOf,
25	evm::fees::InfoT as FeeInfo,
26};
27use core::marker::PhantomData;
28use frame_support::traits::{
29	Get,
30	fungible::{
31		Balanced as _, Inspect as _, InspectHold as _, Mutate as _, MutateHold as _,
32		Unbalanced as _,
33	},
34	tokens::{
35		DepositConsequence, Fortitude, Precision, Preservation, Provenance, Restriction, fungibles,
36	},
37};
38use sp_runtime::{
39	DispatchError, DispatchResult, Perbill, TokenError,
40	traits::{Saturating, Zero},
41};
42
43mod sealed {
44	use super::PGasDeposit;
45
46	pub trait Sealed {}
47
48	impl Sealed for () {}
49
50	impl<T, Mutator, Holder, Freezer, Id, RefundPercent> Sealed
51		for PGasDeposit<T, Mutator, Holder, Freezer, Id, RefundPercent>
52	{
53	}
54}
55
56/// Identifies where the native side of a storage deposit lives.
57///
58/// Charges treat it as the source; refunds treat it as the destination.
59pub enum Funds<'a, AccountId> {
60	/// The free balance of the given account.
61	Balance(&'a AccountId),
62	/// The tx fee hold.
63	TxFee(&'a AccountId),
64}
65
66/// Payment backend used to charge storage deposits.
67pub trait Deposit<T: Config>: sealed::Sealed {
68	/// Whether this backend supports PGAS.
69	///
70	/// When `false`, the v4 multi-block migration's PGAS-related phases (steps 1 and 2)
71	/// are no-ops, since there is no PGAS asset to migrate native deposits over to.
72	const SUPPORTS_PGAS: bool;
73
74	/// Mint each backend's existential deposit into `contract`.
75	///
76	/// Used by [`crate::exec`] when bringing a new contract account into existence.
77	fn init_contract(contract: &T::AccountId) -> DispatchResult;
78
79	/// Tear down the per-backend balance state that [`Self::init_contract`] set up.
80	///
81	/// Used by [`crate::exec::Stack::do_terminate`] when destroying a contract.
82	fn destroy_contract(contract: &T::AccountId) -> DispatchResult;
83
84	/// Charge `amount` from `src` to `to` and place it on hold under `reason`.
85	///
86	/// # Parameters
87	/// - `reason`: hold reason to place the charge under.
88	/// - `src`: source of the charge. See [`Funds`].
89	/// - `to`: account on which the hold is placed.
90	/// - `amount`: amount to charge.
91	fn charge_and_hold(
92		reason: HoldReason,
93		src: Funds<T::AccountId>,
94		to: &T::AccountId,
95		amount: BalanceOf<T>,
96	) -> DispatchResult;
97
98	/// Refund `amount` of held funds from contract `from`.
99	///
100	/// # Parameters
101	/// - `reason`: hold reason the funds were placed under.
102	/// - `from`: contract whose hold is being released.
103	/// - `dst`: destination of the refund. See [`Funds`]. Also the attribution key used to cap the
104	///   native portion via [`NativeDepositOf`].
105	/// - `amount`: amount to refund.
106	fn refund_on_hold(
107		reason: HoldReason,
108		from: &T::AccountId,
109		dst: Funds<T::AccountId>,
110		amount: BalanceOf<T>,
111	) -> DispatchResult;
112
113	/// Total amount held for `who` under `reason`.
114	///
115	/// # Parameters
116	/// - `reason`: hold reason to query.
117	/// - `who`: account whose held balance is returned.
118	fn total_on_hold(reason: HoldReason, who: &T::AccountId) -> BalanceOf<T>;
119
120	/// Refund the storage-deposit funds held on `from` to `dst`, ignoring the per-contributor
121	/// caps that govern partial refunds. Used at contract termination.
122	///
123	/// Only as much as the freezes on `from` allow is released: a held amount that a freeze
124	/// needs to stay on the account is left on hold instead of failing the refund.
125	///
126	/// Returns the amount released, so the storage meter can finalise its deposit accounting.
127	///
128	/// # Parameters
129	/// - `from`: contract whose hold is being released.
130	/// - `dst`: destination of the refund. See [`Funds`].
131	fn refund_all(
132		from: &T::AccountId,
133		dst: Funds<T::AccountId>,
134	) -> Result<BalanceOf<T>, DispatchError>;
135
136	/// Burn the native currency held on `contract` under `reason` and replace it with the same
137	/// amount of PGAS, minted into `contract` and placed on hold under the same reason.
138	///
139	/// Only used by the v4 multi-block migration (see [`crate::migrations::v4`]) to move
140	/// pre-existing native storage deposits over to PGAS. Not part of the regular charge/refund
141	/// flow.
142	///
143	/// # Parameters
144	/// - `reason`: hold reason whose balance is being migrated.
145	/// - `contract`: account holding the funds to migrate.
146	/// - `amount`: amount to migrate from native to PGAS.
147	fn migrate_native_to_pgas(
148		reason: HoldReason,
149		contract: &T::AccountId,
150		amount: BalanceOf<T>,
151	) -> DispatchResult;
152}
153
154/// Default backend: every storage deposit charge goes through the native currency.
155impl<T: Config> Deposit<T> for () {
156	const SUPPORTS_PGAS: bool = false;
157
158	/// The native ED is freshly minted and immediately
159	/// [`deactivated`](frame_support::traits::fungible::Unbalanced::deactivate) so that
160	/// active issuance, and therefore opengov conviction, inflation accounting, etc., is
161	/// undisturbed by contract creation. The contract holds a system consumer for as long as it
162	/// exists, so this minted ED is not extractable: the account cannot be reaped.
163	fn init_contract(to: &T::AccountId) -> DispatchResult {
164		let ed = T::Currency::minimum_balance();
165		T::Currency::mint_into(to, ed)?;
166		T::Currency::deactivate(ed);
167		Ok(())
168	}
169
170	fn destroy_contract(contract: &T::AccountId) -> DispatchResult {
171		let ed = T::Currency::minimum_balance();
172		T::Currency::burn_from(
173			contract,
174			ed,
175			Preservation::Expendable,
176			Precision::Exact,
177			Fortitude::Polite,
178		)?;
179		// Pair with [`Self::init_contract`]: shrink the inactive pool first so the burn only
180		// nets out the mint, rather than also taking an ED off the active issuance.
181		T::Currency::reactivate(ed);
182		Ok(())
183	}
184
185	fn charge_and_hold(
186		reason: HoldReason,
187		src: Funds<T::AccountId>,
188		to: &T::AccountId,
189		amount: BalanceOf<T>,
190	) -> DispatchResult {
191		match src {
192			Funds::Balance(from) => {
193				T::Currency::transfer_and_hold(
194					&reason.into(),
195					from,
196					to,
197					amount,
198					Precision::Exact,
199					Preservation::Preserve,
200					Fortitude::Polite,
201				)?;
202			},
203			Funds::TxFee(_) => {
204				let credit = T::FeeInfo::withdraw_txfee(amount)
205					.ok_or(DispatchError::Token(TokenError::FundsUnavailable))?;
206				T::Currency::resolve(to, credit)
207					.map_err(|_| DispatchError::Token(TokenError::FundsUnavailable))?;
208				T::Currency::hold(&reason.into(), to, amount)?;
209			},
210		}
211		Ok(())
212	}
213
214	fn refund_on_hold(
215		reason: HoldReason,
216		from: &T::AccountId,
217		dst: Funds<T::AccountId>,
218		amount: BalanceOf<T>,
219	) -> DispatchResult {
220		match dst {
221			Funds::Balance(to) => {
222				T::Currency::transfer_on_hold(
223					&reason.into(),
224					from,
225					to,
226					amount,
227					Precision::Exact,
228					Restriction::Free,
229					Fortitude::Polite,
230				)?;
231			},
232			Funds::TxFee(_) => {
233				let released =
234					T::Currency::release(&reason.into(), from, amount, Precision::Exact)?;
235				let credit = T::Currency::withdraw(
236					from,
237					released,
238					Precision::Exact,
239					Preservation::Preserve,
240					Fortitude::Polite,
241				)?;
242				T::FeeInfo::deposit_txfee(credit);
243			},
244		}
245		Ok(())
246	}
247
248	fn total_on_hold(reason: HoldReason, who: &T::AccountId) -> BalanceOf<T> {
249		T::Currency::balance_on_hold(&reason.into(), who)
250	}
251
252	fn refund_all(
253		from: &T::AccountId,
254		dst: Funds<T::AccountId>,
255	) -> Result<BalanceOf<T>, DispatchError> {
256		let reason = HoldReason::StorageDepositReserve;
257		// Holds count towards the freeze, so the part of the freeze that the free balance does
258		// not cover has to stay on hold.
259		let amount = T::Currency::balance_on_hold(&reason.into(), from)
260			.min(T::Currency::reducible_total_balance_on_hold(from, Fortitude::Polite));
261		if !amount.is_zero() {
262			<Self as Deposit<T>>::refund_on_hold(reason, from, dst, amount)?;
263		}
264		Ok(amount)
265	}
266
267	fn migrate_native_to_pgas(
268		_reason: HoldReason,
269		_contract: &T::AccountId,
270		_amount: BalanceOf<T>,
271	) -> DispatchResult {
272		Ok(())
273	}
274}
275
276/// PGAS-backed payment backend. Charges prefer PGAS and fall back to the native currency;
277/// refunds return native first (capped by [`NativeDepositOf`]) then `RefundPercent` of the
278/// PGAS portion, burning the rest.
279pub struct PGasDeposit<T, Mutator, Holder, Freezer, Id, RefundPercent>(
280	PhantomData<(T, Mutator, Holder, Freezer, Id, RefundPercent)>,
281);
282
283impl<T, Mutator, Holder, Freezer, Id, RefundPercent> Deposit<T>
284	for PGasDeposit<T, Mutator, Holder, Freezer, Id, RefundPercent>
285where
286	T: Config,
287	Mutator: fungibles::Mutate<T::AccountId, Balance = BalanceOf<T>>,
288	Holder: fungibles::MutateHold<
289			T::AccountId,
290			Balance = BalanceOf<T>,
291			AssetId = <Mutator as fungibles::Inspect<T::AccountId>>::AssetId,
292		>,
293	<Holder as fungibles::InspectHold<T::AccountId>>::Reason: From<HoldReason>,
294	Freezer: fungibles::freeze::Mutate<
295			T::AccountId,
296			Balance = BalanceOf<T>,
297			AssetId = <Mutator as fungibles::Inspect<T::AccountId>>::AssetId,
298		>,
299	<Freezer as fungibles::freeze::Inspect<T::AccountId>>::Id: From<FreezeReason>,
300	Id: Get<<Mutator as fungibles::Inspect<T::AccountId>>::AssetId>,
301	RefundPercent: Get<Perbill>,
302{
303	const SUPPORTS_PGAS: bool = true;
304
305	/// Mints one native ED and one PGAS ED into `to`, so the account can subsequently receive
306	/// deposits in either asset without tripping existential-deposit checks. The minted native
307	/// ED is [`deactivated`](frame_support::traits::fungible::Unbalanced::deactivate) so it stays
308	/// outside active issuance. The minted PGAS ED is frozen under
309	/// [`FreezeReason::PGasMinBalance`] so the contract cannot transfer or burn it:
310	/// pallet-assets' `reducible_balance` treats any frozen amount as untouchable, regardless
311	/// of `Preservation` / `Fortitude`.
312	fn init_contract(to: &T::AccountId) -> DispatchResult {
313		<() as Deposit<T>>::init_contract(to)?;
314		let pgas_ed = <Mutator as fungibles::Inspect<T::AccountId>>::minimum_balance(Id::get());
315		<Mutator as fungibles::Mutate<T::AccountId>>::mint_into(Id::get(), to, pgas_ed)?;
316		<Freezer as fungibles::freeze::Mutate<T::AccountId>>::set_freeze(
317			Id::get(),
318			&FreezeReason::PGasMinBalance.into(),
319			to,
320			pgas_ed,
321		)?;
322		Ok(())
323	}
324
325	/// Thaws and burns the PGAS ED frozen by [`Self::init_contract`], plus the native ED.
326	fn destroy_contract(contract: &T::AccountId) -> DispatchResult {
327		<() as Deposit<T>>::destroy_contract(contract)?;
328
329		<Freezer as fungibles::freeze::Mutate<T::AccountId>>::thaw(
330			Id::get(),
331			&FreezeReason::PGasMinBalance.into(),
332			contract,
333		)?;
334		let ed = <Mutator as fungibles::Inspect<T::AccountId>>::balance(Id::get(), contract);
335		<Mutator as fungibles::Mutate<T::AccountId>>::burn_from(
336			Id::get(),
337			contract,
338			ed,
339			Preservation::Expendable,
340			Precision::BestEffort,
341			Fortitude::Polite,
342		)?;
343
344		Ok(())
345	}
346
347	/// Charges a deposit and places it on hold.
348	///
349	/// Uses PGAS when the payer has enough reducible PGAS, otherwise falls back to the native
350	/// currency and records the contribution in [`NativeDepositOf`] so refunds return native up
351	/// to the contributed amount. The native fallback honours [`Funds::TxFee`] by withdrawing
352	/// from the txfee pool instead of the payer's free balance.
353	fn charge_and_hold(
354		reason: HoldReason,
355		src: Funds<T::AccountId>,
356		to: &T::AccountId,
357		amount: BalanceOf<T>,
358	) -> DispatchResult {
359		let from = match &src {
360			Funds::Balance(from) | Funds::TxFee(from) => *from,
361		};
362
363		if Self::pgas_reducible_balance(from) >= amount {
364			<Holder as fungibles::MutateHold<T::AccountId>>::transfer_and_hold(
365				Id::get(),
366				&reason.into(),
367				from,
368				to,
369				amount,
370				Precision::Exact,
371				Preservation::Expendable,
372				Fortitude::Polite,
373			)?;
374		} else {
375			<() as Deposit<T>>::charge_and_hold(reason, src, to, amount)?;
376			Self::record_native_deposit(from, to, amount);
377		}
378
379		Ok(())
380	}
381
382	/// Refunds native currency first (capped by [`NativeDepositOf`]); any shortfall is taken from
383	/// PGAS with `RefundPercent` refunded and the rest burned. When `dst` is [`Funds::TxFee`],
384	/// the native portion is routed into the tx fee pool instead of the embedded account's
385	/// free balance. The PGAS portion (if any) is always settled to the account embedded in
386	/// `dst`.
387	///
388	/// Note: callers must run inside a storage layer so partial state rolls back on error.
389	fn refund_on_hold(
390		reason: HoldReason,
391		from: &T::AccountId,
392		dst: Funds<T::AccountId>,
393		amount: BalanceOf<T>,
394	) -> DispatchResult {
395		let to = match &dst {
396			Funds::Balance(to) | Funds::TxFee(to) => *to,
397		};
398		let contribution = NativeDepositOf::<T>::get(from, to);
399		let native_requested = amount.min(contribution);
400
401		let native_refunded = if !native_requested.is_zero() {
402			<() as Deposit<T>>::refund_on_hold(reason, from, dst, native_requested)?;
403			let new_val = contribution.saturating_sub(native_requested);
404			if new_val.is_zero() {
405				NativeDepositOf::<T>::remove(from, to);
406			} else {
407				NativeDepositOf::<T>::insert(from, to, new_val);
408			}
409			native_requested
410		} else {
411			BalanceOf::<T>::zero()
412		};
413
414		let pgas_needed = amount.saturating_sub(native_refunded);
415		Self::settle_pgas_refund(reason, from, to, pgas_needed)?;
416		Ok(())
417	}
418
419	/// Sum of `who`'s native and PGAS balances on hold for `reason`.
420	fn total_on_hold(reason: HoldReason, who: &T::AccountId) -> BalanceOf<T> {
421		let native_held = <() as Deposit<T>>::total_on_hold(reason, who);
422		let pgas_held = Self::pgas_on_hold(reason, who);
423		native_held.saturating_add(pgas_held)
424	}
425
426	/// Refunds the native hold to `dst` as far as the freezes on `from` allow, ignoring the
427	/// per-contributor cap, then settles the PGAS hold via [`Self::settle_pgas_refund`]
428	/// (refunding `RefundPercent` to `dst` and burning the rest). The native cap only makes
429	/// sense for partial refunds on a live contract; at termination there is one recipient and
430	/// the contract is gone.
431	///
432	/// Note: callers must run inside a storage layer so partial state rolls back on error.
433	fn refund_all(
434		from: &T::AccountId,
435		dst: Funds<T::AccountId>,
436	) -> Result<BalanceOf<T>, DispatchError> {
437		let to = match &dst {
438			Funds::Balance(to) | Funds::TxFee(to) => *to,
439		};
440		let native = <() as Deposit<T>>::refund_all(from, dst)?;
441		let reason = HoldReason::StorageDepositReserve;
442
443		let pgas = Self::pgas_on_hold(reason, from);
444		let pgas = Self::settle_pgas_refund(reason, from, to, pgas)?;
445		Ok(native.saturating_add(pgas))
446	}
447
448	/// Bring a pre-existing contract up to the post-[`Self::init_contract`] invariant:
449	/// mint and freeze the PGAS ED if missing, then burn the native hold under `reason` and
450	/// replace it with the same amount of PGAS held on `contract`.
451	fn migrate_native_to_pgas(
452		reason: HoldReason,
453		contract: &T::AccountId,
454		amount: BalanceOf<T>,
455	) -> DispatchResult {
456		let pgas_ed = <Mutator as fungibles::Inspect<T::AccountId>>::minimum_balance(Id::get());
457		let freeze_id = FreezeReason::PGasMinBalance.into();
458		if <Freezer as fungibles::freeze::Inspect<T::AccountId>>::balance_frozen(
459			Id::get(),
460			&freeze_id,
461			contract,
462		) < pgas_ed
463		{
464			if <Mutator as fungibles::Inspect<T::AccountId>>::balance(Id::get(), contract) < pgas_ed
465			{
466				<Mutator as fungibles::Mutate<T::AccountId>>::mint_into(
467					Id::get(),
468					contract,
469					pgas_ed,
470				)
471				.inspect_err(|err| {
472					log::debug!(
473						target: LOG_TARGET,
474						"Failed to mint PGAS ED for contract: {err:?}",
475					)
476				})?;
477			}
478			<Freezer as fungibles::freeze::Mutate<T::AccountId>>::set_freeze(
479				Id::get(),
480				&freeze_id,
481				contract,
482				pgas_ed,
483			)
484			.inspect_err(|err| {
485				log::debug!(
486					target: LOG_TARGET,
487					"Failed to freeze PGAS ED for contract: {err:?}",
488				)
489			})?;
490		}
491
492		if amount.is_zero() {
493			return Ok(());
494		}
495
496		T::Currency::burn_held(
497			&reason.into(),
498			contract,
499			amount,
500			Precision::Exact,
501			Fortitude::Polite,
502		)
503		.inspect_err(
504			|err| log::debug!(target: LOG_TARGET, "Failed to burn held amount {amount:?}: {err:?}"),
505		)?;
506
507		<Mutator as fungibles::Mutate<T::AccountId>>::mint_into(Id::get(), contract, amount)
508			.inspect_err(
509				|err| log::debug!(target: LOG_TARGET, "Failed to mint to {contract:?} amount: {amount:?}: {err:?}"),
510			)?;
511
512		<Holder as fungibles::MutateHold<T::AccountId>>::hold(
513			Id::get(),
514			&reason.into(),
515			contract,
516			amount,
517		)
518		.inspect_err(
519			|err| log::debug!(target: LOG_TARGET, "Failed to hold amount in {contract:?}: {amount:?}: {err:?}"),
520		)?;
521		Ok(())
522	}
523}
524
525impl<T, Mutator, Holder, Freezer, Id, RefundPercent>
526	PGasDeposit<T, Mutator, Holder, Freezer, Id, RefundPercent>
527where
528	T: Config,
529	Mutator: fungibles::Mutate<T::AccountId, Balance = BalanceOf<T>>,
530	Holder: fungibles::MutateHold<
531			T::AccountId,
532			Balance = BalanceOf<T>,
533			AssetId = <Mutator as fungibles::Inspect<T::AccountId>>::AssetId,
534		>,
535	<Holder as fungibles::InspectHold<T::AccountId>>::Reason: From<HoldReason>,
536	Freezer: fungibles::freeze::Mutate<
537			T::AccountId,
538			Balance = BalanceOf<T>,
539			AssetId = <Mutator as fungibles::Inspect<T::AccountId>>::AssetId,
540		>,
541	<Freezer as fungibles::freeze::Inspect<T::AccountId>>::Id: From<FreezeReason>,
542	Id: Get<<Mutator as fungibles::Inspect<T::AccountId>>::AssetId>,
543	RefundPercent: Get<Perbill>,
544{
545	fn pgas_reducible_balance(who: &T::AccountId) -> BalanceOf<T> {
546		<Mutator as fungibles::Inspect<T::AccountId>>::reducible_balance(
547			Id::get(),
548			who,
549			Preservation::Expendable,
550			Fortitude::Polite,
551		)
552	}
553
554	fn pgas_on_hold(reason: HoldReason, who: &T::AccountId) -> BalanceOf<T> {
555		<Holder as fungibles::InspectHold<T::AccountId>>::balance_on_hold(
556			Id::get(),
557			&reason.into(),
558			who,
559		)
560	}
561
562	/// Record that user `from` contributed `amount` in native balance to contract `to`.
563	/// Read by [`Self::refund_on_hold`] to cap the native portion of refunds.
564	fn record_native_deposit(from: &T::AccountId, to: &T::AccountId, amount: BalanceOf<T>) {
565		NativeDepositOf::<T>::mutate(to, from, |entitlement| {
566			*entitlement = entitlement.saturating_add(amount);
567		});
568	}
569
570	/// Refund `RefundPercent` of `amount` from `from`'s PGAS hold to `to`'s free balance and
571	/// burn the rest. Returns the amount actually transferred to `to` (excludes the burned
572	/// portion).
573	///
574	/// If crediting `to` would violate its existential deposit (e.g. `to` has no asset
575	/// account and the refund would create one below ED), the refund portion is folded into
576	/// the burn rather than aborting the whole refund.
577	///
578	/// `amount` is capped at the PGAS actually held by `from`: when a recipient with no
579	/// [`NativeDepositOf`] credit triggers a refund on a contract whose deposit was paid in
580	/// native, the call settles whatever PGAS is actually held instead of reverting.
581	fn settle_pgas_refund(
582		reason: HoldReason,
583		from: &T::AccountId,
584		to: &T::AccountId,
585		amount: BalanceOf<T>,
586	) -> Result<BalanceOf<T>, DispatchError> {
587		if amount.is_zero() {
588			return Ok(BalanceOf::<T>::zero());
589		}
590		// Cap the amount we settle at what's actually held in PGAS. A refund recipient with
591		// no `NativeDepositOf` credit on a contract whose deposit was paid in native would
592		// otherwise route the full amount through PGAS and revert on `Precision::Exact`.
593		let pgas_held = Self::pgas_on_hold(reason, from);
594		let amount = amount.min(pgas_held);
595		if amount.is_zero() {
596			return Ok(BalanceOf::<T>::zero());
597		}
598		let refund = RefundPercent::get().mul_floor(amount);
599		let mut burn = amount.saturating_sub(refund);
600		let mut refunded = BalanceOf::<T>::zero();
601
602		if !refund.is_zero() {
603			let can_credit = matches!(
604				<Mutator as fungibles::Inspect<T::AccountId>>::can_deposit(
605					Id::get(),
606					to,
607					refund,
608					Provenance::Extant,
609				),
610				DepositConsequence::Success
611			);
612			if can_credit {
613				refunded = <Holder as fungibles::MutateHold<T::AccountId>>::transfer_on_hold(
614					Id::get(),
615					&reason.into(),
616					from,
617					to,
618					refund,
619					Precision::BestEffort,
620					Restriction::Free,
621					Fortitude::Polite,
622				)?;
623			} else {
624				burn = burn.saturating_add(refund);
625			}
626		}
627
628		if !burn.is_zero() {
629			<Holder as fungibles::MutateHold<T::AccountId>>::burn_held(
630				Id::get(),
631				&reason.into(),
632				from,
633				burn,
634				Precision::Exact,
635				Fortitude::Polite,
636			)?;
637		}
638		Ok(refunded)
639	}
640}