Repository: paritytech/ink
Pull request #780 (2021-04-27 03:27:36)

Enable dependabot for examples
Closes https://github.com/paritytech/ink/issues/772.

Unfortunately it will still create two PR's when upgrading a package in all examples: one for `delegator`, one for the rest. This is because `delegator` is its own, separate workspace. Still, IMHO it's an improvement over the status quo of not having dependabot for the examples at all.

Let's hope it doesn't go rogue this time 🤞.


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 219c13acb0..c755240b2e 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,3 +4,13 @@ updates:
     directory: "/"
     schedule:
       interval: "daily"
+  - package-ecosystem: "cargo"
+    directory: "/examples/"
+    schedule:
+        interval: "daily"
+
+  # `delegator` needs to be a separate stage, since it's a separate workspace.
+  - package-ecosystem: "cargo"
+    directory: "/examples/delegator/"
+    schedule:
+        interval: "daily"
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2498b59d8a..983d3c38c9 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -124,6 +124,12 @@ test:
     - cargo test --verbose --all-features --no-fail-fast --workspace
     - cargo test --verbose --all-features --no-fail-fast --workspace --doc
 
+      # Just needed as long as we have the `ink-experimental-engine` feature.
+      # We do not invoke `--all-features` here -- this would imply the feature
+      # `ink-experimental-engine`. So in order to still run the tests without the
+      # experimental engine feature we need this command.
+    - cargo test --verbose --features std --no-fail-fast --workspace
+
 docs:
   stage:                           workspace
   <<:                              *docker-env
@@ -179,6 +185,13 @@ codecov:
     # RUSTFLAGS are the cause target cache can't be used here
     - cargo build --verbose --all-features --workspace
     - cargo test --verbose --all-features --no-fail-fast --workspace
+
+    # Just needed as long as we have the `ink-experimental-engine` feature.
+    # We must additionally run the coverage without `--all-features` here -- this
+    # would imply the feature `ink-experimental-engine`. So in order to still run
+    # the tests without the experimental engine feature we need this command.
+    - cargo test --verbose --features std --no-fail-fast --workspace
+
     # coverage with branches
     - grcov . --source-dir . --output-type lcov --llvm --branch --ignore-not-existing
         --ignore "/*" --ignore "tests/*" --output-path lcov-w-branch.info
@@ -240,6 +253,19 @@ examples-test:
         cargo test --verbose --manifest-path examples/delegator/${contract}/Cargo.toml;
       done
 
+examples-test-experimental-engine:
+  stage:                           examples
+  <<:                              *docker-env
+  needs:
+    - job:                         clippy-std
+      artifacts:                   false
+  script:
+    # We test only the examples for which the tests have already been migrated to
+    # use the experimental engine.
+    - cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/erc20/Cargo.toml
+    - cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/contract-terminate/Cargo.toml
+    - cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/contract-transfer/Cargo.toml
+
 examples-fmt:
   stage:                           examples
   <<:                              *docker-env
@@ -325,8 +351,7 @@ publish-docs:
     - job:                       docs
       artifacts:                 true
   variables:
-    GIT_DEPTH:                     0
-    GIT_STRATEGY:                  none
+    GIT_DEPTH:                   100
   rules:
     - if: $CI_PIPELINE_SOURCE == "web"
     - if: $CI_PIPELINE_SOURCE == "schedule"
