seal: Prevent contracts from going below subsistence (#6623)

* seal: Do not allow transfers to bring total balance below subsistence deposit

This also reworks the rent system to take the total balance into account
when evaluating whether the account is above the subsistence deposit.

* Fix nits from review

* Fix typo

* Do not enforce subsistence when called from EOA

* Rename CallOrigin to TransactorKind

* Add debug asserts to check the invariants of a plain account transactor

* Fix typo

Co-authored-by: Sergei Shulepov <sergei@parity.io>

Co-authored-by: Sergei Shulepov <sergei@parity.io>
This commit is contained in:
Alexander Theißen
2020-07-10 14:45:31 +02:00
committed by GitHub
parent 64114267b2
commit efc69d8219
5 changed files with 116 additions and 47 deletions
+24 -25
View File
@@ -18,7 +18,7 @@
use crate::{
AliveContractInfo, BalanceOf, ContractInfo, ContractInfoOf, Module, RawEvent,
TombstoneContractInfo, Trait, CodeHash,
TombstoneContractInfo, Trait, CodeHash, Config
};
use sp_std::prelude::*;
use sp_io::hashing::blake2_256;
@@ -87,10 +87,10 @@ enum Verdict<T: Trait> {
/// This function accounts for the storage rent deposit. I.e. if the contract possesses enough funds
/// then the fee can drop to zero.
fn compute_fee_per_block<T: Trait>(
balance: &BalanceOf<T>,
free_balance: &BalanceOf<T>,
contract: &AliveContractInfo<T>,
) -> BalanceOf<T> {
let free_storage = balance
let free_storage = free_balance
.checked_div(&T::RentDepositOffset::get())
.unwrap_or_else(Zero::zero);
@@ -107,30 +107,27 @@ fn compute_fee_per_block<T: Trait>(
.unwrap_or(<BalanceOf<T>>::max_value())
}
/// Subsistence threshold is the extension of the minimum balance (aka existential deposit) by the
/// tombstone deposit, required for leaving a tombstone.
///
/// Rent mechanism cannot make the balance lower than subsistence threshold.
fn subsistence_threshold<T: Trait>() -> BalanceOf<T> {
T::Currency::minimum_balance() + T::TombstoneDeposit::get()
}
/// Returns amount of funds available to consume by rent mechanism.
///
/// Rent mechanism cannot consume more than `rent_allowance` set by the contract and it cannot make
/// the balance lower than [`subsistence_threshold`].
///
/// In case the balance is below the subsistence threshold, this function returns `None`.
/// In case the toal_balance is below the subsistence threshold, this function returns `None`.
fn rent_budget<T: Trait>(
balance: &BalanceOf<T>,
total_balance: &BalanceOf<T>,
free_balance: &BalanceOf<T>,
contract: &AliveContractInfo<T>,
) -> Option<BalanceOf<T>> {
let subsistence_threshold = subsistence_threshold::<T>();
if *balance < subsistence_threshold {
let subsistence_threshold = Config::<T>::subsistence_threshold_uncached();
// Reserved balance contributes towards the subsistence threshold to stay consistent
// with the existential deposit where the reserved balance is also counted.
if *total_balance < subsistence_threshold {
return None;
}
let rent_allowed_to_charge = *balance - subsistence_threshold;
// However, reserved balance cannot be charged so we need to use the free balance
// to calculate the actual budget (which can be 0).
let rent_allowed_to_charge = free_balance.saturating_sub(subsistence_threshold);
Some(<BalanceOf<T>>::min(
contract.rent_allowance,
rent_allowed_to_charge,
@@ -158,21 +155,22 @@ fn consider_case<T: Trait>(
return Verdict::Exempt;
}
let balance = T::Currency::free_balance(account);
let total_balance = T::Currency::total_balance(account);
let free_balance = T::Currency::free_balance(account);
// An amount of funds to charge per block for storage taken up by the contract.
let fee_per_block = compute_fee_per_block::<T>(&balance, contract);
let fee_per_block = compute_fee_per_block::<T>(&free_balance, contract);
if fee_per_block.is_zero() {
// The rent deposit offset reduced the fee to 0. This means that the contract
// gets the rent for free.
return Verdict::Exempt;
}
let rent_budget = match rent_budget::<T>(&balance, contract) {
let rent_budget = match rent_budget::<T>(&total_balance, &free_balance, contract) {
Some(rent_budget) => rent_budget,
None => {
// The contract's balance is already below subsistence threshold. That indicates that
// the contract cannot afford to leave a tombstone.
// The contract's total balance is already below subsistence threshold. That
// indicates that the contract cannot afford to leave a tombstone.
//
// So cleanly wipe the contract.
return Verdict::Kill;
@@ -195,7 +193,7 @@ fn consider_case<T: Trait>(
account,
dues_limited,
WithdrawReason::Fee.into(),
balance.saturating_sub(dues_limited),
free_balance.saturating_sub(dues_limited),
)
.is_ok();
@@ -369,14 +367,15 @@ pub fn compute_rent_projection<T: Trait>(
};
// Compute how much would the fee per block be with the *updated* balance.
let balance = T::Currency::free_balance(account);
let fee_per_block = compute_fee_per_block::<T>(&balance, &alive_contract_info);
let total_balance = T::Currency::total_balance(account);
let free_balance = T::Currency::free_balance(account);
let fee_per_block = compute_fee_per_block::<T>(&free_balance, &alive_contract_info);
if fee_per_block.is_zero() {
return Ok(RentProjection::NoEviction);
}
// Then compute how much the contract will sustain under these circumstances.
let rent_budget = rent_budget::<T>(&balance, &alive_contract_info).expect(
let rent_budget = rent_budget::<T>(&total_balance, &free_balance, &alive_contract_info).expect(
"the contract exists and in the alive state;
the updated balance must be greater than subsistence deposit;
this function doesn't return `None`;