diff --git a/crates/env/src/engine/off_chain/db/accounts.rs b/crates/env/src/engine/off_chain/db/accounts.rs index cc01273363..93a767fe11 100644 --- a/crates/env/src/engine/off_chain/db/accounts.rs +++ b/crates/env/src/engine/off_chain/db/accounts.rs @@ -162,6 +162,14 @@ impl AccountsDb { }, ); } + + /// Removes an account. + pub fn remove_account(&mut self, account_id: T::AccountId) + where + T: Environment, + { + self.accounts.remove(&OffAccountId::new(&account_id)); + } } /// An account within the chain. diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index 3af506df0a..18eb19256a 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -203,7 +203,7 @@ impl EnvBackend for EnvInstance { impl EnvInstance { fn transfer_impl( &mut self, - destination: T::AccountId, + destination: &T::AccountId, value: T::Balance, ) -> Result<()> where @@ -220,18 +220,50 @@ impl EnvInstance { } let dst_value = self .accounts - .get_or_create_account::(&destination) + .get_or_create_account::(destination) .balance::()?; self.accounts .get_account_mut::(&src_id) .expect("account of executed contract must exist") .set_balance::(src_value - value)?; self.accounts - .get_account_mut::(&destination) + .get_account_mut::(destination) .expect("the account must exist already or has just been created") .set_balance::(dst_value + value)?; Ok(()) } + + // Remove the calling account and transfer remaining balance. + // + // This function never returns. Either the termination was successful and the + // execution of the destroyed contract is halted. Or it failed during the termination + // which is considered fatal. + fn terminate_contract_impl(&mut self, beneficiary: T::AccountId) -> ! + where + T: Environment, + { + // Send the remaining balance to the beneficiary + let all: T::Balance = self.balance::().expect("could not decode balance"); + self.transfer_impl::(&beneficiary, all) + .expect("transfer did not work "); + + // Remove account + let contract_id = self.account_id::().expect("could not decode account id"); + self.accounts.remove_account::(contract_id); + + // The on-chain implementation would set a tombstone with a code hash here + // and remove the contract storage subsequently. Both is not easily achievable + // with our current off-chain env, hence we left it out here for the moment. + + // Encode the result of the termination and panic with it. + // This enables testing for the proper result and makes sure this + // method returns `Never`. + let res = crate::test::ContractTerminationResult:: { + beneficiary, + transferred: all, + }; + panic!(scale::Encode::encode(&res)); + } } impl TypedEnvBackend for EnvInstance { @@ -349,7 +381,7 @@ impl TypedEnvBackend for EnvInstance { T: Environment, Args: scale::Encode, { - unimplemented!("off-chain environment does not support contract invokation") + unimplemented!("off-chain environment does not support contract invocation") } fn eval_contract( @@ -375,11 +407,11 @@ impl TypedEnvBackend for EnvInstance { unimplemented!("off-chain environment does not support contract instantiation") } - fn terminate_contract(&mut self, _beneficiary: T::AccountId) -> ! + fn terminate_contract(&mut self, beneficiary: T::AccountId) -> ! where T: Environment, { - unimplemented!("off-chain environment does not support contract termination") + self.terminate_contract_impl::(beneficiary) } fn restore_contract( @@ -398,7 +430,7 @@ impl TypedEnvBackend for EnvInstance { where T: Environment, { - self.transfer_impl::(destination, value) + self.transfer_impl::(&destination, value) } fn random(&mut self, subject: &[u8]) -> Result diff --git a/crates/env/src/engine/off_chain/test_api.rs b/crates/env/src/engine/off_chain/test_api.rs index 9c2675c83b..b47aca36f4 100644 --- a/crates/env/src/engine/off_chain/test_api.rs +++ b/crates/env/src/engine/off_chain/test_api.rs @@ -363,3 +363,56 @@ where Ok(callee) }) } + +/// The result of a successful contract termination. +#[derive(scale::Encode, scale::Decode)] +pub struct ContractTerminationResult +where + E: Environment, +{ + /// The beneficiary account who received the remaining value in the contract. + pub beneficiary: ::AccountId, + /// The value which was transferred to the `beneficiary`. + pub transferred: ::Balance, +} + +#[cfg(feature = "std")] +use std::panic::UnwindSafe; + +/// Tests if a contract terminates successfully after `self.env().terminate()` +/// has been called. +/// +/// # Usage +/// +/// ```no_compile +/// let should_terminate = move || your_contract.fn_which_should_terminate(); +/// ink_env::test::assert_contract_termination::( +/// should_terminate, +/// expected_beneficiary, +/// expected_value_transferred_to_beneficiary +/// ); +/// ``` +/// +/// See `examples/contract-terminate` for a complete usage example. +#[cfg(feature = "std")] +pub fn assert_contract_termination( + should_terminate: F, + expected_beneficiary: T::AccountId, + expected_balance: T::Balance, +) where + T: Environment, + F: FnMut() + UnwindSafe, + ::AccountId: core::fmt::Debug, + ::Balance: core::fmt::Debug, +{ + let value_any = ::std::panic::catch_unwind(should_terminate) + .expect_err("contract did not terminate"); + let encoded_input: &Vec = value_any + .downcast_ref::>() + .expect("panic object can not be cast"); + let res: ContractTerminationResult = + scale::Decode::decode(&mut &encoded_input[..]).expect("input can not be decoded"); + + assert_eq!(res.beneficiary, expected_beneficiary); + assert_eq!(res.transferred, expected_balance); +} diff --git a/crates/env/src/engine/off_chain/typed_encoded.rs b/crates/env/src/engine/off_chain/typed_encoded.rs index 44a841598a..4bc9ca24b7 100644 --- a/crates/env/src/engine/off_chain/typed_encoded.rs +++ b/crates/env/src/engine/off_chain/typed_encoded.rs @@ -123,7 +123,7 @@ impl TypedEncoded { } } - /// Creates a new typed-encoded ininitialized by `value` of type `T`. + /// Creates a new typed-encoded initialized by `value` of type `T`. pub fn new(value: &T) -> Self where T: scale::Encode + 'static, diff --git a/crates/lang/ir/src/ir/attrs.rs b/crates/lang/ir/src/ir/attrs.rs index 6088d0339a..27ddec458e 100644 --- a/crates/lang/ir/src/ir/attrs.rs +++ b/crates/lang/ir/src/ir/attrs.rs @@ -453,7 +453,7 @@ where { let (ink_attrs, other_attrs) = ir::partition_attributes(attrs)?; let normalized = ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| { - err.into_combine(format_err!(parent_span, "at this invokation",)) + err.into_combine(format_err!(parent_span, "at this invocation",)) })?; normalized.ensure_first(is_valid_first).map_err(|err| { err.into_combine(format_err!( diff --git a/crates/lang/ir/src/ir/item/event.rs b/crates/lang/ir/src/ir/item/event.rs index 2137584eaa..a1635c07cd 100644 --- a/crates/lang/ir/src/ir/item/event.rs +++ b/crates/lang/ir/src/ir/item/event.rs @@ -108,7 +108,7 @@ impl TryFrom for Event { } let normalized = ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| { - err.into_combine(format_err!(field_span, "at this invokation",)) + err.into_combine(format_err!(field_span, "at this invocation",)) })?; if !matches!(normalized.first().kind(), ir::AttributeArgKind::Topic) { return Err(format_err!( diff --git a/crates/lang/ir/src/ir/item_impl/mod.rs b/crates/lang/ir/src/ir/item_impl/mod.rs index 171a3a78fa..f8f84c20a6 100644 --- a/crates/lang/ir/src/ir/item_impl/mod.rs +++ b/crates/lang/ir/src/ir/item_impl/mod.rs @@ -177,7 +177,7 @@ impl ItemImpl { if !ink_attrs.is_empty() { let normalized = ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| { - err.into_combine(format_err!(impl_block_span, "at this invokation",)) + err.into_combine(format_err!(impl_block_span, "at this invocation",)) })?; if normalized .ensure_first(&ir::AttributeArgKind::Implementation) @@ -295,7 +295,7 @@ impl TryFrom for ItemImpl { if !ink_attrs.is_empty() { let normalized = ir::InkAttribute::from_expanded(ink_attrs).map_err(|err| { - err.into_combine(format_err!(impl_block_span, "at this invokation",)) + err.into_combine(format_err!(impl_block_span, "at this invocation",)) })?; normalized.ensure_no_conflicts(|arg| { !matches!(arg.kind(), ir::AttributeArgKind::Implementation | ir::AttributeArgKind::Namespace(_)) diff --git a/crates/lang/ir/src/lib.rs b/crates/lang/ir/src/lib.rs index 9010f61045..158d5f4f4f 100644 --- a/crates/lang/ir/src/lib.rs +++ b/crates/lang/ir/src/lib.rs @@ -18,7 +18,7 @@ //! parse, analyze and generate code for ink! smart contracts. //! //! The entry point for every ink! smart contract is the [`Contract`](`crate::ir::Contract`) -//! with its [`Config`](`crate::ir::Config`) provided in the initial invokation at +//! with its [`Config`](`crate::ir::Config`) provided in the initial invocation at //! `#[ink::contract(... configuration ...)]`. //! //! The ink! IR tries to stay close to the original Rust syntactic structure. diff --git a/crates/lang/src/env_access.rs b/crates/lang/src/env_access.rs index df733744dc..ac2526063b 100644 --- a/crates/lang/src/env_access.rs +++ b/crates/lang/src/env_access.rs @@ -129,7 +129,7 @@ where ink_env::gas_left::().expect("couldn't decode gas left") } - /// Returns the timstamp of the current block. + /// Returns the timestamp of the current block. /// /// # Note /// diff --git a/examples/contract-terminate/.gitignore b/examples/contract-terminate/.gitignore new file mode 100644 index 0000000000..bf910de10a --- /dev/null +++ b/examples/contract-terminate/.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 \ No newline at end of file diff --git a/examples/contract-terminate/Cargo.toml b/examples/contract-terminate/Cargo.toml new file mode 100644 index 0000000000..05b208706b --- /dev/null +++ b/examples/contract-terminate/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "contract_terminate" +version = "3.0.0-rc1" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +ink_primitives = { version = "3.0.0-rc1", path = "../../crates/primitives", default-features = false } +ink_metadata = { version = "3.0.0-rc1", path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true } +ink_env = { version = "3.0.0-rc1", path = "../../crates/env", default-features = false } +ink_storage = { version = "3.0.0-rc1", path = "../../crates/storage", default-features = false } +ink_lang = { version = "3.0.0-rc1", path = "../../crates/lang", default-features = false } + +scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] } +scale-info = { version = "0.4", default-features = false, features = ["derive"], optional = true } + + +[lib] +name = "contract_terminate" +path = "lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["std"] +std = [ + "ink_primitives/std", + "ink_metadata", + "ink_metadata/std", + "ink_env/std", + "ink_storage/std", + "ink_lang/std", + "scale/std", + "scale-info", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/examples/contract-terminate/lib.rs b/examples/contract-terminate/lib.rs new file mode 100644 index 0000000000..c97f638831 --- /dev/null +++ b/examples/contract-terminate/lib.rs @@ -0,0 +1,101 @@ +// Copyright 2018-2020 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A smart contract which demonstrates behavior of the `self.env().terminate()` +//! function. It terminates itself once `terminate_me()` is called. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::new_without_default)] + +use ink_lang as ink; + +#[ink::contract] +pub mod just_terminates { + /// No storage is needed for this simple contract. + #[ink(storage)] + pub struct JustTerminate {} + + impl JustTerminate { + /// Creates a new instance of this contract. + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + /// Terminates with the caller as beneficiary. + #[ink(message)] + pub fn terminate_me(&mut self) { + self.env().terminate_contract(self.env().caller()); + } + } + + #[cfg(test)] + mod tests { + use super::*; + + use ink_env::{ + call, + test, + }; + use ink_lang as ink; + + #[ink::test] + fn terminating_works() { + // given + let accounts = default_accounts(); + let contract_id = ink_env::test::get_current_contract_account_id::< + ink_env::DefaultEnvironment, + >() + .expect("Cannot get contract id"); + set_sender(accounts.alice); + set_balance(contract_id, 100); + let mut contract = JustTerminate::new(); + + // when + let should_terminate = move || contract.terminate_me(); + + // then + ink_env::test::assert_contract_termination::( + should_terminate, + accounts.alice, + 100, + ); + } + + fn default_accounts( + ) -> ink_env::test::DefaultAccounts { + ink_env::test::default_accounts::() + .expect("Off-chain environment should have been initialized already") + } + + fn set_sender(sender: AccountId) { + let callee = ink_env::account_id::() + .unwrap_or([0x0; 32].into()); + test::push_execution_context::( + sender, + callee, + 1000000, + 1000000, + test::CallData::new(call::Selector::new([0x00; 4])), // dummy + ); + } + + fn set_balance(account_id: AccountId, balance: Balance) { + ink_env::test::set_account_balance::( + account_id, balance, + ) + .expect("Cannot set account balance"); + } + } +} diff --git a/examples/contract-transfer/.gitignore b/examples/contract-transfer/.gitignore new file mode 100644 index 0000000000..bf910de10a --- /dev/null +++ b/examples/contract-transfer/.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 \ No newline at end of file diff --git a/examples/contract-transfer/Cargo.toml b/examples/contract-transfer/Cargo.toml new file mode 100644 index 0000000000..8c8c798bb8 --- /dev/null +++ b/examples/contract-transfer/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "contract-transfer" +version = "3.0.0-rc1" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +ink_primitives = { version = "3.0.0-rc1", path = "../../crates/primitives", default-features = false } +ink_metadata = { version = "3.0.0-rc1", path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true } +ink_env = { version = "3.0.0-rc1", path = "../../crates/env", default-features = false } +ink_storage = { version = "3.0.0-rc1", path = "../../crates/storage", default-features = false } +ink_lang = { version = "3.0.0-rc1", path = "../../crates/lang", default-features = false } + +scale = { package = "parity-scale-codec", version = "1.3", default-features = false, features = ["derive"] } +scale-info = { version = "0.4", default-features = false, features = ["derive"], optional = true } + + +[lib] +name = "contract_transfer" +path = "lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["std"] +std = [ + "ink_primitives/std", + "ink_metadata", + "ink_metadata/std", + "ink_env/std", + "ink_storage/std", + "ink_lang/std", + "scale/std", + "scale-info", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/examples/contract-transfer/lib.rs b/examples/contract-transfer/lib.rs new file mode 100644 index 0000000000..51b2a18c69 --- /dev/null +++ b/examples/contract-transfer/lib.rs @@ -0,0 +1,165 @@ +// Copyright 2018-2020 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A smart contract which demonstrates behavior of the `self.env().transfer()` function. +//! It transfers some of it's balance to the caller. + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::new_without_default)] + +use ink_lang as ink; + +#[ink::contract] +pub mod give_me { + /// No storage is needed for this simple contract. + #[ink(storage)] + pub struct GiveMe {} + + /// The error types. + #[derive(Debug, PartialEq, Eq, scale::Encode)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum Error { + /// Returned if the transfer failed. + TransferFailed, + /// Insufficient funds to execute transfer. + InsufficientFunds, + /// Transfer failed because it would have brought the contract's + /// balance below the subsistence threshold. + /// This is necessary to keep enough funds in the contract to + /// allow for a tombstone to be created. + BelowSubsistenceThreshold, + } + + impl GiveMe { + /// Creates a new instance of this contract. + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + /// Transfers `value` amount of tokens to the caller. + /// + /// # Errors + /// + /// - Returns `Error::InsufficientFunds` in case the requested transfer of + /// `value` exceeds the contracts balance. + /// - Returns `Error::BelowSubsistenceThreshold` in case the requested transfer + /// of `value` would have brought the contract's balance below the subsistence + /// threshold. + /// - Returns `Error::TransferFailed` in case the transfer failed for another + /// reason. + #[ink(message)] + pub fn give_me(&mut self, value: Balance) -> Result<(), Error> { + if value > self.env().balance() { + return Err(Error::InsufficientFunds) + } + self.env() + .transfer(self.env().caller(), value) + .map_err(|err| { + match err { + ink_env::Error::BelowSubsistenceThreshold => { + Error::BelowSubsistenceThreshold + } + _ => Error::TransferFailed, + } + }) + } + } + + #[cfg(test)] + mod tests { + use super::*; + + use ink_env::{ + call, + test, + }; + use ink_lang as ink; + + #[ink::test] + fn transfer_works() { + // given + let contract_balance = 100; + let accounts = default_accounts(); + let mut give_me = create_contract(contract_balance); + + // when + set_sender(accounts.eve); + set_balance(accounts.eve, 0); + assert_eq!(give_me.give_me(80), Ok(())); + + // then + assert_eq!(get_balance(accounts.eve), 80); + } + + #[ink::test] + fn transfer_fails_insufficient_funds() { + // given + let contract_balance = 100; + let accounts = default_accounts(); + let mut give_me = create_contract(contract_balance); + + // when + set_sender(accounts.eve); + let ret = give_me.give_me(120); + + // then + assert_eq!(ret, Err(Error::InsufficientFunds)); + } + + /// Creates a new instance of `GiveMe` with `initial_balance`. + /// + /// Returns the `contract_instance`. + fn create_contract(initial_balance: Balance) -> GiveMe { + let accounts = default_accounts(); + let contract_id = ink_env::test::get_current_contract_account_id::< + ink_env::DefaultEnvironment, + >() + .expect("Cannot get contract id"); + set_sender(accounts.alice); + set_balance(contract_id, initial_balance); + GiveMe::new() + } + + fn set_sender(sender: AccountId) { + let callee = ink_env::account_id::() + .unwrap_or([0x0; 32].into()); + test::push_execution_context::( + sender, + callee, + 1000000, + 1000000, + test::CallData::new(call::Selector::new([0x00; 4])), // dummy + ); + } + + fn default_accounts( + ) -> ink_env::test::DefaultAccounts { + ink_env::test::default_accounts::() + .expect("Off-chain environment should have been initialized already") + } + + fn set_balance(account_id: AccountId, balance: Balance) { + ink_env::test::set_account_balance::( + account_id, balance, + ) + .expect("Cannot set account balance"); + } + + fn get_balance(account_id: AccountId) -> Balance { + ink_env::test::get_account_balance::(account_id) + .expect("Cannot set account balance") + } + } +}