From b08eb2f8bc840962e5f213c4e51aed468590a5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Thei=C3=9Fen?= Date: Wed, 19 May 2021 14:15:02 +0200 Subject: [PATCH] Add new `seal_call` that offers new features --- Cargo.lock | 1 + frame/contracts/CHANGELOG.md | 2 + frame/contracts/Cargo.toml | 1 + frame/contracts/src/exec.rs | 162 +++++++++++++++++---- frame/contracts/src/lib.rs | 13 +- frame/contracts/src/wasm/mod.rs | 214 ++++++++++++++++++++++++++-- frame/contracts/src/wasm/runtime.rs | 180 +++++++++++++++++++---- 7 files changed, 502 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3e69b4db11b2..87c30bf35fcfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4786,6 +4786,7 @@ name = "pallet-contracts" version = "3.0.0" dependencies = [ "assert_matches", + "bitflags", "frame-benchmarking", "frame-support", "frame-system", diff --git a/frame/contracts/CHANGELOG.md b/frame/contracts/CHANGELOG.md index 76fc09ad17355..498a4019cca22 100644 --- a/frame/contracts/CHANGELOG.md +++ b/frame/contracts/CHANGELOG.md @@ -20,6 +20,8 @@ In other words: Upgrading this pallet will not break pre-existing contracts. ### Added +- Add new **unstable** version of `seal_call` that offers more features. + - New **unstable** `seal_rent_params` and `seal_rent_status` contract callable function. [#8231](https://github.com/paritytech/substrate/pull/8231) [#8780](https://github.com/paritytech/substrate/pull/8780) diff --git a/frame/contracts/Cargo.toml b/frame/contracts/Cargo.toml index f09e61c3e5baf..d9d096dc767de 100644 --- a/frame/contracts/Cargo.toml +++ b/frame/contracts/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +bitflags = "1.0" codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] } log = { version = "0.4", default-features = false } parity-wasm = { version = "0.42", default-features = false } diff --git a/frame/contracts/src/exec.rs b/frame/contracts/src/exec.rs index d5b489d8912e3..2b9a5908ea064 100644 --- a/frame/contracts/src/exec.rs +++ b/frame/contracts/src/exec.rs @@ -167,6 +167,7 @@ pub trait Ext: sealing::Sealed { to: AccountIdOf, value: BalanceOf, input_data: Vec, + allows_reentry: bool, ) -> Result<(ExecReturnValue, u32), (ExecError, u32)>; /// Instantiate a contract from the given code. @@ -457,6 +458,8 @@ pub struct Frame { entry_point: ExportedFunction, /// The gas meter capped to the supplied gas limit. nested_meter: GasMeter, + /// If `false` the contract enabled its defense against reentrance attacks. + allows_reentry: bool, } /// Parameter passed in when creating a new `Frame`. @@ -731,6 +734,7 @@ where entry_point, nested_meter: gas_meter.nested(gas_limit) .map_err(|e| (e.into(), executable.code_len()))?, + allows_reentry: true, }; Ok((frame, executable)) @@ -1014,6 +1018,11 @@ where self.frames().skip(1).any(|f| &f.account_id == account_id) } + /// Returns whether the specified contract allows to be reentered right now. + fn allows_reentry(&self, id: &AccountIdOf) -> bool { + !self.frames().any(|f| &f.account_id == id && !f.allows_reentry) + } + /// Increments the cached account id and returns the value to be used for the trie_id. fn next_trie_seed(&mut self) -> u64 { let next = if let Some(current) = self.account_counter { @@ -1045,25 +1054,44 @@ where to: T::AccountId, value: BalanceOf, input_data: Vec, + allows_reentry: bool, ) -> Result<(ExecReturnValue, u32), (ExecError, u32)> { - // We ignore instantiate frames in our search for a cached contract. - // Otherwise it would be possible to recursively call a contract from its own - // constructor: We disallow calling not fully constructed contracts. - let cached_info = self - .frames() - .find(|f| f.entry_point == ExportedFunction::Call && f.account_id == to) - .and_then(|f| { - match &f.contract_info { - CachedContract::Cached(contract) => Some(contract.clone()), - _ => None, - } - }); - let executable = self.push_frame( - FrameArgs::Call{dest: to, cached_info}, - value, - gas_limit - )?; - self.run(executable, input_data) + // Before pushing the new frame: Protect the caller contract against reentrancy attacks. + // It is important to do this before calling `allows_reentry` so that a direct recursion + // is caught by it. + self.top_frame_mut().allows_reentry = allows_reentry; + + let try_call = || { + if !self.allows_reentry(&to) { + return Err((>::ReentranceDenied.into(), 0)); + } + // We ignore instantiate frames in our search for a cached contract. + // Otherwise it would be possible to recursively call a contract from its own + // constructor: We disallow calling not fully constructed contracts. + let cached_info = self + .frames() + .find(|f| f.entry_point == ExportedFunction::Call && f.account_id == to) + .and_then(|f| { + match &f.contract_info { + CachedContract::Cached(contract) => Some(contract.clone()), + _ => None, + } + }); + let executable = self.push_frame( + FrameArgs::Call{dest: to, cached_info}, + value, + gas_limit + )?; + self.run(executable, input_data) + }; + + // We need to make sure to reset `allows_reentry` even on failure. + let result = try_call(); + + // Protection is on a per call basis. + self.top_frame_mut().allows_reentry = true; + + result } fn instantiate( @@ -1097,7 +1125,7 @@ where beneficiary: &AccountIdOf, ) -> Result { if self.is_recursive() { - return Err((Error::::ReentranceDenied.into(), 0)); + return Err((Error::::TerminatedWhileReentrant.into(), 0)); } let frame = self.top_frame_mut(); let info = frame.terminate(); @@ -1125,7 +1153,7 @@ where delta: Vec, ) -> Result<(u32, u32), (DispatchError, u32, u32)> { if self.is_recursive() { - return Err((Error::::ReentranceDenied.into(), 0, 0)); + return Err((Error::::TerminatedWhileReentrant.into(), 0, 0)); } let origin_contract = self.top_frame_mut().contract_info().clone(); let result = Rent::::restore_to( @@ -1308,12 +1336,14 @@ mod tests { exec::ExportedFunction::*, Error, Weight, }; + use codec::{Encode, Decode}; use sp_core::Bytes; use sp_runtime::DispatchError; use assert_matches::assert_matches; use std::{cell::RefCell, collections::HashMap, rc::Rc}; use pretty_assertions::{assert_eq, assert_ne}; use pallet_contracts_primitives::ReturnFlags; + use frame_support::{assert_ok, assert_err}; type MockStack<'a> = Stack<'a, Test, MockExecutable>; @@ -1731,7 +1761,7 @@ mod tests { let value = Default::default(); let recurse_ch = MockLoader::insert(Call, |ctx, _| { // Try to call into yourself. - let r = ctx.ext.call(0, BOB, 0, vec![]); + let r = ctx.ext.call(0, BOB, 0, vec![], true); REACHED_BOTTOM.with(|reached_bottom| { let mut reached_bottom = reached_bottom.borrow_mut(); @@ -1789,7 +1819,7 @@ mod tests { // Call into CHARLIE contract. assert_matches!( - ctx.ext.call(0, CHARLIE, 0, vec![]), + ctx.ext.call(0, CHARLIE, 0, vec![], true), Ok(_) ); exec_success() @@ -1832,7 +1862,7 @@ mod tests { // Call into charlie contract. assert_matches!( - ctx.ext.call(0, CHARLIE, 0, vec![]), + ctx.ext.call(0, CHARLIE, 0, vec![], true), Ok(_) ); exec_success() @@ -2263,7 +2293,7 @@ mod tests { assert_ne!(original_allowance, changed_allowance); ctx.ext.set_rent_allowance(changed_allowance); assert_eq!( - ctx.ext.call(0, CHARLIE, 0, vec![]).map(|v| v.0).map_err(|e| e.0), + ctx.ext.call(0, CHARLIE, 0, vec![], true).map(|v| v.0).map_err(|e| e.0), exec_trapped() ); assert_eq!(ctx.ext.rent_allowance(), changed_allowance); @@ -2272,7 +2302,7 @@ mod tests { exec_success() }); let code_charlie = MockLoader::insert(Call, |ctx, _| { - assert!(ctx.ext.call(0, BOB, 0, vec![99]).is_ok()); + assert!(ctx.ext.call(0, BOB, 0, vec![99], true).is_ok()); exec_trapped() }); @@ -2299,7 +2329,7 @@ mod tests { fn recursive_call_during_constructor_fails() { let code = MockLoader::insert(Constructor, |ctx, _| { assert_matches!( - ctx.ext.call(0, ctx.ext.address().clone(), 0, vec![]), + ctx.ext.call(0, ctx.ext.address().clone(), 0, vec![], true), Err((ExecError{error, ..}, _)) if error == >::ContractNotFound.into() ); exec_success() @@ -2390,4 +2420,84 @@ mod tests { assert_eq!(&String::from_utf8(debug_buffer).unwrap(), "This is a testMore text"); } + + #[test] + fn call_reentry_direct_recursion() { + // call the contract passed as input with disabled reentry + let code_bob = MockLoader::insert(Call, |ctx, _| { + let dest = Decode::decode(&mut ctx.input_data.as_ref()).unwrap(); + ctx.ext.call(0, dest, 0, vec![], false).map(|v| v.0).map_err(|e| e.0) + }); + + let code_charlie = MockLoader::insert(Call, |_, _| { + exec_success() + }); + + ExtBuilder::default().build().execute_with(|| { + let schedule = ::Schedule::get(); + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + + // Calling another contract should succeed + assert_ok!(MockStack::run_call( + ALICE, + BOB, + &mut GasMeter::::new(GAS_LIMIT), + &schedule, + 0, + CHARLIE.encode(), + None, + )); + + // Calling into oneself fails + assert_err!( + MockStack::run_call( + ALICE, + BOB, + &mut GasMeter::::new(GAS_LIMIT), + &schedule, + 0, + BOB.encode(), + None, + ).map_err(|e| e.0.error), + >::ReentranceDenied, + ); + }); + } + + #[test] + fn call_deny_reentry() { + let code_bob = MockLoader::insert(Call, |ctx, _| { + if ctx.input_data[0] == 0 { + ctx.ext.call(0, CHARLIE, 0, vec![], false).map(|v| v.0).map_err(|e| e.0) + } else { + exec_success() + } + }); + + // call BOB with input set to '1' + let code_charlie = MockLoader::insert(Call, |ctx, _| { + ctx.ext.call(0, BOB, 0, vec![1], true).map(|v| v.0).map_err(|e| e.0) + }); + + ExtBuilder::default().build().execute_with(|| { + let schedule = ::Schedule::get(); + place_contract(&BOB, code_bob); + place_contract(&CHARLIE, code_charlie); + + // Calling another contract should succeed + assert_err!( + MockStack::run_call( + ALICE, + BOB, + &mut GasMeter::::new(GAS_LIMIT), + &schedule, + 0, + vec![0], + None, + ).map_err(|e| e.0.error), + >::ReentranceDenied, + ); + }); + } } diff --git a/frame/contracts/src/lib.rs b/frame/contracts/src/lib.rs index c655a926d8034..6d66069aa1dfa 100644 --- a/frame/contracts/src/lib.rs +++ b/frame/contracts/src/lib.rs @@ -562,12 +562,11 @@ pub mod pallet { ContractTrapped, /// The size defined in `T::MaxValueSize` was exceeded. ValueTooLarge, - /// The action performed is not allowed while the contract performing it is already - /// on the call stack. Those actions are contract self destruction and restoration - /// of a tombstone. - ReentranceDenied, - /// `seal_input` was called twice from the same contract execution context. - InputAlreadyRead, + /// Termination of a conteact is not allowed while the contract is already + /// on the call stack. Can be triggered by `seal_terminate` or `seal_restore_to. + TerminatedWhileReentrant, + /// `seal_call` forwarded this contracts input. It therefore no longer available. + InputForwarded, /// The subject passed to `seal_random` exceeds the limit. RandomSubjectTooLong, /// The amount of topics passed to `seal_deposit_events` exceeds the limit. @@ -602,6 +601,8 @@ pub mod pallet { TerminatedInConstructor, /// The debug message specified to `seal_debug_message` does contain invalid UTF-8. DebugMessageInvalidUTF8, + /// A call tried to invoke a contract that is flagged as non-reentrant. + ReentranceDenied, } /// A mapping from an original code hash to the original code, untouched by instrumentation. diff --git a/frame/contracts/src/wasm/mod.rs b/frame/contracts/src/wasm/mod.rs index ed603732f6c02..5f9936c68dfbe 100644 --- a/frame/contracts/src/wasm/mod.rs +++ b/frame/contracts/src/wasm/mod.rs @@ -289,7 +289,14 @@ mod tests { struct TransferEntry { to: AccountIdOf, value: u64, + } + + #[derive(Debug, PartialEq, Eq)] + struct CallEntry { + to: AccountIdOf, + value: u64, data: Vec, + allows_reentry: bool, } pub struct MockExt { @@ -297,6 +304,7 @@ mod tests { rent_allowance: u64, instantiates: Vec, terminations: Vec, + calls: Vec, transfers: Vec, restores: Vec, // (topics, data) @@ -307,6 +315,11 @@ mod tests { debug_buffer: Vec, } + /// The call is mocked and just returns this hardcoded value. + fn call_return_data() -> Bytes { + Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]) + } + impl Default for MockExt { fn default() -> Self { Self { @@ -314,6 +327,7 @@ mod tests { rent_allowance: Default::default(), instantiates: Default::default(), terminations: Default::default(), + calls: Default::default(), transfers: Default::default(), restores: Default::default(), events: Default::default(), @@ -334,13 +348,15 @@ mod tests { to: AccountIdOf, value: u64, data: Vec, + allows_reentry: bool, ) -> Result<(ExecReturnValue, u32), (ExecError, u32)> { - self.transfers.push(TransferEntry { + self.calls.push(CallEntry { to, value, - data: data, + data, + allows_reentry, }); - Ok((ExecReturnValue { flags: ReturnFlags::empty(), data: Bytes(Vec::new()) }, 0)) + Ok((ExecReturnValue { flags: ReturnFlags::empty(), data: call_return_data() }, 0)) } fn instantiate( &mut self, @@ -374,7 +390,6 @@ mod tests { self.transfers.push(TransferEntry { to: to.clone(), value, - data: Vec::new(), }); Ok(()) } @@ -526,7 +541,6 @@ mod tests { &[TransferEntry { to: ALICE, value: 153, - data: Vec::new(), }] ); } @@ -587,11 +601,192 @@ mod tests { )); assert_eq!( - &mock_ext.transfers, - &[TransferEntry { + &mock_ext.calls, + &[CallEntry { to: ALICE, value: 6, data: vec![1, 2, 3, 4], + allows_reentry: true, + }] + ); + } + + #[test] + #[cfg(feature = "unstable-interface")] + fn contract_call_forward_input() { + const CODE: &str = r#" +(module + (import "__unstable__" "seal_call" (func $seal_call (param i32 i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32))) + (import "seal0" "seal_input" (func $seal_input (param i32 i32))) + (import "env" "memory" (memory 1 1)) + (func (export "call") + (drop + (call $seal_call + (i32.const 1) ;; Set FORWARD_INPUT bit + (i32.const 4) ;; Pointer to "callee" address. + (i32.const 32) ;; Length of "callee" address. + (i64.const 0) ;; How much gas to devote for the execution. 0 = all. + (i32.const 36) ;; Pointer to the buffer with value to transfer + (i32.const 8) ;; Length of the buffer with value to transfer. + (i32.const 44) ;; Pointer to input data buffer address + (i32.const 4) ;; Length of input data buffer + (i32.const 4294967295) ;; u32 max value is the sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this case + ) + ) + + ;; triggers a trap because we already forwarded the input + (call $seal_input (i32.const 1) (i32.const 44)) + ) + + (func (export "deploy")) + + ;; Destination AccountId (ALICE) + (data (i32.const 4) + "\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" + "\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" + ) + + ;; Amount of value to transfer. + ;; Represented by u64 (8 bytes long) in little endian. + (data (i32.const 36) "\2A\00\00\00\00\00\00\00") + + ;; The input is ignored because we forward our own input + (data (i32.const 44) "\01\02\03\04") +) +"#; + let mut mock_ext = MockExt::default(); + let input = vec![0xff, 0x2a, 0x99, 0x88]; + frame_support::assert_err!( + execute(CODE, input.clone(), &mut mock_ext), + >::InputForwarded, + ); + + assert_eq!( + &mock_ext.calls, + &[CallEntry { + to: ALICE, + value: 0x2a, + data: input, + allows_reentry: false, + }] + ); + } + + #[test] + #[cfg(feature = "unstable-interface")] + fn contract_call_clone_input() { + const CODE: &str = r#" +(module + (import "__unstable__" "seal_call" (func $seal_call (param i32 i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32))) + (import "seal0" "seal_input" (func $seal_input (param i32 i32))) + (import "seal0" "seal_return" (func $seal_return (param i32 i32 i32))) + (import "env" "memory" (memory 1 1)) + (func (export "call") + (drop + (call $seal_call + (i32.const 11) ;; Set FORWARD_INPUT | CLONE_INPUT | ALLOW_REENTRY bits + (i32.const 4) ;; Pointer to "callee" address. + (i32.const 32) ;; Length of "callee" address. + (i64.const 0) ;; How much gas to devote for the execution. 0 = all. + (i32.const 36) ;; Pointer to the buffer with value to transfer + (i32.const 8) ;; Length of the buffer with value to transfer. + (i32.const 44) ;; Pointer to input data buffer address + (i32.const 4) ;; Length of input data buffer + (i32.const 4294967295) ;; u32 max value is the sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this case + ) + ) + + ;; works because the input was cloned + (call $seal_input (i32.const 0) (i32.const 44)) + + ;; return the input to caller for inspection + (call $seal_return (i32.const 0) (i32.const 0) (i32.load (i32.const 44))) + ) + + (func (export "deploy")) + + ;; Destination AccountId (ALICE) + (data (i32.const 4) + "\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" + "\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" + ) + + ;; Amount of value to transfer. + ;; Represented by u64 (8 bytes long) in little endian. + (data (i32.const 36) "\2A\00\00\00\00\00\00\00") + + ;; The input is ignored because we forward our own input + (data (i32.const 44) "\01\02\03\04") +) +"#; + let mut mock_ext = MockExt::default(); + let input = vec![0xff, 0x2a, 0x99, 0x88]; + let result = execute(CODE, input.clone(), &mut mock_ext).unwrap(); + assert_eq!(result.data.0, input); + assert_eq!( + &mock_ext.calls, + &[CallEntry { + to: ALICE, + value: 0x2a, + data: input, + allows_reentry: true, + }] + ); + } + + #[test] + #[cfg(feature = "unstable-interface")] + fn contract_call_tail_call() { + const CODE: &str = r#" +(module + (import "__unstable__" "seal_call" (func $seal_call (param i32 i32 i32 i64 i32 i32 i32 i32 i32 i32) (result i32))) + (import "env" "memory" (memory 1 1)) + (func (export "call") + (drop + (call $seal_call + (i32.const 5) ;; Set FORWARD_INPUT | TAIL_CALL bit + (i32.const 4) ;; Pointer to "callee" address. + (i32.const 32) ;; Length of "callee" address. + (i64.const 0) ;; How much gas to devote for the execution. 0 = all. + (i32.const 36) ;; 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 value is the sentinel value: do not copy output + (i32.const 0) ;; Length is ignored in this case + ) + ) + + ;; a tail call never returns + (unreachable) + ) + + (func (export "deploy")) + + ;; Destination AccountId (ALICE) + (data (i32.const 4) + "\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" + "\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01" + ) + + ;; Amount of value to transfer. + ;; Represented by u64 (8 bytes long) in little endian. + (data (i32.const 36) "\2A\00\00\00\00\00\00\00") +) +"#; + let mut mock_ext = MockExt::default(); + let input = vec![0xff, 0x2a, 0x99, 0x88]; + let result = execute(CODE, input.clone(), &mut mock_ext).unwrap(); + assert_eq!(result.data, call_return_data()); + assert_eq!( + &mock_ext.calls, + &[CallEntry { + to: ALICE, + value: 0x2a, + data: input, + allows_reentry: false, }] ); } @@ -772,11 +967,12 @@ mod tests { )); assert_eq!( - &mock_ext.transfers, - &[TransferEntry { + &mock_ext.calls, + &[CallEntry { to: ALICE, value: 6, data: vec![1, 2, 3, 4], + allows_reentry: true, }] ); } diff --git a/frame/contracts/src/wasm/runtime.rs b/frame/contracts/src/wasm/runtime.rs index 3701c0d607345..88791bf3842ca 100644 --- a/frame/contracts/src/wasm/runtime.rs +++ b/frame/contracts/src/wasm/runtime.rs @@ -24,6 +24,7 @@ use crate::{ wasm::env_def::ConvertibleToWasm, schedule::HostFnWeights, }; +use bitflags::bitflags; use parity_wasm::elements::ValueType; use frame_support::{dispatch::DispatchError, ensure, traits::Get, weights::Weight}; use sp_std::prelude::*; @@ -318,6 +319,47 @@ where } } +bitflags! { + /// Flags used to change the behaviour of `seal_call`. + struct CallFlags: u32 { + /// Forward the input of current function to the callee. + /// + /// Supplied input pointers are ignored when set. + /// + /// # Note + /// + /// A forwarding call will consume the current contracts input. Any attempt to + /// access the input after this call returns will lead to `[Error::InputForwarded]`. + /// It does not matter if this is due to calling `seal_input` or trying another + /// forwarding call. Consider using [`Self::CLONE_INPUT`] in order to preserve + /// the input. + const FORWARD_INPUT = 0b0000_0001; + /// Identical to [`Self::FORWARD_INPUT`] but without consuming the input. + /// + /// This adds some additional weight costs to the call. + /// + /// # Note + /// + /// This implies `[Self::FORWARD_INPUT]` and takes precendence when both are set. + const CLONE_INPUT = 0b0000_0010; + /// Do not return from the call but rather return the result of the callee to the + /// callers caller. + /// + /// # Note + /// + /// This makes the current contract completely transparent to its caller by replacing + /// this contracts potential output by the callee ones. Any code after `seal_call` + /// can be safely considered unreachable. + const TAIL_CALL = 0b0000_0100; + /// Allow the callee to reenter into the current contract. + /// + /// Without this flag any reentrancy into the current contract that originates from + /// the callee (or any of its callees) is denied. This includes the first callee: + /// You cannot call into yourself with this flag set. + const ALLOW_REENTRY = 0b0000_1000; + } +} + /// This is only appropriate when writing out data of constant size that does not depend on user /// input. In this case the costs for this copy was already charged as part of the token at /// the beginning of the API entry point. @@ -402,8 +444,7 @@ where // // Because panics are really undesirable in the runtime code, we treat this as // a trap for now. Eventually, we might want to revisit this. - Err(sp_sandbox::Error::Module) => - Err("validation error")?, + Err(sp_sandbox::Error::Module) => Err("validation error")?, // Any other kind of a trap should result in a failure. Err(sp_sandbox::Error::Execution) | Err(sp_sandbox::Error::OutOfBounds) => Err(Error::::ContractTrapped)? @@ -629,6 +670,65 @@ where (err, _) => Self::err_into_return_code(err) } } + + fn call( + &mut self, + flags: CallFlags, + callee_ptr: u32, + callee_len: u32, + gas: u64, + value_ptr: u32, + value_len: u32, + input_data_ptr: u32, + input_data_len: u32, + output_ptr: u32, + output_len_ptr: u32 + ) -> Result { + self.charge_gas(RuntimeCosts::CallBase(input_data_len))?; + let callee: <::T as frame_system::Config>::AccountId = + self.read_sandbox_memory_as(callee_ptr, callee_len)?; + let value: BalanceOf<::T> = self.read_sandbox_memory_as(value_ptr, value_len)?; + let input_data = if flags.contains(CallFlags::CLONE_INPUT) { + self.input_data.as_ref().ok_or_else(|| Error::::InputForwarded)?.clone() + } else if flags.contains(CallFlags::FORWARD_INPUT) { + self.input_data.take().ok_or_else(|| Error::::InputForwarded)? + } else { + self.read_sandbox_memory(input_data_ptr, input_data_len)? + }; + if value > 0u32.into() { + self.charge_gas(RuntimeCosts::CallSurchargeTransfer)?; + } + let charged = self.charge_gas( + RuntimeCosts::CallSurchargeCodeSize(::Schedule::get().limits.code_len) + )?; + let ext = &mut self.ext; + let call_outcome = ext.call( + gas, callee, value, input_data, flags.contains(CallFlags::ALLOW_REENTRY), + ); + let code_len = match &call_outcome { + Ok((_, len)) => len, + Err((_, len)) => len, + }; + self.adjust_gas(charged, RuntimeCosts::CallSurchargeCodeSize(*code_len)); + + // `TAIL_CALL` only matters on an `OK` result. Otherwise the call stack comes to + // a halt anyways without anymore code being executed. + if flags.contains(CallFlags::TAIL_CALL) { + if let Ok((return_value, _)) = call_outcome { + return Err(TrapReason::Return(ReturnData { + flags: return_value.flags.bits(), + data: return_value.data.0, + })); + } + } + + if let Ok((output, _)) = &call_outcome { + self.write_sandbox_output(output_ptr, output_len_ptr, &output.data, true, |len| { + Some(RuntimeCosts::CallCopyOut(len)) + })?; + } + Ok(Runtime::::exec_into_return_code(call_outcome.map(|r| r.0).map_err(|r| r.0))?) + } } // *********************************************************** @@ -758,6 +858,36 @@ define_env!(Env, , } }, + // Make a call to another contract. + // + // This is equivalent to calling the newer version of this function with + // `flags` set to `ALLOW_REENTRY`. See the newer version for documentation. + [seal0] seal_call( + ctx, + callee_ptr: u32, + callee_len: u32, + gas: u64, + value_ptr: u32, + value_len: u32, + input_data_ptr: u32, + input_data_len: u32, + output_ptr: u32, + output_len_ptr: u32 + ) -> ReturnCode => { + ctx.call( + CallFlags::ALLOW_REENTRY, + callee_ptr, + callee_len, + gas, + value_ptr, + value_len, + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + }, + // Make a call to another contract. // // The callees output buffer is copied to `output_ptr` and its length to `output_len_ptr`. @@ -766,6 +896,7 @@ define_env!(Env, , // // # Parameters // + // - flags: See [`CallFlags`] for a documenation of the supported flags. // - callee_ptr: a pointer to the address of the callee contract. // Should be decodable as an `T::AccountId`. Traps otherwise. // - callee_len: length of the address buffer. @@ -789,8 +920,9 @@ define_env!(Env, , // `ReturnCode::BelowSubsistenceThreshold` // `ReturnCode::TransferFailed` // `ReturnCode::NotCallable` - [seal0] seal_call( + [__unstable__] seal_call( ctx, + flags: u32, callee_ptr: u32, callee_len: u32, gas: u64, @@ -801,30 +933,18 @@ define_env!(Env, , output_ptr: u32, output_len_ptr: u32 ) -> ReturnCode => { - ctx.charge_gas(RuntimeCosts::CallBase(input_data_len))?; - let callee: <::T as frame_system::Config>::AccountId = - ctx.read_sandbox_memory_as(callee_ptr, callee_len)?; - let value: BalanceOf<::T> = ctx.read_sandbox_memory_as(value_ptr, value_len)?; - let input_data = ctx.read_sandbox_memory(input_data_ptr, input_data_len)?; - if value > 0u32.into() { - ctx.charge_gas(RuntimeCosts::CallSurchargeTransfer)?; - } - let charged = ctx.charge_gas( - RuntimeCosts::CallSurchargeCodeSize(::Schedule::get().limits.code_len) - )?; - let ext = &mut ctx.ext; - let call_outcome = ext.call(gas, callee, value, input_data); - let code_len = match &call_outcome { - Ok((_, len)) => len, - Err((_, len)) => len, - }; - ctx.adjust_gas(charged, RuntimeCosts::CallSurchargeCodeSize(*code_len)); - if let Ok((output, _)) = &call_outcome { - ctx.write_sandbox_output(output_ptr, output_len_ptr, &output.data, true, |len| { - Some(RuntimeCosts::CallCopyOut(len)) - })?; - } - Ok(Runtime::::exec_into_return_code(call_outcome.map(|r| r.0).map_err(|r| r.0))?) + ctx.call( + CallFlags::from_bits(flags).ok_or_else(|| "used rerved bit in CallFlags")?, + callee_ptr, + callee_len, + gas, + value_ptr, + value_len, + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) }, // Instantiate a contract with the specified code hash. @@ -945,7 +1065,6 @@ define_env!(Env, , ctx.charge_gas(RuntimeCosts::Terminate)?; let beneficiary: <::T as frame_system::Config>::AccountId = ctx.read_sandbox_memory_as(beneficiary_ptr, beneficiary_len)?; - let charged = ctx.charge_gas( RuntimeCosts::TerminateSurchargeCodeSize( ::Schedule::get().limits.code_len @@ -969,16 +1088,17 @@ define_env!(Env, , // // # Note // - // This function can only be called once. Calling it multiple times will trigger a trap. + // This function traps if the input was previously forwarded by a `seal_call`. [seal0] seal_input(ctx, out_ptr: u32, out_len_ptr: u32) => { ctx.charge_gas(RuntimeCosts::InputBase)?; if let Some(input) = ctx.input_data.take() { ctx.write_sandbox_output(out_ptr, out_len_ptr, &input, false, |len| { Some(RuntimeCosts::InputCopyOut(len)) })?; + ctx.input_data = Some(input); Ok(()) } else { - Err(Error::::InputAlreadyRead.into()) + Err(Error::::InputForwarded.into()) } },