diff --git a/crates/engine/src/database.rs b/crates/engine/src/database.rs index 730aebeec25..9f250b36d71 100644 --- a/crates/engine/src/database.rs +++ b/crates/engine/src/database.rs @@ -39,7 +39,7 @@ pub fn storage_of_contract_key(who: &[u8], key: &[u8]) -> [u8; 32] { /// /// Everything is stored in here: accounts, balances, contract storage, etc.. /// Just like in Substrate a prefix hash is computed for every contract. -#[derive(Default)] +#[derive(Default, Clone)] pub struct Database { hmap: HashMap, Vec>, } diff --git a/crates/engine/src/exec_context.rs b/crates/engine/src/exec_context.rs index 32d069e471e..817197c7fd2 100644 --- a/crates/engine/src/exec_context.rs +++ b/crates/engine/src/exec_context.rs @@ -21,7 +21,7 @@ use super::types::{ /// The context of a contract execution. #[cfg_attr(test, derive(Debug, PartialEq, Eq))] -#[derive(Default)] +#[derive(Default, Clone)] pub struct ExecContext { /// The caller of the contract execution. Might be user or another contract. /// @@ -43,6 +43,14 @@ pub struct ExecContext { pub block_number: BlockNumber, /// The current block timestamp. pub block_timestamp: BlockTimestamp, + /// The input of the call. + pub input: Vec, + /// The output buffer of the call. + pub output: Vec, + /// Is contract reverted. + pub reverted: bool, + /// Origin of the call. + pub origin: Option>, } impl ExecContext { diff --git a/crates/engine/src/ext.rs b/crates/engine/src/ext.rs index f6d302407c0..4c442ba9cb5 100644 --- a/crates/engine/src/ext.rs +++ b/crates/engine/src/ext.rs @@ -32,7 +32,12 @@ use crate::{ }, }; use scale::Encode; -use std::panic::panic_any; +use std::{ + cell::RefCell, + collections::HashMap, + panic::panic_any, + rc::Rc, +}; type Result = core::result::Result<(), Error>; @@ -110,7 +115,66 @@ impl ReturnCode { } } +#[derive(Default, Clone)] +pub struct ContractStorage { + pub instantiated: HashMap, Vec>, + pub entrance_count: HashMap, u32>, + pub reentrancy_allowed: HashMap, bool>, + pub deployed: HashMap, Contract>, +} + +impl ContractStorage { + pub fn get_entrance_count(&self, callee: Vec) -> u32 { + *self.entrance_count.get(&callee).unwrap_or(&0) + } + + pub fn get_allow_reentry(&self, callee: Vec) -> bool { + *self.reentrancy_allowed.get(&callee).unwrap_or(&false) + } + + pub fn set_allow_reentry(&mut self, callee: Vec, allow: bool) { + if allow { + self.reentrancy_allowed.insert(callee, allow); + } else { + self.reentrancy_allowed.remove(&callee); + } + } + + pub fn increase_entrance_count(&mut self, callee: Vec) -> Result { + let entrance_count = self + .entrance_count + .get(&callee) + .map_or(1, |count| count + 1); + self.entrance_count.insert(callee, entrance_count); + + Ok(()) + } + + pub fn decrease_entrance_count(&mut self, callee: Vec) -> Result { + let entrance_count = self.entrance_count.get(&callee).map_or_else( + || Err(Error::CalleeTrapped), + |count| { + if *count == 0 { + Err(Error::CalleeTrapped) + } else { + Ok(count - 1) + } + }, + )?; + + self.entrance_count.insert(callee, entrance_count); + Ok(()) + } +} + +#[derive(Clone)] +pub struct Contract { + pub deploy: fn(), + pub call: fn(), +} + /// The off-chain engine. +#[derive(Clone)] pub struct Engine { /// The environment database. pub database: Database, @@ -123,10 +187,13 @@ pub struct Engine { /// The chain specification. pub chain_spec: ChainSpec, /// Handler for registered chain extensions. - pub chain_extension_handler: ChainExtensionHandler, + pub chain_extension_handler: Rc>, + /// Contracts' store. + pub contracts: ContractStorage, } /// The chain specification. +#[derive(Clone)] pub struct ChainSpec { /// The current gas price. pub gas_price: Balance, @@ -162,7 +229,8 @@ impl Engine { exec_context: ExecContext::new(), debug_info: DebugInfo::new(), chain_spec: ChainSpec::default(), - chain_extension_handler: ChainExtensionHandler::new(), + chain_extension_handler: Rc::new(RefCell::new(ChainExtensionHandler::new())), + contracts: ContractStorage::default(), } } } @@ -323,8 +391,8 @@ impl Engine { .caller .as_ref() .expect("no caller has been set") - .as_bytes(); - set_output(output, caller); + .clone(); + set_output(output, caller.as_bytes()); } /// Returns the balance of the executed contract. @@ -333,7 +401,8 @@ impl Engine { .exec_context .callee .as_ref() - .expect("no callee has been set"); + .expect("no callee has been set") + .clone(); let balance_in_storage = self .database @@ -357,8 +426,8 @@ impl Engine { .callee .as_ref() .expect("no callee has been set") - .as_bytes(); - set_output(output, callee) + .clone(); + set_output(output, callee.as_bytes()) } /// Records the given debug message and appends to stdout. @@ -453,8 +522,8 @@ impl Engine { output: &mut &mut [u8], ) { let encoded_input = input.encode(); - let (status_code, out) = self - .chain_extension_handler + let mut chain_extension_hanler = self.chain_extension_handler.borrow_mut(); + let (status_code, out) = chain_extension_hanler .eval(func_id, &encoded_input) .unwrap_or_else(|error| { panic!( @@ -510,6 +579,79 @@ impl Engine { Err(_) => Err(Error::EcdsaRecoveryFailed), } } + + /// Register the contract into the local storage. + pub fn register_contract( + &mut self, + hash: &[u8], + deploy: fn(), + call: fn(), + ) -> Option { + self.contracts + .deployed + .insert(hash.to_vec(), Contract { deploy, call }) + } + + /// Apply call flags for the call and return the input that might be changed + pub fn apply_code_flags_before_call( + &mut self, + caller: Option, + callee: Vec, + call_flags: u32, + input: Vec, + ) -> core::result::Result, Error> { + let forward_input = (call_flags & 1) != 0; + let clone_input = ((call_flags & 2) >> 1) != 0; + let allow_reentry = ((call_flags & 8) >> 3) != 0; + + // Allow/deny reentrancy to the caller + if let Some(caller) = caller { + self.contracts + .set_allow_reentry(caller.as_bytes().to_vec(), allow_reentry); + } + + // Check if reentrancy that is not allowed is encountered + if !self.contracts.get_allow_reentry(callee.clone()) + && self.contracts.get_entrance_count(callee.clone()) > 0 + { + return Err(Error::CalleeTrapped) + } + + self.contracts.increase_entrance_count(callee)?; + + let new_input = if forward_input { + let previous_input = self.exec_context.input.clone(); + + // delete the input because we will forward it + self.exec_context.input.clear(); + + previous_input + } else if clone_input { + self.exec_context.input.clone() + } else { + input + }; + + Ok(new_input) + } + + /// Apply call flags after the call + pub fn apply_code_flags_after_call( + &mut self, + caller: Option, + callee: Vec, + _call_flags: u32, + _output: Vec, + ) -> core::result::Result<(), Error> { + self.contracts.decrease_entrance_count(callee)?; + + if let Some(caller) = caller { + self.contracts + .reentrancy_allowed + .remove(&caller.as_bytes().to_vec()); + } + Ok(()) + } } /// Copies the `slice` into `output`. @@ -525,3 +667,36 @@ fn set_output(output: &mut &mut [u8], slice: &[u8]) { ); output[..slice.len()].copy_from_slice(slice); } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn contract_storage_works() { + let mut storage = ContractStorage::default(); + + let account = [0u8; 32].to_vec(); + + assert!(!storage.get_allow_reentry(account.clone())); + storage.set_allow_reentry(account.clone(), true); + assert!(storage.get_allow_reentry(account.clone())); + + assert_eq!(storage.increase_entrance_count(account.clone()), Ok(())); + assert_eq!(storage.get_entrance_count(account.clone()), 1); + assert_eq!(storage.decrease_entrance_count(account.clone()), Ok(())); + assert_eq!(storage.get_entrance_count(account), 0); + } + + #[test] + pub fn decrease_entrance_count_fails() { + let mut storage = ContractStorage::default(); + + let account = [0u8; 32].to_vec(); + + assert_eq!( + storage.decrease_entrance_count(account), + Err(Error::CalleeTrapped) + ); + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 1fdafb4a9dd..fbb297b0f6a 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -22,7 +22,7 @@ pub mod test_api; mod chain_extension; mod database; -mod exec_context; +pub mod exec_context; mod hashing; mod types; diff --git a/crates/engine/src/test_api.rs b/crates/engine/src/test_api.rs index 4ee8d67745d..f1168bea44d 100644 --- a/crates/engine/src/test_api.rs +++ b/crates/engine/src/test_api.rs @@ -72,6 +72,7 @@ impl IntoIterator for RecordedDebugMessages { } /// Recorder for relevant interactions with this crate. +#[derive(Clone)] pub struct DebugInfo { /// Emitted events recorder. emitted_events: Vec, @@ -181,9 +182,9 @@ impl Engine { /// Returns the total number of reads and writes of the contract's storage. pub fn get_contract_storage_rw(&self, account_id: Vec) -> (usize, usize) { let account_id = AccountId::from(account_id); - let reads = self.debug_info.count_reads.get(&account_id).unwrap_or(&0); - let writes = self.debug_info.count_writes.get(&account_id).unwrap_or(&0); - (*reads, *writes) + let reads = *self.debug_info.count_reads.get(&account_id).unwrap_or(&0); + let writes = *self.debug_info.count_writes.get(&account_id).unwrap_or(&0); + (reads, writes) } /// Returns the total number of reads executed. @@ -210,14 +211,15 @@ impl Engine { /// /// Returns `None` if the `account_id` is non-existent. pub fn count_used_storage_cells(&self, account_id: &[u8]) -> Result { - let cells = self + let cells_len = self .debug_info .cells_per_account .get(&account_id.to_owned().into()) .ok_or_else(|| { Error::Account(AccountError::NoAccountForId(account_id.to_vec())) - })?; - Ok(cells.len()) + })? + .len(); + Ok(cells_len) } /// Advances the chain by a single block. diff --git a/crates/engine/src/types.rs b/crates/engine/src/types.rs index 73307264ac6..89c1b983b31 100644 --- a/crates/engine/src/types.rs +++ b/crates/engine/src/types.rs @@ -27,7 +27,7 @@ pub type BlockTimestamp = u64; pub type Balance = u128; /// The Account Id type used by this crate. -#[derive(Debug, From, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Default, Debug, From, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct AccountId(Vec); diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index 1449e707388..d83e1b259f6 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -423,7 +423,7 @@ where /// # Note /// /// This function stops the execution of the contract immediately. -pub fn return_value(return_flags: ReturnFlags, return_value: &R) -> ! +pub fn return_value(return_flags: ReturnFlags, return_value: &R) where R: scale::Encode, { diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index 9f16631ff27..f07f3cd6aa6 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -32,7 +32,7 @@ use crate::{ use ink_storage_traits::Storable; /// The flags to indicate further information about the end of a contract execution. -#[derive(Default)] +#[derive(Default, Clone)] pub struct ReturnFlags { value: u32, } @@ -53,6 +53,11 @@ impl ReturnFlags { self } + /// Returns `true` if the execution is going to be reverted. + pub fn is_reverted(&self) -> bool { + self.value & 1 > 0 + } + /// Returns the underlying `u32` representation. #[cfg(not(feature = "std"))] pub(crate) fn into_u32(self) -> u32 { @@ -238,7 +243,6 @@ pub trait EnvBackend { T: scale::Decode; /// Returns the value back to the caller of the executed contract. - /// /// # Note /// /// Calling this method will end contract execution immediately. @@ -246,7 +250,7 @@ pub trait EnvBackend { /// /// The `flags` parameter can be used to revert the state changes of the /// entire execution if necessary. - fn return_value(&mut self, flags: ReturnFlags, return_value: &R) -> ! + fn return_value(&mut self, flags: ReturnFlags, return_value: &R) where R: scale::Encode; diff --git a/crates/env/src/contract.rs b/crates/env/src/contract.rs index 4eef18fa49b..fd4f329462f 100644 --- a/crates/env/src/contract.rs +++ b/crates/env/src/contract.rs @@ -132,3 +132,12 @@ pub trait ContractReference { /// The generated contract reference type. type Type; } + +/// Entrypoint of the contract to execute constructors or messages. +pub trait Entrypoint { + /// Entrypoint to run a constructor for the contract. It deploys the contract to the environment. + fn deploy(); + + /// Entrypoint to run a message of the contract. + fn call(); +} diff --git a/crates/env/src/engine/mod.rs b/crates/env/src/engine/mod.rs index f75a32c4aed..e18a521280f 100644 --- a/crates/env/src/engine/mod.rs +++ b/crates/env/src/engine/mod.rs @@ -30,7 +30,6 @@ use ink_primitives::{ ConstructorResult, LangError, }; - pub trait OnInstance: EnvBackend + TypedEnvBackend { fn on_instance(f: F) -> R where diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index ea7a2b4f674..8c18ebfb9cb 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -42,12 +42,14 @@ use crate::{ ReturnFlags, TypedEnvBackend, }; +use core::mem; use ink_engine::{ + exec_context::ExecContext, ext, ext::Engine, }; use ink_storage_traits::Storable; - +use scale::Encode; /// The capacity of the static buffer. /// This is the same size as the ink! on-chain environment. We chose to use the same size /// to be as close to the on-chain behavior as possible. @@ -101,6 +103,20 @@ impl CryptoHash for Keccak256 { } } +pub fn generate_address( + caller: Vec, + code_hash: Vec, + input_data: Vec, + salt: Vec, +) -> [u8; 32] { + let mut output = [0u8; 32]; + Sha2x256::hash( + [caller, code_hash, input_data, salt].concat().as_slice(), + &mut output, + ); + output +} + impl From for crate::Error { fn from(ext_error: ext::Error) -> Self { match ext_error { @@ -180,9 +196,38 @@ impl EnvInstance { { let mut full_scope: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; let full_scope = &mut &mut full_scope[..]; - ext_fn(&self.engine, full_scope); + ext_fn(&self.engine.borrow_mut(), full_scope); scale::Decode::decode(&mut &full_scope[..]).map_err(Into::into) } + + /// Generates new execution context, replaces it as current and returns previous one + fn create_new_exec_context( + &mut self, + callee: Vec, + input: Vec, + transferred_value: u128, + ) -> ExecContext { + let callee_context = ExecContext { + caller: self.engine.borrow().exec_context.callee.clone(), + callee: Some(callee.clone().into()), + value_transferred: transferred_value, + block_number: self.engine.borrow().exec_context.block_number, + block_timestamp: self.engine.borrow().exec_context.block_timestamp, + input, + output: vec![], + reverted: false, + origin: Some( + self.engine + .borrow() + .exec_context + .origin + .clone() + .unwrap_or(callee), + ), + }; + + mem::replace(&mut self.engine.borrow_mut().exec_context, callee_context) + } } impl EnvBackend for EnvInstance { @@ -193,7 +238,7 @@ impl EnvBackend for EnvInstance { { let mut v = vec![]; Storable::encode(value, &mut v); - self.engine.set_storage(&key.encode(), &v[..]) + self.engine.borrow_mut().set_storage(&key.encode(), &v[..]) } fn get_contract_storage(&mut self, key: &K) -> Result> @@ -202,7 +247,11 @@ impl EnvBackend for EnvInstance { R: Storable, { let mut output: [u8; 9600] = [0; 9600]; - match self.engine.get_storage(&key.encode(), &mut &mut output[..]) { + match self + .engine + .borrow_mut() + .get_storage(&key.encode(), &mut &mut output[..]) + { Ok(_) => (), Err(ext::Error::KeyNotFound) => return Ok(None), Err(_) => panic!("encountered unexpected error"), @@ -219,6 +268,7 @@ impl EnvBackend for EnvInstance { let mut output: [u8; 9600] = [0; 9600]; match self .engine + .borrow_mut() .take_storage(&key.encode(), &mut &mut output[..]) { Ok(_) => (), @@ -233,32 +283,36 @@ impl EnvBackend for EnvInstance { where K: scale::Encode, { - self.engine.contains_storage(&key.encode()) + self.engine.borrow_mut().contains_storage(&key.encode()) } fn clear_contract_storage(&mut self, key: &K) -> Option where K: scale::Encode, { - self.engine.clear_storage(&key.encode()) + self.engine.borrow_mut().clear_storage(&key.encode()) } fn decode_input(&mut self) -> Result where T: scale::Decode, { - unimplemented!("the off-chain env does not implement `input`") + T::decode(&mut self.engine.borrow().exec_context.input.as_slice()) + .map_err(|_| Error::CalleeTrapped) } - fn return_value(&mut self, _flags: ReturnFlags, _return_value: &R) -> ! + fn return_value(&mut self, flags: ReturnFlags, return_value: &R) where R: scale::Encode, { - unimplemented!("the off-chain env does not implement `return_value`") + if flags.is_reverted() { + self.engine.borrow_mut().exec_context.reverted = true; + } + self.engine.borrow_mut().exec_context.output = return_value.encode(); } fn debug_message(&mut self, message: &str) { - self.engine.debug_message(message) + self.engine.borrow_mut().debug_message(message) } fn hash_bytes(&mut self, input: &[u8], output: &mut ::Type) @@ -349,8 +403,11 @@ impl EnvBackend for EnvInstance { let enc_input = &scale::Encode::encode(input)[..]; let mut output: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; - self.engine - .call_chain_extension(func_id, enc_input, &mut &mut output[..]); + self.engine.borrow_mut().call_chain_extension( + func_id, + enc_input, + &mut &mut output[..], + ); let (status, out): (u32, Vec) = scale::Decode::decode(&mut &output[..]) .unwrap_or_else(|error| { panic!("could not decode `call_chain_extension` output: {error:?}") @@ -361,8 +418,21 @@ impl EnvBackend for EnvInstance { Ok(decoded) } - fn set_code_hash(&mut self, _code_hash: &[u8]) -> Result<()> { - unimplemented!("off-chain environment does not support `set_code_hash`") + fn set_code_hash(&mut self, code_hash: &[u8]) -> Result<()> { + let account_id = self + .engine + .borrow() + .exec_context + .callee + .clone() + .ok_or(Error::CalleeTrapped)?; + + self.engine + .borrow_mut() + .contracts + .instantiated + .insert(account_id.as_bytes().to_vec(), code_hash.to_vec()); + Ok(()) } } @@ -429,7 +499,9 @@ impl TypedEnvBackend for EnvInstance { let builder = TopicsBuilder::default(); let enc_topics = event.topics::(builder.into()); let enc_data = &scale::Encode::encode(&event)[..]; - self.engine.deposit_event(&enc_topics[..], enc_data); + self.engine + .borrow_mut() + .deposit_event(&enc_topics[..], enc_data); } fn invoke_contract( @@ -441,12 +513,82 @@ impl TypedEnvBackend for EnvInstance { Args: scale::Encode, R: scale::Decode, { + let callee = params.callee().as_ref().to_vec(); let _gas_limit = params.gas_limit(); - let _callee = params.callee(); - let _call_flags = params.call_flags().into_u32(); - let _transferred_value = params.transferred_value(); - let _input = params.exec_input(); - unimplemented!("off-chain environment does not support contract invocation") + + let call_flags = params.call_flags().into_u32(); + let transferred_value = params.transferred_value(); + let caller = self.engine.borrow().exec_context.callee.clone(); + + // apply call flags before making a call and return the input that might be changed after that + let input = self.engine.borrow_mut().apply_code_flags_before_call( + caller.clone(), + callee.clone(), + call_flags, + params.exec_input().encode(), + )?; + + let mut previous_context = self.create_new_exec_context( + callee.clone(), + input, + ::decode( + &mut scale::Encode::encode(transferred_value).as_slice(), + )?, + ); + + let code_hash = self + .engine + .borrow() + .contracts + .instantiated + .get(&callee) + .ok_or(Error::NotCallable)? + .clone(); + + let call_fn = self + .engine + .borrow() + .contracts + .deployed + .get(&code_hash) + .ok_or(Error::CodeNotFound)? + .call; + + // save previous version of storage in case call will revert + let storage = self + .engine + .borrow() + .database + .get_from_contract_storage(callee.as_slice(), &[0; 4]) + .expect("contract storage not found") + .clone(); + + call_fn(); + + // revert contract's state in case of error + if self.engine.borrow().exec_context.reverted { + self.engine + .borrow_mut() + .database + .insert_into_contract_storage(callee.as_slice(), &[0; 4], storage) + .unwrap(); + } + + // if the call was reverted, previous one should be reverted too + previous_context.reverted |= self.engine.borrow().exec_context.reverted; + + let output = self.engine.borrow().exec_context.output.clone(); + let return_value = scale::Decode::decode(&mut output.as_slice())?; + + let _ = + mem::replace(&mut self.engine.borrow_mut().exec_context, previous_context); + + // apply code flags after the call + self.engine + .borrow_mut() + .apply_code_flags_after_call(caller, callee, call_flags, output)?; + + Ok(return_value) } fn invoke_contract_delegate( @@ -458,10 +600,77 @@ impl TypedEnvBackend for EnvInstance { Args: scale::Encode, R: scale::Decode, { - let _code_hash = params.code_hash(); - unimplemented!( - "off-chain environment does not support delegated contract invocation" - ) + let code_hash = params.code_hash().as_ref().to_vec(); + let callee = self.engine.borrow().exec_context.callee.clone(); + let call_flags = params.call_flags().into_u32(); + + // apply call flags before making a call and return the input that might be changed after that + let input = self.engine.borrow_mut().apply_code_flags_before_call( + callee.clone(), + callee.clone().unwrap_or_default().as_bytes().to_vec(), + call_flags, + params.exec_input().encode(), + )?; + + let call_fn = self + .engine + .borrow() + .contracts + .deployed + .get(&code_hash) + .ok_or(Error::CodeNotFound)? + .call; + + let mut previous_context = self.create_new_exec_context( + callee.clone().unwrap_or_default().as_bytes().to_vec(), + input, + 0, + ); + + let storage = self + .engine + .borrow() + .database + .get_from_contract_storage( + callee.clone().unwrap_or_default().as_bytes(), + &[0; 4], + ) + .expect("contract storage not found") + .clone(); + + call_fn(); + + // revert contract's state in case of error + if self.engine.borrow().exec_context.reverted { + self.engine + .borrow_mut() + .database + .insert_into_contract_storage( + callee.clone().unwrap_or_default().as_bytes(), + &[0; 4], + storage, + ) + .unwrap(); + } + + // if the call was reverted, previous one should be reverted too + previous_context.reverted |= self.engine.borrow().exec_context.reverted; + + let output = self.engine.borrow().exec_context.output.clone(); + let return_value = R::decode(&mut output.as_slice())?; + + let _ = + mem::replace(&mut self.engine.borrow_mut().exec_context, previous_context); + + // apply code flags after the call + self.engine.borrow_mut().apply_code_flags_after_call( + callee.clone(), + callee.unwrap_or_default().as_bytes().to_vec(), + call_flags, + output, + )?; + + Ok(return_value) } fn instantiate_contract( @@ -479,12 +688,63 @@ impl TypedEnvBackend for EnvInstance { Salt: AsRef<[u8]>, R: ConstructorReturnType, { - let _code_hash = params.code_hash(); + let code_hash = params.code_hash().as_ref().to_vec(); + // Gas is not supported by off-chain env. let _gas_limit = params.gas_limit(); - let _endowment = params.endowment(); - let _input = params.exec_input(); - let _salt_bytes = params.salt_bytes(); - unimplemented!("off-chain environment does not support contract instantiation") + let caller = self.engine.borrow().exec_context.callee.clone(); + let endowment = params.endowment(); + let input = params.exec_input(); + let salt_bytes = params.salt_bytes(); + + let callee = generate_address( + caller.unwrap_or_default().as_bytes().to_vec(), + code_hash.clone(), + input.encode(), + salt_bytes.as_ref().to_vec(), + ) + .to_vec(); + + let previous_context = self.create_new_exec_context( + callee.clone(), + input.encode(), + ::decode( + &mut scale::Encode::encode(endowment).as_slice(), + )?, + ); + + let deploy_fn = self + .engine + .borrow() + .contracts + .deployed + .get(&code_hash) + .ok_or(Error::CodeNotFound)? + .deploy; + + self.engine + .borrow_mut() + .contracts + .instantiated + .insert(callee.clone(), code_hash); + + deploy_fn(); + + let output = self.engine.borrow().exec_context.output.clone(); + + let instantiate_result = if self.engine.borrow().exec_context.reverted { + Err(Error::CalleeReverted) + } else { + Ok(()) + }; + + let _ = + mem::replace(&mut self.engine.borrow_mut().exec_context, previous_context); + + crate::engine::decode_instantiate_result::<_, E, ContractRef, R>( + instantiate_result.map_err(Into::into), + &mut &callee[..], + &mut &output[..], + ) } fn terminate_contract(&mut self, beneficiary: E::AccountId) -> ! @@ -492,7 +752,7 @@ impl TypedEnvBackend for EnvInstance { E: Environment, { let buffer = scale::Encode::encode(&beneficiary); - self.engine.terminate(&buffer[..]) + self.engine.borrow_mut().terminate(&buffer[..]) } fn transfer(&mut self, destination: E::AccountId, value: E::Balance) -> Result<()> @@ -502,44 +762,73 @@ impl TypedEnvBackend for EnvInstance { let enc_destination = &scale::Encode::encode(&destination)[..]; let enc_value = &scale::Encode::encode(&value)[..]; self.engine + .borrow_mut() .transfer(enc_destination, enc_value) .map_err(Into::into) } fn weight_to_fee(&mut self, gas: u64) -> E::Balance { let mut output: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; - self.engine.weight_to_fee(gas, &mut &mut output[..]); + self.engine + .borrow_mut() + .weight_to_fee(gas, &mut &mut output[..]); scale::Decode::decode(&mut &output[..]).unwrap_or_else(|error| { panic!("could not read `weight_to_fee` property: {error:?}") }) } - fn is_contract(&mut self, _account: &E::AccountId) -> bool + fn is_contract(&mut self, account: &E::AccountId) -> bool where E: Environment, { - unimplemented!("off-chain environment does not support contract instantiation") + self.engine + .borrow() + .contracts + .instantiated + .contains_key(account.as_ref().to_vec().as_slice()) } fn caller_is_origin(&mut self) -> bool where E: Environment, { - unimplemented!("off-chain environment does not support cross-contract calls") - } - - fn code_hash(&mut self, _account: &E::AccountId) -> Result + let engine = self.engine.borrow(); + + engine + .exec_context + .origin + .clone() + .expect("origin should exist") + == engine + .exec_context + .caller + .clone() + .expect("caller should exist") + .as_bytes() + } + + fn code_hash(&mut self, account: &E::AccountId) -> Result where E: Environment, { - unimplemented!("off-chain environment does not support `code_hash`") + let code_hash = self + .engine + .borrow() + .contracts + .instantiated + .get(&account.as_ref().to_vec()) + .ok_or(Error::NotCallable)? + .clone(); + + Ok(<_ as scale::Decode>::decode(&mut code_hash.as_slice())?) } fn own_code_hash(&mut self) -> Result where E: Environment, { - unimplemented!("off-chain environment does not support `own_code_hash`") + let account_id = self.account_id::(); + self.code_hash::(&account_id) } #[cfg(feature = "call-runtime")] diff --git a/crates/env/src/engine/off_chain/mod.rs b/crates/env/src/engine/off_chain/mod.rs index 2d2d0be6e62..bb876b33be7 100644 --- a/crates/env/src/engine/off_chain/mod.rs +++ b/crates/env/src/engine/off_chain/mod.rs @@ -20,17 +20,21 @@ mod types; #[cfg(test)] mod tests; -pub use call_data::CallData; - use super::OnInstance; use crate::Error; +pub use call_data::CallData; +use std::{ + cell::RefCell, + rc::Rc, +}; use derive_more::From; use ink_engine::ext::Engine; /// The off-chain environment. +#[derive(Clone)] pub struct EnvInstance { - engine: Engine, + engine: Rc>, } impl OnInstance for EnvInstance { @@ -38,15 +42,12 @@ impl OnInstance for EnvInstance { where F: FnOnce(&mut Self) -> R, { - use core::cell::RefCell; thread_local!( - static INSTANCE: RefCell = RefCell::new( - EnvInstance { - engine: Engine::new() + static INSTANCE: EnvInstance = EnvInstance { + engine: Rc::new(RefCell::new(Engine::new())) } - ) ); - INSTANCE.with(|instance| f(&mut instance.borrow_mut())) + INSTANCE.with(|instance| f(&mut instance.clone())) } } diff --git a/crates/env/src/engine/off_chain/test_api.rs b/crates/env/src/engine/off_chain/test_api.rs index c0ccb47a256..d457c2f4153 100644 --- a/crates/env/src/engine/off_chain/test_api.rs +++ b/crates/env/src/engine/off_chain/test_api.rs @@ -27,6 +27,8 @@ use ink_engine::test_api::RecordedDebugMessages; use std::panic::UnwindSafe; pub use super::call_data::CallData; +use crate::contract::Entrypoint; +use ink_engine::ext::Contract; pub use ink_engine::ChainExtension; /// Record for an emitted event. @@ -57,6 +59,7 @@ where ::on_instance(|instance| { instance .engine + .borrow_mut() .set_balance(scale::Encode::encode(&account_id), new_balance); }) } @@ -80,6 +83,7 @@ where ::on_instance(|instance| { instance .engine + .borrow() .get_balance(scale::Encode::encode(&account_id)) .map_err(Into::into) }) @@ -93,7 +97,9 @@ where ::on_instance(|instance| { instance .engine + .borrow_mut() .chain_extension_handler + .borrow_mut() .register(Box::new(extension)); }) } @@ -101,7 +107,7 @@ where /// Returns the contents of the past performed environmental debug messages in order. pub fn recorded_debug_messages() -> RecordedDebugMessages { ::on_instance(|instance| { - instance.engine.get_emitted_debug_messages() + instance.engine.borrow().get_emitted_debug_messages() }) } @@ -123,7 +129,7 @@ where T: Environment, { ::on_instance(|instance| { - instance.engine.advance_block(); + instance.engine.borrow_mut().advance_block(); }) } @@ -134,7 +140,10 @@ where ::AccountId: From<[u8; 32]>, { ::on_instance(|instance| { - instance.engine.set_caller(scale::Encode::encode(&caller)); + instance + .engine + .borrow_mut() + .set_caller(scale::Encode::encode(&caller)); }) } @@ -145,7 +154,10 @@ where ::AccountId: From<[u8; 32]>, { ::on_instance(|instance| { - instance.engine.set_callee(scale::Encode::encode(&callee)); + instance + .engine + .borrow_mut() + .set_callee(scale::Encode::encode(&callee)); }) } @@ -157,7 +169,7 @@ where T: Environment, { ::on_instance(|instance| { - let callee = instance.engine.get_callee(); + let callee = instance.engine.borrow().get_callee(); scale::Decode::decode(&mut &callee[..]) .unwrap_or_else(|err| panic!("encoding failed: {err}")) }) @@ -171,6 +183,7 @@ where ::on_instance(|instance| { instance .engine + .borrow() .get_contract_storage_rw(scale::Encode::encode(&account_id)) }) } @@ -183,7 +196,7 @@ where T: Environment, // Just temporary for the MVP! { ::on_instance(|instance| { - instance.engine.set_value_transferred(value); + instance.engine.borrow_mut().set_value_transferred(value); }) } @@ -197,6 +210,7 @@ where ::on_instance(|instance| { let caller = instance .engine + .borrow() .exec_context .caller .as_ref() @@ -206,22 +220,26 @@ where let caller_old_balance = instance .engine + .borrow() .get_balance(caller.clone()) .unwrap_or_default(); - let callee = instance.engine.get_callee(); + let callee = instance.engine.borrow().get_callee(); let contract_old_balance = instance .engine + .borrow() .get_balance(callee.clone()) .unwrap_or_default(); instance .engine + .borrow_mut() .set_balance(caller, caller_old_balance - value); instance .engine + .borrow_mut() .set_balance(callee, contract_old_balance + value); - instance.engine.set_value_transferred(value); + instance.engine.borrow_mut().set_value_transferred(value); }); } @@ -235,6 +253,7 @@ where ::on_instance(|instance| { instance .engine + .borrow() .count_used_storage_cells(&scale::Encode::encode(&account_id)) .map_err(Into::into) }) @@ -250,31 +269,23 @@ where { let default_accounts = default_accounts::(); ::on_instance(|instance| { - instance.engine.initialize_or_reset(); + let mut engine = instance.engine.borrow_mut(); + engine.initialize_or_reset(); let encoded_alice = scale::Encode::encode(&default_accounts.alice); - instance.engine.set_caller(encoded_alice.clone()); - instance.engine.set_callee(encoded_alice.clone()); + engine.set_caller(encoded_alice.clone()); + engine.set_callee(encoded_alice.clone()); // set up the funds for the default accounts let substantial = 1_000_000; let some = 1_000; - instance.engine.set_balance(encoded_alice, substantial); - instance - .engine - .set_balance(scale::Encode::encode(&default_accounts.bob), some); - instance - .engine - .set_balance(scale::Encode::encode(&default_accounts.charlie), some); - instance - .engine - .set_balance(scale::Encode::encode(&default_accounts.django), 0); - instance - .engine - .set_balance(scale::Encode::encode(&default_accounts.eve), 0); - instance - .engine - .set_balance(scale::Encode::encode(&default_accounts.frank), 0); + + engine.set_balance(encoded_alice, substantial); + engine.set_balance(scale::Encode::encode(&default_accounts.bob), some); + engine.set_balance(scale::Encode::encode(&default_accounts.charlie), some); + engine.set_balance(scale::Encode::encode(&default_accounts.django), 0); + engine.set_balance(scale::Encode::encode(&default_accounts.eve), 0); + engine.set_balance(scale::Encode::encode(&default_accounts.frank), 0); }); f(default_accounts) } @@ -320,12 +331,28 @@ pub fn recorded_events() -> impl Iterator { ::on_instance(|instance| { instance .engine + .borrow() .get_emitted_events() .into_iter() .map(|evt: ink_engine::test_api::EmittedEvent| evt.into()) }) } +/// Registers the contract by the code hash. After registration, the contract can be instantiated. +pub fn register_contract(code_hash: &[u8]) -> Option +where + C: Entrypoint + ?Sized, +{ + ::on_instance(|instance| { + let deploy = C::deploy; + let call = C::call; + instance + .engine + .borrow_mut() + .register_contract(code_hash, deploy, call) + }) +} + /// Tests if a contract terminates successfully after `self.env().terminate()` /// has been called. /// diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index f49296d56b0..00fe2d04f79 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -281,7 +281,7 @@ impl EnvBackend for EnvInstance { self.get_property::(ext::input) } - fn return_value(&mut self, flags: ReturnFlags, return_value: &R) -> ! + fn return_value(&mut self, flags: ReturnFlags, return_value: &R) where R: scale::Encode, { diff --git a/crates/env/src/lib.rs b/crates/env/src/lib.rs index e181868e8c5..2beb88f82a5 100644 --- a/crates/env/src/lib.rs +++ b/crates/env/src/lib.rs @@ -73,7 +73,8 @@ mod arithmetic; mod backend; pub mod call; pub mod chain_extension; -mod contract; +/// Contract entrypoints +pub mod contract; mod engine; mod error; pub mod hash; diff --git a/crates/ink/codegen/src/generator/dispatch.rs b/crates/ink/codegen/src/generator/dispatch.rs index 4f34d5f309b..b8ec1c51483 100644 --- a/crates/ink/codegen/src/generator/dispatch.rs +++ b/crates/ink/codegen/src/generator/dispatch.rs @@ -79,12 +79,7 @@ impl GenerateCode for Dispatch<'_> { #contract_dispatchable_messages_infos #constructor_decoder_type #message_decoder_type - - #[cfg(not(test))] - #[cfg(not(feature = "ink-as-dependency"))] - const _: () = { - #entry_points - }; + #entry_points } } } @@ -412,79 +407,93 @@ impl Dispatch<'_> { self.any_message_accepts_payment_expr(message_spans); quote_spanned!(span=> #[cfg(not(test))] - #[no_mangle] - #[allow(clippy::nonminimal_bool)] - fn deploy() { - if !#any_constructor_accept_payment { - ::ink::codegen::deny_payment::<<#storage_ident as ::ink::env::ContractEnv>::Env>() - .unwrap_or_else(|error| ::core::panic!("{}", error)) + #[cfg(not(feature = "ink-as-dependency"))] + const _: () = { + #[no_mangle] + fn deploy() { + <#storage_ident as ::ink::env::contract::Entrypoint>::deploy() } - let dispatchable = match ::ink::env::decode_input::< - <#storage_ident as ::ink::reflect::ContractConstructorDecoder>::Type, - >() { - ::core::result::Result::Ok(decoded_dispatchable) => { - decoded_dispatchable - } - ::core::result::Result::Err(_decoding_error) => { - let error = ::ink::ConstructorResult::Err(::ink::LangError::CouldNotReadInput); - - // At this point we're unable to set the `Ok` variant to be the any "real" - // constructor output since we were unable to figure out what the caller wanted - // to dispatch in the first place, so we set it to `()`. - // - // This is okay since we're going to only be encoding the `Err` variant - // into the output buffer anyways. - ::ink::env::return_value::<::ink::ConstructorResult<()>>( - ::ink::env::ReturnFlags::new_with_reverted(true), - &error, - ); + #[no_mangle] + fn call() { + <#storage_ident as ::ink::env::contract::Entrypoint>::call() + } + }; + + impl ::ink::env::contract::Entrypoint for #storage_ident { + #[allow(clippy::nonminimal_bool)] + fn deploy() { + if !#any_constructor_accept_payment { + ::ink::codegen::deny_payment::<<#storage_ident as ::ink::env::ContractEnv>::Env>() + .unwrap_or_else(|error| ::core::panic!("{}", error)) } - }; - <<#storage_ident as ::ink::reflect::ContractConstructorDecoder>::Type - as ::ink::reflect::ExecuteDispatchable>::execute_dispatchable(dispatchable) - .unwrap_or_else(|error| { - ::core::panic!("dispatching ink! message failed: {}", error) - }) - } + let dispatchable = match ::ink::env::decode_input::< + <#storage_ident as ::ink::reflect::ContractConstructorDecoder>::Type, + >() { + ::core::result::Result::Ok(decoded_dispatchable) => { + decoded_dispatchable + } + ::core::result::Result::Err(_decoding_error) => { + let error = ::ink::ConstructorResult::Err(::ink::LangError::CouldNotReadInput); + + // At this point we're unable to set the `Ok` variant to be the any "real" + // constructor output since we were unable to figure out what the caller wanted + // to dispatch in the first place, so we set it to `()`. + // + // This is okay since we're going to only be encoding the `Err` variant + // into the output buffer anyways. + ::ink::env::return_value::<::ink::ConstructorResult<()>>( + ::ink::env::ReturnFlags::new_with_reverted(true), + &error, + ); + ::core::panic!("execute_constructor reverted"); + } + }; - #[cfg(not(test))] - #[no_mangle] - #[allow(clippy::nonminimal_bool)] - fn call() { - if !#any_message_accept_payment { - ::ink::codegen::deny_payment::<<#storage_ident as ::ink::env::ContractEnv>::Env>() - .unwrap_or_else(|error| ::core::panic!("{}", error)) + <<#storage_ident as ::ink::reflect::ContractConstructorDecoder>::Type + as ::ink::reflect::ExecuteDispatchable>::execute_dispatchable(dispatchable) + .unwrap_or_else(|error| { + ::core::panic!("dispatching ink! message failed: {}", error) + }) } - let dispatchable = match ::ink::env::decode_input::< - <#storage_ident as ::ink::reflect::ContractMessageDecoder>::Type, - >() { - ::core::result::Result::Ok(decoded_dispatchable) => { - decoded_dispatchable - } - ::core::result::Result::Err(_decoding_error) => { - let error = ::ink::MessageResult::Err(::ink::LangError::CouldNotReadInput); - - // At this point we're unable to set the `Ok` variant to be the any "real" - // message output since we were unable to figure out what the caller wanted - // to dispatch in the first place, so we set it to `()`. - // - // This is okay since we're going to only be encoding the `Err` variant - // into the output buffer anyways. - ::ink::env::return_value::<::ink::MessageResult<()>>( - ::ink::env::ReturnFlags::new_with_reverted(true), - &error, - ); + #[allow(clippy::nonminimal_bool)] + fn call() { + if !#any_message_accept_payment { + ::ink::codegen::deny_payment::<<#storage_ident as ::ink::env::ContractEnv>::Env>() + .unwrap_or_else(|error| ::core::panic!("{}", error)) } - }; - <<#storage_ident as ::ink::reflect::ContractMessageDecoder>::Type - as ::ink::reflect::ExecuteDispatchable>::execute_dispatchable(dispatchable) - .unwrap_or_else(|error| { - ::core::panic!("dispatching ink! message failed: {}", error) - }) + let dispatchable = match ::ink::env::decode_input::< + <#storage_ident as ::ink::reflect::ContractMessageDecoder>::Type, + >() { + ::core::result::Result::Ok(decoded_dispatchable) => { + decoded_dispatchable + } + ::core::result::Result::Err(_decoding_error) => { + let error = ::ink::MessageResult::Err(::ink::LangError::CouldNotReadInput); + + // At this point we're unable to set the `Ok` variant to be the any "real" + // message output since we were unable to figure out what the caller wanted + // to dispatch in the first place, so we set it to `()`. + // + // This is okay since we're going to only be encoding the `Err` variant + // into the output buffer anyways. + ::ink::env::return_value::<::ink::MessageResult<()>>( + ::ink::env::ReturnFlags::new_with_reverted(true), + &error, + ); + ::core::panic!("execute_message reverted"); + } + }; + + <<#storage_ident as ::ink::reflect::ContractMessageDecoder>::Type + as ::ink::reflect::ExecuteDispatchable>::execute_dispatchable(dispatchable) + .unwrap_or_else(|error| { + ::core::panic!("dispatching ink! message failed: {}", error) + }) + } } ) } @@ -632,7 +641,7 @@ impl Dispatch<'_> { ); } - ::ink::env::return_value::< + ::core::result::Result::Ok(::ink::env::return_value::< ::ink::ConstructorResult< ::core::result::Result<(), &#constructor_value::Error> >, @@ -641,7 +650,7 @@ impl Dispatch<'_> { // Currently no `LangError`s are raised at this level of the // dispatch logic so `Ok` is always returned to the caller. &::ink::ConstructorResult::Ok(output_result.map(|_| ())), - ); + )) } ) }); @@ -834,12 +843,12 @@ impl Dispatch<'_> { push_contract(contract, #mutates_storage); } - ::ink::env::return_value::<::ink::MessageResult::<#message_output>>( + ::core::result::Result::Ok(::ink::env::return_value::<::ink::MessageResult::<#message_output>>( ::ink::env::ReturnFlags::new_with_reverted(is_reverted), // Currently no `LangError`s are raised at this level of the // dispatch logic so `Ok` is always returned to the caller. &::ink::MessageResult::Ok(result), - ) + )) } ) }); @@ -909,7 +918,7 @@ impl Dispatch<'_> { match self { #( #message_execute ),* - }; + } } } diff --git a/integration-tests/delegator/adder/lib.rs b/integration-tests/delegator/adder/lib.rs index 839915c6175..a4ca6c7a981 100644 --- a/integration-tests/delegator/adder/lib.rs +++ b/integration-tests/delegator/adder/lib.rs @@ -30,3 +30,38 @@ mod adder { } } } + +#[cfg(test)] +mod test { + use ink::primitives::Hash; + + #[test] + fn cross_contract_call_works_off_chain() { + use super::*; + use accumulator::{ + Accumulator, + AccumulatorRef, + }; + + // register Accumulator & Adder + let hash1 = Hash::from([10u8; 32]); + let hash2 = Hash::from([20u8; 32]); + ink::env::test::register_contract::(hash1.as_ref()); + ink::env::test::register_contract::(hash2.as_ref()); + + let acc = AccumulatorRef::new(0) + .code_hash(hash1) + .endowment(0) + .salt_bytes([0u8; 0]) + .instantiate(); + let mut adder = AdderRef::new(acc.clone()) + .code_hash(hash2) + .endowment(0) + .salt_bytes([0u8; 0]) + .instantiate(); + + assert_eq!(acc.get(), 0); + adder.inc(1); + assert_eq!(acc.get(), 1); + } +} diff --git a/integration-tests/lang-err-integration-tests/constructors-return-value/lib.rs b/integration-tests/lang-err-integration-tests/constructors-return-value/lib.rs index c25fa6e1a5a..1a6ddf0538f 100644 --- a/integration-tests/lang-err-integration-tests/constructors-return-value/lib.rs +++ b/integration-tests/lang-err-integration-tests/constructors-return-value/lib.rs @@ -41,7 +41,8 @@ pub mod constructors_return_value { ink::env::return_value::>( ink::env::ReturnFlags::new_with_reverted(true), &Ok(AccountId::from([0u8; 32])), - ) + ); + unreachable!() } /// A constructor which reverts and fills the output buffer with an erroneously encoded @@ -56,7 +57,8 @@ pub mod constructors_return_value { ink::env::return_value::< ink::ConstructorResult>, - >(ink::env::ReturnFlags::new_with_reverted(true), &value) + >(ink::env::ReturnFlags::new_with_reverted(true), &value); + unreachable!(); } /// Returns the current value of the contract storage. diff --git a/integration-tests/reentrancy/.gitignore b/integration-tests/reentrancy/.gitignore new file mode 100755 index 00000000000..619b0eaaa87 --- /dev/null +++ b/integration-tests/reentrancy/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +fallback_contract/Cargo.lock diff --git a/integration-tests/reentrancy/Cargo.toml b/integration-tests/reentrancy/Cargo.toml new file mode 100755 index 00000000000..2ec26247329 --- /dev/null +++ b/integration-tests/reentrancy/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "fallback_contract" +version = "4.0.0-beta" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } + +main_contract = { path = "main_contract", default-features = false, features = ["ink-as-dependency"] } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "fallback_contract" +path = "lib.rs" +crate-type = ["cdylib", "rlib"] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", + "main_contract/std", +] +ink-as-dependency = [] diff --git a/integration-tests/reentrancy/README.md b/integration-tests/reentrancy/README.md new file mode 100644 index 00000000000..65fe52837bb --- /dev/null +++ b/integration-tests/reentrancy/README.md @@ -0,0 +1,47 @@ +# Reentrancy example + +This example shows how reentrancy works in ink! smart contracts. +It consists of two smart contracts: + +- Main contract, which we initially call +- Fallback contract, which will reenter the main contract + +## Workflow + +The mechanism of work is the following: + +- The `inc` method in the main contract is called +- `inc` method increases the `value` by 1 +- `inc` method makes a call to `fallback` method in fallback contract, as it has no selector +- `fallback` makes a call back to `inc` method in main contract +- `inc` increases the `value` by 1 again and returns 2 as a result + +## Testing + +### Unit testing +You can run unit-test for this example by running the following command: + +```bash +cargo test +``` +### On-chain testing + +If you want to test example locally on your node, first you should run `substrate-contracts-node`: + +```bash +substrate-contracts-node --dev +``` + +Then build both contracts using `cargo-contract`: + +```bash +cargo contract build --release +``` + +Then you can use `contracts-ui` to upload and instantiate the contracts: + +- Firstly deploy `main_contract` +- Take it's code hash(you can get it from `main_contract.contract` file in `target/ink` folder of main contract) +- Then deploy `fallback_contract` using the code hash of `main_contract` as a constructor argument, it will instantiate `main_contract` inside. +- Also if you want to test both contracts, you need to get address of main contract from fallback contract, and instantiate main_contract with it and +it's metadata, which you can find in `metadata.json` file in `target/ink` folder of main contract. \ No newline at end of file diff --git a/integration-tests/reentrancy/lib.rs b/integration-tests/reentrancy/lib.rs new file mode 100755 index 00000000000..458112b2e75 --- /dev/null +++ b/integration-tests/reentrancy/lib.rs @@ -0,0 +1,99 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use self::fallback_contract::{ + FallbackContract, + FallbackContractRef, +}; + +#[ink::contract] +mod fallback_contract { + use main_contract::MainContractRef; + + /// Defines the storage of your contract. + /// Add new fields to the below struct in order + /// to add new static storage fields to your contract. + #[derive(Clone)] + #[ink(storage)] + pub struct FallbackContract { + callee: MainContractRef, + } + + impl FallbackContract { + /// Constructor that initializes the `bool` value to the given `init_value`. + #[ink(constructor)] + pub fn new(callee_code_hash: Hash) -> Self { + let callee = MainContractRef::new() + .code_hash(callee_code_hash) + .endowment(0) + .salt_bytes([0u8; 0]) + .instantiate(); + Self { callee } + } + + #[ink(message)] + pub fn get_callee(&self) -> MainContractRef { + self.callee.clone() + } + + #[ink(message)] + pub fn get_callee_address(&self) -> AccountId { + self.callee.get_address() + } + + #[ink(message)] + pub fn get_address(&self) -> AccountId { + self.env().account_id() + } + + #[ink(message, selector = _)] + pub fn fallback(&mut self) { + self.callee.inc().unwrap(); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + pub fn reentrancy_works() { + use fallback_contract::{ + FallbackContract, + FallbackContractRef, + }; + use ink::primitives::Hash; + use main_contract::MainContract; + + let hash1 = Hash::from([10u8; 32]); + let hash2 = Hash::from([20u8; 32]); + + ink::env::test::register_contract::(hash1.as_ref()); + ink::env::test::register_contract::(hash2.as_ref()); + + let fallback_contract = FallbackContractRef::new(hash1) + .code_hash(hash2) + .endowment(0) + .salt_bytes([0u8; 0]) + .instantiate(); + + let mut main_contract = fallback_contract.get_callee(); + + let address1 = main_contract.get_address(); + let address2 = fallback_contract.get_address(); + + main_contract.set_callee(address2); + + assert_eq!(main_contract.get_callee(), Some(address2)); + + println!( + "main_contract.get_callee_address(): {:?}", + main_contract.get_callee() + ); + + assert_eq!(fallback_contract.get_callee_address(), address1); + + assert_eq!(main_contract.inc(), Ok(2)); + assert_eq!(main_contract.get(), 2); + } +} diff --git a/integration-tests/reentrancy/main_contract/.gitignore b/integration-tests/reentrancy/main_contract/.gitignore new file mode 100755 index 00000000000..8de8f877e47 --- /dev/null +++ b/integration-tests/reentrancy/main_contract/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock diff --git a/integration-tests/reentrancy/main_contract/Cargo.toml b/integration-tests/reentrancy/main_contract/Cargo.toml new file mode 100755 index 00000000000..827e22d10b6 --- /dev/null +++ b/integration-tests/reentrancy/main_contract/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "main_contract" +version = "4.0.0-beta" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "main_contract" +path = "lib.rs" +crate-type = ["cdylib", "rlib"] + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/integration-tests/reentrancy/main_contract/lib.rs b/integration-tests/reentrancy/main_contract/lib.rs new file mode 100755 index 00000000000..a444cdacfe4 --- /dev/null +++ b/integration-tests/reentrancy/main_contract/lib.rs @@ -0,0 +1,99 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use self::main_contract::{ + Error, + MainContract, + MainContractRef, +}; + +#[ink::contract] +mod main_contract { + use core::mem::ManuallyDrop; + use ink::env::{ + call::{ + build_call, + Call, + }, + CallFlags, + DefaultEnvironment, + }; + + /// Defines the storage of your contract. + /// Add new fields to the below struct in order + /// to add new static storage fields to your contract. + #[derive(Default, Clone)] + #[ink(storage)] + pub struct MainContract { + /// Stores a single `bool` value on the storage. + value: u32, + + callee: Option, + } + + #[derive(scale::Encode, scale::Decode, Debug, Ord, PartialOrd, Eq, PartialEq)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum Error { + CalleeReverted, + Unknown, + } + + impl MainContract { + /// Constructor that initializes the `bool` value to the given `init_value`. + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + #[ink(message)] + pub fn get(&self) -> u32 { + self.value + } + + #[ink(message)] + pub fn get_address(&self) -> AccountId { + self.env().account_id() + } + + #[ink(message)] + pub fn set_callee(&mut self, callee: AccountId) { + self.callee = Some(callee); + } + + #[ink(message)] + pub fn get_callee(&self) -> Option { + self.callee + } + + #[ink(message)] + pub fn inc(&mut self) -> Result { + self.value += 1; + + if self.value > 1 { + return Ok(self.value) + } + + ink::env::set_contract_storage( + &::KEY, + self, + ); + + build_call::() + .call_type(Call::new(self.callee.unwrap())) + .call_flags(CallFlags::default().set_allow_reentry(true)) + .try_invoke() + .unwrap_or_else(|err| panic!("failed to call callee: {:?}", err)) + .unwrap_or_else(|err| panic!("callee reverted: {:?}", err)); + + let mut state = ink::env::get_contract_storage( + &::KEY, + ) + .unwrap_or_else(|error| panic!("Failed to load contract state: {:?}", error)) + .unwrap_or_else(|| panic!("Contract state is not initialized")); + + core::mem::swap(self, &mut state); + let _ = ManuallyDrop::new(state); + + Ok(self.value) + } + } +}