use crate::{
BalanceOf, Config, FreezeReason, HoldReason, LOG_TARGET, NativeDepositOf,
evm::fees::InfoT as FeeInfo,
};
use core::marker::PhantomData;
use frame_support::traits::{
Get,
fungible::{
Balanced as _, Inspect as _, InspectHold as _, Mutate as _, MutateHold as _,
Unbalanced as _,
},
tokens::{
DepositConsequence, Fortitude, Precision, Preservation, Provenance, Restriction, fungibles,
},
};
use sp_runtime::{
DispatchError, DispatchResult, Perbill, TokenError,
traits::{Saturating, Zero},
};
mod sealed {
use super::PGasDeposit;
pub trait Sealed {}
impl Sealed for () {}
impl<T, Mutator, Holder, Freezer, Id, RefundPercent> Sealed
for PGasDeposit<T, Mutator, Holder, Freezer, Id, RefundPercent>
{
}
}
pub enum Funds<'a, AccountId> {
Balance(&'a AccountId),
TxFee(&'a AccountId),
}
pub trait Deposit<T: Config>: sealed::Sealed {
const SUPPORTS_PGAS: bool;
fn init_contract(contract: &T::AccountId) -> DispatchResult;
fn destroy_contract(contract: &T::AccountId) -> DispatchResult;
fn charge_and_hold(
reason: HoldReason,
src: Funds<T::AccountId>,
to: &T::AccountId,
amount: BalanceOf<T>,
) -> DispatchResult;
fn refund_on_hold(
reason: HoldReason,
from: &T::AccountId,
dst: Funds<T::AccountId>,
amount: BalanceOf<T>,
) -> DispatchResult;
fn total_on_hold(reason: HoldReason, who: &T::AccountId) -> BalanceOf<T>;
fn refund_all(
from: &T::AccountId,
dst: Funds<T::AccountId>,
) -> Result<BalanceOf<T>, DispatchError>;
fn migrate_native_to_pgas(
reason: HoldReason,
contract: &T::AccountId,
amount: BalanceOf<T>,
) -> DispatchResult;
}
impl<T: Config> Deposit<T> for () {
const SUPPORTS_PGAS: bool = false;
fn init_contract(to: &T::AccountId) -> DispatchResult {
let ed = T::Currency::minimum_balance();
T::Currency::mint_into(to, ed)?;
T::Currency::deactivate(ed);
Ok(())
}
fn destroy_contract(contract: &T::AccountId) -> DispatchResult {
let ed = T::Currency::minimum_balance();
T::Currency::burn_from(
contract,
ed,
Preservation::Expendable,
Precision::Exact,
Fortitude::Polite,
)?;
T::Currency::reactivate(ed);
Ok(())
}
fn charge_and_hold(
reason: HoldReason,
src: Funds<T::AccountId>,
to: &T::AccountId,
amount: BalanceOf<T>,
) -> DispatchResult {
match src {
Funds::Balance(from) => {
T::Currency::transfer_and_hold(
&reason.into(),
from,
to,
amount,
Precision::Exact,
Preservation::Preserve,
Fortitude::Polite,
)?;
},
Funds::TxFee(_) => {
let credit = T::FeeInfo::withdraw_txfee(amount)
.ok_or(DispatchError::Token(TokenError::FundsUnavailable))?;
T::Currency::resolve(to, credit)
.map_err(|_| DispatchError::Token(TokenError::FundsUnavailable))?;
T::Currency::hold(&reason.into(), to, amount)?;
},
}
Ok(())
}
fn refund_on_hold(
reason: HoldReason,
from: &T::AccountId,
dst: Funds<T::AccountId>,
amount: BalanceOf<T>,
) -> DispatchResult {
match dst {
Funds::Balance(to) => {
T::Currency::transfer_on_hold(
&reason.into(),
from,
to,
amount,
Precision::Exact,
Restriction::Free,
Fortitude::Polite,
)?;
},
Funds::TxFee(_) => {
let released =
T::Currency::release(&reason.into(), from, amount, Precision::Exact)?;
let credit = T::Currency::withdraw(
from,
released,
Precision::Exact,
Preservation::Preserve,
Fortitude::Polite,
)?;
T::FeeInfo::deposit_txfee(credit);
},
}
Ok(())
}
fn total_on_hold(reason: HoldReason, who: &T::AccountId) -> BalanceOf<T> {
T::Currency::balance_on_hold(&reason.into(), who)
}
fn refund_all(
from: &T::AccountId,
dst: Funds<T::AccountId>,
) -> Result<BalanceOf<T>, DispatchError> {
let reason = HoldReason::StorageDepositReserve;
let amount = T::Currency::balance_on_hold(&reason.into(), from);
if !amount.is_zero() {
<Self as Deposit<T>>::refund_on_hold(reason, from, dst, amount)?;
}
Ok(amount)
}
fn migrate_native_to_pgas(
_reason: HoldReason,
_contract: &T::AccountId,
_amount: BalanceOf<T>,
) -> DispatchResult {
Ok(())
}
}
pub struct PGasDeposit<T, Mutator, Holder, Freezer, Id, RefundPercent>(
PhantomData<(T, Mutator, Holder, Freezer, Id, RefundPercent)>,
);
impl<T, Mutator, Holder, Freezer, Id, RefundPercent> Deposit<T>
for PGasDeposit<T, Mutator, Holder, Freezer, Id, RefundPercent>
where
T: Config,
Mutator: fungibles::Mutate<T::AccountId, Balance = BalanceOf<T>>,
Holder: fungibles::MutateHold<
T::AccountId,
Balance = BalanceOf<T>,
AssetId = <Mutator as fungibles::Inspect<T::AccountId>>::AssetId,
>,
<Holder as fungibles::InspectHold<T::AccountId>>::Reason: From<HoldReason>,
Freezer: fungibles::freeze::Mutate<
T::AccountId,
Balance = BalanceOf<T>,
AssetId = <Mutator as fungibles::Inspect<T::AccountId>>::AssetId,
>,
<Freezer as fungibles::freeze::Inspect<T::AccountId>>::Id: From<FreezeReason>,
Id: Get<<Mutator as fungibles::Inspect<T::AccountId>>::AssetId>,
RefundPercent: Get<Perbill>,
{
const SUPPORTS_PGAS: bool = true;
fn init_contract(to: &T::AccountId) -> DispatchResult {
<() as Deposit<T>>::init_contract(to)?;
let pgas_ed = <Mutator as fungibles::Inspect<T::AccountId>>::minimum_balance(Id::get());
<Mutator as fungibles::Mutate<T::AccountId>>::mint_into(Id::get(), to, pgas_ed)?;
<Freezer as fungibles::freeze::Mutate<T::AccountId>>::set_freeze(
Id::get(),
&FreezeReason::PGasMinBalance.into(),
to,
pgas_ed,
)?;
Ok(())
}
fn destroy_contract(contract: &T::AccountId) -> DispatchResult {
<() as Deposit<T>>::destroy_contract(contract)?;
<Freezer as fungibles::freeze::Mutate<T::AccountId>>::thaw(
Id::get(),
&FreezeReason::PGasMinBalance.into(),
contract,
)?;
let ed = <Mutator as fungibles::Inspect<T::AccountId>>::balance(Id::get(), contract);
<Mutator as fungibles::Mutate<T::AccountId>>::burn_from(
Id::get(),
contract,
ed,
Preservation::Expendable,
Precision::BestEffort,
Fortitude::Polite,
)?;
Ok(())
}
fn charge_and_hold(
reason: HoldReason,
src: Funds<T::AccountId>,
to: &T::AccountId,
amount: BalanceOf<T>,
) -> DispatchResult {
let from = match &src {
Funds::Balance(from) | Funds::TxFee(from) => *from,
};
if Self::pgas_reducible_balance(from) >= amount {
<Holder as fungibles::MutateHold<T::AccountId>>::transfer_and_hold(
Id::get(),
&reason.into(),
from,
to,
amount,
Precision::Exact,
Preservation::Expendable,
Fortitude::Polite,
)?;
} else {
<() as Deposit<T>>::charge_and_hold(reason, src, to, amount)?;
Self::record_native_deposit(from, to, amount);
}
Ok(())
}
fn refund_on_hold(
reason: HoldReason,
from: &T::AccountId,
dst: Funds<T::AccountId>,
amount: BalanceOf<T>,
) -> DispatchResult {
let to = match &dst {
Funds::Balance(to) | Funds::TxFee(to) => *to,
};
let contribution = NativeDepositOf::<T>::get(from, to);
let native_requested = amount.min(contribution);
let native_refunded = if !native_requested.is_zero() {
<() as Deposit<T>>::refund_on_hold(reason, from, dst, native_requested)?;
let new_val = contribution.saturating_sub(native_requested);
if new_val.is_zero() {
NativeDepositOf::<T>::remove(from, to);
} else {
NativeDepositOf::<T>::insert(from, to, new_val);
}
native_requested
} else {
BalanceOf::<T>::zero()
};
let pgas_needed = amount.saturating_sub(native_refunded);
Self::settle_pgas_refund(reason, from, to, pgas_needed)?;
Ok(())
}
fn total_on_hold(reason: HoldReason, who: &T::AccountId) -> BalanceOf<T> {
let native_held = <() as Deposit<T>>::total_on_hold(reason, who);
let pgas_held = Self::pgas_on_hold(reason, who);
native_held.saturating_add(pgas_held)
}
fn refund_all(
from: &T::AccountId,
dst: Funds<T::AccountId>,
) -> Result<BalanceOf<T>, DispatchError> {
let to = match &dst {
Funds::Balance(to) | Funds::TxFee(to) => *to,
};
let native = <() as Deposit<T>>::refund_all(from, dst)?;
let reason = HoldReason::StorageDepositReserve;
let pgas = Self::pgas_on_hold(reason, from);
let pgas = Self::settle_pgas_refund(reason, from, to, pgas)?;
Ok(native.saturating_add(pgas))
}
fn migrate_native_to_pgas(
reason: HoldReason,
contract: &T::AccountId,
amount: BalanceOf<T>,
) -> DispatchResult {
let pgas_ed = <Mutator as fungibles::Inspect<T::AccountId>>::minimum_balance(Id::get());
let freeze_id = FreezeReason::PGasMinBalance.into();
if <Freezer as fungibles::freeze::Inspect<T::AccountId>>::balance_frozen(
Id::get(),
&freeze_id,
contract,
) < pgas_ed
{
if <Mutator as fungibles::Inspect<T::AccountId>>::balance(Id::get(), contract) < pgas_ed
{
<Mutator as fungibles::Mutate<T::AccountId>>::mint_into(
Id::get(),
contract,
pgas_ed,
)
.inspect_err(|err| {
log::debug!(
target: LOG_TARGET,
"Failed to mint PGAS ED for contract: {err:?}",
)
})?;
}
<Freezer as fungibles::freeze::Mutate<T::AccountId>>::set_freeze(
Id::get(),
&freeze_id,
contract,
pgas_ed,
)
.inspect_err(|err| {
log::debug!(
target: LOG_TARGET,
"Failed to freeze PGAS ED for contract: {err:?}",
)
})?;
}
if amount.is_zero() {
return Ok(());
}
T::Currency::burn_held(
&reason.into(),
contract,
amount,
Precision::Exact,
Fortitude::Polite,
)
.inspect_err(
|err| log::debug!(target: LOG_TARGET, "Failed to burn held amount {amount:?}: {err:?}"),
)?;
<Mutator as fungibles::Mutate<T::AccountId>>::mint_into(Id::get(), contract, amount)
.inspect_err(
|err| log::debug!(target: LOG_TARGET, "Failed to mint to {contract:?} amount: {amount:?}: {err:?}"),
)?;
<Holder as fungibles::MutateHold<T::AccountId>>::hold(
Id::get(),
&reason.into(),
contract,
amount,
)
.inspect_err(
|err| log::debug!(target: LOG_TARGET, "Failed to hold amount in {contract:?}: {amount:?}: {err:?}"),
)?;
Ok(())
}
}
impl<T, Mutator, Holder, Freezer, Id, RefundPercent>
PGasDeposit<T, Mutator, Holder, Freezer, Id, RefundPercent>
where
T: Config,
Mutator: fungibles::Mutate<T::AccountId, Balance = BalanceOf<T>>,
Holder: fungibles::MutateHold<
T::AccountId,
Balance = BalanceOf<T>,
AssetId = <Mutator as fungibles::Inspect<T::AccountId>>::AssetId,
>,
<Holder as fungibles::InspectHold<T::AccountId>>::Reason: From<HoldReason>,
Freezer: fungibles::freeze::Mutate<
T::AccountId,
Balance = BalanceOf<T>,
AssetId = <Mutator as fungibles::Inspect<T::AccountId>>::AssetId,
>,
<Freezer as fungibles::freeze::Inspect<T::AccountId>>::Id: From<FreezeReason>,
Id: Get<<Mutator as fungibles::Inspect<T::AccountId>>::AssetId>,
RefundPercent: Get<Perbill>,
{
fn pgas_reducible_balance(who: &T::AccountId) -> BalanceOf<T> {
<Mutator as fungibles::Inspect<T::AccountId>>::reducible_balance(
Id::get(),
who,
Preservation::Expendable,
Fortitude::Polite,
)
}
fn pgas_on_hold(reason: HoldReason, who: &T::AccountId) -> BalanceOf<T> {
<Holder as fungibles::InspectHold<T::AccountId>>::balance_on_hold(
Id::get(),
&reason.into(),
who,
)
}
fn record_native_deposit(from: &T::AccountId, to: &T::AccountId, amount: BalanceOf<T>) {
NativeDepositOf::<T>::mutate(to, from, |entitlement| {
*entitlement = entitlement.saturating_add(amount);
});
}
fn settle_pgas_refund(
reason: HoldReason,
from: &T::AccountId,
to: &T::AccountId,
amount: BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError> {
if amount.is_zero() {
return Ok(BalanceOf::<T>::zero());
}
let pgas_held = Self::pgas_on_hold(reason, from);
let amount = amount.min(pgas_held);
if amount.is_zero() {
return Ok(BalanceOf::<T>::zero());
}
let refund = RefundPercent::get().mul_floor(amount);
let mut burn = amount.saturating_sub(refund);
let mut refunded = BalanceOf::<T>::zero();
if !refund.is_zero() {
let can_credit = matches!(
<Mutator as fungibles::Inspect<T::AccountId>>::can_deposit(
Id::get(),
to,
refund,
Provenance::Extant,
),
DepositConsequence::Success
);
if can_credit {
refunded = <Holder as fungibles::MutateHold<T::AccountId>>::transfer_on_hold(
Id::get(),
&reason.into(),
from,
to,
refund,
Precision::BestEffort,
Restriction::Free,
Fortitude::Polite,
)?;
} else {
burn = burn.saturating_add(refund);
}
}
if !burn.is_zero() {
<Holder as fungibles::MutateHold<T::AccountId>>::burn_held(
Id::get(),
&reason.into(),
from,
burn,
Precision::Exact,
Fortitude::Polite,
)?;
}
Ok(refunded)
}
}