@@ -339,7 +364,7 @@ publish-docs:
   script:
     - rm -rf /tmp/*
     # Set git config
-    - rm .git/config
+    - rm -rf .git/config
     - git config user.email "devops-team@parity.io"
     - git config user.name "${GITHUB_USER}"
     - git config remote.origin.url "https://${GITHUB_TOKEN}@github.com/paritytech/ink.git"
diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml
new file mode 100644
index 0000000000..42a7ec73c1
--- /dev/null
+++ b/crates/engine/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "ink_engine"
+version = "3.0.0-rc3"
+authors = ["Parity Technologies <admin@parity.io>", "Michael Müller <michi@parity.io>"]
+edition = "2018"
+
+license = "Apache-2.0"
+readme = "README.md"
+repository = "https://github.com/paritytech/ink"
+documentation = "https://docs.rs/ink_engine/"
+homepage = "https://www.parity.io/"
+description = "[ink!] Experimental off-chain environment for testing."
+keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl"]
+categories = ["no-std", "embedded"]
+include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"]
+
+[dependencies]
+scale = { package = "parity-scale-codec", version = "2.0", default-features = false, features = ["derive", "full"] }
+derive_more = { version = "0.99", default-features = false, features = ["from", "display"] }
+
+sha2 = { version = "0.9" }
+sha3 = { version = "0.9" }
+blake2 = { version = "0.9" }
+
+[features]
+default = ["std"]
+std = [
+    "scale/std",
+]
diff --git a/crates/engine/LICENSE b/crates/engine/LICENSE
new file mode 120000
index 0000000000..30cff7403d
--- /dev/null
+++ b/crates/engine/LICENSE
@@ -0,0 +1,1 @@
+../../LICENSE
\ No newline at end of file
diff --git a/crates/engine/README.md b/crates/engine/README.md
new file mode 120000
index 0000000000..fe84005413
--- /dev/null
+++ b/crates/engine/README.md
@@ -0,0 +1,1 @@
+../../README.md
\ No newline at end of file
diff --git a/crates/engine/src/database.rs b/crates/engine/src/database.rs
new file mode 100644
index 0000000000..388f311b8d
--- /dev/null
+++ b/crates/engine/src/database.rs
@@ -0,0 +1,207 @@
+// Copyright 2018-2021 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.
+
+use crate::types::Balance;
+use scale::KeyedVec;
+use std::collections::HashMap;
+
+const BALANCE_OF: &[u8] = b"balance:";
+const STORAGE_OF: &[u8] = b"contract-storage:";
+
+/// Returns the database key under which to find the balance for account `who`.
+pub fn balance_of_key(who: &[u8]) -> [u8; 32] {
+    let keyed = who.to_vec().to_keyed_vec(BALANCE_OF);
+    let mut hashed_key: [u8; 32] = [0; 32];
+    super::hashing::blake2b_256(&keyed[..], &mut hashed_key);
+    hashed_key
+}
+
+/// Returns the database key under which to find the balance for account `who`.
+pub fn storage_of_contract_key(who: &[u8], key: &[u8]) -> [u8; 32] {
+    let keyed = who.to_vec().to_keyed_vec(key).to_keyed_vec(STORAGE_OF);
+    let mut hashed_key: [u8; 32] = [0; 32];
+    super::hashing::blake2b_256(&keyed[..], &mut hashed_key);
+    hashed_key
+}
+
+/// The chain database.
+///
+/// Everything is stored in here: accounts, balances, contract storage, etc..
+/// Just like in Substrate a prefix hash is computed for every contract.
+#[derive(Default)]
+pub struct Database {
+    hmap: HashMap<Vec<u8>, Vec<u8>>,
+}
+
+impl Database {
+    /// Creates a new database instance.
+    pub fn new() -> Self {
+        Database {
+            hmap: HashMap::new(),
+        }
+    }
+
+    /// Returns the amount of entries in the database.
+    #[cfg(test)]
+    fn len(&self) -> usize {
+        self.hmap.len()
+    }
+
+    /// Returns a reference to the value corresponding to the key.
+    fn get(&self, key: &[u8]) -> Option<&Vec<u8>> {
+        self.hmap.get(key)
+    }
+
+    /// Returns a reference to the value corresponding to the key.
+    pub fn get_from_contract_storage(
+        &self,
+        account_id: &[u8],
+        key: &[u8],
+    ) -> Option<&Vec<u8>> {
+        let hashed_key = storage_of_contract_key(&account_id, key);
+        self.hmap.get(&hashed_key.to_vec())
+    }
+
+    /// Inserts `value` into the contract storage of `account_id` at storage key `key`.
+    pub fn insert_into_contract_storage(
+        &mut self,
+        account_id: &[u8],
+        key: &[u8],
+        value: Vec<u8>,
+    ) -> Option<Vec<u8>> {
+        let hashed_key = storage_of_contract_key(&account_id, key);
+        self.hmap.insert(hashed_key.to_vec(), value)
+    }
+
+    /// Removes the value at the contract storage of `account_id` at storage key `key`.
+    pub fn remove_contract_storage(
+        &mut self,
+        account_id: &[u8],
+        key: &[u8],
+    ) -> Option<Vec<u8>> {
+        let hashed_key = storage_of_contract_key(&account_id, key);
+        self.hmap.remove(&hashed_key.to_vec())
+    }
+
+    /// Removes a key from the storage, returning the value at the key if the key
+    /// was previously in storage.
+    pub fn remove(&mut self, key: &[u8]) -> Option<Vec<u8>> {
+        self.hmap.remove(key)
+    }
+
+    /// Sets the value of the entry, and returns the entry's old value.
+    pub fn insert(&mut self, key: Vec<u8>, value: Vec<u8>) -> Option<Vec<u8>> {
+        self.hmap.insert(key, value)
+    }
+
+    /// Clears the database, removing all key-value pairs.
+    pub fn clear(&mut self) {
+        self.hmap.clear();
+    }
+
+    /// Returns the balance of `account_id`, if available.
+    pub fn get_balance(&self, account_id: &[u8]) -> Option<Balance> {
+        let hashed_key = balance_of_key(&account_id);
+        self.get(&hashed_key).map(|encoded_balance| {
+            scale::Decode::decode(&mut &encoded_balance[..])
+                .expect("unable to decode balance from database")
+        })
+    }
+
+    /// Sets the balance of `account_id` to `new_balance`.
+    pub fn set_balance(&mut self, account_id: &[u8], new_balance: Balance) {
+        let hashed_key = balance_of_key(&account_id);
+        let encoded_balance = scale::Encode::encode(&new_balance);
+        self.hmap
+            .entry(hashed_key.to_vec())
+            .and_modify(|v| *v = encoded_balance.clone())
+            .or_insert(encoded_balance);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::Database;
+
+    #[test]
+    fn basic_operations() {
+        let mut database = Database::new();
+        let key1 = vec![42];
+        let key2 = vec![43];
+        let val1 = vec![44];
+        let val2 = vec![45];
+        let val3 = vec![46];
+
+        assert_eq!(database.len(), 0);
+        assert_eq!(database.get(&key1), None);
+        assert_eq!(database.insert(key1.clone(), val1.clone()), None);
+        assert_eq!(database.get(&key1), Some(&val1));
+        assert_eq!(
+            database.insert(key1.clone(), val2.clone()),
+            Some(val1.clone())
+        );
+        assert_eq!(database.get(&key1), Some(&val2));
+        assert_eq!(database.insert(key2.clone(), val3.clone()), None);
+        assert_eq!(database.len(), 2);
+        assert_eq!(database.remove(&key2), Some(val3));
+        assert_eq!(database.len(), 1);
+        database.clear();
+        assert_eq!(database.len(), 0);
+    }
+
+    #[test]
+    fn contract_storage() {
+        let account_id = vec![1; 32];
+        let mut storage = Database::new();
+        let key1 = vec![42];
+        let key2 = vec![43];
+        let val1 = vec![44];
+        let val2 = vec![45];
+        let val3 = vec![46];
+
+        assert_eq!(storage.len(), 0);
+        assert_eq!(storage.get_from_contract_storage(&account_id, &key1), None);
+        assert_eq!(
+            storage.insert_into_contract_storage(&account_id, &key1, val1.clone()),
+            None
+        );
+        assert_eq!(
+            storage.get_from_contract_storage(&account_id, &key1),
+            Some(&val1)
+        );
+        assert_eq!(
+            storage.insert_into_contract_storage(&account_id, &key1, val2.clone()),
+            Some(val1.clone())
+        );
+        assert_eq!(
+            storage.get_from_contract_storage(&account_id, &key1),
+            Some(&val2)
+        );
+        assert_eq!(
+            storage.insert_into_contract_storage(&account_id, &key2, val3.clone()),
+            None
+        );
+        assert_eq!(storage.len(), 2);
+        assert_eq!(
+            storage.remove_contract_storage(&account_id, &key2),
+            Some(val3)
+        );
+        assert_eq!(storage.len(), 1);
+        assert_eq!(
+            storage.remove_contract_storage(&account_id, &key1),
+            Some(val2)
+        );
+        assert_eq!(storage.len(), 0);
+    }
+}
diff --git a/crates/engine/src/exec_context.rs b/crates/engine/src/exec_context.rs
new file mode 100644
index 0000000000..873cfc7506
--- /dev/null
+++ b/crates/engine/src/exec_context.rs
@@ -0,0 +1,88 @@
+// Copyright 2018-2021 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.
+
+use super::types::{
+    AccountId,
+    Balance,
+};
+
+/// The context of a contract execution.
+#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
+pub struct ExecContext {
+    /// The caller of the contract execution. Might be user or another contract.
+    ///
+    /// We don't know the specifics of the AccountId ‒ like how many bytes or what
+    /// type of default `AccountId` makes sense ‒ they are left to be initialized
+    /// by the crate which uses the `engine`. Methods which require a caller might
+    /// panic when it has not been set.
+    pub caller: Option<AccountId>,
+    /// The callee of the contract execution. Might be user or another contract.
+    ///
+    /// We don't know the specifics of the AccountId ‒ like how many bytes or what
+    /// type of default `AccountId` makes sense ‒ they are left to be initialized
+    /// by the crate which uses the `engine`. Methods which require a callee might
+    /// panic when it has not been set.
+    pub callee: Option<AccountId>,
+    /// The value transferred to the contract as part of the call.
+    pub value_transferred: Balance,
+}
+
+#[allow(clippy::new_without_default)]
+impl ExecContext {
+    /// Creates a new execution context.
+    pub fn new() -> Self {
+        Self {
+            caller: None,
+            callee: None,
+            value_transferred: 0,
+        }
+    }
+
+    /// Returns the callee.
+    pub fn callee(&self) -> Vec<u8> {
+        self.callee
+            .as_ref()
+            .expect("no callee has been set")
+            .as_bytes()
+            .into()
+    }
+
+    /// Resets the execution context
+    pub fn reset(&mut self) {
+        self.caller = None;
+        self.callee = None;
+        self.value_transferred = Default::default();
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{
+        AccountId,
+        ExecContext,
+    };
+
+    #[test]
+    fn basic_operations() {
+        let mut exec_cont = ExecContext::new();
+
+        exec_cont.callee = Some(AccountId::from_bytes(&[13]));
+        exec_cont.caller = Some(AccountId::from_bytes(&[14]));
+        exec_cont.value_transferred = 15;
+        assert_eq!(exec_cont.callee(), vec![13]);
+
+        exec_cont.reset();
+        assert_eq!(exec_cont, ExecContext::new());
+    }
+}
diff --git a/crates/engine/src/ext.rs b/crates/engine/src/ext.rs
new file mode 100644
index 0000000000..8fa8f900f2
--- /dev/null
+++ b/crates/engine/src/ext.rs
@@ -0,0 +1,432 @@
+// Copyright 2018-2021 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.
+
+//! Provides the same interface as Substrate's FRAME `contract` module.
+//!
+//! See [the documentation for the `contract` module](https://docs.rs/crate/pallet-contracts)
+//! for more information.
+
+use crate::{
+    database::Database,
+    exec_context::ExecContext,
+    test_api::{
+        DebugInfo,
+        EmittedEvent,
+    },
+    types::{
+        AccountId,
+        Key,
+    },
+};
+use std::panic::panic_any;
+
+type Result = core::result::Result<(), Error>;
+
+macro_rules! define_error_codes {
+    (
+        $(
+            $( #[$attr:meta] )*
+            $name:ident = $discr:literal,
+        )*
+    ) => {
+        /// Every error that can be returned to a contract when it calls any of the host functions.
+        #[cfg_attr(test, derive(PartialEq, Eq))]
+        #[derive(Debug)]
+        #[repr(u32)]
+        pub enum Error {
+            $(
+                $( #[$attr] )*
+                $name = $discr,
+            )*
+            /// Returns if an unknown error was received from the host module.
+            UnknownError,
+        }
+
+        impl From<ReturnCode> for Result {
+            #[inline]
+            fn from(return_code: ReturnCode) -> Self {
+                match return_code.0 {
+                    0 => Ok(()),
+                    $(
+                        $discr => Err(Error::$name),
+                    )*
+                    _ => Err(Error::UnknownError),
+                }
+            }
+        }
+    };
+}
+define_error_codes! {
+    /// The called function trapped and has its state changes reverted.
+    /// In this case no output buffer is returned.
+    /// Can only be returned from `seal_call` and `seal_instantiate`.
+    CalleeTrapped = 1,
+    /// The called function ran to completion but decided to revert its state.
+    /// An output buffer is returned when one was supplied.
+    /// Can only be returned from `seal_call` and `seal_instantiate`.
+    CalleeReverted = 2,
+    /// The passed key does not exist in storage.
+    KeyNotFound = 3,
+    /// Transfer failed because it would have brought the sender's total balance
+    /// below the subsistence threshold.
+    BelowSubsistenceThreshold = 4,
+    /// Transfer failed for other not further specified reason. Most probably
+    /// reserved or locked balance of the sender that was preventing the transfer.
+    TransferFailed = 5,
+    /// The newly created contract is below the subsistence threshold after executing
+    /// its constructor so no usable contract instance will be created.
+    NewContractNotFunded = 6,
+    /// No code could be found at the supplied code hash.
+    CodeNotFound = 7,
+    /// The account that was called is either no contract (e.g. user account) or is a tombstone.
+    NotCallable = 8,
+}
+
+/// The raw return code returned by the host side.
+#[repr(transparent)]
+pub struct ReturnCode(u32);
+
+impl ReturnCode {
+    /// Returns the raw underlying `u32` representation.
+    pub fn into_u32(self) -> u32 {
+        self.0
+    }
+}
+
+/// The off-chain engine.
+pub struct Engine {
+    /// The environment database.
+    pub database: Database,
+    /// The current execution context.
+    pub exec_context: ExecContext,
+    /// Recorder for relevant interactions with the engine.
+    /// This is specifically about debug info. This info is
+    /// not available in the `contracts` pallet.
+    pub(crate) debug_info: DebugInfo,
+}
+
+impl Engine {
+    // Creates a new `Engine instance.
+    pub fn new() -> Self {
+        Self {
+            database: Database::new(),
+            exec_context: ExecContext::new(),
+            debug_info: DebugInfo::new(),
+        }
+    }
+}
+
+impl Default for Engine {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Engine {
+    /// Transfers value from the contract to the destination account.
+    pub fn transfer(&mut self, account_id: &[u8], mut value: &[u8]) -> Result {
+        // Note that a transfer of `0` is allowed here
+        let increment = <u128 as scale::Decode>::decode(&mut value)
+            .map_err(|_| Error::TransferFailed)?;
+
+        let dest = account_id.to_vec();
+        // Note that the destination account does not have to exist
+        let dest_old_balance = self.get_balance(dest.clone()).unwrap_or_default();
+
+        let contract = self.get_callee();
+        let contract_old_balance = self
+            .get_balance(contract.clone())
+            .map_err(|_| Error::TransferFailed)?;
+
+        self.database
+            .set_balance(&contract, contract_old_balance - increment);
+        self.database
+            .set_balance(&dest, dest_old_balance + increment);
+        Ok(())
+    }
+
+    /// Deposits an event identified by the supplied topics and data.
+    pub fn deposit_event(&mut self, topics: &[u8], data: &[u8]) {
+        // The first byte contains the number of topics in the slice
+        let topics_count: scale::Compact<u32> = scale::Decode::decode(&mut &topics[0..1])
+            .expect("decoding number of topics failed");
+        let topics_count = topics_count.0 as usize;
+
+        let topics_vec = if topics_count > 0 {
+            // The rest of the slice contains the topics
+            let topics = &topics[1..];
+            let bytes_per_topic = topics.len() / topics_count;
+            let topics_vec: Vec<Vec<u8>> = topics
+                .chunks(bytes_per_topic)
+                .map(|chunk| chunk.to_vec())
+                .collect();
+            assert_eq!(topics_count, topics_vec.len());
+            topics_vec
+        } else {
+            Vec::new()
+        };
+
+        self.debug_info.record_event(EmittedEvent {
+            topics: topics_vec,
+            data: data.to_vec(),
+        });
+    }
+
+    /// Writes the encoded value into the storage at the given key.
+    pub fn set_storage(&mut self, key: &[u8; 32], encoded_value: &[u8]) {
+        let callee = self.get_callee();
+        let account_id = AccountId::from_bytes(&callee[..]);
+
+        self.debug_info.inc_writes(account_id.clone());
+        self.debug_info
+            .record_cell_for_account(account_id, key.to_vec());
+
+        // We ignore if storage is already set for this key
+        let _ = self.database.insert_into_contract_storage(
+            &callee,
+            key,
+            encoded_value.to_vec(),
+        );
+    }
+
+    /// Returns the decoded contract storage at the key if any.
+    pub fn get_storage(&mut self, key: &[u8; 32], output: &mut &mut [u8]) -> Result {
+        let callee = self.get_callee();
+        let account_id = AccountId::from_bytes(&callee[..]);
+
+        self.debug_info.inc_reads(account_id);
+        match self.database.get_from_contract_storage(&callee, key) {
+            Some(val) => {
+                set_output(output, val);
+                Ok(())
+            }
+            None => Err(Error::KeyNotFound),
+        }
+    }
+
+    /// Removes the storage entries at the given key.
+    pub fn clear_storage(&mut self, key: &[u8; 32]) {
+        let callee = self.get_callee();
+        let account_id = AccountId::from_bytes(&callee[..]);
+        self.debug_info.inc_writes(account_id.clone());
+        let _ = self
+            .debug_info
+            .remove_cell_for_account(account_id, key.to_vec());
+        let _ = self.database.remove_contract_storage(&callee, key);
+    }
+
+    /// 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.
+    pub fn terminate(&mut self, beneficiary: &[u8]) -> ! {
+        // Send the remaining balance to the beneficiary
+        let contract = self.get_callee();
+        let all = self.get_balance(contract).expect("could not get balance");
+        let value = &scale::Encode::encode(&all)[..];
+        self.transfer(beneficiary, value)
+            .expect("transfer did not work");
+
+        // What is currently missing is to set a tombstone with a code hash here
+        // and remove the contract storage subsequently.
+
+        // 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 = (all, beneficiary.to_vec());
+        panic_any(scale::Encode::encode(&res));
+    }
+
+    /// Returns the address of the caller.
+    pub fn caller(&self, output: &mut &mut [u8]) {
+        let caller = self
+            .exec_context
+            .caller
+            .as_ref()
+            .expect("no caller has been set")
+            .as_bytes();
+        set_output(output, caller);
+    }
+
+    /// Returns the balance of the executed contract.
+    pub fn balance(&self, output: &mut &mut [u8]) {
+        let contract = self
+            .exec_context
+            .callee
+            .as_ref()
+            .expect("no callee has been set");
+
+        let balance_in_storage = self
+            .database
+            .get_balance(&contract.as_bytes().to_vec())
+            .expect("currently executing contract must exist");
+        let balance = scale::Encode::encode(&balance_in_storage);
+        set_output(output, &balance[..])
+    }
+
+    /// Returns the transferred value for the called contract.
+    pub fn value_transferred(&self, output: &mut &mut [u8]) {
+        let value_transferred: Vec<u8> =
+            scale::Encode::encode(&self.exec_context.value_transferred);
+        set_output(output, &value_transferred[..])
+    }
+
+    /// Returns the address of the executed contract.
+    pub fn address(&self, output: &mut &mut [u8]) {
+        let callee = self
+            .exec_context
+            .callee
+            .as_ref()
+            .expect("no callee has been set")
+            .as_bytes();
+        set_output(output, &callee)
+    }
+
+    /// Restores a tombstone to the original smart contract.
+    ///
+    /// # Params
+    ///
+    /// - `account_id`: Encoded bytes of the `AccountId` of the to-be-restored contract.
+    /// - `code_hash`: Encoded code hash of the to-be-restored contract.
+    /// - `rent_allowance`: The encoded rent allowance of the restored contract
+    ///                     upon successful restoration.
+    /// - `filtered_keys`: Storage keys that will be ignored for the tombstone hash
+    ///                    match calculation that decide whether the original contract
+    ///                    storage and the storage of the restorer contract is equal.
+    pub fn restore_to(
+        &mut self,
+        _account_id: &[u8],
+        _code_hash: &[u8],
+        _rent_allowance: &[u8],
+        filtered_keys: &[&[u8]],
+    ) {
+        let _filtered_keys: Vec<crate::Key> =
+            filtered_keys.iter().map(|k| Key::from_bytes(k)).collect();
+        unimplemented!("off-chain environment does not yet support `restore_to`");
+    }
+
+    /// Prints the given contents to the console log.
+    pub fn println(&mut self, content: &str) {
+        self.debug_info.record_println(String::from(content));
+        println!("{}", content);
+    }
+
+    /// Conduct the BLAKE-2 256-bit hash and place the result into `output`.
+    pub fn hash_blake2_256(input: &[u8], output: &mut [u8; 32]) {
+        super::hashing::blake2b_256(input, output);
+    }
+
+    /// Conduct the BLAKE-2 128-bit hash and place the result into `output`.
+    pub fn hash_blake2_128(input: &[u8], output: &mut [u8; 16]) {
+        super::hashing::blake2b_128(input, output);
+    }
+
+    /// Conduct the SHA-2 256-bit hash and place the result into `output`.
+    pub fn hash_sha2_256(input: &[u8], output: &mut [u8; 32]) {
+        super::hashing::sha2_256(input, output);
+    }
+
+    /// Conduct the KECCAK 256-bit hash and place the result into `output`.
+    pub fn hash_keccak_256(input: &[u8], output: &mut [u8; 32]) {
+        super::hashing::keccak_256(input, output);
+    }
+
+    pub fn now(&self, _output: &mut &mut [u8]) {
+        unimplemented!("off-chain environment does not yet support `now`");
+    }
+
+    pub fn block_number(&self, _output: &mut &mut [u8]) {
+        unimplemented!("off-chain environment does not yet support `block_number`");
+    }
+
+    pub fn gas_left(&self, _output: &mut &mut [u8]) {
+        unimplemented!("off-chain environment does not yet support `gas_left`");
+    }
+
+    pub fn rent_allowance(&self, _output: &mut &mut [u8]) {
+        unimplemented!("off-chain environment does not yet support `rent_allowance`");
+    }
+
+    pub fn minimum_balance(&self, _output: &mut &mut [u8]) {
+        unimplemented!("off-chain environment does not yet support `minimum_balance`");
+    }
+
+    pub fn tombstone_deposit(&self, _output: &mut &mut [u8]) {
+        unimplemented!("off-chain environment does not yet support `tombstone_deposit`");
+    }
+
+    #[allow(clippy::too_many_arguments)]
+    pub fn instantiate(
+        &mut self,
+        _code_hash: &[u8],
+        _gas_limit: u64,
+        _endowment: &[u8],
+        _input: &[u8],
+        _out_address: &mut &mut [u8],
+        _out_return_value: &mut &mut [u8],
+        _salt: &[u8],
+    ) -> Result {
+        unimplemented!("off-chain environment does not yet support `instantiate`");
+    }
+
+    pub fn call(
+        &mut self,
+        _callee: &[u8],
+        _gas_limit: u64,
+        _value: &[u8],
+        _input: &[u8],
+        _output: &mut &mut [u8],
+    ) -> Result {
+        unimplemented!("off-chain environment does not yet support `call`");
+    }
+
+    pub fn weight_to_fee(&self, _gas: u64, _output: &mut &mut [u8]) {
+        unimplemented!("off-chain environment does not yet support `weight_to_fee`");
+    }
+
+    pub fn set_rent_allowance(&mut self, _value: &[u8]) {
+        unimplemented!("off-chain environment does not yet support `set_rent_allowance`");
+    }
+
+    pub fn random(&self, _subject: &[u8], _output: &mut &mut [u8]) {
+        unimplemented!("off-chain environment does not yet support `random`");
+    }
+
+    pub fn call_chain_extension(
+        &mut self,
+        _func_id: u32,
+        _input: &[u8],
+        _output: &mut &mut [u8],
+    ) -> u32 {
+        unimplemented!(
+            "off-chain environment does not yet support `call_chain_extension`"
+        );
+    }
+}
+
+/// Copies the `slice` into `output`.
+///
+/// Panics if the slice is too large and doesn't fit.
+fn set_output(output: &mut &mut [u8], slice: &[u8]) {
+    if slice.len() > output.len() {
+        panic!(
+            "the output buffer is too small! the decoded storage is of size {} bytes, \
+            but the output buffer has only room for {}.",
+            slice.len(),
+            output.len()
+        );
+    }
+    output[..slice.len()].copy_from_slice(slice);
+}
diff --git a/crates/engine/src/hashing.rs b/crates/engine/src/hashing.rs
new file mode 100644
index 0000000000..e80f4d45b4
--- /dev/null
+++ b/crates/engine/src/hashing.rs
@@ -0,0 +1,125 @@
+// Copyright 2018-2021 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.
+
+//! Implementations of supported cryptographic hash functions.
+
+/// Helper routine implementing variable size BLAKE2b hash computation.
+fn blake2b_var(size: usize, input: &[u8], output: &mut [u8]) {
+    use blake2::digest::{
+        Update as _,
+        VariableOutput as _,
+    };
+    let mut blake2 = blake2::VarBlake2b::new_keyed(&[], size);
+    blake2.update(input);
+    blake2.finalize_variable(|result| output.copy_from_slice(result));
+}
+
+/// Conduct the BLAKE2 256-bit hash and place the result into `output`.
+pub fn blake2b_256(input: &[u8], output: &mut [u8; 32]) {
+    blake2b_var(32, input, output)
+}
+
+/// Conduct the BLAKE2 128-bit hash and place the result into `output`.
+pub fn blake2b_128(input: &[u8], output: &mut [u8; 16]) {
+    blake2b_var(16, input, output)
+}
+
+/// Conduct the KECCAK 256-bit hash and place the result into `output`.
+pub fn keccak_256(input: &[u8], output: &mut [u8; 32]) {
+    use sha3::{
+        digest::{
+            generic_array::GenericArray,
+            FixedOutput as _,
+        },
+        Digest as _,
+    };
+    let mut hasher = sha3::Keccak256::new();
+    hasher.update(input);
+    hasher.finalize_into(<&mut GenericArray<u8, _>>::from(&mut output[..]));
+}
+
+/// Conduct the SHA2 256-bit hash and place the result into `output`.
+pub fn sha2_256(input: &[u8], output: &mut [u8; 32]) {
+    use sha2::{
+        digest::{
+            generic_array::GenericArray,
+            FixedOutput as _,
+        },
+        Digest as _,
+    };
+    let mut hasher = sha2::Sha256::new();
+    hasher.update(input);
+    hasher.finalize_into(<&mut GenericArray<u8, _>>::from(&mut output[..]));
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    static TEST_INPUT: &[u8] = b"DEAD_BEEF";
+
+    #[test]
+    fn test_hash_keccak_256() {
+        let mut output = [0x00_u8; 32];
+        keccak_256(TEST_INPUT, &mut output);
+        assert_eq!(
+            output,
+            [
+                24, 230, 209, 59, 127, 30, 158, 244, 60, 177, 132, 150, 167, 244, 64, 69,
+                184, 123, 185, 44, 211, 199, 208, 179, 14, 64, 126, 140, 217, 69, 36,
+                216
+            ]
+        );
+    }
+
+    #[test]
+    fn test_hash_sha2_256() {
+        let mut output = [0x00_u8; 32];
+        sha2_256(TEST_INPUT, &mut output);
+        assert_eq!(
+            output,
+            [
+                136, 15, 25, 218, 88, 54, 49, 152, 115, 168, 147, 189, 207, 171, 243,
+                129, 161, 76, 15, 141, 197, 106, 111, 213, 19, 197, 133, 219, 181, 233,
+                195, 120
+            ]
+        );
+    }
+
+    #[test]
+    fn test_hash_blake2_256() {
+        let mut output = [0x00_u8; 32];
+        blake2b_256(TEST_INPUT, &mut output);
+        assert_eq!(
+            output,
+            [
+                244, 247, 235, 182, 194, 161, 28, 69, 34, 106, 237, 7, 57, 87, 190, 12,
+                92, 171, 91, 176, 135, 52, 247, 94, 8, 112, 94, 183, 140, 101, 208, 120
+            ]
+        );
+    }
+
+    #[test]
+    fn test_hash_blake2_128() {
+        let mut output = [0x00_u8; 16];
+        blake2b_128(TEST_INPUT, &mut output);
+        assert_eq!(
+            output,
+            [
+                180, 158, 48, 21, 171, 163, 217, 175, 145, 160, 25, 159, 213, 142, 103,
+                242
+            ]
+        );
+    }
+}
diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
new file mode 100644
index 0000000000..a6deba086e
--- /dev/null
+++ b/crates/engine/src/lib.rs
@@ -0,0 +1,41 @@
+// Copyright 2018-2021 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.
+
+pub mod ext;
+pub mod test_api;
+
+mod database;
+mod exec_context;
+mod hashing;
+mod types;
+
+#[cfg(test)]
+mod tests;
+
+pub use types::AccountError;
+use types::Key;
+
+use derive_more::From;
+
+/// Errors which can happen when interacting with this crate.
+#[derive(Debug, From, PartialEq, Eq)]
+pub enum Error {
+    Account(AccountError),
+    #[from(ignore)]
+    UninitializedBlocks,
+    #[from(ignore)]
+    UninitializedExecutionContext,
+    #[from(ignore)]
+    UnregisteredChainExtension,
+}
diff --git a/crates/engine/src/types.rs b/crates/engine/src/types.rs
new file mode 100644
index 0000000000..5237f7a594
--- /dev/null
+++ b/crates/engine/src/types.rs
@@ -0,0 +1,65 @@
+// Copyright 2018-2021 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.
+
+use derive_more::From;
+
+/// This is just a temporary solution for the MVP!
+/// As a temporary solution we choose the same type as the default
+/// `env` `Balance` type.
+///
+/// In the long-term this type should be `Vec<u8>` as well, as to not
+/// be dependent on the specific off-chain environment type, so that
+/// the `engine` crate can be used with an arbitrary `Environment` configuration.
+pub type Balance = u128;
+
+/// The Account Id type used by this crate.
+#[derive(Debug, From, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[repr(transparent)]
+pub struct AccountId(Vec<u8>);
+
+impl AccountId {
+    /// Creates a new `AccountId` from the given raw bytes.
+    pub fn from_bytes(bytes: &[u8]) -> Self {
+        Self(bytes.to_vec())
+    }
+
+    /// Returns the `AccountId` as bytes.
+    pub fn as_bytes(&self) -> &[u8] {
+        &self.0[..]
+    }
+}
+
+/// Key into the database.
+///
+/// Used to identify contract storage cells for read and write operations.
+#[derive(Default, From, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[repr(transparent)]
+pub struct Key(Vec<u8>);
+
+impl Key {
+    /// Creates a new `Key` from the given raw bytes.
+    pub fn from_bytes(bytes: &[u8]) -> Self {
+        Self(bytes.to_vec())
+    }
+}
+
+/// Errors encountered upon interacting with accounts.
+#[derive(Clone, Debug, From, PartialEq, Eq)]
+pub enum AccountError {
+    Decoding(scale::Error),
+    #[from(ignore)]
+    UnexpectedUserAccount,
+    #[from(ignore)]
+    NoAccountForId(Vec<u8>),
+}
diff --git a/crates/env/Cargo.toml b/crates/env/Cargo.toml
index 5f5c5d1f75..bb712d4b48 100644
--- a/crates/env/Cargo.toml
+++ b/crates/env/Cargo.toml
@@ -15,6 +15,7 @@ categories = ["no-std", "embedded"]
 include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"]
 
 [dependencies]
+ink_engine = { version = "3.0.0-rc3", path = "../engine/", default-features = false, optional = true }
 ink_metadata = { version = "3.0.0-rc3", path = "../metadata/", default-features = false, features = ["derive"], optional = true }
 ink_allocator = { version = "3.0.0-rc3", path = "../allocator/", default-features = false }
 ink_primitives = { version = "3.0.0-rc3", path = "../primitives/", default-features = false }
@@ -60,3 +61,4 @@ std = [
     "sha3",
     "blake2",
 ]
+ink-experimental-engine = ["ink_engine"]
diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs
index 1ba69a4e76..f3d3b58882 100644
--- a/crates/env/src/backend.rs
+++ b/crates/env/src/backend.rs
@@ -50,6 +50,7 @@ impl ReturnFlags {
     }
 
     /// Returns the underlying `u32` representation.
+    #[cfg(not(feature = "ink-experimental-engine"))]
     pub(crate) fn into_u32(self) -> u32 {
         self.value
     }
diff --git a/crates/env/src/engine/experimental_off_chain/impls.rs b/crates/env/src/engine/experimental_off_chain/impls.rs
new file mode 100644
index 0000000000..a1a254e8bb
--- /dev/null
+++ b/crates/env/src/engine/experimental_off_chain/impls.rs
@@ -0,0 +1,426 @@
+// Copyright 2018-2021 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.
+
+use super::EnvInstance;
+use crate::{
+    call::{
+        utils::ReturnType,
+        CallParams,
+        CreateParams,
+    },
+    hash::{
+        Blake2x128,
+        Blake2x256,
+        CryptoHash,
+        HashOutput,
+        Keccak256,
+        Sha2x256,
+    },
+    topics::{
+        Topics,
+        TopicsBuilderBackend,
+    },
+    Clear,
+    EnvBackend,
+    Environment,
+    Result,
+    ReturnFlags,
+    TypedEnvBackend,
+};
+use ink_engine::{
+    ext,
+    ext::Engine,
+};
+use ink_primitives::Key;
+
+/// 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.
+const BUFFER_SIZE: usize = 1 << 14; // 16kB
+
+impl CryptoHash for Blake2x128 {
+    fn hash(input: &[u8], output: &mut <Self as HashOutput>::Type) {
+        type OutputType = [u8; 16];
+        static_assertions::assert_type_eq_all!(
+            <Blake2x128 as HashOutput>::Type,
+            OutputType
+        );
+        let output: &mut OutputType = arrayref::array_mut_ref!(output, 0, 16);
+        Engine::hash_blake2_128(input, output);
+    }
+}
+
+impl CryptoHash for Blake2x256 {
+    fn hash(input: &[u8], output: &mut <Self as HashOutput>::Type) {
+        type OutputType = [u8; 32];
+        static_assertions::assert_type_eq_all!(
+            <Blake2x256 as HashOutput>::Type,
+            OutputType
+        );
+        let output: &mut OutputType = arrayref::array_mut_ref!(output, 0, 32);
+        Engine::hash_blake2_256(input, output);
+    }
+}
+
+impl CryptoHash for Sha2x256 {
+    fn hash(input: &[u8], output: &mut <Self as HashOutput>::Type) {
+        type OutputType = [u8; 32];
+        static_assertions::assert_type_eq_all!(
+            <Sha2x256 as HashOutput>::Type,
+            OutputType
+        );
+        let output: &mut OutputType = arrayref::array_mut_ref!(output, 0, 32);
+        Engine::hash_sha2_256(input, output);
+    }
+}
+
+impl CryptoHash for Keccak256 {
+    fn hash(input: &[u8], output: &mut <Self as HashOutput>::Type) {
+        type OutputType = [u8; 32];
+        static_assertions::assert_type_eq_all!(
+            <Keccak256 as HashOutput>::Type,
+            OutputType
+        );
+        let output: &mut OutputType = arrayref::array_mut_ref!(output, 0, 32);
+        Engine::hash_keccak_256(input, output);
+    }
+}
+
+impl From<ext::Error> for crate::Error {
+    fn from(ext_error: ext::Error) -> Self {
+        match ext_error {
+            ext::Error::UnknownError => Self::UnknownError,
+            ext::Error::CalleeTrapped => Self::CalleeTrapped,
+            ext::Error::CalleeReverted => Self::CalleeReverted,
+            ext::Error::KeyNotFound => Self::KeyNotFound,
+            ext::Error::BelowSubsistenceThreshold => Self::BelowSubsistenceThreshold,
+            ext::Error::TransferFailed => Self::TransferFailed,
+            ext::Error::NewContractNotFunded => Self::NewContractNotFunded,
+            ext::Error::CodeNotFound => Self::CodeNotFound,
+            ext::Error::NotCallable => Self::NotCallable,
+        }
+    }
+}
+
+#[derive(Default)]
+pub struct TopicsBuilder {
+    pub topics: Vec<Vec<u8>>,
+}
+
+impl<E> TopicsBuilderBackend<E> for TopicsBuilder
+where
+    E: Environment,
+{
+    type Output = Vec<u8>;
+
+    fn expect(&mut self, _expected_topics: usize) {}
+
+    fn push_topic<T>(&mut self, topic_value: &T)
+    where
+        T: scale::Encode,
+    {
+        let encoded = topic_value.encode();
+        let len_encoded = encoded.len();
+        let mut result = <E as Environment>::Hash::clear();
+        let len_result = result.as_ref().len();
+        if len_encoded <= len_result {
+            result.as_mut()[..len_encoded].copy_from_slice(&encoded[..]);
+        } else {
+            let mut hash_output = <Blake2x256 as HashOutput>::Type::default();
+            <Blake2x256 as CryptoHash>::hash(&encoded[..], &mut hash_output);
+            let copy_len = core::cmp::min(hash_output.len(), len_result);
+            result.as_mut()[0..copy_len].copy_from_slice(&hash_output[0..copy_len]);
+        }
+        let off_hash = result.as_ref();
+        let off_hash = off_hash.to_vec();
+        debug_assert!(
+            !self.topics.contains(&off_hash),
+            "duplicate topic hash discovered!"
+        );
+        self.topics.push(off_hash);
+    }
+
+    fn output(self) -> Self::Output {
+        let mut all: Vec<u8> = Vec::new();
+
+        let topics_len_compact = &scale::Compact(self.topics.len() as u32);
+        let topics_encoded = &scale::Encode::encode(&topics_len_compact)[..];
+        all.append(&mut topics_encoded.to_vec());
+
+        self.topics.into_iter().for_each(|mut v| all.append(&mut v));
+        all
+    }
+}
+
+impl EnvInstance {
+    /// Returns the contract property value.
+    fn get_property<T>(
+        &mut self,
+        ext_fn: fn(engine: &Engine, output: &mut &mut [u8]),
+    ) -> Result<T>
+    where
+        T: scale::Decode,
+    {
+        let mut full_scope: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
+        let full_scope = &mut &mut full_scope[..];
+        ext_fn(&self.engine, full_scope);
+        scale::Decode::decode(&mut &full_scope[..]).map_err(Into::into)
+    }
+}
+
+impl EnvBackend for EnvInstance {
+    fn set_contract_storage<V>(&mut self, key: &Key, value: &V)
+    where
+        V: scale::Encode,
+    {
+        let v = scale::Encode::encode(value);
+        self.engine.set_storage(key.as_bytes(), &v[..]);
+    }
+
+    fn get_contract_storage<R>(&mut self, key: &Key) -> Result<Option<R>>
+    where
+        R: scale::Decode,
+    {
+        let mut output: [u8; 9600] = [0; 9600];
+        match self
+            .engine
+            .get_storage(key.as_bytes(), &mut &mut output[..])
+        {
+            Ok(_) => (),
+            Err(ext::Error::KeyNotFound) => return Ok(None),
+            Err(_) => panic!("encountered unexpected error"),
+        }
+        let decoded = scale::Decode::decode(&mut &output[..])?;
+        Ok(Some(decoded))
+    }
+
+    fn clear_contract_storage(&mut self, key: &Key) {
+        self.engine.clear_storage(key.as_bytes())
+    }
+
+    fn decode_input<T>(&mut self) -> Result<T>
+    where
+        T: scale::Decode,
+    {
+        unimplemented!("the experimental off chain env does not implement `seal_input`")
+    }
+
+    fn return_value<R>(&mut self, _flags: ReturnFlags, _return_value: &R) -> !
+    where
+        R: scale::Encode,
+    {
+        unimplemented!(
+            "the experimental off chain env does not implement `seal_return_value`"
+        )
+    }
+
+    fn println(&mut self, content: &str) {
+        self.engine.println(content)
+    }
+
+    fn hash_bytes<H>(&mut self, input: &[u8], output: &mut <H as HashOutput>::Type)
+    where
+        H: CryptoHash,
+    {
+        <H as CryptoHash>::hash(input, output)
+    }
+
+    fn hash_encoded<H, T>(&mut self, input: &T, output: &mut <H as HashOutput>::Type)
+    where
+        H: CryptoHash,
+        T: scale::Encode,
+    {
+        let enc_input = &scale::Encode::encode(input)[..];
+        <H as CryptoHash>::hash(enc_input, output)
+    }
+
+    fn call_chain_extension<I, T, E, ErrorCode, F, D>(
+        &mut self,
+        func_id: u32,
+        input: &I,
+        status_to_result: F,
+        decode_to_result: D,
+    ) -> ::core::result::Result<T, E>
+    where
+        I: scale::Encode,
+        T: scale::Decode,
+        E: From<ErrorCode>,
+        F: FnOnce(u32) -> ::core::result::Result<(), ErrorCode>,
+        D: FnOnce(&[u8]) -> ::core::result::Result<T, E>,
+    {
+        let enc_input = &scale::Encode::encode(input)[..];
+        let mut output: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
+
+        status_to_result(self.engine.call_chain_extension(
+            func_id,
+            enc_input,
+            &mut &mut output[..],
+        ))?;
+        let decoded = decode_to_result(&mut &output[..])?;
+        Ok(decoded)
+    }
+}
+
+impl TypedEnvBackend for EnvInstance {
+    fn caller<T: Environment>(&mut self) -> Result<T::AccountId> {
+        self.get_property::<T::AccountId>(Engine::caller)
+    }
+
+    fn transferred_balance<T: Environment>(&mut self) -> Result<T::Balance> {
+        self.get_property::<T::Balance>(Engine::value_transferred)
+    }
+
+    fn gas_left<T: Environment>(&mut self) -> Result<T::Balance> {
+        self.get_property::<T::Balance>(Engine::gas_left)
+    }
+
+    fn block_timestamp<T: Environment>(&mut self) -> Result<T::Timestamp> {
+        self.get_property::<T::Timestamp>(Engine::now)
+    }
+
+    fn account_id<T: Environment>(&mut self) -> Result<T::AccountId> {
+        self.get_property::<T::AccountId>(Engine::address)
+    }
+
+    fn balance<T: Environment>(&mut self) -> Result<T::Balance> {
+        self.get_property::<T::Balance>(Engine::balance)
+    }
+
+    fn rent_allowance<T: Environment>(&mut self) -> Result<T::Balance> {
+        self.get_property::<T::Balance>(Engine::rent_allowance)
+    }
+
+    fn block_number<T: Environment>(&mut self) -> Result<T::BlockNumber> {
+        self.get_property::<T::BlockNumber>(Engine::block_number)
+    }
+
+    fn minimum_balance<T: Environment>(&mut self) -> Result<T::Balance> {
+        self.get_property::<T::Balance>(Engine::minimum_balance)
+    }
+
+    fn tombstone_deposit<T: Environment>(&mut self) -> Result<T::Balance> {
+        self.get_property::<T::Balance>(Engine::tombstone_deposit)
+    }
+
+    fn emit_event<T, Event>(&mut self, event: Event)
+    where
+        T: Environment,
+        Event: Topics + scale::Encode,
+    {
+        let builder = TopicsBuilder::default();
+        let enc_topics = event.topics::<T, _>(builder.into());
+        let enc_data = &scale::Encode::encode(&event)[..];
+        self.engine.deposit_event(&enc_topics[..], enc_data);
+    }
+
+    fn set_rent_allowance<T>(&mut self, new_value: T::Balance)
+    where
+        T: Environment,
+    {
+        let buffer = &scale::Encode::encode(&new_value)[..];
+        self.engine.set_rent_allowance(buffer)
+    }
+
+    fn invoke_contract<T, Args>(
+        &mut self,
+        _call_params: &CallParams<T, Args, ()>,
+    ) -> Result<()>
+    where
+        T: Environment,
+        Args: scale::Encode,
+    {
+        unimplemented!("off-chain environment does not support contract invocation")
+    }
+
+    fn eval_contract<T, Args, R>(
+        &mut self,
+        _call_params: &CallParams<T, Args, ReturnType<R>>,
+    ) -> Result<R>
+    where
+        T: Environment,
+        Args: scale::Encode,
+        R: scale::Decode,
+    {
+        unimplemented!("off-chain environment does not support contract evaluation")
+    }
+
+    fn instantiate_contract<T, Args, Salt, C>(
+        &mut self,
+        _params: &CreateParams<T, Args, Salt, C>,
+    ) -> Result<T::AccountId>
+    where
+        T: Environment,
+        Args: scale::Encode,
+        Salt: AsRef<[u8]>,
+    {
+        unimplemented!("off-chain environment does not support contract instantiation")
+    }
+
+    fn restore_contract<T>(
+        &mut self,
+        account_id: T::AccountId,
+        code_hash: T::Hash,
+        rent_allowance: T::Balance,
+        filtered_keys: &[Key],
+    ) where
+        T: Environment,
+    {
+        let enc_account_id = &scale::Encode::encode(&account_id)[..];
+        let enc_code_hash = &scale::Encode::encode(&code_hash)[..];
+        let enc_rent_allowance = &scale::Encode::encode(&rent_allowance)[..];
+
+        let filtered: Vec<&[u8]> =
+            filtered_keys.iter().map(|k| &k.as_bytes()[..]).collect();
+        self.engine.restore_to(
+            enc_account_id,
+            enc_code_hash,
+            enc_rent_allowance,
+            &filtered[..],
+        );
+    }
+
+    fn terminate_contract<T>(&mut self, beneficiary: T::AccountId) -> !
+    where
+        T: Environment,
+    {
+        let buffer = scale::Encode::encode(&beneficiary);
+        self.engine.terminate(&buffer[..])
+    }
+
+    fn transfer<T>(&mut self, destination: T::AccountId, value: T::Balance) -> Result<()>
+    where
+        T: Environment,
+    {
+        let enc_destination = &scale::Encode::encode(&destination)[..];
+        let enc_value = &scale::Encode::encode(&value)[..];
+        self.engine
+            .transfer(enc_destination, enc_value)
+            .map_err(Into::into)
+    }
+
+    fn weight_to_fee<T: Environment>(&mut self, gas: u64) -> Result<T::Balance> {
+        let mut output: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
+        self.engine.weight_to_fee(gas, &mut &mut output[..]);
+        scale::Decode::decode(&mut &output[..]).map_err(Into::into)
+    }
+
+    fn random<T>(&mut self, subject: &[u8]) -> Result<(T::Hash, T::BlockNumber)>
+    where
+        T: Environment,
+    {
+        let mut output: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE];
+        self.engine.random(subject, &mut &mut output[..]);
+        scale::Decode::decode(&mut &output[..]).map_err(Into::into)
+    }
+}
diff --git a/crates/env/src/engine/experimental_off_chain/mod.rs b/crates/env/src/engine/experimental_off_chain/mod.rs
new file mode 100644
index 0000000000..0f0e00e053
--- /dev/null
+++ b/crates/env/src/engine/experimental_off_chain/mod.rs
@@ -0,0 +1,69 @@
+// Copyright 2018-2021 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.
+
+mod impls;
+pub mod test_api;
+mod types;
+
+#[cfg(test)]
+mod tests;
+
+use super::OnInstance;
+use crate::Error;
+
+use derive_more::From;
+use ink_engine::ext::Engine;
+
+/// The experimental off-chain environment.
+pub struct EnvInstance {
+    engine: Engine,
+}
+
+impl OnInstance for EnvInstance {
+    fn on_instance<F, R>(f: F) -> R
+    where
+        F: FnOnce(&mut Self) -> R,
+    {
+        use core::cell::RefCell;
+        thread_local!(
+            static INSTANCE: RefCell<EnvInstance> = RefCell::new(
+                EnvInstance {
+                    engine: Engine::new()
+                }
+            )
+        );
+        INSTANCE.with(|instance| f(&mut instance.borrow_mut()))
+    }
+}
+
+#[derive(Debug, From, PartialEq, Eq)]
+pub enum OffChainError {
+    Account(AccountError),
+    #[from(ignore)]
+    UninitializedBlocks,
+    #[from(ignore)]
+    UninitializedExecutionContext,
+    #[from(ignore)]
+    UnregisteredChainExtension,
+}
+
+/// Errors encountered upon interacting with the accounts database.
+#[derive(Debug, From, PartialEq, Eq)]
+pub enum AccountError {
+    Decoding(scale::Error),
+    #[from(ignore)]
+    UnexpectedUserAccount,
+    #[from(ignore)]
+    NoAccountForId(Vec<u8>),
+}
diff --git a/crates/env/src/engine/experimental_off_chain/types.rs b/crates/env/src/engine/experimental_off_chain/types.rs
new file mode 100644
index 0000000000..fd88b168bd
--- /dev/null
+++ b/crates/env/src/engine/experimental_off_chain/types.rs
@@ -0,0 +1,68 @@
+// Copyright 2018-2021 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.
+
+//! Contains the necessary conversions from `ink_engine` types to types
+//! of this crate.
+
+use super::{
+    test_api::EmittedEvent,
+    AccountError,
+    Error,
+    OffChainError,
+};
+
+impl From<ink_engine::test_api::EmittedEvent> for EmittedEvent {
+    fn from(evt: ink_engine::test_api::EmittedEvent) -> Self {
+        EmittedEvent {
+            topics: evt.topics,
+            data: evt.data,
+        }
+    }
+}
+
+impl From<ink_engine::Error> for Error {
+    fn from(err: ink_engine::Error) -> Self {
+        let e = match err {
+            ink_engine::Error::Account(acc) => OffChainError::Account(acc.into()),
+            ink_engine::Error::UninitializedBlocks => OffChainError::UninitializedBlocks,
+            ink_engine::Error::UninitializedExecutionContext => {
+                OffChainError::UninitializedExecutionContext
+            }
+            ink_engine::Error::UnregisteredChainExtension => {
+                OffChainError::UnregisteredChainExtension
+            }
+        };
+        Error::OffChain(e)
+    }
+}
+
+impl From<ink_engine::AccountError> for AccountError {
+    fn from(err: ink_engine::AccountError) -> Self {
+        match err {
+            ink_engine::AccountError::Decoding(e) => AccountError::Decoding(e),
+            ink_engine::AccountError::UnexpectedUserAccount => {
+                AccountError::UnexpectedUserAccount
+            }
+            ink_engine::AccountError::NoAccountForId(acc) => {
+                AccountError::NoAccountForId(acc)
+            }
+        }
+    }
+}
+
+impl From<ink_engine::AccountError> for Error {
+    fn from(account_error: ink_engine::AccountError) -> Self {
+        Error::OffChain(OffChainError::Account(account_error.into()))
+    }
+}
diff --git a/crates/env/src/engine/mod.rs b/crates/env/src/engine/mod.rs
index 561df940c1..c340cf2d4c 100644
--- a/crates/env/src/engine/mod.rs
+++ b/crates/env/src/engine/mod.rs
@@ -28,6 +28,10 @@ cfg_if! {
     if #[cfg(all(not(feature = "std"), target_arch = "wasm32"))] {
         mod on_chain;
         pub use self::on_chain::EnvInstance;
+    } else if #[cfg(all(feature = "std", feature = "ink-experimental-engine"))] {
+        pub mod experimental_off_chain;
+        pub use experimental_off_chain as off_chain;
+        pub use self::experimental_off_chain::EnvInstance;
     } else if #[cfg(feature = "std")] {
         pub mod off_chain;
         pub use self::off_chain::EnvInstance;
diff --git a/crates/lang/macro/Cargo.toml b/crates/lang/macro/Cargo.toml
index 5451f841e0..5adc6f3a70 100644
--- a/crates/lang/macro/Cargo.toml
+++ b/crates/lang/macro/Cargo.toml
@@ -44,3 +44,4 @@ std = [
     "ink_lang_ir/std",
     "ink_primitives/std",
 ]
+ink-experimental-engine = []
diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml
index 77f8fd044c..bd2b636d31 100644
--- a/crates/storage/Cargo.toml
+++ b/crates/storage/Cargo.toml
@@ -53,6 +53,7 @@ std = [
     "scale-info/std",
 ]
 ink-fuzz-tests = ["std"]
+ink-experimental-engine = ["ink_env/ink-experimental-engine"]
 
 [[bench]]
 name = "bench_lazy"
diff --git a/crates/storage/src/lazy/lazy_cell.rs b/crates/storage/src/lazy/lazy_cell.rs
index f143da9f50..7fd912a2c7 100644
--- a/crates/storage/src/lazy/lazy_cell.rs
+++ b/crates/storage/src/lazy/lazy_cell.rs
@@ -710,6 +710,7 @@ mod tests {
 
     #[test]
     #[should_panic(expected = "encountered empty storage cell")]
+    #[cfg(not(feature = "ink-experimental-engine"))]
     fn nested_lazies_are_cleared_completely_after_pull() {
         ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
             // given
@@ -743,6 +744,7 @@ mod tests {
 
     #[test]
     #[should_panic(expected = "encountered empty storage cell")]
+    #[cfg(not(feature = "ink-experimental-engine"))]
     fn lazy_drop_works() {
         ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
             // given
@@ -777,6 +779,7 @@ mod tests {
 
     #[test]
     #[should_panic(expected = "encountered empty storage cell")]
+    #[cfg(not(feature = "ink-experimental-engine"))]
     fn lazy_drop_works_with_greater_footprint() {
         ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
             // given
@@ -809,4 +812,100 @@ mod tests {
         })
         .unwrap()
     }
+
+    #[test]
+    #[should_panic(expected = "encountered empty storage cell")]
+    #[cfg(feature = "ink-experimental-engine")]
+    fn nested_lazies_are_cleared_completely_after_pull() {
+        ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+            // given
+            let root_key = Key::from([0x42; 32]);
+            let nested_lazy: Lazy<Lazy<u32>> = Lazy::new(Lazy::new(13u32));
+            SpreadLayout::push_spread(&nested_lazy, &mut KeyPtr::from(root_key));
+            let pulled_lazy = <Lazy<Lazy<u32>> as SpreadLayout>::pull_spread(
+                &mut KeyPtr::from(root_key),
+            );
+
+            // when
+            SpreadLayout::clear_spread(&pulled_lazy, &mut KeyPtr::from(root_key));
+
+            // then
+            let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+            let used_cells = ink_env::test::count_used_storage_cells::<
+                ink_env::DefaultEnvironment,
+            >(&contract_id)
+            .expect("used cells must be returned");
+            assert_eq!(used_cells, 0);
+            let _ = *<Lazy<Lazy<u32>> as SpreadLayout>::pull_spread(&mut KeyPtr::from(
+                root_key,
+            ));
+            Ok(())
+        })
+        .unwrap()
+    }
+
+    #[test]
+    #[should_panic(expected = "encountered empty storage cell")]
+    #[cfg(feature = "ink-experimental-engine")]
+    fn lazy_drop_works() {
+        ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+            // given
+            let root_key = Key::from([0x42; 32]);
+
+            // when
+            let setup_result = std::panic::catch_unwind(|| {
+                let lazy: Lazy<u32> = Lazy::new(13u32);
+                SpreadLayout::push_spread(&lazy, &mut KeyPtr::from(root_key));
+                let _pulled_lazy =
+                    <Lazy<u32> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+                // lazy is dropped which should clear the cells
+            });
+            assert!(setup_result.is_ok(), "setup should not panic");
+
+            // then
+            let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+            let used_cells = ink_env::test::count_used_storage_cells::<
+                ink_env::DefaultEnvironment,
+            >(&contract_id)
+            .expect("used cells must be returned");
+            assert_eq!(used_cells, 0);
+            let _ =
+                *<Lazy<u32> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+            Ok(())
+        })
+        .unwrap()
+    }
+
+    #[test]
+    #[should_panic(expected = "encountered empty storage cell")]
+    #[cfg(feature = "ink-experimental-engine")]
+    fn lazy_drop_works_with_greater_footprint() {
+        ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+            // given
+            let root_key = Key::from([0x42; 32]);
+
+            // when
+            let setup_result = std::panic::catch_unwind(|| {
+                let lazy: Lazy<[u32; 5]> = Lazy::new([13, 14, 15, 16, 17]);
+                SpreadLayout::push_spread(&lazy, &mut KeyPtr::from(root_key));
+                let _pulled_lazy = <Lazy<[u32; 5]> as SpreadLayout>::pull_spread(
+                    &mut KeyPtr::from(root_key),
+                );
+                // lazy is dropped which should clear the cells
+            });
+            assert!(setup_result.is_ok(), "setup should not panic");
+
+            // then
+            let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+            let used_cells = ink_env::test::count_used_storage_cells::<
+                ink_env::DefaultEnvironment,
+            >(&contract_id)
+            .expect("used cells must be returned");
+            assert_eq!(used_cells, 0);
+            let _ =
+                *<Lazy<u32> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+            Ok(())
+        })
+        .unwrap()
+    }
 }
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
new file mode 100644
index 0000000000..cddb28c9ac
--- /dev/null
+++ b/examples/Cargo.toml
@@ -0,0 +1,10 @@
+# This workspace has just been created so that dependabot can
+# batch upgrades to the example dependencies into a single PR,
+# instead of multiple isolated ones.
+[workspace]
+members = ["./*"]
+exclude = [
+    # we need to exclude `delegator` since it's a separate workspace
+    # and nested workspaces are not allowed.
+    "delegator",
+]
diff --git a/examples/contract-terminate/Cargo.toml b/examples/contract-terminate/Cargo.toml
index d27e3dc9e9..fb9b1da158 100644
--- a/examples/contract-terminate/Cargo.toml
+++ b/examples/contract-terminate/Cargo.toml
@@ -33,3 +33,4 @@ std = [
     "scale-info/std",
 ]
 ink-as-dependency = []
+ink-experimental-engine = ["ink_env/ink-experimental-engine"]
diff --git a/examples/contract-terminate/lib.rs b/examples/contract-terminate/lib.rs
index b71a0e814e..a03998cb13 100644
--- a/examples/contract-terminate/lib.rs
+++ b/examples/contract-terminate/lib.rs
@@ -40,6 +40,7 @@ pub mod just_terminates {
         }
     }
 
+    #[cfg(not(feature = "ink-experimental-engine"))]
     #[cfg(test)]
     mod tests {
         use super::*;
@@ -98,4 +99,35 @@ pub mod just_terminates {
             .expect("Cannot set account balance");
         }
     }
+
+    #[cfg(feature = "ink-experimental-engine")]
+    #[cfg(test)]
+    mod tests_experimental_engine {
+        use super::*;
+        use ink_lang as ink;
+
+        #[ink::test]
+        fn terminating_works() {
+            // given
+            let accounts =
+                ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
+            let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+            ink_env::test::set_caller::<ink_env::DefaultEnvironment>(accounts.alice);
+            ink_env::test::set_account_balance::<ink_env::DefaultEnvironment>(
+                contract_id,
+                100,
+            );
+            let mut contract = JustTerminate::new();
+
+            // when
+            let should_terminate = move || contract.terminate_me();
+
+            // then
+            ink_env::test::assert_contract_termination::<ink_env::DefaultEnvironment, _>(
+                should_terminate,
+                accounts.alice,
+                100,
+            );
+        }
+    }
 }
diff --git a/examples/contract-transfer/Cargo.toml b/examples/contract-transfer/Cargo.toml
index d45f64e1ae..3755f94cd8 100644
--- a/examples/contract-transfer/Cargo.toml
+++ b/examples/contract-transfer/Cargo.toml
@@ -33,3 +33,4 @@ std = [
     "scale-info/std",
 ]
 ink-as-dependency = []
+ink-experimental-engine = ["ink_env/ink-experimental-engine"]
diff --git a/examples/contract-transfer/lib.rs b/examples/contract-transfer/lib.rs
index b756fafb14..95a48eda29 100644
--- a/examples/contract-transfer/lib.rs
+++ b/examples/contract-transfer/lib.rs
@@ -89,6 +89,7 @@ pub mod give_me {
         }
     }
 
+    #[cfg(not(feature = "ink-experimental-engine"))]
     #[cfg(test)]
     mod tests {
         use super::*;
@@ -134,7 +135,7 @@ pub mod give_me {
         fn test_transferred_value() {
             // given
             let accounts = default_accounts();
-            let mut give_me = create_contract(100);
+            let give_me = create_contract(100);
 
             // when
             set_sender(accounts.eve);
@@ -205,4 +206,93 @@ pub mod give_me {
                 .expect("Cannot set account balance")
         }
     }
+
+    #[cfg(feature = "ink-experimental-engine")]
+    #[cfg(test)]
+    mod tests_experimental_engine {
+        use super::*;
+        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));
+        }
+
+        #[ink::test]
+        fn test_transferred_value() {
+            // given
+            let accounts = default_accounts();
+            let give_me = create_contract(100);
+
+            // when
+            // Push the new execution context which sets Eve as caller and
+            // the `mock_transferred_balance` as the value which the contract
+            // will see as transferred to it.
+            set_sender(accounts.eve);
+            ink_env::test::set_value_transferred::<ink_env::DefaultEnvironment>(10);
+
+            // then
+            assert_eq!(give_me.was_it_ten(), true);
+        }
+
+        /// Creates a new instance of `GiveMe` with `initial_balance`.
+        ///
+        /// Returns the `contract_instance`.
+        fn create_contract(initial_balance: Balance) -> GiveMe {
+            let accounts = default_accounts();
+            set_sender(accounts.alice);
+            set_balance(contract_id(), initial_balance);
+            GiveMe::new()
+        }
+
+        fn contract_id() -> AccountId {
+            ink_env::test::callee::<ink_env::DefaultEnvironment>()
+        }
+
+        fn set_sender(sender: AccountId) {
+            ink_env::test::set_caller::<ink_env::DefaultEnvironment>(sender);
+        }
+
+        fn default_accounts(
+        ) -> ink_env::test::DefaultAccounts<ink_env::DefaultEnvironment> {
+            ink_env::test::default_accounts::<ink_env::DefaultEnvironment>()
+        }
+
+        fn set_balance(account_id: AccountId, balance: Balance) {
+            ink_env::test::set_account_balance::<ink_env::DefaultEnvironment>(
+                account_id, balance,
+            )
+        }
+
+        fn get_balance(account_id: AccountId) -> Balance {
+            ink_env::test::get_account_balance::<ink_env::DefaultEnvironment>(account_id)
+                .expect("Cannot get account balance")
+        }
+    }
 }
diff --git a/examples/delegator/README.md b/examples/delegator/README.md
index 86697abe7e..d3cdb21e11 100644
--- a/examples/delegator/README.md
+++ b/examples/delegator/README.md
@@ -34,3 +34,10 @@ is an easy way to get a local smart contract chain running.
    the value stored in the Accumulator smart contract respectively and `switch` to switch the currently
    delegated-to smart contract.
    The initial delegated-to smart contract is the Adder.
+
+   <br/>
+
+   > __Note:__<br/>
+   > Depending on your Substrate version you might encounter [a bug with the pre-filled gas estimation of the UI](https://github.com/paritytech/substrate/issues/8693)
+   > and get the error `ExtrinsicFailed: OutOfGas`.
+   > As a workaround set the maximum allowed gas manually (e.g. to 2500).
diff --git a/examples/erc20/Cargo.toml b/examples/erc20/Cargo.toml
index 7885b7ed4d..9067f4f3de 100644
--- a/examples/erc20/Cargo.toml
+++ b/examples/erc20/Cargo.toml
@@ -33,3 +33,4 @@ std = [
     "scale-info/std",
 ]
 ink-as-dependency = []
+ink-experimental-engine = ["ink_env/ink-experimental-engine"]
diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs
index 901750d584..ea48c56542 100644
--- a/examples/erc20/lib.rs
+++ b/examples/erc20/lib.rs
@@ -206,18 +206,11 @@ mod erc20 {
     }
 
     /// Unit tests.
+    #[cfg(not(feature = "ink-experimental-engine"))]
     #[cfg(test)]
     mod tests {
         /// Imports all the definitions from the outer scope so we can use them here.
         use super::*;
-        use ink_env::{
-            hash::{
-                Blake2x256,
-                CryptoHash,
-                HashOutput,
-            },
-            Clear,
-        };
 
         type Event = <Erc20 as ::ink_lang::BaseEvent>::Type;
 
@@ -238,25 +231,6 @@ mod erc20 {
             } else {
                 panic!("encountered unexpected event kind: expected a Transfer event")
             }
-            fn encoded_into_hash<T>(entity: &T) -> Hash
-            where
-                T: scale::Encode,
-            {
-                let mut result = Hash::clear();
-                let len_result = result.as_ref().len();
-                let encoded = entity.encode();
-                let len_encoded = encoded.len();
-                if len_encoded <= len_result {
-                    result.as_mut()[..len_encoded].copy_from_slice(&encoded);
-                    return result
-                }
-                let mut hash_output =
-                    <<Blake2x256 as HashOutput>::Type as Default>::default();
-                <Blake2x256 as CryptoHash>::hash(&encoded, &mut hash_output);
-                let copy_len = core::cmp::min(hash_output.len(), len_result);
-                result.as_mut()[0..copy_len].copy_from_slice(&hash_output[0..copy_len]);
-                result
-            }
             let expected_topics = vec![
                 encoded_into_hash(&PrefixedValue {
                     value: b"Erc20::Transfer",
@@ -551,4 +525,300 @@ mod erc20 {
             self.value.encode_to(dest);
         }
     }
+
+    #[cfg(feature = "ink-experimental-engine")]
+    #[cfg(test)]
+    mod tests_experimental_engine {
+        use super::*;
+
+        use ink_env::Clear;
+        use ink_lang as ink;
+
+        type Event = <Erc20 as ::ink_lang::BaseEvent>::Type;
+
+        fn assert_transfer_event(
+            event: &ink_env::test::EmittedEvent,
+            expected_from: Option<AccountId>,
+            expected_to: Option<AccountId>,
+            expected_value: Balance,
+        ) {
+            let decoded_event = <Event as scale::Decode>::decode(&mut &event.data[..])
+                .expect("encountered invalid contract event data buffer");
+            if let Event::Transfer(Transfer { from, to, value }) = decoded_event {
+                assert_eq!(from, expected_from, "encountered invalid Transfer.from");
+                assert_eq!(to, expected_to, "encountered invalid Transfer.to");
+                assert_eq!(value, expected_value, "encountered invalid Trasfer.value");
+            } else {
+                panic!("encountered unexpected event kind: expected a Transfer event")
+            }
+            let expected_topics = vec![
+                encoded_into_hash(&PrefixedValue {
+                    value: b"Erc20::Transfer",
+                    prefix: b"",
+                }),
+                encoded_into_hash(&PrefixedValue {
+                    prefix: b"Erc20::Transfer::from",
+                    value: &expected_from,
+                }),
+                encoded_into_hash(&PrefixedValue {
+                    prefix: b"Erc20::Transfer::to",
+                    value: &expected_to,
+                }),
+                encoded_into_hash(&PrefixedValue {
+                    prefix: b"Erc20::Transfer::value",
+                    value: &expected_value,
+                }),
+            ];
+
+            let topics = event.topics.clone();
+            for (n, (actual_topic, expected_topic)) in
+                topics.iter().zip(expected_topics).enumerate()
+            {
+                let mut topic_hash = Hash::clear();
+                let len = actual_topic.len();
+                topic_hash.as_mut()[0..len].copy_from_slice(&actual_topic[0..len]);
+
+                assert_eq!(
+                    topic_hash, expected_topic,
+                    "encountered invalid topic at {}",
+                    n
+                );
+            }
+        }
+
+        /// The default constructor does its job.
+        #[ink::test]
+        fn new_works() {
+            // Constructor works.
+            let _erc20 = Erc20::new(100);
+
+            // Transfer event triggered during initial construction.
+            let emitted_events = ink_env::test::recorded_events().collect::<Vec<_>>();
+            assert_eq!(1, emitted_events.len());
+
+            assert_transfer_event(
+                &emitted_events[0],
+                None,
+                Some(AccountId::from([0x01; 32])),
+                100,
+            );
+        }
+
+        /// The total supply was applied.
+        #[ink::test]
+        fn total_supply_works() {
+            // Constructor works.
+            let erc20 = Erc20::new(100);
+            // Transfer event triggered during initial construction.
+            let emitted_events = ink_env::test::recorded_events().collect::<Vec<_>>();
+            assert_transfer_event(
+                &emitted_events[0],
+                None,
+                Some(AccountId::from([0x01; 32])),
+                100,
+            );
+            // Get the token total supply.
+            assert_eq!(erc20.total_supply(), 100);
+        }
+
+        /// Get the actual balance of an account.
+        #[ink::test]
+        fn balance_of_works() {
+            // Constructor works
+            let erc20 = Erc20::new(100);
+            // Transfer event triggered during initial construction
+            let emitted_events = ink_env::test::recorded_events().collect::<Vec<_>>();
+            assert_transfer_event(
+                &emitted_events[0],
+                None,
+                Some(AccountId::from([0x01; 32])),
+                100,
+            );
+            let accounts =
+                ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
+            // Alice owns all the tokens on deployment
+            assert_eq!(erc20.balance_of(accounts.alice), 100);
+            // Bob does not owns tokens
+            assert_eq!(erc20.balance_of(accounts.bob), 0);
+        }
+
+        #[ink::test]
+        fn transfer_works() {
+            // Constructor works.
+            let mut erc20 = Erc20::new(100);
+            // Transfer event triggered during initial construction.
+            let accounts =
+                ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
+
+            assert_eq!(erc20.balance_of(accounts.bob), 0);
+            // Alice transfers 10 tokens to Bob.
+            assert_eq!(erc20.transfer(accounts.bob, 10), Ok(()));
+            // Bob owns 10 tokens.
+            assert_eq!(erc20.balance_of(accounts.bob), 10);
+
+            let emitted_events = ink_env::test::recorded_events().collect::<Vec<_>>();
+            assert_eq!(emitted_events.len(), 2);
+            // Check first transfer event related to ERC-20 instantiation.
+            assert_transfer_event(
+                &emitted_events[0],
+                None,
+                Some(AccountId::from([0x01; 32])),
+                100,
+            );
+            // Check the second transfer event relating to the actual trasfer.
+            assert_transfer_event(
+                &emitted_events[1],
+                Some(AccountId::from([0x01; 32])),
+                Some(AccountId::from([0x02; 32])),
+                10,
+            );
+        }
+
+        #[ink::test]
+        fn invalid_transfer_should_fail() {
+            // Constructor works.
+            let mut erc20 = Erc20::new(100);
+            let accounts =
+                ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
+
+            assert_eq!(erc20.balance_of(accounts.bob), 0);
+
+            // Set the contract as callee and Bob as caller.
+            let contract = ink_env::account_id::<ink_env::DefaultEnvironment>()?;
+            ink_env::test::set_callee::<ink_env::DefaultEnvironment>(contract);
+            ink_env::test::set_caller::<ink_env::DefaultEnvironment>(accounts.bob);
+
+            // Bob fails to transfers 10 tokens to Eve.
+            assert_eq!(
+                erc20.transfer(accounts.eve, 10),
+                Err(Error::InsufficientBalance)
+            );
+            // Alice owns all the tokens.
+            assert_eq!(erc20.balance_of(accounts.alice), 100);
+            assert_eq!(erc20.balance_of(accounts.bob), 0);
+            assert_eq!(erc20.balance_of(accounts.eve), 0);
+
+            // Transfer event triggered during initial construction.
+            let emitted_events = ink_env::test::recorded_events().collect::<Vec<_>>();
+            assert_eq!(emitted_events.len(), 1);
+            assert_transfer_event(
+                &emitted_events[0],
+                None,
+                Some(AccountId::from([0x01; 32])),
+                100,
+            );
+        }
+
+        #[ink::test]
+        fn transfer_from_works() {
+            // Constructor works.
+            let mut erc20 = Erc20::new(100);
+            // Transfer event triggered during initial construction.
+            let accounts =
+                ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
+
+            // Bob fails to transfer tokens owned by Alice.
+            assert_eq!(
+                erc20.transfer_from(accounts.alice, accounts.eve, 10),
+                Err(Error::InsufficientAllowance)
+            );
+            // Alice approves Bob for token transfers on her behalf.
+            assert_eq!(erc20.approve(accounts.bob, 10), Ok(()));
+
+            // The approve event takes place.
+            assert_eq!(ink_env::test::recorded_events().count(), 2);
+
+            // Set the contract as callee and Bob as caller.
+            let contract = ink_env::account_id::<ink_env::DefaultEnvironment>()?;
+            ink_env::test::set_callee::<ink_env::DefaultEnvironment>(contract);
+            ink_env::test::set_caller::<ink_env::DefaultEnvironment>(accounts.bob);
+
+            // Bob transfers tokens from Alice to Eve.
+            assert_eq!(
+                erc20.transfer_from(accounts.alice, accounts.eve, 10),
+                Ok(())
+            );
+            // Eve owns tokens.
+            assert_eq!(erc20.balance_of(accounts.eve), 10);
+
+            // Check all transfer events that happened during the previous calls:
+            let emitted_events = ink_env::test::recorded_events().collect::<Vec<_>>();
+            assert_eq!(emitted_events.len(), 3);
+            assert_transfer_event(
+                &emitted_events[0],
+                None,
+                Some(AccountId::from([0x01; 32])),
+                100,
+            );
+            // The second event `emitted_events[1]` is an Approve event that we skip checking.
+            assert_transfer_event(
+                &emitted_events[2],
+                Some(AccountId::from([0x01; 32])),
+                Some(AccountId::from([0x05; 32])),
+                10,
+            );
+        }
+
+        #[ink::test]
+        fn allowance_must_not_change_on_failed_transfer() {
+            let mut erc20 = Erc20::new(100);
+            let accounts =
+                ink_env::test::default_accounts::<ink_env::DefaultEnvironment>();
+
+            // Alice approves Bob for token transfers on her behalf.
+            let alice_balance = erc20.balance_of(accounts.alice);
+            let initial_allowance = alice_balance + 2;
+            assert_eq!(erc20.approve(accounts.bob, initial_allowance), Ok(()));
+
+            // Get contract address.
+            let callee = ink_env::account_id::<ink_env::DefaultEnvironment>()?;
+            ink_env::test::set_callee::<ink_env::DefaultEnvironment>(callee);
+            ink_env::test::set_caller::<ink_env::DefaultEnvironment>(accounts.bob);
+
+            // Bob tries to transfer tokens from Alice to Eve.
+            let emitted_events_before =
+                ink_env::test::recorded_events().collect::<Vec<_>>();
+            assert_eq!(
+                erc20.transfer_from(accounts.alice, accounts.eve, alice_balance + 1),
+                Err(Error::InsufficientBalance)
+            );
+            // Allowance must have stayed the same
+            assert_eq!(
+                erc20.allowance(accounts.alice, accounts.bob),
+                initial_allowance
+            );
+            // No more events must have been emitted
+            let emitted_events_after =
+                ink_env::test::recorded_events().collect::<Vec<_>>();
+            assert_eq!(emitted_events_before.len(), emitted_events_after.len());
+        }
+    }
+
+    #[cfg(test)]
+    fn encoded_into_hash<T>(entity: &T) -> Hash
+    where
+        T: scale::Encode,
+    {
+        use ink_env::{
+            hash::{
+                Blake2x256,
+                CryptoHash,
+                HashOutput,
+            },
+            Clear,
+        };
+        let mut result = Hash::clear();
+        let len_result = result.as_ref().len();
+        let encoded = entity.encode();
+        let len_encoded = encoded.len();
+        if len_encoded <= len_result {
+            result.as_mut()[..len_encoded].copy_from_slice(&encoded);
+            return result
+        }
+        let mut hash_output = <<Blake2x256 as HashOutput>::Type as Default>::default();
+        <Blake2x256 as CryptoHash>::hash(&encoded, &mut hash_output);
+        let copy_len = core::cmp::min(hash_output.len(), len_result);
+        result.as_mut()[0..copy_len].copy_from_slice(&hash_output[0..copy_len]);
+        result
+    }
 }
diff --git a/examples/trait-erc20/Cargo.toml b/examples/trait-erc20/Cargo.toml
index 7885b7ed4d..625314fd57 100644
--- a/examples/trait-erc20/Cargo.toml
+++ b/examples/trait-erc20/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "erc20"
+name = "trait_erc20"
 version = "3.0.0-rc3"
 authors = ["Parity Technologies <admin@parity.io>"]
 edition = "2018"
@@ -15,7 +15,7 @@ scale = { package = "parity-scale-codec", version = "2.1", default-features = fa
 scale-info = { version = "0.6", default-features = false, features = ["derive"], optional = true }
 
 [lib]
-name = "erc20"
+name = "trait_erc20"
 path = "lib.rs"
 crate-type = ["cdylib"]
 
diff --git a/examples/trait-flipper/Cargo.toml b/examples/trait-flipper/Cargo.toml
index 5daa4e5a9f..2d74c36de1 100644
--- a/examples/trait-flipper/Cargo.toml
+++ b/examples/trait-flipper/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "flipper"
+name = "trait_flipper"
 version = "3.0.0-rc3"
 authors = ["Parity Technologies <admin@parity.io>"]
 edition = "2018"
@@ -15,7 +15,7 @@ scale = { package = "parity-scale-codec", version = "2.1", default-features = fa
 scale-info = { version = "0.6", default-features = false, features = ["derive"], optional = true }
 
 [lib]
-name = "flipper"
+name = "trait_flipper"
 path = "lib.rs"
 crate-type = ["cdylib"]
 


Test patch:
diff --git a/crates/engine/src/test_api.rs b/crates/engine/src/test_api.rs
new file mode 100644
index 0000000000..71d7b3e01c
--- /dev/null
+++ b/crates/engine/src/test_api.rs
@@ -0,0 +1,293 @@
+// Copyright 2018-2021 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.
+
+use crate::{
+    ext::Engine,
+    types::{
+        AccountId,
+        Balance,
+    },
+    AccountError,
+    Error,
+};
+use std::collections::HashMap;
+
+/// Record for an emitted event.
+#[derive(Clone)]
+pub struct EmittedEvent {
+    /// Recorded topics of the emitted event.
+    pub topics: Vec<Vec<u8>>,
+    /// Recorded encoding of the emitted event.
+    pub data: Vec<u8>,
+}
+
+#[derive(Clone)]
+pub struct RecordedPrintlns {
+    printlns: Vec<String>,
+}
+
+impl RecordedPrintlns {
+    // Creates a new `Engine instance.
+    pub fn new() -> Self {
+        Self {
+            printlns: Vec::new(),
+        }
+    }
+
+    // Records a new println.
+    pub fn record(&mut self, println: String) {
+        self.printlns.push(println);
+    }
+
+    // Clears all recorded printlns.
+    pub fn clear(&mut self) {
+        self.printlns.clear();
+    }
+}
+
+impl Default for RecordedPrintlns {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl IntoIterator for RecordedPrintlns {
+    type Item = String;
+    type IntoIter = std::vec::IntoIter<Self::Item>;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.printlns.into_iter()
+    }
+}
+
+/// Recorder for relevant interactions with this crate.
+pub struct DebugInfo {
+    /// Emitted events recorder.
+    emitted_events: Vec<EmittedEvent>,
+    /// Emitted print messages recorder.
+    emitted_printlns: RecordedPrintlns,
+    /// The total number of reads to the storage.
+    count_reads: HashMap<AccountId, usize>,
+    /// The total number of writes to the storage.
+    count_writes: HashMap<AccountId, usize>,
+    /// The number of storage cells used by each account id.
+    cells_per_account: HashMap<AccountId, HashMap<Vec<u8>, bool>>,
+}
+
+impl Default for DebugInfo {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl DebugInfo {
+    // Creates a new `RecInstance instance.
+    pub fn new() -> Self {
+        Self {
+            emitted_events: Vec::new(),
+            emitted_printlns: RecordedPrintlns::new(),
+            count_reads: HashMap::new(),
+            count_writes: HashMap::new(),
+            cells_per_account: HashMap::new(),
+        }
+    }
+
+    /// Resets the recorder.
+    pub fn reset(&mut self) {
+        self.count_reads.clear();
+        self.count_writes.clear();
+        self.emitted_events.clear();
+        self.emitted_printlns.clear();
+        self.cells_per_account.clear();
+    }
+
+    /// Increases the number of storage writes for the supplied account by one.
+    pub fn inc_writes(&mut self, account_id: AccountId) {
+        self.count_writes
+            .entry(account_id)
+            .and_modify(|v| *v += 1)
+            .or_insert(1);
+    }
+
+    /// Increases the number of storage reads for the supplied account by one.
+    pub fn inc_reads(&mut self, account_id: AccountId) {
+        self.count_reads
+            .entry(account_id)
+            .and_modify(|v| *v += 1)
+            .or_insert(1);
+    }
+
+    /// Records that a cell exists for an account under `key`.
+    ///
+    /// Calling this function multiple times won't change the fact that only
+    /// one cell is recorded.
+    pub fn record_cell_for_account(&mut self, account_id: AccountId, key: Vec<u8>) {
+        self.cells_per_account
+            .entry(account_id)
+            .and_modify(|hm| {
+                let _ = hm.insert(key.clone(), true);
+            })
+            .or_insert({
+                let mut hm = HashMap::new();
+                hm.insert(key, true);
+                hm
+            });
+    }
+
+    /// Removes the cell under `key` for the supplied account.
+    ///
+    /// Returns the removed cell, if there was one.
+    pub fn remove_cell_for_account(
+        &mut self,
+        account_id: AccountId,
+        key: Vec<u8>,
+    ) -> Option<bool> {
+        self.cells_per_account
+            .get_mut(&account_id)
+            .map(|hm| hm.remove(&key))
+            .unwrap_or(None)
+    }
+
+    /// Records a println.
+    pub fn record_println(&mut self, println: String) {
+        self.emitted_printlns.record(println);
+    }
+
+    /// Records an event.
+    pub fn record_event(&mut self, event: EmittedEvent) {
+        self.emitted_events.push(event);
+    }
+}
+
+impl Engine {
+    /// Resets the environment.
+    pub fn initialize_or_reset(&mut self) {
+        self.exec_context.reset();
+        self.database.clear();
+        self.debug_info.reset();
+    }
+
+    /// Returns the total number of reads and writes of the contract's storage.
+    pub fn get_contract_storage_rw(&self, account_id: Vec<u8>) -> (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)
+    }
+
+    /// Sets a caller for the next call.
+    pub fn set_caller(&mut self, caller: Vec<u8>) {
+        self.exec_context.caller = Some(caller.into());
+    }
+
+    /// Sets the callee for the next call.
+    pub fn set_callee(&mut self, callee: Vec<u8>) {
+        self.exec_context.callee = Some(callee.into());
+    }
+
+    /// Returns the amount of storage cells used by the account `account_id`.
+    ///
+    /// Returns `None` if the `account_id` is non-existent.
+    pub fn count_used_storage_cells(&self, account_id: &[u8]) -> Result<usize, Error> {
+        let cells = 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())
+    }
+
+    /// Returns the callee, i.e. the currently executing contract.
+    pub fn get_callee(&self) -> Vec<u8> {
+        self.exec_context.callee()
+    }
+
+    /// Returns the contents of the past performed environmental `println` in order.
+    pub fn get_recorded_printlns(&self) -> RecordedPrintlns {
+        self.debug_info.emitted_printlns.clone()
+    }
+
+    /// Returns the recorded emitted events in order.
+    pub fn get_emitted_events(&self) -> impl Iterator<Item = EmittedEvent> {
+        self.debug_info.emitted_events.clone().into_iter()
+    }
+
+    /// Returns the current balance of `account_id`.
+    pub fn get_balance(&self, account_id: Vec<u8>) -> Result<Balance, Error> {
+        self.database
+            .get_balance(&account_id)
+            .ok_or(Error::Account(AccountError::NoAccountForId(account_id)))
+    }
+
+    /// Sets the balance of `account_id` to `new_balance`.
+    pub fn set_balance(&mut self, account_id: Vec<u8>, new_balance: Balance) {
+        self.database.set_balance(&account_id, new_balance);
+    }
+
+    /// Sets the value transferred from the caller to the callee as part of the call.
+    pub fn set_value_transferred(&mut self, value: Balance) {
+        self.exec_context.value_transferred = value;
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn setting_getting_callee() {
+        let mut engine = Engine::new();
+        let account_id = vec![1; 32];
+        engine.set_callee(account_id.clone());
+        assert_eq!(engine.get_callee(), account_id);
+    }
+
+    #[test]
+    fn count_cells_per_account_must_stay_the_same() {
+        // given
+        let mut engine = Engine::new();
+        let account_id = vec![1; 32];
+        engine.set_callee(account_id.clone());
+        let key: &[u8; 32] = &[0x42; 32];
+        engine.set_storage(key, &[0x05_u8; 5]);
+        assert_eq!(engine.count_used_storage_cells(&account_id), Ok(1));
+
+        // when
+        // we set the storage a second time
+        engine.set_storage(key, &[0x05_u8; 6]);
+
+        // then
+        // the amount of storage cells used must have stayed the same
+        assert_eq!(engine.count_used_storage_cells(&account_id), Ok(1));
+    }
+
+    #[test]
+    fn count_cells_per_account_must_be_reset() {
+        // given
+        let mut engine = Engine::new();
+        let account_id = vec![1; 32];
+        engine.set_callee(account_id.clone());
+        let key: &[u8; 32] = &[0x42; 32];
+        engine.set_storage(key, &[0x05_u8; 5]);
+        assert_eq!(engine.count_used_storage_cells(&account_id), Ok(1));
+
+        // when
+        engine.clear_storage(key);
+
+        // then
+        assert_eq!(engine.count_used_storage_cells(&account_id), Ok(0));
+    }
+}
diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs
new file mode 100644
index 0000000000..ed83e45557
--- /dev/null
+++ b/crates/engine/src/tests.rs
@@ -0,0 +1,194 @@
+// Copyright 2018-2021 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.
+
+use crate::ext::{
+    Engine,
+    Error,
+};
+
+/// The public methods of the `contracts` pallet write their result into an
+/// `output` buffer instead of returning them. Since we aim to emulate this
+/// behavior, we have to provide some buffer for our tests to pass into these
+/// emulated methods, so that they can write their result into it.
+///
+/// The number 1024 is more or less arbitrary, it just satisfies the need of
+/// our tests without being too large.
+fn get_buffer() -> [u8; 1024] {
+    [0; 1024]
+}
+
+#[test]
+fn store_load_clear() {
+    let mut engine = Engine::new();
+    engine.set_callee(vec![1; 32]);
+    let key: &[u8; 32] = &[0x42; 32];
+    let output = &mut &mut get_buffer()[..];
+    let res = engine.get_storage(key, output);
+    assert_eq!(res, Err(Error::KeyNotFound));
+
+    engine.set_storage(key, &[0x05_u8; 5]);
+    let res = engine.get_storage(key, output);
+    assert_eq!(res, Ok(()),);
+    assert_eq!(output[..5], [0x05; 5]);
+
+    engine.clear_storage(key);
+    let res = engine.get_storage(key, output);
+    assert_eq!(res, Err(Error::KeyNotFound));
+}
+
+#[test]
+fn setting_getting_balance() {
+    // given
+    let mut engine = Engine::new();
+    let account_id = vec![1; 32];
+    let balance = 1337;
+    engine.set_callee(account_id.clone());
+    engine.set_balance(account_id, balance);
+
+    // when
+    let mut output = get_buffer();
+    engine.balance(&mut &mut output[..]);
+
+    // then
+    let output = <u128 as scale::Decode>::decode(&mut &output[..16])
+        .expect("decoding balance failed");
+    assert_eq!(output, balance);
+}
+
+#[test]
+fn setting_getting_caller() {
+    // given
+    let mut engine = Engine::new();
+    let account_id = vec![1; 32];
+
+    // when
+    engine.set_caller(account_id.clone());
+
+    // then
+    let mut output = get_buffer();
+    engine.caller(&mut &mut output[..]);
+    assert_eq!(&output[..account_id.len()], &account_id);
+}
+
+#[test]
+fn address() {
+    // given
+    let mut engine = Engine::new();
+    let account_id = vec![1; 32];
+    engine.set_callee(account_id.clone());
+
+    // when
+    let mut output = get_buffer();
+    engine.address(&mut &mut output[..]);
+
+    // then
+    assert_eq!(&output[..account_id.len()], &account_id);
+}
+
+#[test]
+fn transfer() {
+    // given
+    let mut engine = Engine::new();
+    let alice = vec![1; 32];
+    let bob = vec![2; 32];
+    engine.set_callee(alice.clone());
+    engine.set_balance(alice.clone(), 1337);
+
+    // when
+    let val = scale::Encode::encode(&337u128);
+    assert_eq!(engine.transfer(&bob, &val), Ok(()));
+
+    // then
+    assert_eq!(engine.get_balance(alice), Ok(1000));
+    assert_eq!(engine.get_balance(bob), Ok(337));
+}
+
+#[test]
+fn printlns() {
+    let mut engine = Engine::new();
+    engine.println("foobar");
+    let mut recorded = engine.get_recorded_printlns().into_iter();
+    assert_eq!(recorded.next(), Some("foobar".into()));
+    assert_eq!(recorded.next(), None);
+}
+
+#[test]
+fn events() {
+    // given
+    let mut engine = Engine::new();
+    let topics_count: scale::Compact<u32> = scale::Compact(2u32);
+    let mut enc_topics_count = scale::Encode::encode(&topics_count);
+    let topic1 = vec![12u8, 13];
+    let topic2 = vec![14u8, 15];
+    let data = &vec![21, 22, 23];
+
+    // when
+    let mut enc_topics_info: Vec<u8> = Vec::new();
+    enc_topics_info.append(&mut enc_topics_count);
+    enc_topics_info.append(&mut topic1.clone());
+    enc_topics_info.append(&mut topic2.clone());
+    engine.deposit_event(&enc_topics_info, data);
+
+    // then
+    let mut events = engine.get_emitted_events();
+    let event = events.next().expect("event must exist");
+    assert_eq!(event.topics.len(), 2);
+    assert_eq!(
+        event.topics.get(0).expect("first topic must exist"),
+        &topic1
+    );
+    assert_eq!(
+        event.topics.get(1).expect("second topic must exist"),
+        &topic2
+    );
+    assert_eq!(&event.data, data);
+    assert!(events.next().is_none());
+}
+
+#[test]
+fn value_transferred() {
+    // given
+    let mut engine = Engine::new();
+    let value = 1337;
+    engine.set_value_transferred(value);
+
+    // when
+    let output = &mut &mut get_buffer()[..];
+    engine.value_transferred(output);
+
+    // then
+    let output = <u128 as scale::Decode>::decode(&mut &output[..16])
+        .expect("decoding value transferred failed");
+    assert_eq!(output, value);
+}
+
+#[test]
+#[should_panic(
+    expected = "the output buffer is too small! the decoded storage is of size 16 bytes, but the output buffer has only room for 8."
+)]
+fn must_panic_when_buffer_too_small() {
+    // given
+    let mut engine = Engine::new();
+    engine.set_callee(vec![1; 32]);
+    let key: &[u8; 32] = &[0x42; 32];
+    engine.set_storage(key, &[0x05_u8; 16]);
+
+    // when
+    let mut small_buffer = [0; 8];
+    let output = &mut &mut small_buffer[..];
+    let _ = engine.get_storage(key, output);
+
+    // then
+    unreachable!("`get_storage` must already have panicked");
+}
diff --git a/crates/env/src/engine/experimental_off_chain/test_api.rs b/crates/env/src/engine/experimental_off_chain/test_api.rs
new file mode 100644
index 0000000000..8608270cbe
--- /dev/null
+++ b/crates/env/src/engine/experimental_off_chain/test_api.rs
@@ -0,0 +1,366 @@
+// Copyright 2018-2021 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.
+
+//! Operations on the off-chain testing environment.
+
+use super::{
+    EnvInstance,
+    OnInstance,
+};
+use crate::{
+    Environment,
+    Result,
+};
+use core::fmt::Debug;
+use ink_engine::test_api::RecordedPrintlns;
+use std::panic::UnwindSafe;
+
+/// Record for an emitted event.
+#[derive(Clone)]
+pub struct EmittedEvent {
+    /// Recorded topics of the emitted event.
+    pub topics: Vec<Vec<u8>>,
+    /// Recorded encoding of the emitted event.
+    pub data: Vec<u8>,
+}
+
+/// Sets the balance of the account to the given balance.
+///
+/// # Note
+///
+/// Note that account could refer to either a user account or
+/// a smart contract account.
+///
+/// # Errors
+///
+/// - If `account` does not exist.
+/// - If the underlying `account` type does not match.
+/// - If the underlying `new_balance` type does not match.
+pub fn set_account_balance<T>(account_id: T::AccountId, new_balance: T::Balance)
+where
+    T: Environment<Balance = u128>, // Just temporary for the MVP!
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance
+            .engine
+            .set_balance(scale::Encode::encode(&account_id), new_balance);
+    })
+}
+
+/// Returns the balance of the account.
+///
+/// # Note
+///
+/// Note that account could refer to either a user account or
+/// a smart contract account. This returns the same as `env::api::balance`
+/// if given the account id of the currently executed smart contract.
+///
+/// # Errors
+///
+/// - If `account` does not exist.
+/// - If the underlying `account` type does not match.
+pub fn get_account_balance<T>(account_id: T::AccountId) -> Result<T::Balance>
+where
+    T: Environment<Balance = u128>, // Just temporary for the MVP!
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance
+            .engine
+            .get_balance(scale::Encode::encode(&account_id))
+            .map_err(Into::into)
+    })
+}
+
+/// Sets the rent allowance of the contract account to the given rent allowance.
+///
+/// # Errors
+///
+/// - If `account` does not exist.
+/// - If the underlying `account` type does not match.
+/// - If the underlying `new_rent_allowance` type does not match.
+pub fn set_contract_rent_allowance<T>(
+    _account_id: T::AccountId,
+    _new_rent_allowance: T::Balance,
+) -> Result<()>
+where
+    T: Environment,
+{
+    unimplemented!(
+        "off-chain environment does not yet support `set_contract_rent_allowance`"
+    );
+}
+
+/// Returns the rent allowance of the contract account.
+///
+/// # Errors
+///
+/// - If `account` does not exist.
+/// - If the underlying `account` type does not match.
+/// - If the returned rent allowance cannot be properly decoded.
+pub fn get_contract_rent_allowance<T>(_account_id: T::AccountId) -> Result<T::Balance>
+where
+    T: Environment,
+{
+    unimplemented!(
+        "off-chain environment does not yet support `get_contract_rent_allowance`"
+    );
+}
+
+/// Set the entropy hash of the current block.
+///
+/// # Note
+///
+/// This allows to control what [`random`][`crate::random`] returns.
+pub fn set_block_entropy<T>(_entropy: T::Hash) -> Result<()>
+where
+    T: Environment,
+{
+    unimplemented!("off-chain environment does not yet support `set_block_entropy`");
+}
+
+/// Returns the contents of the past performed environmental `println` in order.
+pub fn recorded_printlns() -> RecordedPrintlns {
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance.engine.get_recorded_printlns()
+    })
+}
+
+/// Set to true to disable clearing storage
+///
+/// # Note
+///
+/// Useful for benchmarking because it ensures the initialized storage is maintained across runs,
+/// because lazy storage structures automatically clear their associated cells when they are dropped.
+pub fn set_clear_storage_disabled(_disable: bool) {
+    unimplemented!(
+        "off-chain environment does not yet support `set_clear_storage_disabled`"
+    );
+}
+
+/// Sets a caller for the next call.
+pub fn set_caller<T>(caller: T::AccountId)
+where
+    T: Environment,
+    <T as Environment>::AccountId: From<[u8; 32]>,
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance.engine.set_caller(scale::Encode::encode(&caller));
+    })
+}
+
+/// Sets the callee for the next call.
+pub fn set_callee<T>(callee: T::AccountId)
+where
+    T: Environment,
+    <T as Environment>::AccountId: From<[u8; 32]>,
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance.engine.set_callee(scale::Encode::encode(&callee));
+    })
+}
+
+/// Gets the currently set callee.
+///
+/// This is account id of the currently executing contract.
+pub fn callee<T>() -> T::AccountId
+where
+    T: Environment,
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        let callee = instance.engine.get_callee();
+        scale::Decode::decode(&mut &callee[..]).expect("encoding failed")
+    })
+}
+
+/// Returns the total number of reads and writes of the contract's storage.
+pub fn get_contract_storage_rw<T>(account_id: &T::AccountId) -> (usize, usize)
+where
+    T: Environment,
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance
+            .engine
+            .get_contract_storage_rw(scale::Encode::encode(&account_id))
+    })
+}
+
+/// Sets the balance of `account_id` to `new_balance`.
+pub fn set_balance<T>(account_id: T::AccountId, new_balance: T::Balance)
+where
+    T: Environment<Balance = u128>, // Just temporary for the MVP!
+    <T as Environment>::AccountId: From<[u8; 32]>,
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance
+            .engine
+            .set_balance(scale::Encode::encode(&account_id), new_balance);
+    })
+}
+
+/// Sets the value transferred from the caller to the callee as part of the call.
+pub fn set_value_transferred<T>(value: T::Balance)
+where
+    T: Environment<Balance = u128>, // Just temporary for the MVP!
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance.engine.set_value_transferred(value);
+    })
+}
+
+/// Returns the amount of storage cells used by the account `account_id`.
+///
+/// Returns `None` if the `account_id` is non-existent.
+pub fn count_used_storage_cells<T>(account_id: &T::AccountId) -> Result<usize>
+where
+    T: Environment,
+{
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance
+            .engine
+            .count_used_storage_cells(&scale::Encode::encode(&account_id))
+            .map_err(Into::into)
+    })
+}
+
+/// Runs the given closure test function with the default configuartion
+/// for the off-chain environment.
+pub fn run_test<T, F>(f: F) -> Result<()>
+where
+    T: Environment,
+    F: FnOnce(DefaultAccounts<T>) -> Result<()>,
+    <T as Environment>::AccountId: From<[u8; 32]>,
+{
+    let default_accounts = default_accounts::<T>();
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance.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());
+
+        // 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);
+    });
+    f(default_accounts)
+}
+
+/// Returns the default accounts for testing purposes:
+/// Alice, Bob, Charlie, Django, Eve and Frank.
+pub fn default_accounts<T>() -> DefaultAccounts<T>
+where
+    T: Environment,
+    <T as Environment>::AccountId: From<[u8; 32]>,
+{
+    DefaultAccounts {
+        alice: T::AccountId::from([0x01; 32]),
+        bob: T::AccountId::from([0x02; 32]),
+        charlie: T::AccountId::from([0x03; 32]),
+        django: T::AccountId::from([0x04; 32]),
+        eve: T::AccountId::from([0x05; 32]),
+        frank: T::AccountId::from([0x06; 32]),
+    }
+}
+
+/// The default accounts.
+pub struct DefaultAccounts<T>
+where
+    T: Environment,
+{
+    /// The predefined `ALICE` account holding substantial amounts of value.
+    pub alice: T::AccountId,
+    /// The predefined `BOB` account holding some amounts of value.
+    pub bob: T::AccountId,
+    /// The predefined `CHARLIE` account holding some amounts of value.
+    pub charlie: T::AccountId,
+    /// The predefined `DJANGO` account holding no value.
+    pub django: T::AccountId,
+    /// The predefined `EVE` account holding no value.
+    pub eve: T::AccountId,
+    /// The predefined `FRANK` account holding no value.
+    pub frank: T::AccountId,
+}
+
+/// Returns the recorded emitted events in order.
+pub fn recorded_events() -> impl Iterator<Item = EmittedEvent> {
+    <EnvInstance as OnInstance>::on_instance(|instance| {
+        instance
+            .engine
+            .get_emitted_events()
+            .into_iter()
+            .map(|evt: ink_engine::test_api::EmittedEvent| evt.into())
+    })
+}
+
+/// Tests if a contract terminates successfully after `self.env().terminate()`
+/// has been called.
+///
+/// The arguments denote:
+///
+/// * `should_terminate`: A closure in which the function supposed to terminate is called.
+/// * `expected_beneficiary`: The beneficiary account who should have received the
+///    remaining value in the contract
+/// * `expected_value_transferred_to_beneficiary`: The value which should have been transferred
+///   to the `expected_beneficiary`.
+/// # Usage
+///
+/// ```no_compile
+/// let should_terminate = move || your_contract.fn_which_should_terminate();
+/// ink_env::test::assert_contract_termination::<ink_env::DefaultEnvironment, _>(
+///     should_terminate,
+///     expected_beneficiary,
+///     expected_value_transferred_to_beneficiary
+/// );
+/// ```
+///
+/// See `examples/contract-terminate` for a complete usage example.
+pub fn assert_contract_termination<T, F>(
+    should_terminate: F,
+    expected_beneficiary: T::AccountId,
+    expected_value_transferred_to_beneficiary: T::Balance,
+) where
+    T: Environment,
+    F: FnMut() + UnwindSafe,
+    <T as Environment>::AccountId: Debug,
+    <T as Environment>::Balance: Debug,
+{
+    let value_any = ::std::panic::catch_unwind(should_terminate)
+        .expect_err("contract did not terminate");
+    let encoded_input = value_any
+        .downcast_ref::<Vec<u8>>()
+        .expect("panic object can not be cast");
+    let (value_transferred, encoded_beneficiary): (T::Balance, Vec<u8>) =
+        scale::Decode::decode(&mut &encoded_input[..]).expect("input can not be decoded");
+    let beneficiary =
+        <T::AccountId as scale::Decode>::decode(&mut &encoded_beneficiary[..])
+            .expect("input can not be decoded");
+    assert_eq!(value_transferred, expected_value_transferred_to_beneficiary);
+    assert_eq!(beneficiary, expected_beneficiary);
+}
diff --git a/crates/env/src/engine/experimental_off_chain/tests.rs b/crates/env/src/engine/experimental_off_chain/tests.rs
new file mode 100644
index 0000000000..ccfd1e6c9d
--- /dev/null
+++ b/crates/env/src/engine/experimental_off_chain/tests.rs
@@ -0,0 +1,43 @@
+// Copyright 2018-2021 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.
+
+use crate::{
+    engine::experimental_off_chain::impls::TopicsBuilder,
+    topics::TopicsBuilderBackend,
+    Result,
+};
+
+#[test]
+fn topics_builder() -> Result<()> {
+    crate::test::run_test::<crate::DefaultEnvironment, _>(|_| {
+        // given
+        let mut builder = TopicsBuilder::default();
+
+        // when
+        TopicsBuilderBackend::<crate::DefaultEnvironment>::push_topic(&mut builder, &13);
+        TopicsBuilderBackend::<crate::DefaultEnvironment>::push_topic(&mut builder, &17);
+
+        // then
+        assert_eq!(builder.topics.len(), 2);
+
+        let topics_len_compact = &scale::Compact(2u32);
+        let topics_len_encoded = scale::Encode::encode(&topics_len_compact);
+        let output = TopicsBuilderBackend::<crate::DefaultEnvironment>::output(builder);
+        #[rustfmt::skip]
+        let expected = vec![topics_len_encoded[0], 13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+        assert_eq!(output, expected);
+
+        Ok(())
+    })
+}
diff --git a/crates/lang/macro/tests/unique_topics.rs b/crates/lang/macro/tests/unique_topics.rs
index 6a96a0872c..502be60dbd 100644
--- a/crates/lang/macro/tests/unique_topics.rs
+++ b/crates/lang/macro/tests/unique_topics.rs
@@ -44,7 +44,7 @@ mod my_contract {
         /// Emits a `MyEvent`.
         #[ink(message)]
         pub fn emit_my_event(&self) {
-            Self::env().emit_event(MyEvent {
+            self.env().emit_event(MyEvent {
                 v0: None,
                 v1: 0,
                 v2: false,
@@ -60,6 +60,28 @@ mod my_contract {
         use ink_lang as ink;
 
         #[ink::test]
+        #[cfg(feature = "ink-experimental-engine")]
+        fn event_must_have_unique_topics() {
+            // given
+            let my_contract = MyContract::new();
+
+            // when
+            MyContract::emit_my_event(&my_contract);
+
+            // then
+            // all topics must be unique
+            let emitted_events =
+                ink_env::test::recorded_events().collect::<Vec<EmittedEvent>>();
+            let mut encoded_topics: std::vec::Vec<&[u8]> = emitted_events[0]
+                .topics
+                .iter()
+                .map(|topic| topic.as_slice())
+                .collect();
+            assert!(!has_duplicates(&mut encoded_topics));
+        }
+
+        #[ink::test]
+        #[cfg(not(feature = "ink-experimental-engine"))]
         fn event_must_have_unique_topics() {
             // given
             let my_contract = MyContract::new();
diff --git a/crates/storage/src/collections/binary_heap/tests.rs b/crates/storage/src/collections/binary_heap/tests.rs
index 63253c6244..47e48cde7d 100644
--- a/crates/storage/src/collections/binary_heap/tests.rs
+++ b/crates/storage/src/collections/binary_heap/tests.rs
@@ -238,6 +238,7 @@ fn spread_layout_clear_works() {
 
 #[test]
 #[should_panic(expected = "encountered empty storage cell")]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn drop_works() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         let root_key = Key::from([0x42; 32]);
@@ -272,6 +273,40 @@ fn drop_works() {
     .unwrap()
 }
 
+#[test]
+#[should_panic(expected = "encountered empty storage cell")]
+#[cfg(feature = "ink-experimental-engine")]
+fn drop_works() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        let root_key = Key::from([0x42; 32]);
+
+        // if the setup panics it should not cause the test to pass
+        let setup_result = std::panic::catch_unwind(|| {
+            let heap = heap_from_slice(&[23, 25, 65]);
+            SpreadLayout::push_spread(&heap, &mut KeyPtr::from(root_key));
+
+            let _ = <BinaryHeap<u8> as SpreadLayout>::pull_spread(&mut KeyPtr::from(
+                root_key,
+            ));
+            // heap is dropped which should clear the cells
+        });
+
+        assert!(setup_result.is_ok(), "setup should not panic");
+
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let used_cells = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("Used cells must be returned");
+        assert_eq!(used_cells, 0);
+
+        let _ =
+            <BinaryHeap<u8> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+        Ok(())
+    })
+    .unwrap()
+}
+
 #[test]
 fn clear_works_on_filled_heap() {
     let mut heap = heap_from_slice(&[b'a', b'b', b'c', b'd']);
@@ -286,6 +321,7 @@ fn clear_works_on_empty_heap() {
     assert!(heap.is_empty());
 }
 
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn check_complexity_read_writes<F>(
     heap_size: u32,
     heap_op: F,
@@ -299,9 +335,11 @@ where
         let heap1 = heap_of_size(heap_size);
         let root_key = Key::from([0x42; 32]);
         SpreadLayout::push_spread(&heap1, &mut KeyPtr::from(root_key));
+
         let contract_account = ink_env::test::get_current_contract_account_id::<
             ink_env::DefaultEnvironment,
-        >()?;
+        >()
+        .expect("Cannot get contract id");
 
         let mut lazy_heap =
             <BinaryHeap<u32> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
@@ -325,6 +363,65 @@ where
         let (reads, writes) = ink_env::test::get_contract_storage_rw::<
             ink_env::DefaultEnvironment,
         >(&contract_account)?;
+
+        let net_reads = reads - base_reads;
+        let net_writes = writes - base_writes;
+
+        assert_eq!(
+            net_reads, expected_net_reads,
+            "size {}: storage reads",
+            heap_size
+        );
+        assert_eq!(
+            net_writes, expected_net_writes,
+            "size {}: storage writes",
+            heap_size
+        );
+
+        Ok(())
+    })
+}
+
+#[cfg(feature = "ink-experimental-engine")]
+fn check_complexity_read_writes<F>(
+    heap_size: u32,
+    heap_op: F,
+    expected_net_reads: usize,
+    expected_net_writes: usize,
+) -> ink_env::Result<()>
+where
+    F: FnOnce(&mut BinaryHeap<u32>),
+{
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        let heap1 = heap_of_size(heap_size);
+        let root_key = Key::from([0x42; 32]);
+        SpreadLayout::push_spread(&heap1, &mut KeyPtr::from(root_key));
+
+        let contract_account = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+
+        let mut lazy_heap =
+            <BinaryHeap<u32> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+
+        let (base_reads, base_writes) = ink_env::test::get_contract_storage_rw::<
+            ink_env::DefaultEnvironment,
+        >(&contract_account);
+
+        // elements.len + vec.len
+        const CONST_WRITES: u32 = 2;
+        assert_eq!(
+            (base_reads as u32, base_writes as u32),
+            (0, CONST_WRITES + get_count_cells(heap_size))
+        );
+
+        heap_op(&mut lazy_heap);
+
+        // write back to storage so we can see how many writes required
+        SpreadLayout::push_spread(&lazy_heap, &mut KeyPtr::from(root_key));
+
+        let (reads, writes) = ink_env::test::get_contract_storage_rw::<
+            ink_env::DefaultEnvironment,
+        >(&contract_account);
+
         let net_reads = reads - base_reads;
         let net_writes = writes - base_writes;
 
diff --git a/crates/storage/src/collections/hashmap/tests.rs b/crates/storage/src/collections/hashmap/tests.rs
index ebb4341ac2..c25c1a1a42 100644
--- a/crates/storage/src/collections/hashmap/tests.rs
+++ b/crates/storage/src/collections/hashmap/tests.rs
@@ -324,6 +324,7 @@ fn spread_layout_clear_works() {
 }
 
 #[test]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn storage_is_cleared_completely_after_pull_lazy() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         // given
@@ -355,6 +356,7 @@ fn storage_is_cleared_completely_after_pull_lazy() {
 
 #[test]
 #[should_panic(expected = "storage entry was empty")]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn drop_works() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         let root_key = Key::from([0x42; 32]);
@@ -387,3 +389,64 @@ fn drop_works() {
     })
     .unwrap()
 }
+
+#[test]
+#[cfg(feature = "ink-experimental-engine")]
+fn storage_is_cleared_completely_after_pull_lazy() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        // given
+        let root_key = Key::from([0x42; 32]);
+        let lazy_hmap = Lazy::new(filled_hmap());
+        SpreadLayout::push_spread(&lazy_hmap, &mut KeyPtr::from(root_key));
+        let pulled_hmap = <Lazy<StorageHashMap<u8, i32>> as SpreadLayout>::pull_spread(
+            &mut KeyPtr::from(root_key),
+        );
+
+        // when
+        SpreadLayout::clear_spread(&pulled_hmap, &mut KeyPtr::from(root_key));
+
+        // then
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let used_cells = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("used cells must be returned");
+        assert_eq!(used_cells, 0);
+
+        Ok(())
+    })
+    .unwrap()
+}
+
+#[test]
+#[should_panic(expected = "storage entry was empty")]
+#[cfg(feature = "ink-experimental-engine")]
+fn drop_works() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        let root_key = Key::from([0x42; 32]);
+
+        // if the setup panics it should not cause the test to pass
+        let setup_result = std::panic::catch_unwind(|| {
+            let hmap = filled_hmap();
+            SpreadLayout::push_spread(&hmap, &mut KeyPtr::from(root_key));
+            let _ = <StorageHashMap<u8, i32> as SpreadLayout>::pull_spread(
+                &mut KeyPtr::from(root_key),
+            );
+            // hmap is dropped which should clear the cells
+        });
+        assert!(setup_result.is_ok(), "setup should not panic");
+
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let used_cells = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("used cells must be returned");
+        assert_eq!(used_cells, 0);
+
+        let _ = <StorageHashMap<u8, i32> as SpreadLayout>::pull_spread(
+            &mut KeyPtr::from(root_key),
+        );
+        Ok(())
+    })
+    .unwrap()
+}
diff --git a/crates/storage/src/collections/smallvec/tests.rs b/crates/storage/src/collections/smallvec/tests.rs
index a99079e514..084ea4c40b 100644
--- a/crates/storage/src/collections/smallvec/tests.rs
+++ b/crates/storage/src/collections/smallvec/tests.rs
@@ -398,6 +398,7 @@ fn spread_layout_clear_works() {
 }
 
 #[test]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn storage_is_cleared_completely_after_pull_lazy() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         // given
@@ -415,7 +416,7 @@ fn storage_is_cleared_completely_after_pull_lazy() {
         let contract_id = ink_env::test::get_current_contract_account_id::<
             ink_env::DefaultEnvironment,
         >()
-        .expect("contract id must exist");
+        .expect("Cannot get contract id");
         let used_cells = ink_env::test::count_used_storage_cells::<
             ink_env::DefaultEnvironment,
         >(&contract_id)
@@ -429,6 +430,7 @@ fn storage_is_cleared_completely_after_pull_lazy() {
 
 #[test]
 #[should_panic(expected = "encountered empty storage cell")]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn drop_works() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         let root_key = Key::from([0x42; 32]);
@@ -460,3 +462,63 @@ fn drop_works() {
     })
     .unwrap()
 }
+
+#[test]
+#[cfg(feature = "ink-experimental-engine")]
+fn storage_is_cleared_completely_after_pull_lazy() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        // given
+        let root_key = Key::from([0x42; 32]);
+        let lazy_vec = Lazy::new(vec_from_slice(&[b'a', b'b', b'c', b'd']));
+        SpreadLayout::push_spread(&lazy_vec, &mut KeyPtr::from(root_key));
+        let pulled_vec = <Lazy<SmallVec<u8, 4>> as SpreadLayout>::pull_spread(
+            &mut KeyPtr::from(root_key),
+        );
+
+        // when
+        SpreadLayout::clear_spread(&pulled_vec, &mut KeyPtr::from(root_key));
+
+        // then
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let used_cells = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("used cells must be returned");
+        assert_eq!(used_cells, 0);
+
+        Ok(())
+    })
+    .unwrap()
+}
+
+#[test]
+#[should_panic(expected = "encountered empty storage cell")]
+#[cfg(feature = "ink-experimental-engine")]
+fn drop_works() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        let root_key = Key::from([0x42; 32]);
+
+        // if the setup panics it should not cause the test to pass
+        let setup_result = std::panic::catch_unwind(|| {
+            let vec = vec_from_slice(&[b'a', b'b', b'c', b'd']);
+            SpreadLayout::push_spread(&vec, &mut KeyPtr::from(root_key));
+            let _ = <SmallVec<u8, 4> as SpreadLayout>::pull_spread(&mut KeyPtr::from(
+                root_key,
+            ));
+            // vec is dropped which should clear the cells
+        });
+        assert!(setup_result.is_ok(), "setup should not panic");
+
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let used_cells = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("used cells must be returned");
+        assert_eq!(used_cells, 0);
+
+        let _ =
+            <SmallVec<u8, 4> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+        Ok(())
+    })
+    .unwrap()
+}
diff --git a/crates/storage/src/collections/stash/tests.rs b/crates/storage/src/collections/stash/tests.rs
index 1cf09063f9..4b0dc1788f 100644
--- a/crates/storage/src/collections/stash/tests.rs
+++ b/crates/storage/src/collections/stash/tests.rs
@@ -754,6 +754,7 @@ fn spread_layout_clear_works() {
 }
 
 #[test]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn storage_is_cleared_completely_after_pull_lazy() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         // given
@@ -785,6 +786,7 @@ fn storage_is_cleared_completely_after_pull_lazy() {
 
 #[test]
 #[should_panic(expected = "storage entry was empty")]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn drop_works() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         let root_key = Key::from([0x42; 32]);
@@ -816,3 +818,63 @@ fn drop_works() {
     })
     .unwrap()
 }
+
+#[test]
+#[cfg(feature = "ink-experimental-engine")]
+fn storage_is_cleared_completely_after_pull_lazy() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        // given
+        let root_key = Key::from([0x42; 32]);
+        let lazy_stash = Lazy::new(create_holey_stash());
+        SpreadLayout::push_spread(&lazy_stash, &mut KeyPtr::from(root_key));
+        let pulled_stash = <Lazy<StorageStash<u8>> as SpreadLayout>::pull_spread(
+            &mut KeyPtr::from(root_key),
+        );
+
+        // when
+        SpreadLayout::clear_spread(&pulled_stash, &mut KeyPtr::from(root_key));
+
+        // then
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let storage_used = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("used cells must be returned");
+        assert_eq!(storage_used, 0);
+
+        Ok(())
+    })
+    .unwrap()
+}
+
+#[test]
+#[should_panic(expected = "storage entry was empty")]
+#[cfg(feature = "ink-experimental-engine")]
+fn drop_works() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        let root_key = Key::from([0x42; 32]);
+
+        // if the setup panics it should not cause the test to pass
+        let setup_result = std::panic::catch_unwind(|| {
+            let stash = create_holey_stash();
+            SpreadLayout::push_spread(&stash, &mut KeyPtr::from(root_key));
+            let _ = <StorageStash<u8> as SpreadLayout>::pull_spread(&mut KeyPtr::from(
+                root_key,
+            ));
+            // stash is dropped which should clear the cells
+        });
+        assert!(setup_result.is_ok(), "setup should not panic");
+
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let used_cells = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("used cells must be returned");
+        assert_eq!(used_cells, 0);
+
+        let _ =
+            <StorageStash<u8> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+        Ok(())
+    })
+    .unwrap()
+}
diff --git a/crates/storage/src/collections/vec/tests.rs b/crates/storage/src/collections/vec/tests.rs
index 4f8009184d..c1cbdaab40 100644
--- a/crates/storage/src/collections/vec/tests.rs
+++ b/crates/storage/src/collections/vec/tests.rs
@@ -429,6 +429,7 @@ fn clear_works_on_empty_vec() {
 
 #[test]
 #[should_panic(expected = "encountered empty storage cell")]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn storage_is_cleared_completely_after_pull_lazy() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         // given
@@ -464,6 +465,7 @@ fn storage_is_cleared_completely_after_pull_lazy() {
 
 #[test]
 #[should_panic(expected = "encountered empty storage cell")]
+#[cfg(not(feature = "ink-experimental-engine"))]
 fn drop_works() {
     ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
         let root_key = Key::from([0x42; 32]);
@@ -495,3 +497,68 @@ fn drop_works() {
     })
     .unwrap()
 }
+
+#[test]
+#[should_panic(expected = "encountered empty storage cell")]
+#[cfg(feature = "ink-experimental-engine")]
+fn storage_is_cleared_completely_after_pull_lazy() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        // given
+        let root_key = Key::from([0x42; 32]);
+        let mut lazy_vec: Lazy<StorageVec<u32>> = Lazy::new(StorageVec::new());
+        lazy_vec.push(13u32);
+        lazy_vec.push(13u32);
+        SpreadLayout::push_spread(&lazy_vec, &mut KeyPtr::from(root_key));
+        let pulled_vec = <Lazy<StorageVec<u32>> as SpreadLayout>::pull_spread(
+            &mut KeyPtr::from(root_key),
+        );
+
+        // when
+        SpreadLayout::clear_spread(&pulled_vec, &mut KeyPtr::from(root_key));
+
+        // then
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let used_cells = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("used cells must be returned");
+        assert_eq!(used_cells, 0);
+        let _ =
+            *<Lazy<Lazy<u32>> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+
+        Ok(())
+    })
+    .unwrap()
+}
+
+#[test]
+#[should_panic(expected = "encountered empty storage cell")]
+#[cfg(feature = "ink-experimental-engine")]
+fn drop_works() {
+    ink_env::test::run_test::<ink_env::DefaultEnvironment, _>(|_| {
+        let root_key = Key::from([0x42; 32]);
+
+        // if the setup panics it should not cause the test to pass
+        let setup_result = std::panic::catch_unwind(|| {
+            let vec = vec_from_slice(&[b'a', b'b', b'c', b'd']);
+            SpreadLayout::push_spread(&vec, &mut KeyPtr::from(root_key));
+            let _ = <StorageVec<u8> as SpreadLayout>::pull_spread(&mut KeyPtr::from(
+                root_key,
+            ));
+            // vec is dropped which should clear the cells
+        });
+        assert!(setup_result.is_ok(), "setup should not panic");
+
+        let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+        let used_cells = ink_env::test::count_used_storage_cells::<
+            ink_env::DefaultEnvironment,
+        >(&contract_id)
+        .expect("used cells must be returned");
+        assert_eq!(used_cells, 0);
+
+        let _ =
+            <StorageVec<u8> as SpreadLayout>::pull_spread(&mut KeyPtr::from(root_key));
+        Ok(())
+    })
+    .unwrap()
+}
diff --git a/crates/storage/src/test_utils.rs b/crates/storage/src/test_utils.rs
index 1979f880eb..611f1afbad 100644
--- a/crates/storage/src/test_utils.rs
+++ b/crates/storage/src/test_utils.rs
@@ -202,7 +202,11 @@ macro_rules! fuzz_storage {
 }
 
 /// Asserts that the storage is empty, without any leftovers.
-#[cfg(all(test, feature = "ink-fuzz-tests"))]
+#[cfg(all(
+    test,
+    feature = "ink-fuzz-tests",
+    not(feature = "ink-experimental-engine")
+))]
 pub fn assert_storage_clean() {
     let contract_id =
         ink_env::test::get_current_contract_account_id::<ink_env::DefaultEnvironment>()
@@ -214,3 +218,15 @@ pub fn assert_storage_clean() {
         .expect("used cells must be returned");
     assert_eq!(used_cells, 0);
 }
+
+/// Asserts that the storage is empty, without any leftovers.
+#[cfg(all(test, feature = "ink-fuzz-tests", feature = "ink-experimental-engine"))]
+pub fn assert_storage_clean() {
+    let contract_id = ink_env::test::callee::<ink_env::DefaultEnvironment>();
+    let used_cells =
+        ink_env::test::count_used_storage_cells::<ink_env::DefaultEnvironment>(
+            &contract_id,
+        )
+        .expect("used cells must be returned");
+    assert_eq!(used_cells, 0);
+}