srml-contract: Fail calls to removed contracts instead of succeeding. (#2968)

* srml-contract: Refactor to reduce unnecessary storage lookups.

* srml-contract: Fail calls to removed contracts.

Previously, the calls would transfer funds and succeed without executing
any code on the target account, which is unintuitive behavior.

* Bump node runtime spec/impl versions.
This commit is contained in:
Jim Posen
2019-07-04 14:08:40 +02:00
committed by Sergei Pepyakin
parent 51e345c901
commit beea27b0f3
3 changed files with 94 additions and 35 deletions
+22 -11
View File
@@ -15,13 +15,14 @@
// along with Substrate. If not, see <http://www.gnu.org/licenses/>. // along with Substrate. If not, see <http://www.gnu.org/licenses/>.
use super::{CodeHash, Config, ContractAddressFor, Event, RawEvent, Trait, use super::{CodeHash, Config, ContractAddressFor, Event, RawEvent, Trait,
TrieId, BalanceOf, ContractInfoOf}; TrieId, BalanceOf, ContractInfo};
use crate::account_db::{AccountDb, DirectAccountDb, OverlayAccountDb}; use crate::account_db::{AccountDb, DirectAccountDb, OverlayAccountDb};
use crate::gas::{Gas, GasMeter, Token, approx_gas_for_balance}; use crate::gas::{Gas, GasMeter, Token, approx_gas_for_balance};
use crate::rent;
use rstd::prelude::*; use rstd::prelude::*;
use runtime_primitives::traits::{Bounded, CheckedAdd, CheckedSub, Zero}; use runtime_primitives::traits::{Bounded, CheckedAdd, CheckedSub, Zero};
use srml_support::{StorageMap, traits::{WithdrawReason, Currency}}; use srml_support::traits::{WithdrawReason, Currency};
use timestamp; use timestamp;
pub type AccountIdOf<T> = <T as system::Trait>::AccountId; pub type AccountIdOf<T> = <T as system::Trait>::AccountId;
@@ -267,11 +268,11 @@ where
{ {
/// Create the top level execution context. /// Create the top level execution context.
/// ///
/// The specified `origin` address will be used as `sender` for /// The specified `origin` address will be used as `sender` for. The `origin` must be a regular
/// account (not a contract).
pub fn top_level(origin: T::AccountId, cfg: &'a Config<T>, vm: &'a V, loader: &'a L) -> Self { pub fn top_level(origin: T::AccountId, cfg: &'a Config<T>, vm: &'a V, loader: &'a L) -> Self {
ExecutionContext { ExecutionContext {
self_trie_id: <ContractInfoOf<T>>::get(&origin) self_trie_id: None,
.and_then(|i| i.as_alive().map(|i| i.trie_id.clone())),
self_account: origin, self_account: origin,
overlay: OverlayAccountDb::<T>::new(&DirectAccountDb), overlay: OverlayAccountDb::<T>::new(&DirectAccountDb),
depth: 0, depth: 0,
@@ -283,10 +284,11 @@ where
} }
} }
fn nested(&self, overlay: OverlayAccountDb<'a, T>, dest: T::AccountId) -> Self { fn nested(&self, overlay: OverlayAccountDb<'a, T>, dest: T::AccountId, trie_id: Option<TrieId>)
-> Self
{
ExecutionContext { ExecutionContext {
self_trie_id: <ContractInfoOf<T>>::get(&dest) self_trie_id: trie_id,
.and_then(|i| i.as_alive().map(|i| i.trie_id.clone())),
self_account: dest, self_account: dest,
overlay, overlay,
depth: self.depth + 1, depth: self.depth + 1,
@@ -321,14 +323,20 @@ where
// Assumption: pay_rent doesn't collide with overlay because // Assumption: pay_rent doesn't collide with overlay because
// pay_rent will be done on first call and dest contract and balance // pay_rent will be done on first call and dest contract and balance
// cannot be changed before the first call // cannot be changed before the first call
crate::rent::pay_rent::<T>(&dest); let contract_info = rent::pay_rent::<T>(&dest);
// Calls to dead contracts always fail.
if let Some(ContractInfo::Tombstone(_)) = contract_info {
return Err("contract has been evicted");
};
let mut output_data = Vec::new(); let mut output_data = Vec::new();
let (change_set, events, calls) = { let (change_set, events, calls) = {
let mut nested = self.nested( let mut nested = self.nested(
OverlayAccountDb::new(&self.overlay), OverlayAccountDb::new(&self.overlay),
dest.clone() dest.clone(),
contract_info.and_then(|i| i.as_alive().map(|i| i.trie_id.clone()))
); );
if value > BalanceOf::<T>::zero() { if value > BalanceOf::<T>::zero() {
@@ -342,6 +350,8 @@ where
)?; )?;
} }
// If code_hash is not none, then the destination account is a live contract, otherwise
// it is a regular account since tombstone accounts have already been rejected.
if let Some(dest_code_hash) = self.overlay.get_code_hash(&dest) { if let Some(dest_code_hash) = self.overlay.get_code_hash(&dest) {
let executable = self.loader.load_main(&dest_code_hash)?; let executable = self.loader.load_main(&dest_code_hash)?;
output_data = self output_data = self
@@ -400,7 +410,8 @@ where
overlay.create_contract(&dest, code_hash.clone())?; overlay.create_contract(&dest, code_hash.clone())?;
let mut nested = self.nested(overlay, dest.clone()); // TrieId has not been generated yet and storage is empty since contract is new.
let mut nested = self.nested(overlay, dest.clone(), None);
// Send funds unconditionally here. If the `endowment` is below existential_deposit // Send funds unconditionally here. If the `endowment` is below existential_deposit
// then error will be returned here. // then error will be returned here.
+23 -21
View File
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>. // along with Substrate. If not, see <http://www.gnu.org/licenses/>.
use crate::{BalanceOf, ContractInfo, ContractInfoOf, TombstoneContractInfo, Trait}; use crate::{BalanceOf, ContractInfo, ContractInfoOf, TombstoneContractInfo, Trait, AliveContractInfo};
use runtime_primitives::traits::{Bounded, CheckedDiv, CheckedMul, Saturating, Zero, use runtime_primitives::traits::{Bounded, CheckedDiv, CheckedMul, Saturating, Zero,
SaturatedConversion}; SaturatedConversion};
use srml_support::traits::{Currency, ExistenceRequirement, Get, WithdrawReason}; use srml_support::traits::{Currency, ExistenceRequirement, Get, WithdrawReason};
@@ -50,9 +50,10 @@ fn try_evict_or_and_pay_rent<T: Trait>(
account: &T::AccountId, account: &T::AccountId,
handicap: T::BlockNumber, handicap: T::BlockNumber,
pay_rent: bool, pay_rent: bool,
) -> RentOutcome { ) -> (RentOutcome, Option<ContractInfo<T>>) {
let contract = match <ContractInfoOf<T>>::get(account) { let contract_info = <ContractInfoOf<T>>::get(account);
None | Some(ContractInfo::Tombstone(_)) => return RentOutcome::Exempted, let contract = match contract_info {
None | Some(ContractInfo::Tombstone(_)) => return (RentOutcome::Exempted, contract_info),
Some(ContractInfo::Alive(contract)) => contract, Some(ContractInfo::Alive(contract)) => contract,
}; };
@@ -65,7 +66,7 @@ fn try_evict_or_and_pay_rent<T: Trait>(
let n = effective_block_number.saturating_sub(contract.deduct_block); let n = effective_block_number.saturating_sub(contract.deduct_block);
if n.is_zero() { if n.is_zero() {
// Rent has already been paid // Rent has already been paid
return RentOutcome::Exempted; return (RentOutcome::Exempted, Some(ContractInfo::Alive(contract)));
} }
n n
}; };
@@ -89,7 +90,7 @@ fn try_evict_or_and_pay_rent<T: Trait>(
if fee_per_block.is_zero() { if fee_per_block.is_zero() {
// The rent deposit offset reduced the fee to 0. This means that the contract // The rent deposit offset reduced the fee to 0. This means that the contract
// gets the rent for free. // gets the rent for free.
return RentOutcome::Exempted; return (RentOutcome::Exempted, Some(ContractInfo::Alive(contract)));
} }
// The minimal amount of funds required for a contract not to be evicted. // The minimal amount of funds required for a contract not to be evicted.
@@ -99,7 +100,7 @@ fn try_evict_or_and_pay_rent<T: Trait>(
// The contract cannot afford to leave a tombstone, so remove the contract info altogether. // The contract cannot afford to leave a tombstone, so remove the contract info altogether.
<ContractInfoOf<T>>::remove(account); <ContractInfoOf<T>>::remove(account);
runtime_io::kill_child_storage(&contract.trie_id); runtime_io::kill_child_storage(&contract.trie_id);
return RentOutcome::Evicted; return (RentOutcome::Evicted, None);
} }
let dues = fee_per_block let dues = fee_per_block
@@ -149,33 +150,34 @@ fn try_evict_or_and_pay_rent<T: Trait>(
&child_storage_root[..], &child_storage_root[..],
contract.code_hash, contract.code_hash,
); );
<ContractInfoOf<T>>::insert(account, ContractInfo::Tombstone(tombstone)); let tombstone_info = ContractInfo::Tombstone(tombstone);
<ContractInfoOf<T>>::insert(account, &tombstone_info);
runtime_io::kill_child_storage(&contract.trie_id); runtime_io::kill_child_storage(&contract.trie_id);
return RentOutcome::Evicted; return (RentOutcome::Evicted, Some(tombstone_info));
} }
if pay_rent { if pay_rent {
<ContractInfoOf<T>>::mutate(account, |contract| { let contract_info = ContractInfo::Alive(AliveContractInfo::<T> {
let contract = contract rent_allowance: contract.rent_allowance - dues, // rent_allowance is not exceeded
.as_mut() deduct_block: current_block_number,
.and_then(|c| c.as_alive_mut()) ..contract
.expect("Dead or nonexistent account has been exempt above; qed"); });
contract.rent_allowance -= dues; // rent_allowance is not exceeded <ContractInfoOf<T>>::insert(account, &contract_info);
contract.deduct_block = current_block_number;
}) return (RentOutcome::Ok, Some(contract_info));
} }
RentOutcome::Ok (RentOutcome::Ok, Some(ContractInfo::Alive(contract)))
} }
/// Make account paying the rent for the current block number /// Make account paying the rent for the current block number
/// ///
/// NOTE: This function acts eagerly. /// NOTE: This function acts eagerly.
pub fn pay_rent<T: Trait>(account: &T::AccountId) { pub fn pay_rent<T: Trait>(account: &T::AccountId) -> Option<ContractInfo<T>> {
let _ = try_evict_or_and_pay_rent::<T>(account, Zero::zero(), true); try_evict_or_and_pay_rent::<T>(account, Zero::zero(), true).1
} }
/// Evict the account if it should be evicted at the given block number. /// Evict the account if it should be evicted at the given block number.
@@ -186,5 +188,5 @@ pub fn pay_rent<T: Trait>(account: &T::AccountId) {
/// ///
/// NOTE: This function acts eagerly. /// NOTE: This function acts eagerly.
pub fn try_evict<T: Trait>(account: &T::AccountId, handicap: T::BlockNumber) -> RentOutcome { pub fn try_evict<T: Trait>(account: &T::AccountId, handicap: T::BlockNumber) -> RentOutcome {
try_evict_or_and_pay_rent::<T>(account, handicap, false) try_evict_or_and_pay_rent::<T>(account, handicap, false).0
} }
+49 -3
View File
@@ -927,7 +927,11 @@ fn deduct_blocks() {
#[test] #[test]
fn call_contract_removals() { fn call_contract_removals() {
removals(|| Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null()).is_ok()); removals(|| {
// Call on already-removed account might fail, and this is fine.
Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null());
true
});
} }
#[test] #[test]
@@ -1115,6 +1119,45 @@ fn removals(trigger_call: impl Fn() -> bool) {
); );
} }
#[test]
fn call_removed_contract() {
let wasm = wabt::wat2wasm(CODE_SET_RENT).unwrap();
// Balance reached and superior to subsistence threshold
with_externalities(
&mut ExtBuilder::default().existential_deposit(50).build(),
|| {
// Create
Balances::deposit_creating(&ALICE, 1_000_000);
assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm.clone()));
assert_ok!(Contract::create(
Origin::signed(ALICE),
100,
100_000, HASH_SET_RENT.into(),
<Test as balances::Trait>::Balance::from(1_000u32).encode() // rent allowance
));
// Calling contract should succeed.
assert_ok!(Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null()));
// Advance blocks
System::initialize(&10, &[0u8; 32].into(), &[0u8; 32].into(), &Default::default());
// Calling contract should remove contract and fail.
assert_err!(
Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null()),
"contract has been evicted"
);
// Subsequent contract calls should also fail.
assert_err!(
Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null()),
"contract has been evicted"
);
}
)
}
const CODE_CHECK_DEFAULT_RENT_ALLOWANCE: &str = r#" const CODE_CHECK_DEFAULT_RENT_ALLOWANCE: &str = r#"
(module (module
(import "env" "ext_rent_allowance" (func $ext_rent_allowance)) (import "env" "ext_rent_allowance" (func $ext_rent_allowance))
@@ -1294,7 +1337,7 @@ fn restoration(test_different_storage: bool, test_restore_to_with_dirty_storage:
// same copy of this (modulo hex notation differences) in `CODE_RESTORATION`. // same copy of this (modulo hex notation differences) in `CODE_RESTORATION`.
// //
// When this assert is triggered make sure that you update the literals here and in // When this assert is triggered make sure that you update the literals here and in
// `CODE_RESTORATION`. Hopefully, we switch to automatical injection of the code. // `CODE_RESTORATION`. Hopefully, we switch to automatic injection of the code.
const ENCODED_CALL_LITERAL: &str = const ENCODED_CALL_LITERAL: &str =
"0105020000000000000069aedfb4f6c1c398e97f8a5204de0f95ad5e7dc3540960beab11a86c569fbfcf320000\ "0105020000000000000069aedfb4f6c1c398e97f8a5204de0f95ad5e7dc3540960beab11a86c569fbfcf320000\
0000000000080100000000000000000000000000000000000000000000000000000000000000010000000000000\ 0000000000080100000000000000000000000000000000000000000000000000000000000000010000000000000\
@@ -1368,7 +1411,10 @@ fn restoration(test_different_storage: bool, test_restore_to_with_dirty_storage:
// Call `BOB`, which makes it pay rent. Since the rent allowance is set to 0 // Call `BOB`, which makes it pay rent. Since the rent allowance is set to 0
// we expect that it will get removed leaving tombstone. // we expect that it will get removed leaving tombstone.
assert_ok!(Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null())); assert_err!(
Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null()),
"contract has been evicted"
);
assert!(ContractInfoOf::<Test>::get(BOB).unwrap().get_tombstone().is_some()); assert!(ContractInfoOf::<Test>::get(BOB).unwrap().get_tombstone().is_some());
/// Create another account with the address `DJANGO` with `CODE_RESTORATION`. /// Create another account with the address `DJANGO` with `CODE_RESTORATION`.