mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-24 17:05:47 +00:00
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:
committed by
GitHub
parent
0553dabe32
commit
6671d017d6
@@ -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);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user