use crate::{
storage::ContractInfo, AccountIdOf, BalanceOf, CodeInfo, Config, Error, Event, HoldReason,
Inspect, Origin, Pallet, StorageDeposit as Deposit, System, LOG_TARGET,
};
use frame_support::{
dispatch::{fmt::Debug, DispatchError},
ensure,
traits::{
fungible::{Mutate, MutateHold},
tokens::{
Fortitude, Fortitude::Polite, Precision, Preservation, Restriction, WithdrawConsequence,
},
Get,
},
DefaultNoBound, RuntimeDebugNoBound,
};
use sp_api::HashT;
use sp_runtime::{
traits::{Saturating, Zero},
FixedPointNumber, FixedU128,
};
use sp_std::{marker::PhantomData, vec, vec::Vec};
pub type DepositOf<T> = Deposit<BalanceOf<T>>;
pub type Meter<T> = RawMeter<T, ReservingExt, Root>;
pub type NestedMeter<T> = RawMeter<T, ReservingExt, Nested>;
pub type GenericMeter<T, S> = RawMeter<T, ReservingExt, S>;
pub trait Ext<T: Config> {
fn check_limit(
origin: &T::AccountId,
limit: Option<BalanceOf<T>>,
min_leftover: BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError>;
fn charge(
origin: &T::AccountId,
contract: &T::AccountId,
amount: &DepositOf<T>,
state: &ContractState<T>,
) -> Result<(), DispatchError>;
}
pub enum ReservingExt {}
pub trait State: private::Sealed {}
#[derive(Default, Debug)]
pub struct Root;
#[derive(DefaultNoBound, RuntimeDebugNoBound)]
pub enum Nested {
#[default]
DerivedLimit,
OwnLimit,
}
impl State for Root {}
impl State for Nested {}
#[derive(DefaultNoBound, RuntimeDebugNoBound)]
pub struct RawMeter<T: Config, E, S: State + Default + Debug> {
limit: BalanceOf<T>,
total_deposit: DepositOf<T>,
own_contribution: Contribution<T>,
charges: Vec<Charge<T>>,
nested: S,
_phantom: PhantomData<E>,
}
#[derive(Default, RuntimeDebugNoBound)]
pub struct Diff {
pub bytes_added: u32,
pub bytes_removed: u32,
pub items_added: u32,
pub items_removed: u32,
}
impl Diff {
pub fn update_contract<T: Config>(&self, info: Option<&mut ContractInfo<T>>) -> DepositOf<T> {
let per_byte = T::DepositPerByte::get();
let per_item = T::DepositPerItem::get();
let bytes_added = self.bytes_added.saturating_sub(self.bytes_removed);
let items_added = self.items_added.saturating_sub(self.items_removed);
let mut bytes_deposit = Deposit::Charge(per_byte.saturating_mul((bytes_added).into()));
let mut items_deposit = Deposit::Charge(per_item.saturating_mul((items_added).into()));
let info = if let Some(info) = info {
info
} else {
debug_assert_eq!(self.bytes_removed, 0);
debug_assert_eq!(self.items_removed, 0);
return bytes_deposit.saturating_add(&items_deposit)
};
let bytes_removed = self.bytes_removed.saturating_sub(self.bytes_added);
let items_removed = self.items_removed.saturating_sub(self.items_added);
let ratio = FixedU128::checked_from_rational(bytes_removed, info.storage_bytes)
.unwrap_or_default()
.min(FixedU128::from_u32(1));
bytes_deposit = bytes_deposit
.saturating_add(&Deposit::Refund(ratio.saturating_mul_int(info.storage_byte_deposit)));
let ratio = FixedU128::checked_from_rational(items_removed, info.storage_items)
.unwrap_or_default()
.min(FixedU128::from_u32(1));
items_deposit = items_deposit
.saturating_add(&Deposit::Refund(ratio.saturating_mul_int(info.storage_item_deposit)));
info.storage_bytes =
info.storage_bytes.saturating_add(bytes_added).saturating_sub(bytes_removed);
info.storage_items =
info.storage_items.saturating_add(items_added).saturating_sub(items_removed);
match &bytes_deposit {
Deposit::Charge(amount) =>
info.storage_byte_deposit = info.storage_byte_deposit.saturating_add(*amount),
Deposit::Refund(amount) =>
info.storage_byte_deposit = info.storage_byte_deposit.saturating_sub(*amount),
}
match &items_deposit {
Deposit::Charge(amount) =>
info.storage_item_deposit = info.storage_item_deposit.saturating_add(*amount),
Deposit::Refund(amount) =>
info.storage_item_deposit = info.storage_item_deposit.saturating_sub(*amount),
}
bytes_deposit.saturating_add(&items_deposit)
}
}
impl Diff {
fn saturating_add(&self, rhs: &Self) -> Self {
Self {
bytes_added: self.bytes_added.saturating_add(rhs.bytes_added),
bytes_removed: self.bytes_removed.saturating_add(rhs.bytes_removed),
items_added: self.items_added.saturating_add(rhs.items_added),
items_removed: self.items_removed.saturating_add(rhs.items_removed),
}
}
}
#[derive(RuntimeDebugNoBound, Clone, PartialEq, Eq)]
pub enum ContractState<T: Config> {
Alive,
Terminated { beneficiary: AccountIdOf<T> },
}
#[derive(RuntimeDebugNoBound, Clone)]
struct Charge<T: Config> {
contract: T::AccountId,
amount: DepositOf<T>,
state: ContractState<T>,
}
#[derive(RuntimeDebugNoBound)]
enum Contribution<T: Config> {
Alive(Diff),
Checked(DepositOf<T>),
Terminated { deposit: DepositOf<T>, beneficiary: AccountIdOf<T> },
}
impl<T: Config> Contribution<T> {
fn update_contract(&self, info: Option<&mut ContractInfo<T>>) -> DepositOf<T> {
match self {
Self::Alive(diff) => diff.update_contract::<T>(info),
Self::Terminated { deposit, beneficiary: _ } | Self::Checked(deposit) =>
deposit.clone(),
}
}
}
impl<T: Config> Default for Contribution<T> {
fn default() -> Self {
Self::Alive(Default::default())
}
}
impl<T, E, S> RawMeter<T, E, S>
where
T: Config,
E: Ext<T>,
S: State + Default + Debug,
{
pub fn nested(&self, limit: BalanceOf<T>) -> RawMeter<T, E, Nested> {
debug_assert!(matches!(self.contract_state(), ContractState::Alive));
let limit = self.available().min(limit);
if limit.is_zero() {
RawMeter { limit: self.available(), ..Default::default() }
} else {
RawMeter { limit, nested: Nested::OwnLimit, ..Default::default() }
}
}
pub fn absorb(
&mut self,
absorbed: RawMeter<T, E, Nested>,
contract: &T::AccountId,
info: Option<&mut ContractInfo<T>>,
) {
let own_deposit = absorbed.own_contribution.update_contract(info);
self.total_deposit = self
.total_deposit
.saturating_add(&absorbed.total_deposit)
.saturating_add(&own_deposit);
self.charges.extend_from_slice(&absorbed.charges);
if !own_deposit.is_zero() {
self.charges.push(Charge {
contract: contract.clone(),
amount: own_deposit,
state: absorbed.contract_state(),
});
}
}
fn available(&self) -> BalanceOf<T> {
self.total_deposit.available(&self.limit)
}
fn contract_state(&self) -> ContractState<T> {
match &self.own_contribution {
Contribution::Terminated { deposit: _, beneficiary } =>
ContractState::Terminated { beneficiary: beneficiary.clone() },
_ => ContractState::Alive,
}
}
}
impl<T, E> RawMeter<T, E, Root>
where
T: Config,
E: Ext<T>,
{
pub fn new(
origin: &Origin<T>,
limit: Option<BalanceOf<T>>,
min_leftover: BalanceOf<T>,
) -> Result<Self, DispatchError> {
return match origin {
Origin::Root => Ok(Self {
limit: limit.unwrap_or(T::DefaultDepositLimit::get()),
..Default::default()
}),
Origin::Signed(o) => {
let limit = E::check_limit(o, limit, min_leftover)?;
Ok(Self { limit, ..Default::default() })
},
}
}
pub fn try_into_deposit(self, origin: &Origin<T>) -> Result<DepositOf<T>, DispatchError> {
let origin = match origin {
Origin::Root => return Ok(Deposit::Charge(Zero::zero())),
Origin::Signed(o) => o,
};
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Refund(_))) {
E::charge(origin, &charge.contract, &charge.amount, &charge.state)?;
}
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Charge(_))) {
E::charge(origin, &charge.contract, &charge.amount, &charge.state)?;
}
Ok(self.total_deposit)
}
}
impl<T, E> RawMeter<T, E, Nested>
where
T: Config,
E: Ext<T>,
{
pub fn charge(&mut self, diff: &Diff) {
match &mut self.own_contribution {
Contribution::Alive(own) => *own = own.saturating_add(diff),
_ => panic!("Charge is never called after termination; qed"),
};
}
pub fn charge_deposit(&mut self, contract: T::AccountId, amount: DepositOf<T>) {
self.total_deposit = self.total_deposit.saturating_add(&amount);
self.charges.push(Charge { contract, amount, state: ContractState::Alive });
}
pub fn charge_instantiate(
&mut self,
origin: &T::AccountId,
contract: &T::AccountId,
contract_info: &mut ContractInfo<T>,
code_info: &CodeInfo<T>,
) -> Result<DepositOf<T>, DispatchError> {
debug_assert!(matches!(self.contract_state(), ContractState::Alive));
let ed = Pallet::<T>::min_balance();
let deposit = contract_info.update_base_deposit(&code_info);
if deposit > self.limit {
return Err(<Error<T>>::StorageDepositLimitExhausted.into())
}
let deposit = Deposit::Charge(deposit);
self.total_deposit = Deposit::Charge(ed);
T::Currency::transfer(origin, contract, ed, Preservation::Preserve)?;
System::<T>::inc_consumers(contract)?;
self.charge_deposit(contract.clone(), deposit.saturating_sub(&Deposit::Charge(ed)));
Ok(deposit)
}
pub fn terminate(&mut self, info: &ContractInfo<T>, beneficiary: T::AccountId) {
debug_assert!(matches!(self.contract_state(), ContractState::Alive));
self.own_contribution = Contribution::Terminated {
deposit: Deposit::Refund(info.total_deposit()),
beneficiary,
};
}
pub fn enforce_limit(
&mut self,
info: Option<&mut ContractInfo<T>>,
) -> Result<(), DispatchError> {
let deposit = self.own_contribution.update_contract(info);
let total_deposit = self.total_deposit.saturating_add(&deposit);
if matches!(self.contract_state(), ContractState::Alive) {
self.own_contribution = Contribution::Checked(deposit);
}
if let Deposit::Charge(amount) = total_deposit {
if amount > self.limit {
return Err(<Error<T>>::StorageDepositLimitExhausted.into())
}
}
Ok(())
}
pub fn enforce_subcall_limit(
&mut self,
info: Option<&mut ContractInfo<T>>,
) -> Result<(), DispatchError> {
match self.nested {
Nested::OwnLimit => self.enforce_limit(info),
Nested::DerivedLimit => Ok(()),
}
}
}
impl<T: Config> Ext<T> for ReservingExt {
fn check_limit(
origin: &T::AccountId,
limit: Option<BalanceOf<T>>,
min_leftover: BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError> {
let max = T::Currency::reducible_balance(origin, Preservation::Preserve, Polite)
.saturating_sub(min_leftover)
.saturating_sub(Pallet::<T>::min_balance());
let default = max.min(T::DefaultDepositLimit::get());
let limit = limit.unwrap_or(default);
ensure!(
limit <= max &&
matches!(T::Currency::can_withdraw(origin, limit), WithdrawConsequence::Success),
<Error<T>>::StorageDepositNotEnoughFunds,
);
Ok(limit)
}
fn charge(
origin: &T::AccountId,
contract: &T::AccountId,
amount: &DepositOf<T>,
state: &ContractState<T>,
) -> Result<(), DispatchError> {
match amount {
Deposit::Charge(amount) | Deposit::Refund(amount) if amount.is_zero() => return Ok(()),
Deposit::Charge(amount) => {
T::Currency::transfer_and_hold(
&HoldReason::StorageDepositReserve.into(),
origin,
contract,
*amount,
Precision::Exact,
Preservation::Preserve,
Fortitude::Polite,
)?;
Pallet::<T>::deposit_event(
vec![T::Hashing::hash_of(&origin), T::Hashing::hash_of(&contract)],
Event::StorageDepositTransferredAndHeld {
from: origin.clone(),
to: contract.clone(),
amount: *amount,
},
);
},
Deposit::Refund(amount) => {
let transferred = T::Currency::transfer_on_hold(
&HoldReason::StorageDepositReserve.into(),
contract,
origin,
*amount,
Precision::BestEffort,
Restriction::Free,
Fortitude::Polite,
)?;
Pallet::<T>::deposit_event(
vec![T::Hashing::hash_of(&contract), T::Hashing::hash_of(&origin)],
Event::StorageDepositTransferredAndReleased {
from: contract.clone(),
to: origin.clone(),
amount: transferred,
},
);
if transferred < *amount {
log::error!(
target: LOG_TARGET,
"Failed to repatriate full storage deposit {:?} from contract {:?} to origin {:?}. Transferred {:?}.",
amount, contract, origin, transferred,
);
}
},
}
if let ContractState::<T>::Terminated { beneficiary } = state {
System::<T>::dec_consumers(&contract);
T::Currency::transfer(
&contract,
&beneficiary,
T::Currency::reducible_balance(&contract, Preservation::Expendable, Polite),
Preservation::Expendable,
)?;
}
Ok(())
}
}
mod private {
pub trait Sealed {}
impl Sealed for super::Root {}
impl Sealed for super::Nested {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
exec::AccountIdOf,
tests::{Test, ALICE, BOB, CHARLIE},
};
use frame_support::parameter_types;
use pretty_assertions::assert_eq;
type TestMeter = RawMeter<Test, TestExt, Root>;
parameter_types! {
static TestExtTestValue: TestExt = Default::default();
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct LimitCheck {
origin: AccountIdOf<Test>,
limit: BalanceOf<Test>,
min_leftover: BalanceOf<Test>,
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct Charge {
origin: AccountIdOf<Test>,
contract: AccountIdOf<Test>,
amount: DepositOf<Test>,
state: ContractState<Test>,
}
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct TestExt {
limit_checks: Vec<LimitCheck>,
charges: Vec<Charge>,
}
impl TestExt {
fn clear(&mut self) {
self.limit_checks.clear();
self.charges.clear();
}
}
impl Ext<Test> for TestExt {
fn check_limit(
origin: &AccountIdOf<Test>,
limit: Option<BalanceOf<Test>>,
min_leftover: BalanceOf<Test>,
) -> Result<BalanceOf<Test>, DispatchError> {
let limit = limit.unwrap_or(42);
TestExtTestValue::mutate(|ext| {
ext.limit_checks
.push(LimitCheck { origin: origin.clone(), limit, min_leftover })
});
Ok(limit)
}
fn charge(
origin: &AccountIdOf<Test>,
contract: &AccountIdOf<Test>,
amount: &DepositOf<Test>,
state: &ContractState<Test>,
) -> Result<(), DispatchError> {
TestExtTestValue::mutate(|ext| {
ext.charges.push(Charge {
origin: origin.clone(),
contract: contract.clone(),
amount: amount.clone(),
state: state.clone(),
})
});
Ok(())
}
}
fn clear_ext() {
TestExtTestValue::mutate(|ext| ext.clear())
}
struct ChargingTestCase {
origin: Origin<Test>,
deposit: DepositOf<Test>,
expected: TestExt,
}
#[derive(Default)]
struct StorageInfo {
bytes: u32,
items: u32,
bytes_deposit: BalanceOf<Test>,
items_deposit: BalanceOf<Test>,
}
fn new_info(info: StorageInfo) -> ContractInfo<Test> {
ContractInfo::<Test> {
trie_id: Default::default(),
code_hash: Default::default(),
storage_bytes: info.bytes,
storage_items: info.items,
storage_byte_deposit: info.bytes_deposit,
storage_item_deposit: info.items_deposit,
storage_base_deposit: Default::default(),
delegate_dependencies: Default::default(),
}
}
#[test]
fn new_reserves_balance_works() {
clear_ext();
TestMeter::new(&Origin::from_account_id(ALICE), Some(1_000), 0).unwrap();
assert_eq!(
TestExtTestValue::get(),
TestExt {
limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }],
..Default::default()
}
)
}
#[test]
fn empty_charge_works() {
clear_ext();
let mut meter = TestMeter::new(&Origin::from_account_id(ALICE), Some(1_000), 0).unwrap();
assert_eq!(meter.available(), 1_000);
let mut nested0 = meter.nested(BalanceOf::<Test>::zero());
nested0.charge(&Default::default());
meter.absorb(nested0, &BOB, None);
assert_eq!(
TestExtTestValue::get(),
TestExt {
limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }],
..Default::default()
}
)
}
#[test]
fn charging_works() {
let test_cases = vec![
ChargingTestCase {
origin: Origin::<Test>::from_account_id(ALICE),
deposit: Deposit::Refund(28),
expected: TestExt {
limit_checks: vec![LimitCheck { origin: ALICE, limit: 100, min_leftover: 0 }],
charges: vec![
Charge {
origin: ALICE,
contract: CHARLIE,
amount: Deposit::Refund(10),
state: ContractState::Alive,
},
Charge {
origin: ALICE,
contract: CHARLIE,
amount: Deposit::Refund(20),
state: ContractState::Alive,
},
Charge {
origin: ALICE,
contract: BOB,
amount: Deposit::Charge(2),
state: ContractState::Alive,
},
],
},
},
ChargingTestCase {
origin: Origin::<Test>::Root,
deposit: Deposit::Charge(0),
expected: TestExt { limit_checks: vec![], charges: vec![] },
},
];
for test_case in test_cases {
clear_ext();
let mut meter = TestMeter::new(&test_case.origin, Some(100), 0).unwrap();
assert_eq!(meter.available(), 100);
let mut nested0_info = new_info(StorageInfo {
bytes: 100,
items: 5,
bytes_deposit: 100,
items_deposit: 10,
});
let mut nested0 = meter.nested(BalanceOf::<Test>::zero());
nested0.charge(&Diff {
bytes_added: 108,
bytes_removed: 5,
items_added: 1,
items_removed: 2,
});
nested0.charge(&Diff { bytes_removed: 99, ..Default::default() });
let mut nested1_info = new_info(StorageInfo {
bytes: 100,
items: 10,
bytes_deposit: 100,
items_deposit: 20,
});
let mut nested1 = nested0.nested(BalanceOf::<Test>::zero());
nested1.charge(&Diff { items_removed: 5, ..Default::default() });
nested0.absorb(nested1, &CHARLIE, Some(&mut nested1_info));
let mut nested2_info = new_info(StorageInfo {
bytes: 100,
items: 7,
bytes_deposit: 100,
items_deposit: 20,
});
let mut nested2 = nested0.nested(BalanceOf::<Test>::zero());
nested2.charge(&Diff { items_removed: 7, ..Default::default() });
nested0.absorb(nested2, &CHARLIE, Some(&mut nested2_info));
nested0.enforce_limit(Some(&mut nested0_info)).unwrap();
meter.absorb(nested0, &BOB, Some(&mut nested0_info));
assert_eq!(meter.try_into_deposit(&test_case.origin).unwrap(), test_case.deposit);
assert_eq!(nested0_info.extra_deposit(), 112);
assert_eq!(nested1_info.extra_deposit(), 110);
assert_eq!(nested2_info.extra_deposit(), 100);
assert_eq!(TestExtTestValue::get(), test_case.expected)
}
}
#[test]
fn termination_works() {
let test_cases = vec![
ChargingTestCase {
origin: Origin::<Test>::from_account_id(ALICE),
deposit: Deposit::Refund(107),
expected: TestExt {
limit_checks: vec![LimitCheck { origin: ALICE, limit: 1_000, min_leftover: 0 }],
charges: vec![
Charge {
origin: ALICE,
contract: CHARLIE,
amount: Deposit::Refund(119),
state: ContractState::Terminated { beneficiary: CHARLIE },
},
Charge {
origin: ALICE,
contract: BOB,
amount: Deposit::Charge(12),
state: ContractState::Alive,
},
],
},
},
ChargingTestCase {
origin: Origin::<Test>::Root,
deposit: Deposit::Charge(0),
expected: TestExt { limit_checks: vec![], charges: vec![] },
},
];
for test_case in test_cases {
clear_ext();
let mut meter = TestMeter::new(&test_case.origin, Some(1_000), 0).unwrap();
assert_eq!(meter.available(), 1_000);
let mut nested0 = meter.nested(BalanceOf::<Test>::zero());
nested0.charge(&Diff {
bytes_added: 5,
bytes_removed: 1,
items_added: 3,
items_removed: 1,
});
nested0.charge(&Diff { items_added: 2, ..Default::default() });
let mut nested1_info = new_info(StorageInfo {
bytes: 100,
items: 10,
bytes_deposit: 100,
items_deposit: 20,
});
let mut nested1 = nested0.nested(BalanceOf::<Test>::zero());
nested1.charge(&Diff { items_removed: 5, ..Default::default() });
nested1.charge(&Diff { bytes_added: 20, ..Default::default() });
nested1.terminate(&nested1_info, CHARLIE);
nested0.enforce_limit(Some(&mut nested1_info)).unwrap();
nested0.absorb(nested1, &CHARLIE, None);
meter.absorb(nested0, &BOB, None);
assert_eq!(meter.try_into_deposit(&test_case.origin).unwrap(), test_case.deposit);
assert_eq!(TestExtTestValue::get(), test_case.expected)
}
}
}