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