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
@@ -0,0 +1,44 @@
;; This calls Django (4) and transfers 100 balance during this call and copies the return code
;; of this call to the output buffer.
;; It also forwards its input to the callee.
(module
(import "env" "ext_input" (func $ext_input (param i32 i32)))
(import "env" "ext_call" (func $ext_call (param i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32)))
(import "env" "ext_return" (func $ext_return (param i32 i32 i32)))
(import "env" "memory" (memory 1 1))
;; [0, 8) address of django
(data (i32.const 0) "\04\00\00\00\00\00\00\00")
;; [8, 16) 100 balance
(data (i32.const 8) "\64\00\00\00\00\00\00\00")
;; [16, 20) here we store the return code of the transfer
;; [20, 24) here we store the input data
;; [24, 28) size of the input data
(data (i32.const 24) "\04")
(func (export "deploy"))
(func (export "call")
(call $ext_input (i32.const 20) (i32.const 24))
(i32.store
(i32.const 16)
(call $ext_call
(i32.const 0) ;; Pointer to "callee" address.
(i32.const 8) ;; Length of "callee" address.
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 8) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer.
(i32.const 20) ;; Pointer to input data buffer address
(i32.load (i32.const 24)) ;; Length of input data buffer
(i32.const 0xffffffff) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Ptr to output buffer len
)
)
;; exit with success and take transfer return code to the output buffer
(call $ext_return (i32.const 0) (i32.const 16) (i32.const 4))
)
)
@@ -89,7 +89,7 @@
(call $ext_instantiate (call $ext_instantiate
(i32.const 24) ;; Pointer to the code hash. (i32.const 24) ;; Pointer to the code hash.
(i32.const 32) ;; Length of the code hash. (i32.const 32) ;; Length of the code hash.
(i64.const 200) ;; How much gas to devote for the execution. (i64.const 187500000) ;; Just enough to pay for the instantiate
(i32.const 0) ;; Pointer to the buffer with value to transfer (i32.const 0) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 8) ;; Length of the buffer with value to transfer.
(i32.const 8) ;; Pointer to input data buffer address (i32.const 8) ;; Pointer to input data buffer address
@@ -206,7 +206,7 @@
(call $ext_call (call $ext_call
(i32.const 16) ;; Pointer to "callee" address. (i32.const 16) ;; Pointer to "callee" address.
(i32.const 8) ;; Length of "callee" address. (i32.const 8) ;; Length of "callee" address.
(i64.const 100) ;; How much gas to devote for the execution. (i64.const 117500000) ;; Just enough to make the call
(i32.const 0) ;; Pointer to the buffer with value to transfer (i32.const 0) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 8) ;; Length of the buffer with value to transfer.
(i32.const 8) ;; Pointer to input data buffer address (i32.const 8) ;; Pointer to input data buffer address
@@ -3,6 +3,7 @@
(import "env" "ext_get_storage" (func $ext_get_storage (param i32 i32 i32) (result i32))) (import "env" "ext_get_storage" (func $ext_get_storage (param i32 i32 i32) (result i32)))
(import "env" "ext_set_storage" (func $ext_set_storage (param i32 i32 i32))) (import "env" "ext_set_storage" (func $ext_set_storage (param i32 i32 i32)))
(import "env" "ext_call" (func $ext_call (param i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32))) (import "env" "ext_call" (func $ext_call (param i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32)))
(import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32 i32) (result i32)))
(import "env" "ext_instantiate" (func $ext_instantiate (param i32 i32 i64 i32 i32 i32 i32 i32 i32 i32 i32) (result i32))) (import "env" "ext_instantiate" (func $ext_instantiate (param i32 i32 i64 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1)) (import "env" "memory" (memory 1 1))
@@ -139,16 +140,11 @@
;; does not keep the contract alive. ;; does not keep the contract alive.
(call $assert (call $assert
(i32.eq (i32.eq
(call $ext_call (call $ext_transfer
(i32.const 80) ;; Pointer to destination address (i32.const 80) ;; Pointer to destination address
(i32.const 8) ;; Length of destination address (i32.const 8) ;; Length of destination address
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 0) ;; Pointer to the buffer with value to transfer (i32.const 0) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer (i32.const 8) ;; Length of the buffer with value to transfer
(i32.const 0) ;; Pointer to input data buffer address
(i32.const 1) ;; Length of input data buffer
(i32.const 4294967295) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Length is ignored in this case
) )
(i32.const 0) (i32.const 0)
) )
+3 -8
View File
@@ -1,6 +1,6 @@
(module (module
(import "env" "ext_balance" (func $ext_balance (param i32 i32))) (import "env" "ext_balance" (func $ext_balance (param i32 i32)))
(import "env" "ext_call" (func $ext_call (param i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32))) (import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1)) (import "env" "memory" (memory 1 1))
;; [0, 8) reserved for $ext_balance output ;; [0, 8) reserved for $ext_balance output
@@ -36,18 +36,13 @@
;; Self-destruct by sending full balance to the 0 address. ;; Self-destruct by sending full balance to the 0 address.
(call $assert (call $assert
(i32.eq (i32.eq
(call $ext_call (call $ext_transfer
(i32.const 16) ;; Pointer to destination address (i32.const 16) ;; Pointer to destination address
(i32.const 8) ;; Length of destination address (i32.const 8) ;; Length of destination address
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 0) ;; Pointer to the buffer with value to transfer (i32.const 0) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer (i32.const 8) ;; Length of the buffer with value to transfer
(i32.const 0) ;; Pointer to input data buffer address
(i32.const 0) ;; Length of input data buffer
(i32.const 4294967295) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Length is ignored in this case
) )
(i32.const 0) (i32.const 4) ;; ReturnCode::BelowSubsistenceThreshold
) )
) )
) )
@@ -0,0 +1,47 @@
;; This instantiats Charlie (3) and transfers 100 balance during this call and copies the return code
;; of this call to the output buffer.
;; The first 32 byte of input is the code hash to instantiate
;; The rest of the input is forwarded to the constructor of the callee
(module
(import "env" "ext_input" (func $ext_input (param i32 i32)))
(import "env" "ext_instantiate" (func $ext_instantiate (param i32 i32 i64 i32 i32 i32 i32 i32 i32 i32 i32) (result i32)))
(import "env" "ext_return" (func $ext_return (param i32 i32 i32)))
(import "env" "memory" (memory 1 1))
;; [0, 8) address of django
(data (i32.const 0) "\04\00\00\00\00\00\00\00")
;; [8, 16) 100 balance
(data (i32.const 8) "\64\00\00\00\00\00\00\00")
;; [16, 20) here we store the return code of the transfer
;; [20, 24) size of the input buffer
(data (i32.const 20) "\FF")
;; [24, inf) input buffer
(func (export "deploy"))
(func (export "call")
(call $ext_input (i32.const 24) (i32.const 20))
(i32.store
(i32.const 16)
(call $ext_instantiate
(i32.const 24) ;; Pointer to the code hash.
(i32.const 32) ;; Length of the code hash.
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 8) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer.
(i32.const 56) ;; Pointer to input data buffer address
(i32.sub (i32.load (i32.const 20)) (i32.const 32)) ;; Length of input data buffer
(i32.const 0xffffffff) ;; u32 max sentinel value: do not copy address
(i32.const 0) ;; Length is ignored in this case
(i32.const 0xffffffff) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Length is ignored in this case
)
)
;; exit with success and take transfer return code to the output buffer
(call $ext_return (i32.const 0) (i32.const 16) (i32.const 4))
)
)
@@ -0,0 +1,35 @@
(module
(import "env" "ext_input" (func $ext_input (param i32 i32)))
(import "env" "ext_return" (func $ext_return (param i32 i32 i32)))
(import "env" "memory" (memory 1 1))
(func (export "deploy")
(call $ok_trap_revert)
)
(func (export "call")
(call $ok_trap_revert)
)
(func $ok_trap_revert
(i32.store (i32.const 4) (i32.const 4))
(call $ext_input (i32.const 0) (i32.const 4))
(block $IF_2
(block $IF_1
(block $IF_0
(br_table $IF_0 $IF_1 $IF_2
(i32.load8_u (i32.const 0))
)
(unreachable)
)
;; 0 = return with success
return
)
;; 1 = revert
(call $ext_return (i32.const 1) (i32.const 0) (i32.const 0))
(unreachable)
)
;; 2 = trap
(unreachable)
)
)
@@ -1,15 +1,7 @@
(module (module
(import "env" "ext_balance" (func $ext_balance (param i32 i32))) (import "env" "ext_terminate" (func $ext_terminate (param i32 i32)))
(import "env" "ext_call" (func $ext_call (param i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1)) (import "env" "memory" (memory 1 1))
;; [0, 8) reserved for $ext_balance output
;; [8, 16) length of the buffer
(data (i32.const 8) "\08")
;; [16, inf) zero initialized
(func $assert (param i32) (func $assert (param i32)
(block $ok (block $ok
(br_if $ok (br_if $ok
@@ -20,33 +12,10 @@
) )
(func (export "deploy") (func (export "deploy")
;; Send entire remaining balance to the 0 address.
(call $ext_balance (i32.const 0) (i32.const 8))
;; Balance should be encoded as a u64.
(call $assert
(i32.eq
(i32.load (i32.const 8))
(i32.const 8)
)
)
;; Self-destruct by sending full balance to the 0 address. ;; Self-destruct by sending full balance to the 0 address.
(call $assert (call $ext_terminate
(i32.eq (i32.const 0) ;; Pointer to destination address
(call $ext_call
(i32.const 16) ;; Pointer to destination address
(i32.const 8) ;; Length of destination address (i32.const 8) ;; Length of destination address
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 0) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer
(i32.const 0) ;; Pointer to input data buffer address
(i32.const 0) ;; Length of input data buffer
(i32.const 4294967295) ;; u32 max sentinel value: do not copy output
(i32.const 0) ;; Length is ignored in this case
)
(i32.const 0)
)
) )
) )
@@ -1,5 +1,5 @@
(module (module
(import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32 i32))) (import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32 i32) (result i32)))
(import "env" "ext_set_storage" (func $ext_set_storage (param i32 i32 i32))) (import "env" "ext_set_storage" (func $ext_set_storage (param i32 i32 i32)))
(import "env" "ext_clear_storage" (func $ext_clear_storage (param i32))) (import "env" "ext_clear_storage" (func $ext_clear_storage (param i32)))
(import "env" "ext_set_rent_allowance" (func $ext_set_rent_allowance (param i32 i32))) (import "env" "ext_set_rent_allowance" (func $ext_set_rent_allowance (param i32 i32)))
@@ -24,7 +24,12 @@
;; transfer 50 to CHARLIE ;; transfer 50 to CHARLIE
(func $call_2 (func $call_2
(call $assert
(i32.eq
(call $ext_transfer (i32.const 68) (i32.const 8) (i32.const 76) (i32.const 8)) (call $ext_transfer (i32.const 68) (i32.const 8) (i32.const 76) (i32.const 8))
(i32.const 0)
)
)
) )
;; do nothing ;; do nothing
@@ -0,0 +1,31 @@
;; This transfers 100 balance to the zero account and copies the return code
;; of this transfer to the output buffer.
(module
(import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32 i32) (result i32)))
(import "env" "ext_return" (func $ext_return (param i32 i32 i32)))
(import "env" "memory" (memory 1 1))
;; [0, 8) zero-adress
(data (i32.const 0) "\00\00\00\00\00\00\00\00")
;; [8, 16) 100 balance
(data (i32.const 8) "\64\00\00\00\00\00\00\00")
;; [16, 20) here we store the return code of the transfer
(func (export "deploy"))
(func (export "call")
(i32.store
(i32.const 16)
(call $ext_transfer
(i32.const 0) ;; ptr to destination address
(i32.const 8) ;; length of destination address
(i32.const 8) ;; ptr to value to transfer
(i32.const 8) ;; length of value to transfer
)
)
;; exit with success and take transfer return code to the output buffer
(call $ext_return (i32.const 0) (i32.const 16) (i32.const 4))
)
)
+126 -65
View File
@@ -14,9 +14,11 @@
// 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 super::{CodeHash, Config, ContractAddressFor, Event, RawEvent, Trait, use crate::{
TrieId, BalanceOf, ContractInfo, TrieIdGenerator}; CodeHash, Config, ContractAddressFor, Event, RawEvent, Trait,
use crate::{gas::{Gas, GasMeter, Token}, rent, storage, Error, ContractInfoOf}; TrieId, BalanceOf, ContractInfo, TrieIdGenerator,
gas::{Gas, GasMeter, Token}, rent, storage, Error, ContractInfoOf
};
use bitflags::bitflags; use bitflags::bitflags;
use sp_std::prelude::*; use sp_std::prelude::*;
use sp_runtime::traits::{Bounded, Zero, Convert, Saturating}; use sp_runtime::traits::{Bounded, Zero, Convert, Saturating};
@@ -69,7 +71,39 @@ impl ExecReturnValue {
} }
} }
pub type ExecResult = Result<ExecReturnValue, DispatchError>; /// Call or instantiate both call into other contracts and pass through errors happening
/// in those to the caller. This enum is for the caller to distinguish whether the error
/// happened during the execution of the callee or in the current execution context.
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub enum ErrorOrigin {
/// The error happened in the current exeuction context rather than in the one
/// of the contract that is called into.
Caller,
/// The error happened during execution of the called contract.
Callee,
}
/// Error returned by contract exection.
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
pub struct ExecError {
/// The reason why the execution failed.
pub error: DispatchError,
/// Origin of the error.
pub origin: ErrorOrigin,
}
impl<T: Into<DispatchError>> From<T> for ExecError {
fn from(error: T) -> Self {
Self {
error: error.into(),
origin: ErrorOrigin::Caller,
}
}
}
/// The result that is returned from contract execution. It either contains the output
/// buffer or an error describing the reason for failure.
pub type ExecResult = Result<ExecReturnValue, ExecError>;
/// An interface that provides access to the external environment in which the /// An interface that provides access to the external environment in which the
/// smart-contract is executed. /// smart-contract is executed.
@@ -99,7 +133,7 @@ pub trait Ext {
value: BalanceOf<Self::T>, value: BalanceOf<Self::T>,
gas_meter: &mut GasMeter<Self::T>, gas_meter: &mut GasMeter<Self::T>,
input_data: Vec<u8>, input_data: Vec<u8>,
) -> Result<(AccountIdOf<Self::T>, ExecReturnValue), DispatchError>; ) -> Result<(AccountIdOf<Self::T>, ExecReturnValue), ExecError>;
/// Transfer some amount of funds into the specified account. /// Transfer some amount of funds into the specified account.
fn transfer( fn transfer(
@@ -282,12 +316,12 @@ where
} }
} }
fn nested<'b, 'c: 'b>(&'c self, dest: T::AccountId, trie_id: Option<TrieId>) fn nested<'b, 'c: 'b>(&'c self, dest: T::AccountId, trie_id: TrieId)
-> ExecutionContext<'b, T, V, L> -> ExecutionContext<'b, T, V, L>
{ {
ExecutionContext { ExecutionContext {
caller: Some(self), caller: Some(self),
self_trie_id: trie_id, self_trie_id: Some(trie_id),
self_account: dest, self_account: dest,
depth: self.depth + 1, depth: self.depth + 1,
config: self.config, config: self.config,
@@ -307,31 +341,31 @@ where
input_data: Vec<u8>, input_data: Vec<u8>,
) -> ExecResult { ) -> ExecResult {
if self.depth == self.config.max_depth as usize { if self.depth == self.config.max_depth as usize {
Err("reached maximum depth, cannot make a call")? Err(Error::<T>::MaxCallDepthReached)?
} }
if gas_meter if gas_meter
.charge(self.config, ExecFeeToken::Call) .charge(self.config, ExecFeeToken::Call)
.is_out_of_gas() .is_out_of_gas()
{ {
Err("not enough gas to pay base call fee")? Err(Error::<T>::OutOfGas)?
} }
// Assumption: `collect_rent` doesn't collide with overlay because // Assumption: `collect_rent` doesn't collide with overlay because
// `collect_rent` will be done on first call and destination contract and balance // `collect_rent` will be done on first call and destination contract and balance
// cannot be changed before the first call // cannot be changed before the first call
let contract_info = rent::collect_rent::<T>(&dest); // We do not allow 'calling' plain accounts. For transfering value
// `ext_transfer` must be used.
// Calls to dead contracts always fail. let contract = if let Some(ContractInfo::Alive(info)) = rent::collect_rent::<T>(&dest) {
if let Some(ContractInfo::Tombstone(_)) = contract_info { info
Err("contract has been evicted")? } else {
Err(Error::<T>::NotCallable)?
}; };
let transactor_kind = self.transactor_kind(); let transactor_kind = self.transactor_kind();
let caller = self.self_account.clone(); let caller = self.self_account.clone();
let dest_trie_id = contract_info.and_then(|i| i.as_alive().map(|i| i.trie_id.clone()));
self.with_nested_context(dest.clone(), dest_trie_id, |nested| { self.with_nested_context(dest.clone(), contract.trie_id.clone(), |nested| {
if value > BalanceOf::<T>::zero() { if value > BalanceOf::<T>::zero() {
transfer( transfer(
gas_meter, gas_meter,
@@ -344,22 +378,15 @@ where
)? )?
} }
// If code_hash is not none, then the destination account is a live contract, otherwise let executable = nested.loader.load_main(&contract.code_hash)
// it is a regular account since tombstone accounts have already been rejected. .map_err(|_| Error::<T>::CodeNotFound)?;
match storage::code_hash::<T>(&dest) { let output = nested.vm.execute(
Ok(dest_code_hash) => {
let executable = nested.loader.load_main(&dest_code_hash)?;
let output = nested.vm
.execute(
&executable, &executable,
nested.new_call_context(caller, value), nested.new_call_context(caller, value),
input_data, input_data,
gas_meter, gas_meter,
)?; ).map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee })?;
Ok(output) Ok(output)
}
Err(storage::ContractAbsentError) => Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: Vec::new() }),
}
}) })
} }
@@ -369,16 +396,16 @@ where
gas_meter: &mut GasMeter<T>, gas_meter: &mut GasMeter<T>,
code_hash: &CodeHash<T>, code_hash: &CodeHash<T>,
input_data: Vec<u8>, input_data: Vec<u8>,
) -> Result<(T::AccountId, ExecReturnValue), DispatchError> { ) -> Result<(T::AccountId, ExecReturnValue), ExecError> {
if self.depth == self.config.max_depth as usize { if self.depth == self.config.max_depth as usize {
Err("reached maximum depth, cannot instantiate")? Err(Error::<T>::MaxCallDepthReached)?
} }
if gas_meter if gas_meter
.charge(self.config, ExecFeeToken::Instantiate) .charge(self.config, ExecFeeToken::Instantiate)
.is_out_of_gas() .is_out_of_gas()
{ {
Err("not enough gas to pay base instantiate fee")? Err(Error::<T>::OutOfGas)?
} }
let transactor_kind = self.transactor_kind(); let transactor_kind = self.transactor_kind();
@@ -394,7 +421,7 @@ where
// Generate it now. // Generate it now.
let dest_trie_id = <T as Trait>::TrieIdGenerator::trie_id(&dest); let dest_trie_id = <T as Trait>::TrieIdGenerator::trie_id(&dest);
let output = self.with_nested_context(dest.clone(), Some(dest_trie_id), |nested| { let output = self.with_nested_context(dest.clone(), dest_trie_id, |nested| {
storage::place_contract::<T>( storage::place_contract::<T>(
&dest, &dest,
nested nested
@@ -416,21 +443,21 @@ where
nested, nested,
)?; )?;
let executable = nested.loader.load_init(&code_hash)?; let executable = nested.loader.load_init(&code_hash)
.map_err(|_| Error::<T>::CodeNotFound)?;
let output = nested.vm let output = nested.vm
.execute( .execute(
&executable, &executable,
nested.new_call_context(caller.clone(), endowment), nested.new_call_context(caller.clone(), endowment),
input_data, input_data,
gas_meter, gas_meter,
)?; ).map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee })?;
// Error out if insufficient remaining balance.
// We need each contract that exists to be above the subsistence threshold // We need each contract that exists to be above the subsistence threshold
// in order to keep up the guarantuee that we always leave a tombstone behind // in order to keep up the guarantuee that we always leave a tombstone behind
// with the exception of a contract that called `ext_terminate`. // with the exception of a contract that called `ext_terminate`.
if T::Currency::free_balance(&dest) < nested.config.subsistence_threshold() { if T::Currency::total_balance(&dest) < nested.config.subsistence_threshold() {
Err("insufficient remaining balance")? Err(Error::<T>::NewContractNotFunded)?
} }
// Deposit an instantiation event. // Deposit an instantiation event.
@@ -459,7 +486,7 @@ where
} }
/// Execute the given closure within a nested execution context. /// Execute the given closure within a nested execution context.
fn with_nested_context<F>(&mut self, dest: T::AccountId, trie_id: Option<TrieId>, func: F) fn with_nested_context<F>(&mut self, dest: T::AccountId, trie_id: TrieId, func: F)
-> ExecResult -> ExecResult
where F: FnOnce(&mut ExecutionContext<T, V, L>) -> ExecResult where F: FnOnce(&mut ExecutionContext<T, V, L>) -> ExecResult
{ {
@@ -569,7 +596,7 @@ fn transfer<'a, T: Trait, V: Vm<T>, L: Loader<T>>(
}; };
if gas_meter.charge(ctx.config, token).is_out_of_gas() { if gas_meter.charge(ctx.config, token).is_out_of_gas() {
Err("not enough gas to pay transfer fee")? Err(Error::<T>::OutOfGas)?
} }
// Only ext_terminate is allowed to bring the sender below the subsistence // Only ext_terminate is allowed to bring the sender below the subsistence
@@ -580,13 +607,15 @@ fn transfer<'a, T: Trait, V: Vm<T>, L: Loader<T>>(
ensure!( ensure!(
T::Currency::total_balance(transactor).saturating_sub(value) >= T::Currency::total_balance(transactor).saturating_sub(value) >=
ctx.config.subsistence_threshold(), ctx.config.subsistence_threshold(),
Error::<T>::InsufficientBalance, Error::<T>::BelowSubsistenceThreshold,
); );
ExistenceRequirement::KeepAlive ExistenceRequirement::KeepAlive
}, },
(_, PlainAccount) => ExistenceRequirement::KeepAlive, (_, PlainAccount) => ExistenceRequirement::KeepAlive,
}; };
T::Currency::transfer(transactor, dest, value, existence_requirement)?;
T::Currency::transfer(transactor, dest, value, existence_requirement)
.map_err(|_| Error::<T>::TransferFailed)?;
Ok(()) Ok(())
} }
@@ -653,7 +682,7 @@ where
endowment: BalanceOf<T>, endowment: BalanceOf<T>,
gas_meter: &mut GasMeter<T>, gas_meter: &mut GasMeter<T>,
input_data: Vec<u8>, input_data: Vec<u8>,
) -> Result<(AccountIdOf<T>, ExecReturnValue), DispatchError> { ) -> Result<(AccountIdOf<T>, ExecReturnValue), ExecError> {
self.ctx.instantiate(endowment, gas_meter, code_hash, input_data) self.ctx.instantiate(endowment, gas_meter, code_hash, input_data)
} }
@@ -837,13 +866,13 @@ fn deposit_event<T: Trait>(
mod tests { mod tests {
use super::{ use super::{
BalanceOf, Event, ExecFeeToken, ExecResult, ExecutionContext, Ext, Loader, BalanceOf, Event, ExecFeeToken, ExecResult, ExecutionContext, Ext, Loader,
RawEvent, TransferFeeKind, TransferFeeToken, Vm, ReturnFlags, RawEvent, TransferFeeKind, TransferFeeToken, Vm, ReturnFlags, ExecError, ErrorOrigin
}; };
use crate::{ use crate::{
gas::GasMeter, tests::{ExtBuilder, Test, MetaEvent}, gas::GasMeter, tests::{ExtBuilder, Test, MetaEvent},
exec::ExecReturnValue, CodeHash, Config, exec::ExecReturnValue, CodeHash, Config,
gas::Gas, gas::Gas,
storage, storage, Error
}; };
use crate::tests::test_utils::{place_contract, set_balance, get_balance}; use crate::tests::test_utils::{place_contract, set_balance, get_balance};
use sp_runtime::DispatchError; use sp_runtime::DispatchError;
@@ -999,11 +1028,19 @@ mod tests {
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT); let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let result = ctx.call(dest, 0, &mut gas_meter, vec![]); let result = super::transfer(
&mut gas_meter,
super::TransferCause::Call,
super::TransactorKind::PlainAccount,
&origin,
&dest,
0,
&mut ctx,
);
assert_matches!(result, Ok(_)); assert_matches!(result, Ok(_));
let mut toks = gas_meter.tokens().iter(); let mut toks = gas_meter.tokens().iter();
match_tokens!(toks, ExecFeeToken::Call,); match_tokens!(toks, TransferFeeToken { kind: TransferFeeKind::Transfer },);
}); });
// This test verifies that base fee for instantiation is taken. // This test verifies that base fee for instantiation is taken.
@@ -1043,14 +1080,18 @@ mod tests {
set_balance(&origin, 100); set_balance(&origin, 100);
set_balance(&dest, 0); set_balance(&dest, 0);
let output = ctx.call( let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
dest,
super::transfer(
&mut gas_meter,
super::TransferCause::Call,
super::TransactorKind::PlainAccount,
&origin,
&dest,
55, 55,
&mut GasMeter::<Test>::new(GAS_LIMIT), &mut ctx,
vec![],
).unwrap(); ).unwrap();
assert!(output.is_success());
assert_eq!(get_balance(&origin), 45); assert_eq!(get_balance(&origin), 45);
assert_eq!(get_balance(&dest), 55); assert_eq!(get_balance(&dest), 55);
}); });
@@ -1107,13 +1148,20 @@ mod tests {
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT); let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let result = ctx.call(dest, 50, &mut gas_meter, vec![]); let result = super::transfer(
&mut gas_meter,
super::TransferCause::Call,
super::TransactorKind::PlainAccount,
&origin,
&dest,
50,
&mut ctx,
);
assert_matches!(result, Ok(_)); assert_matches!(result, Ok(_));
let mut toks = gas_meter.tokens().iter(); let mut toks = gas_meter.tokens().iter();
match_tokens!( match_tokens!(
toks, toks,
ExecFeeToken::Call,
TransferFeeToken { TransferFeeToken {
kind: TransferFeeKind::Transfer, kind: TransferFeeKind::Transfer,
}, },
@@ -1132,13 +1180,20 @@ mod tests {
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT); let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let result = ctx.call(dest, 50, &mut gas_meter, vec![]); let result = super::transfer(
&mut gas_meter,
super::TransferCause::Call,
super::TransactorKind::PlainAccount,
&origin,
&dest,
50,
&mut ctx,
);
assert_matches!(result, Ok(_)); assert_matches!(result, Ok(_));
let mut toks = gas_meter.tokens().iter(); let mut toks = gas_meter.tokens().iter();
match_tokens!( match_tokens!(
toks, toks,
ExecFeeToken::Call,
TransferFeeToken { TransferFeeToken {
kind: TransferFeeKind::Transfer, kind: TransferFeeKind::Transfer,
}, },
@@ -1189,16 +1244,19 @@ mod tests {
let mut ctx = ExecutionContext::top_level(origin, &cfg, &vm, &loader); let mut ctx = ExecutionContext::top_level(origin, &cfg, &vm, &loader);
set_balance(&origin, 0); set_balance(&origin, 0);
let result = ctx.call( let result = super::transfer(
dest,
100,
&mut GasMeter::<Test>::new(GAS_LIMIT), &mut GasMeter::<Test>::new(GAS_LIMIT),
vec![], super::TransferCause::Call,
super::TransactorKind::PlainAccount,
&origin,
&dest,
100,
&mut ctx,
); );
assert_matches!( assert_eq!(
result, result,
Err(DispatchError::Module { message: Some("InsufficientBalance"), .. }) Err(Error::<Test>::TransferFailed.into())
); );
assert_eq!(get_balance(&origin), 0); assert_eq!(get_balance(&origin), 0);
assert_eq!(get_balance(&dest), 0); assert_eq!(get_balance(&dest), 0);
@@ -1335,9 +1393,9 @@ mod tests {
if !*reached_bottom { if !*reached_bottom {
// We are first time here, it means we just reached bottom. // We are first time here, it means we just reached bottom.
// Verify that we've got proper error and set `reached_bottom`. // Verify that we've got proper error and set `reached_bottom`.
assert_matches!( assert_eq!(
r, r,
Err(DispatchError::Other("reached maximum depth, cannot make a call")) Err(Error::<Test>::MaxCallDepthReached.into())
); );
*reached_bottom = true; *reached_bottom = true;
} else { } else {
@@ -1604,7 +1662,10 @@ mod tests {
ctx.gas_meter, ctx.gas_meter,
vec![] vec![]
), ),
Err(DispatchError::Other("It's a trap!")) Err(ExecError {
error: DispatchError::Other("It's a trap!"),
origin: ErrorOrigin::Callee,
})
); );
exec_success() exec_success()
@@ -1648,14 +1709,14 @@ mod tests {
let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader);
set_balance(&ALICE, 1000); set_balance(&ALICE, 1000);
assert_matches!( assert_eq!(
ctx.instantiate( ctx.instantiate(
100, 100,
&mut GasMeter::<Test>::new(GAS_LIMIT), &mut GasMeter::<Test>::new(GAS_LIMIT),
&terminate_ch, &terminate_ch,
vec![], vec![],
), ),
Err(DispatchError::Other("insufficient remaining balance")) Err(Error::<Test>::NewContractNotFunded.into())
); );
assert_eq!( assert_eq!(
+6 -5
View File
@@ -14,11 +14,11 @@
// 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::Trait; use crate::{Trait, exec::ExecError};
use sp_std::marker::PhantomData; use sp_std::marker::PhantomData;
use sp_runtime::traits::Zero; use sp_runtime::traits::Zero;
use frame_support::dispatch::{ use frame_support::dispatch::{
DispatchError, DispatchResultWithPostInfo, PostDispatchInfo, DispatchErrorWithPostInfo, DispatchResultWithPostInfo, PostDispatchInfo, DispatchErrorWithPostInfo,
}; };
#[cfg(test)] #[cfg(test)]
@@ -189,8 +189,9 @@ impl<T: Trait> GasMeter<T> {
} }
/// Turn this GasMeter into a DispatchResult that contains the actually used gas. /// Turn this GasMeter into a DispatchResult that contains the actually used gas.
pub fn into_dispatch_result<R, E>(self, result: Result<R, E>) -> DispatchResultWithPostInfo where pub fn into_dispatch_result<R, E>(self, result: Result<R, E>) -> DispatchResultWithPostInfo
E: Into<DispatchError>, where
E: Into<ExecError>,
{ {
let post_info = PostDispatchInfo { let post_info = PostDispatchInfo {
actual_weight: Some(self.gas_spent()), actual_weight: Some(self.gas_spent()),
@@ -199,7 +200,7 @@ impl<T: Trait> GasMeter<T> {
result result
.map(|_| post_info) .map(|_| post_info)
.map_err(|e| DispatchErrorWithPostInfo { post_info, error: e.into() }) .map_err(|e| DispatchErrorWithPostInfo { post_info, error: e.into().error })
} }
#[cfg(test)] #[cfg(test)]
+23 -1
View File
@@ -95,6 +95,7 @@ use crate::wasm::{WasmLoader, WasmVm};
pub use crate::gas::{Gas, GasMeter}; pub use crate::gas::{Gas, GasMeter};
pub use crate::exec::{ExecResult, ExecReturnValue}; pub use crate::exec::{ExecResult, ExecReturnValue};
pub use crate::wasm::ReturnCode as RuntimeReturnCode;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@@ -420,9 +421,30 @@ decl_error! {
/// the subsistence threshold. No transfer is allowed to do this in order to allow /// the subsistence threshold. No transfer is allowed to do this in order to allow
/// for a tombstone to be created. Use `ext_terminate` to remove a contract without /// for a tombstone to be created. Use `ext_terminate` to remove a contract without
/// leaving a tombstone behind. /// leaving a tombstone behind.
InsufficientBalance, BelowSubsistenceThreshold,
/// The newly created contract is below the subsistence threshold after executing
/// its contructor. No contracts are allowed to exist below that threshold.
NewContractNotFunded,
/// Performing the requested transfer failed for a reason originating in the
/// chosen currency implementation of the runtime. Most probably the balance is
/// too low or locks are placed on it.
TransferFailed,
/// Performing a call was denied because the calling depth reached the limit
/// of what is specified in the schedule.
MaxCallDepthReached,
/// The contract that was called is either no contract at all (a plain account)
/// or is a tombstone.
NotCallable,
/// The code supplied to `put_code` exceeds the limit specified in the current schedule. /// The code supplied to `put_code` exceeds the limit specified in the current schedule.
CodeTooLarge, CodeTooLarge,
/// No code could be found at the supplied code hash.
CodeNotFound,
/// A buffer outside of sandbox memory was passed to a contract API function.
OutOfBounds,
/// Input passed to a contract API function failed to decode as expected type.
DecodingFailed,
/// Contract trapped during execution.
ContractTrapped,
} }
} }
+1
View File
@@ -149,6 +149,7 @@ pub fn set_rent_allowance<T: Trait>(
} }
/// Returns the code hash of the contract specified by `account` ID. /// Returns the code hash of the contract specified by `account` ID.
#[cfg(test)]
pub fn code_hash<T: Trait>(account: &AccountIdOf<T>) -> Result<CodeHash<T>, ContractAbsentError> { pub fn code_hash<T: Trait>(account: &AccountIdOf<T>) -> Result<CodeHash<T>, ContractAbsentError> {
<ContractInfoOf<T>>::get(account) <ContractInfoOf<T>>::get(account)
.and_then(|i| i.as_alive().map(|i| i.code_hash)) .and_then(|i| i.as_alive().map(|i| i.code_hash))
+243 -20
View File
@@ -17,7 +17,7 @@
use crate::{ use crate::{
BalanceOf, ContractAddressFor, ContractInfo, ContractInfoOf, GenesisConfig, Module, BalanceOf, ContractAddressFor, ContractInfo, ContractInfoOf, GenesisConfig, Module,
RawAliveContractInfo, RawEvent, Trait, TrieId, Schedule, TrieIdGenerator, gas::Gas, RawAliveContractInfo, RawEvent, Trait, TrieId, Schedule, TrieIdGenerator, gas::Gas,
Error, Error, Config, RuntimeReturnCode,
}; };
use assert_matches::assert_matches; use assert_matches::assert_matches;
use hex_literal::*; use hex_literal::*;
@@ -30,8 +30,9 @@ use sp_runtime::{
use frame_support::{ use frame_support::{
assert_ok, assert_err_ignore_postinfo, impl_outer_dispatch, impl_outer_event, assert_ok, assert_err_ignore_postinfo, impl_outer_dispatch, impl_outer_event,
impl_outer_origin, parameter_types, StorageMap, StorageValue, impl_outer_origin, parameter_types, StorageMap, StorageValue,
traits::{Currency, Get}, traits::{Currency, Get, ReservableCurrency},
weights::{Weight, PostDispatchInfo}, weights::{Weight, PostDispatchInfo},
dispatch::DispatchErrorWithPostInfo,
}; };
use std::cell::RefCell; use std::cell::RefCell;
use frame_system::{self as system, EventRecord, Phase}; use frame_system::{self as system, EventRecord, Phase};
@@ -63,6 +64,7 @@ impl_outer_dispatch! {
} }
} }
#[macro_use]
pub mod test_utils { pub mod test_utils {
use super::{Test, Balances}; use super::{Test, Balances};
use crate::{ContractInfoOf, TrieIdGenerator, CodeHash}; use crate::{ContractInfoOf, TrieIdGenerator, CodeHash};
@@ -89,6 +91,12 @@ pub mod test_utils {
pub fn get_balance(who: &u64) -> u64 { pub fn get_balance(who: &u64) -> u64 {
Balances::free_balance(who) 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! { thread_local! {
@@ -279,19 +287,23 @@ where
Ok((wasm_binary, code_hash)) 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. // Then we check that only the base costs are returned as actual costs.
#[test] #[test]
fn returns_base_call_cost() { fn calling_plain_account_fails() {
ExtBuilder::default().build().execute_with(|| { ExtBuilder::default().build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 100_000_000); let _ = Balances::deposit_creating(&ALICE, 100_000_000);
assert_eq!( assert_eq!(
Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, Vec::new()), Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, Vec::new()),
Ok( Err(
PostDispatchInfo { DispatchErrorWithPostInfo {
error: Error::<Test>::NotCallable.into(),
post_info: PostDispatchInfo {
actual_weight: Some(67500000), actual_weight: Some(67500000),
pays_fee: Default::default(), pays_fee: Default::default(),
},
} }
) )
); );
@@ -987,7 +999,7 @@ fn call_removed_contract() {
// Calling contract should remove contract and fail. // Calling contract should remove contract and fail.
assert_err_ignore_postinfo!( assert_err_ignore_postinfo!(
Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, call::null()), 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. // Calling a contract that is about to evict shall emit an event.
assert_eq!(System::events(), vec![ assert_eq!(System::events(), vec![
@@ -1001,7 +1013,7 @@ fn call_removed_contract() {
// Subsequent contract calls should also fail. // Subsequent contract calls should also fail.
assert_err_ignore_postinfo!( assert_err_ignore_postinfo!(
Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, call::null()), 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. // we expect that it will get removed leaving tombstone.
assert_err_ignore_postinfo!( assert_err_ignore_postinfo!(
Contracts::call(Origin::signed(ALICE), BOB, 0, GAS_LIMIT, call::null()), 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!(ContractInfoOf::<Test>::get(BOB).unwrap().get_tombstone().is_some());
assert_eq!(System::events(), vec![ 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!( assert_err_ignore_postinfo!(
perform_the_restoration(), perform_the_restoration(),
"contract trapped during execution" Error::<Test>::ContractTrapped,
); );
assert!(ContractInfoOf::<Test>::get(BOB).unwrap().get_tombstone().is_some()); assert!(ContractInfoOf::<Test>::get(BOB).unwrap().get_tombstone().is_some());
@@ -1309,7 +1321,7 @@ fn storage_max_value_limit() {
GAS_LIMIT, GAS_LIMIT,
Encode::encode(&(self::MaxValueSize::get() + 1)), 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(_)) Some(ContractInfo::Alive(_))
); );
// Call BOB with no input data, forcing it to run until out-of-balance // Call BOB which makes it send all funds to the zero address
// and eventually trapping because below existential deposit. // The contract code asserts that the correct error value is returned.
assert_err_ignore_postinfo!( assert_ok!(
Contracts::call( Contracts::call(
Origin::signed(ALICE), Origin::signed(ALICE),
BOB, BOB,
0, 0,
GAS_LIMIT, GAS_LIMIT,
vec![], vec![],
), )
"contract trapped during execution"
); );
}); });
} }
@@ -1423,7 +1434,7 @@ fn cannot_self_destruct_while_live() {
GAS_LIMIT, GAS_LIMIT,
vec![0], vec![0],
), ),
"contract trapped during execution" Error::<Test>::ContractTrapped,
); );
// Check that BOB is still alive. // Check that BOB is still alive.
@@ -1535,8 +1546,7 @@ fn cannot_self_destruct_in_constructor() {
let _ = Balances::deposit_creating(&ALICE, 1_000_000); let _ = Balances::deposit_creating(&ALICE, 1_000_000);
assert_ok!(Contracts::put_code(Origin::signed(ALICE), wasm)); assert_ok!(Contracts::put_code(Origin::signed(ALICE), wasm));
// Fail to instantiate the BOB because the call that is issued in the deploy // Fail to instantiate the BOB because the contructor calls ext_terminate.
// function exhausts all balances which puts it below the existential deposit.
assert_err_ignore_postinfo!( assert_err_ignore_postinfo!(
Contracts::instantiate( Contracts::instantiate(
Origin::signed(ALICE), Origin::signed(ALICE),
@@ -1545,7 +1555,7 @@ fn cannot_self_destruct_in_constructor() {
code_hash.into(), code_hash.into(),
vec![], 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);
});
}
+90 -11
View File
@@ -36,6 +36,7 @@ use self::runtime::{to_execution_result, Runtime};
use self::code_cache::load as load_code; use self::code_cache::load as load_code;
pub use self::code_cache::save as save_code; pub use self::code_cache::save as save_code;
pub use self::runtime::ReturnCode;
/// A prepared wasm module ready for execution. /// A prepared wasm module ready for execution.
#[derive(Clone, Encode, Decode)] #[derive(Clone, Encode, Decode)]
@@ -152,13 +153,12 @@ mod tests {
use super::*; use super::*;
use std::collections::HashMap; use std::collections::HashMap;
use sp_core::H256; use sp_core::H256;
use crate::exec::{Ext, StorageKey, ExecReturnValue, ReturnFlags}; use crate::exec::{Ext, StorageKey, ExecReturnValue, ReturnFlags, ExecError, ErrorOrigin};
use crate::gas::{Gas, GasMeter}; use crate::gas::{Gas, GasMeter};
use crate::tests::{Test, Call}; use crate::tests::{Test, Call};
use crate::wasm::prepare::prepare_contract; use crate::wasm::prepare::prepare_contract;
use crate::{CodeHash, BalanceOf}; use crate::{CodeHash, BalanceOf, Error};
use hex_literal::hex; use hex_literal::hex;
use assert_matches::assert_matches;
use sp_runtime::DispatchError; use sp_runtime::DispatchError;
use frame_support::weights::Weight; use frame_support::weights::Weight;
@@ -225,7 +225,7 @@ mod tests {
endowment: u64, endowment: u64,
gas_meter: &mut GasMeter<Test>, gas_meter: &mut GasMeter<Test>,
data: Vec<u8>, data: Vec<u8>,
) -> Result<(u64, ExecReturnValue), DispatchError> { ) -> Result<(u64, ExecReturnValue), ExecError> {
self.instantiates.push(InstantiateEntry { self.instantiates.push(InstantiateEntry {
code_hash: code_hash.clone(), code_hash: code_hash.clone(),
endowment, endowment,
@@ -365,7 +365,7 @@ mod tests {
value: u64, value: u64,
gas_meter: &mut GasMeter<Test>, gas_meter: &mut GasMeter<Test>,
input_data: Vec<u8>, input_data: Vec<u8>,
) -> Result<(u64, ExecReturnValue), DispatchError> { ) -> Result<(u64, ExecReturnValue), ExecError> {
(**self).instantiate(code, value, gas_meter, input_data) (**self).instantiate(code, value, gas_meter, input_data)
} }
fn transfer( fn transfer(
@@ -483,9 +483,10 @@ mod tests {
;; value_ptr: u32, ;; value_ptr: u32,
;; value_len: u32, ;; value_len: u32,
;;) -> u32 ;;) -> u32
(import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32 i32))) (import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1)) (import "env" "memory" (memory 1 1))
(func (export "call") (func (export "call")
(drop
(call $ext_transfer (call $ext_transfer
(i32.const 4) ;; Pointer to "account" address. (i32.const 4) ;; Pointer to "account" address.
(i32.const 8) ;; Length of "account" address. (i32.const 8) ;; Length of "account" address.
@@ -493,6 +494,7 @@ mod tests {
(i32.const 8) ;; Length of the buffer with value to transfer. (i32.const 8) ;; Length of the buffer with value to transfer.
) )
) )
)
(func (export "deploy")) (func (export "deploy"))
;; Destination AccountId to transfer the funds. ;; Destination AccountId to transfer the funds.
@@ -521,7 +523,7 @@ mod tests {
to: 7, to: 7,
value: 153, value: 153,
data: Vec::new(), data: Vec::new(),
gas_left: 9989500000, gas_left: 9989000000,
}] }]
); );
} }
@@ -1503,14 +1505,17 @@ mod tests {
// Checks that the runtime traps if there are more than `max_topic_events` topics. // Checks that the runtime traps if there are more than `max_topic_events` topics.
let mut gas_meter = GasMeter::new(GAS_LIMIT); let mut gas_meter = GasMeter::new(GAS_LIMIT);
assert_matches!( assert_eq!(
execute( execute(
CODE_DEPOSIT_EVENT_MAX_TOPICS, CODE_DEPOSIT_EVENT_MAX_TOPICS,
vec![], vec![],
MockExt::default(), MockExt::default(),
&mut gas_meter &mut gas_meter
), ),
Err(DispatchError::Other("contract trapped during execution")) Err(ExecError {
error: Error::<Test>::ContractTrapped.into(),
origin: ErrorOrigin::Caller,
})
); );
} }
@@ -1545,14 +1550,17 @@ mod tests {
// Checks that the runtime traps if there are duplicates. // Checks that the runtime traps if there are duplicates.
let mut gas_meter = GasMeter::new(GAS_LIMIT); let mut gas_meter = GasMeter::new(GAS_LIMIT);
assert_matches!( assert_eq!(
execute( execute(
CODE_DEPOSIT_EVENT_DUPLICATES, CODE_DEPOSIT_EVENT_DUPLICATES,
vec![], vec![],
MockExt::default(), MockExt::default(),
&mut gas_meter &mut gas_meter
), ),
Err(DispatchError::Other("contract trapped during execution")) Err(ExecError {
error: Error::<Test>::ContractTrapped.into(),
origin: ErrorOrigin::Caller,
})
); );
} }
@@ -1666,4 +1674,75 @@ mod tests {
assert_eq!(output, ExecReturnValue { flags: ReturnFlags::REVERT, data: hex!("5566778899").to_vec() }); assert_eq!(output, ExecReturnValue { flags: ReturnFlags::REVERT, data: hex!("5566778899").to_vec() });
assert!(!output.is_success()); assert!(!output.is_success());
} }
const CODE_OUT_OF_BOUNDS_ACCESS: &str = r#"
(module
(import "env" "ext_terminate" (func $ext_terminate (param i32 i32)))
(import "env" "memory" (memory 1 1))
(func (export "deploy"))
(func (export "call")
(call $ext_terminate
(i32.const 65536) ;; Pointer to "account" address (out of bound).
(i32.const 8) ;; Length of "account" address.
)
)
)
"#;
#[test]
fn contract_out_of_bounds_access() {
let mut mock_ext = MockExt::default();
let result = execute(
CODE_OUT_OF_BOUNDS_ACCESS,
vec![],
&mut mock_ext,
&mut GasMeter::new(GAS_LIMIT),
);
assert_eq!(
result,
Err(ExecError {
error: Error::<Test>::OutOfBounds.into(),
origin: ErrorOrigin::Caller,
})
);
}
const CODE_DECODE_FAILURE: &str = r#"
(module
(import "env" "ext_terminate" (func $ext_terminate (param i32 i32)))
(import "env" "memory" (memory 1 1))
(func (export "deploy"))
(func (export "call")
(call $ext_terminate
(i32.const 0) ;; Pointer to "account" address.
(i32.const 4) ;; Length of "account" address (too small -> decode fail).
)
)
)
"#;
#[test]
fn contract_decode_failure() {
let mut mock_ext = MockExt::default();
let result = execute(
CODE_DECODE_FAILURE,
vec![],
&mut mock_ext,
&mut GasMeter::new(GAS_LIMIT),
);
assert_eq!(
result,
Err(ExecError {
error: Error::<Test>::DecodingFailed.into(),
origin: ErrorOrigin::Caller,
})
);
}
} }
+157 -76
View File
@@ -18,7 +18,7 @@
use crate::{Schedule, Trait, CodeHash, BalanceOf, Error}; use crate::{Schedule, Trait, CodeHash, BalanceOf, Error};
use crate::exec::{ use crate::exec::{
Ext, ExecResult, ExecReturnValue, StorageKey, TopicOf, ReturnFlags, Ext, ExecResult, ExecReturnValue, StorageKey, TopicOf, ReturnFlags, ExecError
}; };
use crate::gas::{Gas, GasMeter, Token, GasMeterResult}; use crate::gas::{Gas, GasMeter, Token, GasMeterResult};
use crate::wasm::env_def::ConvertibleToWasm; use crate::wasm::env_def::ConvertibleToWasm;
@@ -36,21 +36,33 @@ use sp_io::hashing::{
sha2_256, sha2_256,
}; };
/// Every error that can be returned from a runtime API call. /// Every error that can be returned to a contract when it calls any of the host functions.
#[repr(u32)] #[repr(u32)]
pub enum ReturnCode { pub enum ReturnCode {
/// API call successful. /// API call successful.
Success = 0, Success = 0,
/// The called function trapped and has its state changes reverted. /// The called function trapped and has its state changes reverted.
/// In this case no output buffer is returned. /// In this case no output buffer is returned.
/// Can only be returned from `ext_call` and `ext_instantiate`.
CalleeTrapped = 1, CalleeTrapped = 1,
/// The called function ran to completion but decided to revert its state. /// The called function ran to completion but decided to revert its state.
/// An output buffer is returned when one was supplied. /// An output buffer is returned when one was supplied.
/// Can only be returned from `ext_call` and `ext_instantiate`.
CalleeReverted = 2, CalleeReverted = 2,
/// The passed key does not exist in storage. /// The passed key does not exist in storage.
KeyNotFound = 3, KeyNotFound = 3,
/// Transfer failed because it would have brought the sender's total balance below the
/// subsistence threshold.
BelowSubsistenceThreshold = 4,
/// Transfer failed for other reasons. Most probably reserved or locked balance of the
/// sender prevents the transfer.
TransferFailed = 5,
/// The newly created contract is below the subsistence threshold after executing
/// its constructor.
NewContractNotFunded = 6,
/// No code could be found at the supplied code hash.
CodeNotFound = 7,
/// The contract that was called is either no contract at all (a plain account)
/// or is a tombstone.
NotCallable = 8,
} }
impl ConvertibleToWasm for ReturnCode { impl ConvertibleToWasm for ReturnCode {
@@ -66,7 +78,7 @@ impl ConvertibleToWasm for ReturnCode {
} }
impl From<ExecReturnValue> for ReturnCode { impl From<ExecReturnValue> for ReturnCode {
fn from(from: ExecReturnValue) -> ReturnCode { fn from(from: ExecReturnValue) -> Self {
if from.flags.contains(ReturnFlags::REVERT) { if from.flags.contains(ReturnFlags::REVERT) {
Self::CalleeReverted Self::CalleeReverted
} else { } else {
@@ -96,7 +108,7 @@ enum TrapReason {
SupervisorError(DispatchError), SupervisorError(DispatchError),
/// Signals that trap was generated in response to call `ext_return` host function. /// Signals that trap was generated in response to call `ext_return` host function.
Return(ReturnData), Return(ReturnData),
/// Signals that a trap was generated in response to a succesful call to the /// Signals that a trap was generated in response to a successful call to the
/// `ext_terminate` host function. /// `ext_terminate` host function.
Termination, Termination,
/// Signals that a trap was generated because of a successful restoration. /// Signals that a trap was generated because of a successful restoration.
@@ -131,35 +143,42 @@ impl<'a, E: Ext + 'a> Runtime<'a, E> {
} }
} }
/// Converts the sandbox result and the runtime state into the execution outcome.
///
/// It evaluates information stored in the `trap_reason` variable of the runtime and
/// bases the outcome on the value if this variable. Only if `trap_reason` is `None`
/// the result of the sandbox is evaluated.
pub(crate) fn to_execution_result<E: Ext>( pub(crate) fn to_execution_result<E: Ext>(
runtime: Runtime<E>, runtime: Runtime<E>,
sandbox_result: Result<sp_sandbox::ReturnValue, sp_sandbox::Error>, sandbox_result: Result<sp_sandbox::ReturnValue, sp_sandbox::Error>,
) -> ExecResult { ) -> ExecResult {
match runtime.trap_reason { // If a trap reason is set we base our decision solely on that.
if let Some(trap_reason) = runtime.trap_reason {
return match trap_reason {
// The trap was the result of the execution `return` host function. // The trap was the result of the execution `return` host function.
Some(TrapReason::Return(ReturnData{ flags, data })) => { TrapReason::Return(ReturnData{ flags, data }) => {
let flags = ReturnFlags::from_bits(flags).ok_or_else(|| let flags = ReturnFlags::from_bits(flags).ok_or_else(||
"used reserved bit in return flags" "used reserved bit in return flags"
)?; )?;
return Ok(ExecReturnValue { Ok(ExecReturnValue {
flags, flags,
data, data,
}) })
}, },
Some(TrapReason::Termination) => { TrapReason::Termination => {
return Ok(ExecReturnValue { Ok(ExecReturnValue {
flags: ReturnFlags::empty(), flags: ReturnFlags::empty(),
data: Vec::new(), data: Vec::new(),
}) })
}, },
Some(TrapReason::Restoration) => { TrapReason::Restoration => {
return Ok(ExecReturnValue { Ok(ExecReturnValue {
flags: ReturnFlags::empty(), flags: ReturnFlags::empty(),
data: Vec::new(), data: Vec::new(),
}) })
},
TrapReason::SupervisorError(error) => Err(error)?,
} }
Some(TrapReason::SupervisorError(error)) => Err(error)?,
None => (),
} }
// Check the exact type of the error. // Check the exact type of the error.
@@ -178,7 +197,7 @@ pub(crate) fn to_execution_result<E: Ext>(
Err("validation error")?, Err("validation error")?,
// Any other kind of a trap should result in a failure. // Any other kind of a trap should result in a failure.
Err(sp_sandbox::Error::Execution) | Err(sp_sandbox::Error::OutOfBounds) => Err(sp_sandbox::Error::Execution) | Err(sp_sandbox::Error::OutOfBounds) =>
Err("contract trapped during execution")?, Err(Error::<E::T>::ContractTrapped)?
} }
} }
@@ -280,7 +299,8 @@ fn read_sandbox_memory<E: Ext>(
)?; )?;
let mut buf = vec![0u8; len as usize]; let mut buf = vec![0u8; len as usize];
ctx.memory.get(ptr, buf.as_mut_slice()).map_err(|_| sp_sandbox::HostError)?; ctx.memory.get(ptr, buf.as_mut_slice())
.map_err(|_| store_err(ctx, Error::<E::T>::OutOfBounds))?;
Ok(buf) Ok(buf)
} }
@@ -304,7 +324,7 @@ fn read_sandbox_memory_into_buf<E: Ext>(
RuntimeToken::ReadMemory(buf.len() as u32), RuntimeToken::ReadMemory(buf.len() as u32),
)?; )?;
ctx.memory.get(ptr, buf).map_err(Into::into) ctx.memory.get(ptr, buf).map_err(|_| store_err(ctx, Error::<E::T>::OutOfBounds))
} }
/// Read designated chunk from the sandbox memory, consuming an appropriate amount of /// Read designated chunk from the sandbox memory, consuming an appropriate amount of
@@ -322,7 +342,7 @@ fn read_sandbox_memory_as<E: Ext, D: Decode>(
len: u32, len: u32,
) -> Result<D, sp_sandbox::HostError> { ) -> Result<D, sp_sandbox::HostError> {
let buf = read_sandbox_memory(ctx, ptr, len)?; let buf = read_sandbox_memory(ctx, ptr, len)?;
D::decode(&mut &buf[..]).map_err(|_| sp_sandbox::HostError) D::decode(&mut &buf[..]).map_err(|_| store_err(ctx, Error::<E::T>::DecodingFailed))
} }
/// Write the given buffer to the designated location in the sandbox memory, consuming /// Write the given buffer to the designated location in the sandbox memory, consuming
@@ -345,9 +365,8 @@ fn write_sandbox_memory<E: Ext>(
RuntimeToken::WriteMemory(buf.len() as u32), RuntimeToken::WriteMemory(buf.len() as u32),
)?; )?;
ctx.memory.set(ptr, buf)?; ctx.memory.set(ptr, buf)
.map_err(|_| store_err(ctx, Error::<E::T>::OutOfBounds))
Ok(())
} }
/// Write the given buffer and its length to the designated locations in sandbox memory. /// Write the given buffer and its length to the designated locations in sandbox memory.
@@ -379,7 +398,7 @@ fn write_sandbox_output<E: Ext>(
let len: u32 = read_sandbox_memory_as(ctx, out_len_ptr, 4)?; let len: u32 = read_sandbox_memory_as(ctx, out_len_ptr, 4)?;
if len < buf_len { if len < buf_len {
Err(map_err(ctx, Error::<E::T>::OutputBufferTooSmall))? Err(store_err(ctx, Error::<E::T>::OutputBufferTooSmall))?
} }
charge_gas( charge_gas(
@@ -398,7 +417,7 @@ fn write_sandbox_output<E: Ext>(
/// Stores a DispatchError returned from an Ext function into the trap_reason. /// Stores a DispatchError returned from an Ext function into the trap_reason.
/// ///
/// This allows through supervisor generated errors to the caller. /// This allows through supervisor generated errors to the caller.
fn map_err<E, Error>(ctx: &mut Runtime<E>, err: Error) -> sp_sandbox::HostError where fn store_err<E, Error>(ctx: &mut Runtime<E>, err: Error) -> sp_sandbox::HostError where
E: Ext, E: Ext,
Error: Into<DispatchError>, Error: Into<DispatchError>,
{ {
@@ -406,12 +425,86 @@ fn map_err<E, Error>(ctx: &mut Runtime<E>, err: Error) -> sp_sandbox::HostError
sp_sandbox::HostError sp_sandbox::HostError
} }
/// Fallible conversion of `DispatchError` to `ReturnCode`.
fn err_into_return_code<T: Trait>(from: DispatchError) -> Result<ReturnCode, DispatchError> {
use ReturnCode::*;
let below_sub = Error::<T>::BelowSubsistenceThreshold.into();
let transfer_failed = Error::<T>::TransferFailed.into();
let not_funded = Error::<T>::NewContractNotFunded.into();
let no_code = Error::<T>::CodeNotFound.into();
let invalid_contract = Error::<T>::NotCallable.into();
match from {
x if x == below_sub => Ok(BelowSubsistenceThreshold),
x if x == transfer_failed => Ok(TransferFailed),
x if x == not_funded => Ok(NewContractNotFunded),
x if x == no_code => Ok(CodeNotFound),
x if x == invalid_contract => Ok(NotCallable),
err => Err(err)
}
}
/// Fallible conversion of a `ExecResult` to `ReturnCode`.
fn exec_into_return_code<T: Trait>(from: ExecResult) -> Result<ReturnCode, DispatchError> {
use crate::exec::ErrorOrigin::Callee;
let ExecError { error, origin } = match from {
Ok(retval) => return Ok(retval.into()),
Err(err) => err,
};
match (error, origin) {
(_, Callee) => Ok(ReturnCode::CalleeTrapped),
(err, _) => err_into_return_code::<T>(err)
}
}
/// Used by Runtime API that calls into other contracts.
///
/// Those need to transform the the `ExecResult` returned from the execution into
/// a `ReturnCode`. If this conversion fails because the `ExecResult` constitutes a
/// a fatal error then this error is stored in the `ExecutionContext` so it can be
/// extracted for display in the UI.
fn map_exec_result<E: Ext>(ctx: &mut Runtime<E>, result: ExecResult)
-> Result<ReturnCode, sp_sandbox::HostError>
{
match exec_into_return_code::<E::T>(result) {
Ok(code) => Ok(code),
Err(err) => Err(store_err(ctx, err)),
}
}
/// Try to convert an error into a `ReturnCode`.
///
/// Used to decide between fatal and non-fatal errors.
fn map_dispatch_result<T, E: Ext>(ctx: &mut Runtime<E>, result: Result<T, DispatchError>)
-> Result<ReturnCode, sp_sandbox::HostError>
{
let err = if let Err(err) = result {
err
} else {
return Ok(ReturnCode::Success)
};
match err_into_return_code::<E::T>(err) {
Ok(code) => Ok(code),
Err(err) => Err(store_err(ctx, err)),
}
}
// *********************************************************** // ***********************************************************
// * AFTER MAKING A CHANGE MAKE SURE TO UPDATE COMPLEXITY.MD * // * AFTER MAKING A CHANGE MAKE SURE TO UPDATE COMPLEXITY.MD *
// *********************************************************** // ***********************************************************
// Define a function `fn init_env<E: Ext>() -> HostFunctionSet<E>` that returns // Define a function `fn init_env<E: Ext>() -> HostFunctionSet<E>` that returns
// a function set which can be imported by an executed contract. // a function set which can be imported by an executed contract.
//
// # Note
//
// Any input that leads to a out of bound error (reading or writing) or failing to decode
// data passed to the supervisor will lead to a trap. This is not documented explicitly
// for every function.
define_env!(Env, <E: Ext>, define_env!(Env, <E: Ext>,
// Account for used gas. Traps if gas used is greater than gas limit. // Account for used gas. Traps if gas used is greater than gas limit.
@@ -441,7 +534,7 @@ define_env!(Env, <E: Ext>,
// - `value_ptr`: pointer into the linear memory where the value to set is placed. // - `value_ptr`: pointer into the linear memory where the value to set is placed.
// - `value_len`: the length of the value in bytes. // - `value_len`: the length of the value in bytes.
// //
// # Errors // # Traps
// //
// - If value length exceeds the configured maximum value length of a storage entry. // - If value length exceeds the configured maximum value length of a storage entry.
// - Upon trying to set an empty storage entry (value length is 0). // - Upon trying to set an empty storage entry (value length is 0).
@@ -480,12 +573,7 @@ define_env!(Env, <E: Ext>,
// //
// # Errors // # Errors
// //
// If there is no entry under the given key then this function will return // `ReturnCode::KeyNotFound`
// `ReturnCode::KeyNotFound`.
//
// # Traps
//
// Traps if the supplied buffer length is smaller than the size of the stored value.
ext_get_storage(ctx, key_ptr: u32, out_ptr: u32, out_len_ptr: u32) -> ReturnCode => { ext_get_storage(ctx, key_ptr: u32, out_ptr: u32, out_len_ptr: u32) -> ReturnCode => {
let mut key: StorageKey = [0; 32]; let mut key: StorageKey = [0; 32];
read_sandbox_memory_into_buf(ctx, key_ptr, &mut key)?; read_sandbox_memory_into_buf(ctx, key_ptr, &mut key)?;
@@ -508,24 +596,24 @@ define_env!(Env, <E: Ext>,
// Should be decodable as a `T::Balance`. Traps otherwise. // Should be decodable as a `T::Balance`. Traps otherwise.
// - value_len: length of the value buffer. // - value_len: length of the value buffer.
// //
// # Traps // # Errors
// //
// Traps if the transfer wasn't succesful. This can happen when the value transfered // `ReturnCode::BelowSubsistenceThreshold`
// brings the sender below the existential deposit. Use `ext_terminate` to remove // `ReturnCode::TransferFailed`
// the caller contract.
ext_transfer( ext_transfer(
ctx, ctx,
account_ptr: u32, account_ptr: u32,
account_len: u32, account_len: u32,
value_ptr: u32, value_ptr: u32,
value_len: u32 value_len: u32
) => { ) -> ReturnCode => {
let callee: <<E as Ext>::T as frame_system::Trait>::AccountId = let callee: <<E as Ext>::T as frame_system::Trait>::AccountId =
read_sandbox_memory_as(ctx, account_ptr, account_len)?; read_sandbox_memory_as(ctx, account_ptr, account_len)?;
let value: BalanceOf<<E as Ext>::T> = let value: BalanceOf<<E as Ext>::T> =
read_sandbox_memory_as(ctx, value_ptr, value_len)?; read_sandbox_memory_as(ctx, value_ptr, value_len)?;
ctx.ext.transfer(&callee, value, ctx.gas_meter).map_err(|e| map_err(ctx, e)) let result = ctx.ext.transfer(&callee, value, ctx.gas_meter);
map_dispatch_result(ctx, result)
}, },
// Make a call to another contract. // Make a call to another contract.
@@ -551,17 +639,14 @@ define_env!(Env, <E: Ext>,
// //
// # Errors // # Errors
// //
// `ReturnCode::CalleeReverted`: The callee ran to completion but decided to have its // An error means that the call wasn't successful output buffer is returned unless
// changes reverted. The delivery of the output buffer is still possible. // stated otherwise.
// `ReturnCode::CalleeTrapped`: The callee trapped during execution. All changes are reverted
// and no output buffer is delivered.
// //
// # Traps // `ReturnCode::CalleeReverted`: Output buffer is returned.
// // `ReturnCode::CalleeTrapped`
// - Transfer of balance failed. This call can not bring the sender below the existential // `ReturnCode::BelowSubsistenceThreshold`
// deposit. Use `ext_terminate` to remove the caller. // `ReturnCode::TransferFailed`
// - Callee does not exist. // `ReturnCode::NotCallable`
// - Supplied output buffer is too small.
ext_call( ext_call(
ctx, ctx,
callee_ptr: u32, callee_ptr: u32,
@@ -594,22 +679,16 @@ define_env!(Env, <E: Ext>,
nested_meter, nested_meter,
input_data, input_data,
) )
.map_err(|_| ())
} }
// there is not enough gas to allocate for the nested call. // there is not enough gas to allocate for the nested call.
None => Err(()), None => Err(Error::<<E as Ext>::T>::OutOfGas.into()),
} }
}); });
match call_outcome { if let Ok(output) = &call_outcome {
Ok(output) => {
write_sandbox_output(ctx, output_ptr, output_len_ptr, &output.data, true)?; write_sandbox_output(ctx, output_ptr, output_len_ptr, &output.data, true)?;
Ok(output.into())
},
Err(_) => {
Ok(ReturnCode::CalleeTrapped)
},
} }
map_exec_result(ctx, call_outcome)
}, },
// Instantiate a contract with the specified code hash. // Instantiate a contract with the specified code hash.
@@ -643,19 +722,18 @@ define_env!(Env, <E: Ext>,
// //
// # Errors // # Errors
// //
// `ReturnCode::CalleeReverted`: The callee's constructor ran to completion but decided to have // Please consult the `ReturnCode` enum declaration for more information on those
// its changes reverted. The delivery of the output buffer is still possible but the // errors. Here we only note things specific to this function.
// account was not created and no address is returned.
// `ReturnCode::CalleeTrapped`: The callee trapped during execution. All changes are reverted
// and no output buffer is delivered. The accounts was not created and no address is
// returned.
// //
// # Traps // An error means that the account wasn't created and no address or output buffer
// is returned unless stated otherwise.
// //
// - Transfer of balance failed. This call can not bring the sender below the existential // `ReturnCode::CalleeReverted`: Output buffer is returned.
// deposit. Use `ext_terminate` to remove the caller. // `ReturnCode::CalleeTrapped`
// - Code hash does not exist. // `ReturnCode::BelowSubsistenceThreshold`
// - Supplied output buffers are too small. // `ReturnCode::TransferFailed`
// `ReturnCode::NewContractNotFunded`
// `ReturnCode::CodeNotFound`
ext_instantiate( ext_instantiate(
ctx, ctx,
code_hash_ptr: u32, code_hash_ptr: u32,
@@ -690,26 +768,20 @@ define_env!(Env, <E: Ext>,
nested_meter, nested_meter,
input_data input_data
) )
.map_err(|_| ())
} }
// there is not enough gas to allocate for the nested call. // there is not enough gas to allocate for the nested call.
None => Err(()), None => Err(Error::<<E as Ext>::T>::OutOfGas.into()),
} }
}); });
match instantiate_outcome { if let Ok((address, output)) = &instantiate_outcome {
Ok((address, output)) => {
if !output.flags.contains(ReturnFlags::REVERT) { if !output.flags.contains(ReturnFlags::REVERT) {
write_sandbox_output( write_sandbox_output(
ctx, address_ptr, address_len_ptr, &address.encode(), true ctx, address_ptr, address_len_ptr, &address.encode(), true
)?; )?;
} }
write_sandbox_output(ctx, output_ptr, output_len_ptr, &output.data, true)?; write_sandbox_output(ctx, output_ptr, output_len_ptr, &output.data, true)?;
Ok(output.into())
},
Err(_) => {
Ok(ReturnCode::CalleeTrapped)
},
} }
map_exec_result(ctx, instantiate_outcome.map(|(_id, retval)| retval))
}, },
// Remove the calling account and transfer remaining balance. // Remove the calling account and transfer remaining balance.
@@ -722,6 +794,10 @@ define_env!(Env, <E: Ext>,
// where all remaining funds of the caller are transfered. // where all remaining funds of the caller are transfered.
// Should be decodable as an `T::AccountId`. Traps otherwise. // Should be decodable as an `T::AccountId`. Traps otherwise.
// - beneficiary_len: length of the address buffer. // - beneficiary_len: length of the address buffer.
//
// # Traps
//
// - The contract is live i.e is already on the call stack.
ext_terminate( ext_terminate(
ctx, ctx,
beneficiary_ptr: u32, beneficiary_ptr: u32,
@@ -939,6 +1015,11 @@ define_env!(Env, <E: Ext>,
// encodes the rent allowance that must be set in the case of successful restoration. // encodes the rent allowance that must be set in the case of successful restoration.
// `delta_ptr` is the pointer to the start of a buffer that has `delta_count` storage keys // `delta_ptr` is the pointer to the start of a buffer that has `delta_count` storage keys
// laid out sequentially. // laid out sequentially.
//
// # Traps
//
// - Tombstone hashes do not match
// - Calling cantract is live i.e is already on the call stack.
ext_restore_to( ext_restore_to(
ctx, ctx,
dest_ptr: u32, dest_ptr: u32,