Skip to content

Commit 8b4c4cd

Browse files
atheipepyakin
andauthored
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>
1 parent 9a3e7ef commit 8b4c4cd

File tree

5 files changed

+116
-47
lines changed

5 files changed

+116
-47
lines changed

src/exec.rs

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,15 @@
1616

1717
use super::{CodeHash, Config, ContractAddressFor, Event, RawEvent, Trait,
1818
TrieId, BalanceOf, ContractInfo, TrieIdGenerator};
19-
use crate::gas::{Gas, GasMeter, Token};
20-
use crate::rent;
21-
use crate::storage;
19+
use crate::{gas::{Gas, GasMeter, Token}, rent, storage, Error, ContractInfoOf};
2220
use bitflags::bitflags;
23-
2421
use sp_std::prelude::*;
25-
use sp_runtime::traits::{Bounded, Zero, Convert};
22+
use sp_runtime::traits::{Bounded, Zero, Convert, Saturating};
2623
use frame_support::{
2724
dispatch::DispatchError,
2825
traits::{ExistenceRequirement, Currency, Time, Randomness},
2926
weights::Weight,
27+
ensure, StorageMap,
3028
};
3129

3230
pub type AccountIdOf<T> = <T as frame_system::Trait>::AccountId;
@@ -46,6 +44,15 @@ bitflags! {
4644
}
4745
}
4846

47+
/// Describes whether we deal with a contract or a plain account.
48+
pub enum TransactorKind {
49+
/// Transaction was initiated from a plain account. That can be either be through a
50+
/// signed transaction or through RPC.
51+
PlainAccount,
52+
/// The call was initiated by a contract account.
53+
Contract,
54+
}
55+
4956
/// Output of a contract call or instantiation which ran to completion.
5057
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
5158
pub struct ExecReturnValue {
@@ -320,6 +327,7 @@ where
320327
Err("contract has been evicted")?
321328
};
322329

330+
let transactor_kind = self.transactor_kind();
323331
let caller = self.self_account.clone();
324332
let dest_trie_id = contract_info.and_then(|i| i.as_alive().map(|i| i.trie_id.clone()));
325333

@@ -328,6 +336,7 @@ where
328336
transfer(
329337
gas_meter,
330338
TransferCause::Call,
339+
transactor_kind,
331340
&caller,
332341
&dest,
333342
value,
@@ -372,6 +381,7 @@ where
372381
Err("not enough gas to pay base instantiate fee")?
373382
}
374383

384+
let transactor_kind = self.transactor_kind();
375385
let caller = self.self_account.clone();
376386
let dest = T::DetermineContractAddress::contract_address_for(
377387
code_hash,
@@ -399,6 +409,7 @@ where
399409
transfer(
400410
gas_meter,
401411
TransferCause::Instantiate,
412+
transactor_kind,
402413
&caller,
403414
&dest,
404415
endowment,
@@ -466,6 +477,17 @@ where
466477
&self.self_account == account ||
467478
self.caller.map_or(false, |caller| caller.is_live(account))
468479
}
480+
481+
fn transactor_kind(&self) -> TransactorKind {
482+
if self.depth == 0 {
483+
debug_assert!(self.self_trie_id.is_none());
484+
debug_assert!(self.caller.is_none());
485+
debug_assert!(ContractInfoOf::<T>::get(&self.self_account).is_none());
486+
TransactorKind::PlainAccount
487+
} else {
488+
TransactorKind::Contract
489+
}
490+
}
469491
}
470492

