seal: Fix and improve error reporting (#6773)

* seal: Rework ext_transfer, ext_instantiate, ext_call error handling

* Deny calling plain accounts (must use transfer now)
* Return proper module error rather than ad-hoc strings
* Return the correct error codes from call,instantiate (documentation was wrong)
* Make ext_transfer fallible again to make it consistent with ext_call

* seal: Improve error messages on memory access failures

* seal: Convert contract trapped to module error

* seal: Add additional tests for transfer, call, instantiate

These tests verify that those functions return the error types
which are declared in its docs.

* Make it more pronounced that to_execution_result handles trap_reason

* Improve ReturnCode docs

* Fix whitespace issues in wat files

* Improve ReturnCode doc

* Improve ErrorOrigin doc and variant naming

* Improve docs on ExecResult and ExecError

* Encode u32 sentinel value as hex

* with_nested_context no longer accepts an Option for trie

* Fix successful typo

* Rename InvalidContractCalled to NotCallable
This commit is contained in:
Alexander Theißen
2020-08-03 12:03:22 +02:00
committed by GitHub
parent 0553dabe32
commit 6671d017d6
16 changed files with 855 additions and 265 deletions
+245 -22
View File
@@ -17,7 +17,7 @@
use crate::{
BalanceOf, ContractAddressFor, ContractInfo, ContractInfoOf, GenesisConfig, Module,
RawAliveContractInfo, RawEvent, Trait, TrieId, Schedule, TrieIdGenerator, gas::Gas,
Error,
Error, Config, RuntimeReturnCode,
};
use assert_matches::assert_matches;
use hex_literal::*;
@@ -30,8 +30,9 @@ use sp_runtime::{
use frame_support::{
assert_ok, assert_err_ignore_postinfo, impl_outer_dispatch, impl_outer_event,
impl_outer_origin, parameter_types, StorageMap, StorageValue,
traits::{Currency, Get},
traits::{Currency, Get, ReservableCurrency},
weights::{Weight, PostDispatchInfo},
dispatch::DispatchErrorWithPostInfo,
};
use std::cell::RefCell;
use frame_system::{self as system, EventRecord, Phase};
@@ -63,6 +64,7 @@ impl_outer_dispatch! {
}
}
#[macro_use]
pub mod test_utils {
use super::{Test, Balances};
use crate::{ContractInfoOf, TrieIdGenerator, CodeHash};
@@ -89,6 +91,12 @@ pub mod test_utils {
pub fn get_balance(who: &u64) -> u64 {
Balances::free_balance(who)
}
macro_rules! assert_return_code {
( $x:expr , $y:expr $(,)? ) => {{
use sp_std::convert::TryInto;
assert_eq!(u32::from_le_bytes($x.data[..].try_into().unwrap()), $y as u32);
}}
}
}
thread_local! {
@@ -279,19 +287,23 @@ where
Ok((wasm_binary, code_hash))
}
// Perform a simple transfer to a non-existent account.
// Perform a call to a plain account.
// The actual transfer fails because we can only call contracts.
// Then we check that only the base costs are returned as actual costs.
#[test]
fn returns_base_call_cost() {
fn calling_plain_account_fails() {
ExtBuilder::default().build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 100_000_000);
assert_eq!(
Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, Vec::new()),
Ok(
PostDispatchInfo {
actual_weight: Some(67500000),
pays_fee: Default::default(),
Err(
DispatchErrorWithPostInfo {
error: Error::<Test>::NotCallable.into(),
post_info: PostDispatchInfo {
actual_weight: Some(67500000),
pays_fee: Default::default(),
},
}
)
);
@@ -987,7 +999,7 @@ fn call_removed_contract() {
// Calling contract should remove contract and fail.
assert_err_ignore_postinfo!(
Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, call::null()),
"contract has been evicted"
Error::<Test>::NotCallable
);
// Calling a contract that is about to evict shall emit an event.
assert_eq!(System::events(), vec![
@@ -1001,7 +1013,7 @@ fn call_removed_contract() {
// Subsequent contract calls should also fail.
assert_err_ignore_postinfo!(
Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, call::null()),
"contract has been evicted"
Error::<Test>::NotCallable
);
})
}
@@ -1128,7 +1140,7 @@ fn restoration(test_different_storage: bool, test_restore_to_with_dirty_storage:
// we expect that it will get removed leaving tombstone.
assert_err_ignore_postinfo!(
Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, call::null()),
"contract has been evicted"
Error::<Test>::NotCallable
);
assert!(ContractInfoOf::<Test>::get(BOB).unwrap().get_tombstone().is_some());
assert_eq!(System::events(), vec![
@@ -1181,7 +1193,7 @@ fn restoration(test_different_storage: bool, test_restore_to_with_dirty_storage:
assert_err_ignore_postinfo!(
perform_the_restoration(),
"contract trapped during execution"
Error::<Test>::ContractTrapped,
);
assert!(ContractInfoOf::<Test>::get(BOB).unwrap().get_tombstone().is_some());
@@ -1309,7 +1321,7 @@ fn storage_max_value_limit() {
GAS_LIMIT,
Encode::encode(&(self::MaxValueSize::get() + 1)),
),
"contract trapped during execution"
Error::<Test>::ContractTrapped,
);
});
}
@@ -1373,17 +1385,16 @@ fn cannot_self_destruct_through_draning() {
Some(ContractInfo::Alive(_))
);
// Call BOB with no input data, forcing it to run until out-of-balance
// and eventually trapping because below existential deposit.
assert_err_ignore_postinfo!(
// Call BOB which makes it send all funds to the zero address
// The contract code asserts that the correct error value is returned.
assert_ok!(
Contracts::call(
Origin::signed(ALICE),
BOB,
0,
GAS_LIMIT,
vec![],
),
"contract trapped during execution"
)
);
});
}
@@ -1423,7 +1434,7 @@ fn cannot_self_destruct_while_live() {
GAS_LIMIT,
vec![0],
),
"contract trapped during execution"
Error::<Test>::ContractTrapped,
);
// Check that BOB is still alive.
@@ -1535,8 +1546,7 @@ fn cannot_self_destruct_in_constructor() {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
assert_ok!(Contracts::put_code(Origin::signed(ALICE), wasm));
// Fail to instantiate the BOB because the call that is issued in the deploy
// function exhausts all balances which puts it below the existential deposit.
// Fail to instantiate the BOB because the contructor calls ext_terminate.
assert_err_ignore_postinfo!(
Contracts::instantiate(
Origin::signed(ALICE),
@@ -1545,7 +1555,7 @@ fn cannot_self_destruct_in_constructor() {
code_hash.into(),
vec![],
),
"contract trapped during execution"
Error::<Test>::NewContractNotFunded,
);
});
}
@@ -1603,3 +1613,216 @@ fn crypto_hashes() {
}
})
}
#[test]
fn transfer_return_code() {
let (wasm, code_hash) = compile_module::<Test>("transfer_return_code").unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
let subsistence = Config::<Test>::subsistence_threshold_uncached();
let _ = Balances::deposit_creating(&ALICE, 10 * subsistence);
assert_ok!(Contracts::put_code(Origin::signed(ALICE), wasm));
assert_ok!(
Contracts::instantiate(
Origin::signed(ALICE),
subsistence,
GAS_LIMIT,
code_hash.into(),
vec![],
),
);
// Contract has only the minimal balance so any transfer will return BelowSubsistence.
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::BelowSubsistenceThreshold);
// Contract has enough total balance in order to not go below the subsistence
// threshold when transfering 100 balance but this balance is reserved so
// the transfer still fails but with another return code.
Balances::make_free_balance_be(&BOB, subsistence + 100);
Balances::reserve(&BOB, subsistence + 100).unwrap();
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::TransferFailed);
});
}
#[test]
fn call_return_code() {
let (caller_code, caller_hash) = compile_module::<Test>("call_return_code").unwrap();
let (callee_code, callee_hash) = compile_module::<Test>("ok_trap_revert").unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
let subsistence = Config::<Test>::subsistence_threshold_uncached();
let _ = Balances::deposit_creating(&ALICE, 10 * subsistence);
let _ = Balances::deposit_creating(&CHARLIE, 10 * subsistence);
assert_ok!(Contracts::put_code(Origin::signed(ALICE), caller_code));
assert_ok!(Contracts::put_code(Origin::signed(ALICE), callee_code));
assert_ok!(
Contracts::instantiate(
Origin::signed(ALICE),
subsistence,
GAS_LIMIT,
caller_hash.into(),
vec![0],
),
);
// Contract calls into Django which is no valid contract
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![0],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::NotCallable);
assert_ok!(
Contracts::instantiate(
Origin::signed(CHARLIE),
subsistence,
GAS_LIMIT,
callee_hash.into(),
vec![0],
),
);
// Contract has only the minimal balance so any transfer will return BelowSubsistence.
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![0],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::BelowSubsistenceThreshold);
// Contract has enough total balance in order to not go below the subsistence
// threshold when transfering 100 balance but this balance is reserved so
// the transfer still fails but with another return code.
Balances::make_free_balance_be(&BOB, subsistence + 100);
Balances::reserve(&BOB, subsistence + 100).unwrap();
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![0],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::TransferFailed);
// Contract has enough balance but callee reverts because "1" is passed.
Balances::make_free_balance_be(&BOB, subsistence + 1000);
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![1],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::CalleeReverted);
// Contract has enough balance but callee traps because "2" is passed.
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![2],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::CalleeTrapped);
});
}
#[test]
fn instantiate_return_code() {
let (caller_code, caller_hash) = compile_module::<Test>("instantiate_return_code").unwrap();
let (callee_code, callee_hash) = compile_module::<Test>("ok_trap_revert").unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
let subsistence = Config::<Test>::subsistence_threshold_uncached();
let _ = Balances::deposit_creating(&ALICE, 10 * subsistence);
let _ = Balances::deposit_creating(&CHARLIE, 10 * subsistence);
assert_ok!(Contracts::put_code(Origin::signed(ALICE), caller_code));
assert_ok!(Contracts::put_code(Origin::signed(ALICE), callee_code));
let callee_hash = callee_hash.as_ref().to_vec();
assert_ok!(
Contracts::instantiate(
Origin::signed(ALICE),
subsistence,
GAS_LIMIT,
caller_hash.into(),
vec![],
),
);
// Contract has only the minimal balance so any transfer will return BelowSubsistence.
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![0; 33],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::BelowSubsistenceThreshold);
// Contract has enough total balance in order to not go below the subsistence
// threshold when transfering 100 balance but this balance is reserved so
// the transfer still fails but with another return code.
Balances::make_free_balance_be(&BOB, subsistence + 100);
Balances::reserve(&BOB, subsistence + 100).unwrap();
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![0; 33],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::TransferFailed);
// Contract has enough balance but the passed code hash is invalid
Balances::make_free_balance_be(&BOB, subsistence + 1000);
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
vec![0; 33],
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::CodeNotFound);
// Contract has enough balance but callee reverts because "1" is passed.
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
callee_hash.iter().cloned().chain(sp_std::iter::once(1)).collect(),
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::CalleeReverted);
// Contract has enough balance but callee traps because "2" is passed.
let result = Contracts::bare_call(
ALICE,
BOB,
0,
GAS_LIMIT,
callee_hash.iter().cloned().chain(sp_std::iter::once(2)).collect(),
).0.unwrap();
assert_return_code!(result, RuntimeReturnCode::CalleeTrapped);
});
}