471493
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
@@ -519,13 +541,15 @@ enum TransferCause {
519541
fn transfer<'a, T: Trait, V: Vm<T>, L: Loader<T>>(
520542
gas_meter: &mut GasMeter<T>,
521543
cause: TransferCause,
544+
origin: TransactorKind,
522545
transactor: &T::AccountId,
523546
dest: &T::AccountId,
524547
value: BalanceOf<T>,
525548
ctx: &mut ExecutionContext<'a, T, V, L>,
526549
) -> Result<(), DispatchError> {
527550
use self::TransferCause::*;
528551
use self::TransferFeeKind::*;
552+
use self::TransactorKind::*;
529553

530554
let token = {
531555
let kind: TransferFeeKind = match cause {
@@ -545,10 +569,19 @@ fn transfer<'a, T: Trait, V: Vm<T>, L: Loader<T>>(
545569
Err("not enough gas to pay transfer fee")?
546570
}
547571

548-
// Only ext_terminate is allowed to bring the sender below the existential deposit
549-
let existence_requirement = match cause {
550-
Terminate => ExistenceRequirement::AllowDeath,
551-
_ => ExistenceRequirement::KeepAlive,
572+
// Only ext_terminate is allowed to bring the sender below the subsistence
573+
// threshold or even existential deposit.
574+
let existence_requirement = match (cause, origin) {
575+
(Terminate, _) => ExistenceRequirement::AllowDeath,
576+
(_, Contract) => {
577+
ensure!(
578+
T::Currency::total_balance(transactor).saturating_sub(value) >=
579+
ctx.config.subsistence_threshold(),
580+
Error::<T>::InsufficientBalance,
581+
);
582+
ExistenceRequirement::KeepAlive
583+
},
584+
(_, PlainAccount) => ExistenceRequirement::KeepAlive,
552585
};
553586
T::Currency::transfer(transactor, dest, value, existence_requirement)?;
554587

@@ -630,6 +663,7 @@ where
630663
transfer(
631664
gas_meter,
632665
TransferCause::Call,
666+
TransactorKind::Contract,
633667
&self.ctx.self_account.clone(),
634668
&to,
635669
value,
@@ -654,6 +688,7 @@ where
654688
transfer(
655689
gas_meter,
656690
TransferCause::Terminate,
691+
TransactorKind::Contract,
657692
&self_id,
658693
beneficiary,
659694
value,

src/lib.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ use sp_std::{prelude::*, marker::PhantomData, fmt::Debug};
102102
use codec::{Codec, Encode, Decode};
103103
use sp_runtime::{
104104
traits::{
105-
Hash, StaticLookup, Zero, MaybeSerializeDeserialize, Member, Convert,
105+
Hash, StaticLookup, Zero, MaybeSerializeDeserialize, Member, Convert, Saturating,
106106
},
107107
RuntimeDebug,
108108
};
@@ -415,6 +415,11 @@ decl_error! {
415415
OutOfGas,
416416
/// The output buffer supplied to a contract API call was too small.
417417
OutputBufferTooSmall,
418+
/// Performing the requested transfer would have brought the contract below
419+
/// the subsistence threshold. No transfer is allowed to do this in order to allow
420+
/// for a tombstone to be created. Use `ext_terminate` to remove a contract without
421+
/// leaving a tombstone behind.
422+
InsufficientBalance,
418423
}
419424
}
420425

@@ -726,6 +731,25 @@ impl<T: Trait> Config<T> {
726731
max_value_size: T::MaxValueSize::get(),
727732
}
728733
}
734+
735+
/// Subsistence threshold is the extension of the minimum balance (aka existential deposit) by the
736+
/// tombstone deposit, required for leaving a tombstone.
737+
///
738+
/// Rent or any contract initiated balance transfer mechanism cannot make the balance lower
739+
/// than the subsistence threshold in order to guarantee that a tombstone is created.
740+
///
741+
/// The only way to completely kill a contract without a tombstone is calling `ext_terminate`.
742+
fn subsistence_threshold(&self) -> BalanceOf<T> {
743+
self.existential_deposit.saturating_add(self.tombstone_deposit)
744+
}
745+
746+
/// The same as `subsistence_threshold` but without the need for a preloaded instance.
747+
///
748+
/// This is for cases where this value is needed in rent calculation rather than
749+
/// during contract execution.
750+
fn subsistence_threshold_uncached() -> BalanceOf<T> {
751+
T::Currency::minimum_balance().saturating_add(T::TombstoneDeposit::get())
752+
}
729753
}
730754

731755
/// Definition of the cost schedule and other parameterizations for wasm vm.

src/rent.rs

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
1919
use crate::{
2020
AliveContractInfo, BalanceOf, ContractInfo, ContractInfoOf, Module, RawEvent,
21-
TombstoneContractInfo, Trait, CodeHash,
21+
TombstoneContractInfo, Trait, CodeHash, Config
2222
};
2323
use sp_std::prelude::*;
2424
use sp_io::hashing::blake2_256;
@@ -87,10 +87,10 @@ enum Verdict<T: Trait> {
8787
/// This function accounts for the storage rent deposit. I.e. if the contract possesses enough funds
8888
/// then the fee can drop to zero.
8989
fn compute_fee_per_block<T: Trait>(
90-
balance: &BalanceOf<T>,
90+
free_balance: &BalanceOf<T>,
9191
contract: &AliveContractInfo<T>,
9292
) -> BalanceOf<T> {
93-
let free_storage = balance
93+
let free_storage = free_balance
9494
.checked_div(&T::RentDepositOffset::get())
9595
.unwrap_or_else(Zero::zero);
9696

@@ -107,30 +107,27 @@ fn compute_fee_per_block<T: Trait>(
107107
.unwrap_or(<BalanceOf<T>>::max_value())
108108
}
109109

110-
/// Subsistence threshold is the extension of the minimum balance (aka existential deposit) by the
111-
/// tombstone deposit, required for leaving a tombstone.
112-
///
113-
/// Rent mechanism cannot make the balance lower than subsistence threshold.
114-
fn subsistence_threshold<T: Trait>() -> BalanceOf<T> {
115-
T::Currency::minimum_balance() + T::TombstoneDeposit::get()
116-
}
117-
118110
/// Returns amount of funds available to consume by rent mechanism.
119111
///
120112
/// Rent mechanism cannot consume more than `rent_allowance` set by the contract and it cannot make
121113
/// the balance lower than [`subsistence_threshold`].
122114
///
123-
/// In case the balance is below the subsistence threshold, this function returns `None`.
115+
/// In case the toal_balance is below the subsistence threshold, this function returns `None`.
124116
fn rent_budget<T: Trait>(
125-
balance: &BalanceOf<T>,
117+
total_balance: &BalanceOf<T>,
118+
free_balance: &BalanceOf<T>,
126119
contract: &AliveContractInfo<T>,
127120
) -> Option<BalanceOf<T>> {
128-
let subsistence_threshold = subsistence_threshold::<T>();
129-
if *balance < subsistence_threshold {
121+
let subsistence_threshold = Config::<T>::subsistence_threshold_uncached();
122+
// Reserved balance contributes towards the subsistence threshold to stay consistent
123+
// with the existential deposit where the reserved balance is also counted.
124+
if *total_balance < subsistence_threshold {
130125
return None;
131126
}
132127

133-
let rent_allowed_to_charge = *balance - subsistence_threshold;
128+
// However, reserved balance cannot be charged so we need to use the free balance
129+
// to calculate the actual budget (which can be 0).
130+
let rent_allowed_to_charge = free_balance.saturating_sub(subsistence_threshold);
134131
Some(<BalanceOf<T>>::min(
135132
contract.rent_allowance,
136133
rent_allowed_to_charge,
@@ -158,21 +155,22 @@ fn consider_case<T: Trait>(
158155
return Verdict::Exempt;
159156
}
160157

161-
let balance = T::Currency::free_balance(account);
158+
let total_balance = T::Currency::total_balance(account);
159+
let free_balance = T::Currency::free_balance(account);
162160

163161
// An amount of funds to charge per block for storage taken up by the contract.
164-
let fee_per_block = compute_fee_per_block::<T>(&balance, contract);
162+
let fee_per_block = compute_fee_per_block::<T>(&free_balance, contract);
165163
if fee_per_block.is_zero() {
166164
// The rent deposit offset reduced the fee to 0. This means that the contract
167165
// gets the rent for free.
168166
return Verdict::Exempt;
169167
}
170168

171-
let rent_budget = match rent_budget::<T>(&balance, contract) {
169+
let rent_budget = match rent_budget::<T>(&total_balance, &free_balance, contract) {
172170
Some(rent_budget) => rent_budget,
173171
None => {
174-
// The contract's balance is already below subsistence threshold. That indicates that
175-
// the contract cannot afford to leave a tombstone.
172+
// The contract's total balance is already below subsistence threshold. That
173+
// indicates that the contract cannot afford to leave a tombstone.
176174
//
177175
// So cleanly wipe the contract.
178176
return Verdict::Kill;
@@ -195,7 +193,7 @@ fn consider_case<T: Trait>(
195193
account,
196194
dues_limited,
197195
WithdrawReason::Fee.into(),
198-
balance.saturating_sub(dues_limited),
196+
free_balance.saturating_sub(dues_limited),
199197
)
200198
.is_ok();
201199

@@ -369,14 +367,15 @@ pub fn compute_rent_projection<T: Trait>(
369367
};
370368

371369
// Compute how much would the fee per block be with the *updated* balance.
372-
let balance = T::Currency::free_balance(account);
373-
let fee_per_block = compute_fee_per_block::<T>(&balance, &alive_contract_info);
370+
let total_balance = T::Currency::total_balance(account);
371+
let free_balance = T::Currency::free_balance(account);
372+
let fee_per_block = compute_fee_per_block::<T>(&free_balance, &alive_contract_info);
374373
if fee_per_block.is_zero() {
375374
return Ok(RentProjection::NoEviction);
376375
}
377376

378377
// Then compute how much the contract will sustain under these circumstances.
379-
let rent_budget = rent_budget::<T>(&balance, &alive_contract_info).expect(
378+
let rent_budget = rent_budget::<T>(&total_balance, &free_balance, &alive_contract_info).expect(
380379
"the contract exists and in the alive state;
381380
the updated balance must be greater than subsistence deposit;
382381
this function doesn't return `None`;

src/tests.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,7 @@ fn claim_surcharge(blocks: u64, trigger_call: impl Fn() -> bool, removes: bool)
797797
/// * if balance is reached and balance > subsistence threshold
798798
/// * if allowance is exceeded
799799
/// * if balance is reached and balance < subsistence threshold
800+
/// * this case cannot be triggered by a contract: we check whether a tombstone is left
800801
fn removals(trigger_call: impl Fn() -> bool) {
801802
let (wasm, code_hash) = compile_module::<Test>("set_rent").unwrap();
802803

@@ -898,10 +899,12 @@ fn removals(trigger_call: impl Fn() -> bool) {
898899
.execute_with(|| {
899900
// Create
900901
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
902+
let subsistence_threshold =
903+
Balances::minimum_balance() + <Test as Trait>::TombstoneDeposit::get();
901904
assert_ok!(Contracts::put_code(Origin::signed(ALICE), wasm.clone()));
902905
assert_ok!(Contracts::instantiate(
903906
Origin::signed(ALICE),
904-
50 + Balances::minimum_balance(),
907+
50 + subsistence_threshold,
905908
GAS_LIMIT,
906909
code_hash.into(),
907910
<Test as pallet_balances::Trait>::Balance::from(1_000u32).encode() // rent allowance
@@ -919,7 +922,7 @@ fn removals(trigger_call: impl Fn() -> bool) {
919922
);
920923
assert_eq!(
921924
Balances::free_balance(BOB),
922-
50 + Balances::minimum_balance()
925+
50 + subsistence_threshold,
923926
);
924927

925928
// Transfer funds
@@ -938,23 +941,23 @@ fn removals(trigger_call: impl Fn() -> bool) {
938941
.rent_allowance,
939942
1_000
940943
);
941-
assert_eq!(Balances::free_balance(BOB), Balances::minimum_balance());
944+
assert_eq!(Balances::free_balance(BOB), subsistence_threshold);
942945

943946
// Advance blocks
944947
initialize_block(10);
945948

946949
// Trigger rent through call
947950
assert!(trigger_call());
948-
assert!(ContractInfoOf::<Test>::get(BOB).is_none());
949-
assert_eq!(Balances::free_balance(BOB), Balances::minimum_balance());
951+
assert_matches!(ContractInfoOf::<Test>::get(BOB), Some(ContractInfo::Tombstone(_)));
952+
assert_eq!(Balances::free_balance(BOB), subsistence_threshold);
950953

951954
// Advance blocks
952955
initialize_block(20);
953956

954957
// Trigger rent must have no effect
955958
assert!(trigger_call());
956-
assert!(ContractInfoOf::<Test>::get(BOB).is_none());
957-
assert_eq!(Balances::free_balance(BOB), Balances::minimum_balance());
959+
assert_matches!(ContractInfoOf::<Test>::get(BOB), Some(ContractInfo::Tombstone(_)));
960+
assert_eq!(Balances::free_balance(BOB), subsistence_threshold);
958961
});
959962
}
960963

0 commit comments

Comments
 (0)