Repository: paritytech/ink
Pull request #510 (2020-10-07 23:29:06)

Implement event topic code generation
**NOTE:** From what I can tell CI is failing due to a recent change in error message displays of the Rust compiler.
Minified code example for better understanding of this PR's techniques:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=be091af94b8407d122d4a092ea4cbb09

The most important thing to review in this PR is the implementation of the on-chain backend:
https://github.com/paritytech/ink/blob/robin-implement-topics/crates/env/src/engine/on_chain/impls.rs#L130

The challenge was to find a design that does not make use of any of the following:
- heap memory allocations
- dynamic dispatch (e.g. of `Encode` trait)

Also ideally the implementation should be as low-overhead as possible.
I am not sure if there is yet a better and more efficient way to implement this but at least we got topics in now.
The current implementation is more complex than I wish it was, happy for comments to reduce complexity before we merge if possible without losing upon any of the properties mentioned above.

The implementation makes heavy use of the scoped buffer provided by the on-chain environment.
For this purpose this PR adds some additional APIs to it: `append_encoded` and `take_appended`
These new APIs allow us to incrementally build up the encoding of the event topics.

Note that we do not encode event topics via `Vec<Hash>` but we do the same encoding manually guided by the event itself but we secure at the same time that the resulting encodings are the same as if it was directly encoded as `Vec<Hash>`: `Compact<u32> + N * Hash`

## ToDo List

- [x] Add test case for ensuring correct encoded topics.
- [x] Include event signature (`"ContractName::EventName"`) - optionally hashed - as first argument to the event topics.
- [x] Think about adding anonymous event definitions in order to prevent the above overhead.

Due to the upfront complexity of our definitions in the `ink_env` crate we are able to minimize the generated code for our event topic implementations:

ERC-20: `BaseEvent` impl

```rust
const _: () = {
    pub enum __ink_UndefinedAmountOfTopics {}
    impl ::ink_env::topics::EventTopicsAmount for __ink_UndefinedAmountOfTopics {
        const AMOUNT: usize = 0;
    }
    #[cfg(not(feature = "ink-as-dependency"))]
    impl ::ink_env::Topics for __ink_EventBase {
        type RemainingTopics = __ink_UndefinedAmountOfTopics;
        fn topics<E, B>(
            &self,
            builder: ::ink_env::topics::TopicsBuilder<::ink_env::topics::state::Uninit, E, B>,
        ) -> <B as ::ink_env::topics::TopicsBuilderBackend<E>>::Output
        where
            E: ::ink_env::EnvTypes,
            B: ::ink_env::topics::TopicsBuilderBackend<E>,
        {
            match self {
                Self::Transfer(event) => {
                    <Transfer as ::ink_env::Topics>::topics::<E, B>(event, builder)
                }
                Self::Approval(event) => {
                    <Approval as ::ink_env::Topics>::topics::<E, B>(event, builder)
                }
            }
        }
    }
};
```

And ERC-20 `Transfer` event impl

```rust
#[cfg(not(feature = "ink-as-dependency"))]
const _: () = {
    impl ::ink_env::Topics for Transfer {
        type RemainingTopics = [::ink_env::topics::state::HasRemainingTopics; 3usize];
        fn topics<E, B>(
            &self,
            builder: ::ink_env::topics::TopicsBuilder<::ink_env::topics::state::Uninit, E, B>,
        ) -> <B as ::ink_env::topics::TopicsBuilderBackend<E>>::Output
        where
            E: ::ink_env::EnvTypes,
            B: ::ink_env::topics::TopicsBuilderBackend<E>,
        {
            builder
                .build::<Self>()
                .push_topic::<Option<AccountId>>(&self.from)
                .push_topic::<Option<AccountId>>(&self.to)
                .push_topic::<Balance>(&self.value)
                .finish()
        }
    }
};
```

Robbepop: > LGTM.
> 
> I would like to see at least one simple sanity test that the topics are produced, even in one of the example contracts e.g.
> 
> https://github.com/paritytech/ink/blob/cfe52e98667a85f3ebb94ee7e23a05823f7d3872/examples/erc20/lib.rs#L166-L184

good point! i added some elaborate tests to ERC-20 example contract.

Patch:
diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs
index 71a172ed2c..bef2df5777 100644
--- a/crates/env/src/api.rs
+++ b/crates/env/src/api.rs
@@ -33,9 +33,9 @@ use crate::{
         CryptoHash,
         HashOutput,
     },
+    topics::Topics,
     EnvTypes,
     Result,
-    Topics,
 };
 use ink_primitives::Key;
 
@@ -195,7 +195,7 @@ where
 pub fn emit_event<T, Event>(event: Event)
 where
     T: EnvTypes,
-    Event: Topics<T> + scale::Encode,
+    Event: Topics + scale::Encode,
 {
     <EnvInstance as OnInstance>::on_instance(|instance| {
         TypedEnv::emit_event::<T, Event>(instance, event)
diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs
index 88038cec8c..d4ceaa93a1 100644
--- a/crates/env/src/backend.rs
+++ b/crates/env/src/backend.rs
@@ -22,9 +22,9 @@ use crate::{
         CryptoHash,
         HashOutput,
     },
+    topics::Topics,
     EnvTypes,
     Result,
-    Topics,
 };
 use ink_primitives::Key;
 
@@ -232,7 +232,7 @@ pub trait TypedEnv: Env {
     fn emit_event<T, Event>(&mut self, event: Event)
     where
         T: EnvTypes,
-        Event: Topics<T> + scale::Encode;
+        Event: Topics + scale::Encode;
 
     /// Sets the rent allowance of the executed contract to the new value.
     ///
diff --git a/crates/env/src/call/call_builder.rs b/crates/env/src/call/call_builder.rs
index c04b6c9c35..21ac00b8cd 100644
--- a/crates/env/src/call/call_builder.rs
+++ b/crates/env/src/call/call_builder.rs
@@ -393,7 +393,7 @@ where
             gas_limit: self.gas_limit.unwrap_or_else(|| 0),
             transferred_value: self
                 .transferred_value
-                .unwrap_or_else(|| E::Balance::from(0)),
+                .unwrap_or_else(|| E::Balance::from(0u32)),
             return_type: Default::default(),
             exec_input: self.exec_input.value(),
         }
diff --git a/crates/env/src/engine/off_chain/db/accounts.rs b/crates/env/src/engine/off_chain/db/accounts.rs
index a7ec20c22b..975bea2b2a 100644
--- a/crates/env/src/engine/off_chain/db/accounts.rs
+++ b/crates/env/src/engine/off_chain/db/accounts.rs
@@ -96,7 +96,7 @@ impl AccountsDb {
             self.get_account_mut::<T>(at)
                 .expect("just checked that account exists")
         } else {
-            self.add_user_account::<T>(at.clone(), 0.into());
+            self.add_user_account::<T>(at.clone(), 0u32.into());
             self.get_account_mut::<T>(at)
                 .expect("just added the account so it must exist")
         }
diff --git a/crates/env/src/engine/off_chain/db/chain_spec.rs b/crates/env/src/engine/off_chain/db/chain_spec.rs
index ba9731212f..842eddd8ed 100644
--- a/crates/env/src/engine/off_chain/db/chain_spec.rs
+++ b/crates/env/src/engine/off_chain/db/chain_spec.rs
@@ -57,13 +57,13 @@ impl ChainSpec {
         <T as EnvTypes>::AccountId: From<[u8; 32]>,
     {
         self.gas_price
-            .try_initialize::<T::Balance>(&T::Balance::from(100))?;
+            .try_initialize::<T::Balance>(&T::Balance::from(100u32))?;
         self.minimum_balance
-            .try_initialize::<T::Balance>(&T::Balance::from(42))?;
+            .try_initialize::<T::Balance>(&T::Balance::from(42u32))?;
         self.tombstone_deposit
-            .try_initialize::<T::Balance>(&T::Balance::from(16))?;
+            .try_initialize::<T::Balance>(&T::Balance::from(16u32))?;
         self.block_time
-            .try_initialize::<T::Timestamp>(&T::Timestamp::from(5))?;
+            .try_initialize::<T::Timestamp>(&T::Timestamp::from(5u32))?;
         Ok(())
     }
 
diff --git a/crates/env/src/engine/off_chain/db/events.rs b/crates/env/src/engine/off_chain/db/events.rs
index 6d337126ec..0deda405c0 100644
--- a/crates/env/src/engine/off_chain/db/events.rs
+++ b/crates/env/src/engine/off_chain/db/events.rs
@@ -14,10 +14,57 @@
 
 use super::super::OffHash;
 use crate::{
+    hash::{
+        Blake2x256,
+        CryptoHash,
+        HashOutput,
+    },
+    topics::{
+        Topics,
+        TopicsBuilderBackend,
+    },
+    Clear,
     EnvTypes,
-    Topics,
 };
 
+#[derive(Default)]
+pub struct TopicsBuilder {
+    topics: Vec<OffHash>,
+}
+
+impl<E> TopicsBuilderBackend<E> for TopicsBuilder
+where
+    E: EnvTypes,
+{
+    type Output = Vec<OffHash>;
+
+    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 EnvTypes>::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 = OffHash::new(&result);
+        self.topics.push(off_hash);
+    }
+
+    fn output(self) -> Self::Output {
+        self.topics
+    }
+}
+
 /// Record for an emitted event.
 #[derive(Debug, Clone)]
 pub struct EmittedEvent {
@@ -32,14 +79,11 @@ impl EmittedEvent {
     pub fn new<T, E>(emitted_event: E) -> Self
     where
         T: EnvTypes,
-        E: Topics<T> + scale::Encode,
+        E: Topics + scale::Encode,
     {
+        let topics = emitted_event.topics::<T, _>(TopicsBuilder::default().into());
         Self {
-            topics: emitted_event
-                .topics()
-                .iter()
-                .map(|hash| OffHash::new(hash))
-                .collect::<Vec<_>>(),
+            topics,
             data: emitted_event.encode(),
         }
     }
@@ -67,9 +111,10 @@ impl EmittedEventsRecorder {
     pub fn record<T, E>(&mut self, new_event: E)
     where
         T: EnvTypes,
-        E: Topics<T> + scale::Encode,
+        E: Topics + scale::Encode,
     {
-        self.emitted_events.push(EmittedEvent::new(new_event));
+        self.emitted_events
+            .push(EmittedEvent::new::<T, E>(new_event));
     }
 
     /// Returns an iterator over the emitted events in their emission order.
diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs
index 807681c35b..e590eeda38 100644
--- a/crates/env/src/engine/off_chain/impls.rs
+++ b/crates/env/src/engine/off_chain/impls.rs
@@ -31,12 +31,12 @@ use crate::{
         Keccak256,
         Sha2x256,
     },
+    topics::Topics,
     Env,
     EnvError,
     EnvTypes,
     Result,
     ReturnFlags,
-    Topics,
     TypedEnv,
 };
 use core::convert::TryInto;
@@ -325,7 +325,7 @@ impl TypedEnv for EnvInstance {
     fn emit_event<T, Event>(&mut self, new_event: Event)
     where
         T: EnvTypes,
-        Event: Topics<T> + scale::Encode,
+        Event: Topics + scale::Encode,
     {
         self.emitted_events.record::<T, Event>(new_event)
     }
diff --git a/crates/env/src/engine/off_chain/mod.rs b/crates/env/src/engine/off_chain/mod.rs
index 4b6d43da15..a00fd849d6 100644
--- a/crates/env/src/engine/off_chain/mod.rs
+++ b/crates/env/src/engine/off_chain/mod.rs
@@ -167,12 +167,12 @@ impl EnvInstance {
         // Alice has half of the maximum possible amount.
         self.accounts.add_user_account::<T>(
             default_accounts.alice.clone(),
-            T::Balance::max_value().div(T::Balance::from(2)),
+            T::Balance::max_value().div(T::Balance::from(2u32)),
         );
         // Bob has half the balance that alice got.
         self.accounts.add_user_account::<T>(
             default_accounts.bob,
-            T::Balance::max_value().div(T::Balance::from(4)),
+            T::Balance::max_value().div(T::Balance::from(4u32)),
         );
         // All other default accounts have zero balance.
         self.accounts
@@ -185,8 +185,8 @@ impl EnvInstance {
             .add_user_account::<T>(default_accounts.frank, T::Balance::zero());
         // Initialize our first block.
         self.blocks.push(Block::new::<T>(
-            T::BlockNumber::from(0),
-            T::Timestamp::from(0),
+            T::BlockNumber::from(0u32),
+            T::Timestamp::from(0u32),
         ));
         // Initialize chain specification.
         self.chain_spec.initialize_as_default::<T>()?;
@@ -194,8 +194,8 @@ impl EnvInstance {
         let contract_account_id = T::AccountId::from([0x07; 32]);
         self.accounts.add_contract_account::<T>(
             contract_account_id.clone(),
-            T::Balance::from(0),
-            T::Balance::from(20),
+            T::Balance::from(0u32),
+            T::Balance::from(20u32),
         );
         // Initialize the execution context for the first contract execution.
         use crate::call::Selector;
@@ -206,8 +206,8 @@ impl EnvInstance {
             ExecContext::build::<T>()
                 .caller(default_accounts.alice)
                 .callee(contract_account_id)
-                .gas(T::Balance::from(500_000))
-                .transferred_value(T::Balance::from(500))
+                .gas(T::Balance::from(500_000u32))
+                .transferred_value(T::Balance::from(500u32))
                 .call_data(CallData::new(Selector::new(selector_bytes_for_call)))
                 .finish(),
         );
diff --git a/crates/env/src/engine/on_chain/buffer.rs b/crates/env/src/engine/on_chain/buffer.rs
index 5c7dce3ae5..deafb6ac24 100644
--- a/crates/env/src/engine/on_chain/buffer.rs
+++ b/crates/env/src/engine/on_chain/buffer.rs
@@ -44,6 +44,9 @@ impl core::ops::IndexMut<core::ops::RangeFull> for StaticBuffer {
     }
 }
 
+/// Utility to allow for non-heap allocating encoding into a static buffer.
+///
+/// Required by `ScopedBuffer` internals.
 struct EncodeScope<'a> {
     buffer: &'a mut [u8],
     len: usize,
@@ -106,19 +109,31 @@ impl<'a> scale::Output for EncodeScope<'a> {
 /// into smaller sub buffers for processing different parts of computations.
 #[derive(Debug)]
 pub struct ScopedBuffer<'a> {
+    offset: usize,
     buffer: &'a mut [u8],
 }
 
 impl<'a> From<&'a mut [u8]> for ScopedBuffer<'a> {
     fn from(buffer: &'a mut [u8]) -> Self {
-        Self { buffer }
+        Self { offset: 0, buffer }
     }
 }
 
 impl<'a> ScopedBuffer<'a> {
+    /// Splits the scoped buffer into yet another piece to operate on it temporarily.
+    ///
+    /// The splitted buffer will have an offset of 0 but be offset by `self`'s offset.
+    pub fn split(&mut self) -> ScopedBuffer {
+        ScopedBuffer {
+            offset: 0,
+            buffer: &mut self.buffer[self.offset..],
+        }
+    }
+
     /// Returns the first `len` bytes of the buffer as mutable slice.
     pub fn take(&mut self, len: usize) -> &'a mut [u8] {
-        assert!(len <= self.buffer.len());
+        debug_assert_eq!(self.offset, 0);
+        debug_assert!(len <= self.buffer.len());
         let len_before = self.buffer.len();
         let buffer = core::mem::take(&mut self.buffer);
         let (lhs, rhs) = buffer.split_at_mut(len);
@@ -131,6 +146,7 @@ impl<'a> ScopedBuffer<'a> {
 
     /// Returns a buffer scope filled with `bytes` with the proper length.
     pub fn take_bytes(&mut self, bytes: &[u8]) -> &'a mut [u8] {
+        debug_assert_eq!(self.offset, 0);
         let buffer = self.take(bytes.len());
         buffer.copy_from_slice(bytes);
         buffer
@@ -142,6 +158,7 @@ impl<'a> ScopedBuffer<'a> {
     where
         T: scale::Encode,
     {
+        debug_assert_eq!(self.offset, 0);
         let buffer = core::mem::take(&mut self.buffer);
         let mut encode_scope = EncodeScope::from(buffer);
         scale::Encode::encode_to(&value, &mut encode_scope);
@@ -150,9 +167,37 @@ impl<'a> ScopedBuffer<'a> {
         self.take(encode_len)
     }
 
+    /// Appends the encoding of `value` to the scoped buffer.
+    ///
+    /// Does not return the buffer immediately so that other values can be appended
+    /// afterwards. The [`take_appended`] method shall be used to return the buffer
+    /// that includes all appended encodings as a single buffer.
+    pub fn append_encoded<T>(&mut self, value: &T)
+    where
+        T: scale::Encode,
+    {
+        let offset = self.offset;
+        let buffer = core::mem::take(&mut self.buffer);
+        let mut encode_scope = EncodeScope::from(&mut buffer[offset..]);
+        scale::Encode::encode_to(&value, &mut encode_scope);
+        let encode_len = encode_scope.len();
+        self.offset += encode_len;
+        let _ = core::mem::replace(&mut self.buffer, buffer);
+    }
+
+    /// Returns the buffer containing all encodings appended via [`append_encoded`]
+    /// in a single byte buffer.
+    pub fn take_appended(&mut self) -> &'a mut [u8] {
+        debug_assert_ne!(self.offset, 0);
+        let offset = self.offset;
+        self.offset = 0;
+        self.take(offset)
+    }
+
     /// Returns all of the remaining bytes of the buffer as mutable slice.
     pub fn take_rest(self) -> &'a mut [u8] {
-        assert!(!self.buffer.is_empty());
+        debug_assert_eq!(self.offset, 0);
+        debug_assert!(!self.buffer.is_empty());
         self.buffer
     }
 }
diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs
index 1bfe4f8a38..00b94c84b4 100644
--- a/crates/env/src/engine/on_chain/impls.rs
+++ b/crates/env/src/engine/on_chain/impls.rs
@@ -32,12 +32,16 @@ use crate::{
         Keccak256,
         Sha2x256,
     },
+    topics::{
+        Topics,
+        TopicsBuilderBackend,
+    },
+    Clear,
     Env,
     EnvError,
     EnvTypes,
     Result,
     ReturnFlags,
-    Topics,
     TypedEnv,
 };
 use ink_primitives::Key;
@@ -106,6 +110,60 @@ impl From<ext::Error> for EnvError {
     }
 }
 
+pub struct TopicsBuilder<'a, E> {
+    scoped_buffer: ScopedBuffer<'a>,
+    marker: core::marker::PhantomData<fn() -> E>,
+}
+
+impl<'a, E> From<ScopedBuffer<'a>> for TopicsBuilder<'a, E>
+where
+    E: EnvTypes,
+{
+    fn from(scoped_buffer: ScopedBuffer<'a>) -> Self {
+        Self {
+            scoped_buffer,
+            marker: Default::default(),
+        }
+    }
+}
+
+impl<'a, E> TopicsBuilderBackend<E> for TopicsBuilder<'a, E>
+where
+    E: EnvTypes,
+{
+    type Output = (ScopedBuffer<'a>, &'a mut [u8]);
+
+    fn expect(&mut self, expected_topics: usize) {
+        self.scoped_buffer
+            .append_encoded(&scale::Compact(expected_topics as u32));
+    }
+
+    fn push_topic<T>(&mut self, topic_value: &T)
+    where
+        T: scale::Encode,
+    {
+        let mut split = self.scoped_buffer.split();
+        let encoded = split.take_encoded(topic_value);
+        let len_encoded = encoded.len();
+        let mut result = <E as EnvTypes>::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]);
+        }
+        self.scoped_buffer.append_encoded(&result);
+    }
+
+    fn output(mut self) -> Self::Output {
+        let encoded_topics = self.scoped_buffer.take_appended();
+        (self.scoped_buffer, encoded_topics)
+    }
+}
+
 impl EnvInstance {
     /// Returns a new scoped buffer for the entire scope of the static 16kB buffer.
     fn scoped_buffer(&mut self) -> ScopedBuffer {
@@ -272,10 +330,10 @@ impl TypedEnv for EnvInstance {
     fn emit_event<T, Event>(&mut self, event: Event)
     where
         T: EnvTypes,
-        Event: Topics<T> + scale::Encode,
+        Event: Topics + scale::Encode,
     {
-        let mut scope = self.scoped_buffer();
-        let enc_topics = scope.take_encoded(&event.topics());
+        let (mut scope, enc_topics) =
+            event.topics::<T, _>(TopicsBuilder::from(self.scoped_buffer()).into());
         let enc_data = scope.take_encoded(&event);
         ext::deposit_event(enc_topics, enc_data);
     }
diff --git a/crates/env/src/lib.rs b/crates/env/src/lib.rs
index d83ada485b..fc666c860b 100644
--- a/crates/env/src/lib.rs
+++ b/crates/env/src/lib.rs
@@ -66,6 +66,8 @@ pub mod call;
 mod engine;
 mod error;
 pub mod hash;
+#[doc(hidden)]
+pub mod topics;
 mod types;
 
 #[cfg(test)]
@@ -86,12 +88,12 @@ pub use self::{
         EnvError,
         Result,
     },
+    topics::Topics,
     types::{
         AccountId,
         Clear,
         DefaultEnvTypes,
         EnvTypes,
         Hash,
-        Topics,
     },
 };
diff --git a/crates/env/src/mod.rs b/crates/env/src/mod.rs
deleted file mode 100644
index c483349729..0000000000
--- a/crates/env/src/mod.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2018-2020 Parity Technologies (UK) Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-//! Environmental interface. (version 3)
-//!
-//! This is the interface with which a smart contract is able to communicate
-//! with the outside world through its sandbox boundaries.
-
-mod api;
-mod arithmetic;
-mod backend;
-pub mod call;
-mod engine;
-mod error;
-pub mod hash;
-mod types;
-
-#[cfg(test)]
-mod tests;
-
-#[cfg(any(feature = "std", test, doc))]
-#[doc(inline)]
-pub use self::engine::off_chain::test_api as test;
-
-use self::backend::{
-    Env,
-    TypedEnv,
-};
-pub use self::{
-    api::*,
-    backend::ReturnFlags,
-    error::{
-        EnvError,
-        Result,
-    },
-    hash::{
-        Blake2x128,
-        Blake2x256,
-        CryptoHash,
-        HashOutput,
-        Keccak256,
-        Sha2x256,
-    },
-    types::{
-        AccountId,
-        Clear,
-        DefaultEnvTypes,
-        EnvTypes,
-        Hash,
-        Topics,
-    },
-};
diff --git a/crates/env/src/topics.rs b/crates/env/src/topics.rs
new file mode 100644
index 0000000000..245c9681dc
--- /dev/null
+++ b/crates/env/src/topics.rs
@@ -0,0 +1,203 @@
+// Copyright 2018-2020 Parity Technologies (UK) Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Docs
+
+use crate::EnvTypes;
+
+/// The concrete implementation that is guided by the topics builder.
+///
+/// To be implemented by the on-chain and off-chain environments respectively.
+#[doc(hidden)]
+pub trait TopicsBuilderBackend<E>
+where
+    E: EnvTypes,
+{
+    /// The type of the serialized event topics.
+    type Output;
+
+    /// Initialized the backend with the expected number of event topics.
+    fn expect(&mut self, expected_topics: usize);
+
+    /// Pushes another topic for serialization to the backend.
+    fn push_topic<T>(&mut self, topic_value: &T)
+    where
+        T: scale::Encode;
+
+    /// Extracts the serialized topics.
+    fn output(self) -> Self::Output;
+}
+
+/// Builder for event topic serialization.
+///
+/// Abstraction to build up event topic serialization with zero-overhead,
+/// no heap-memory allocations and no dynamic dispatch.
+#[doc(hidden)]
+pub struct TopicsBuilder<S, E, B> {
+    backend: B,
+    state: core::marker::PhantomData<fn() -> (S, E)>,
+}
+
+impl<E, B> From<B> for TopicsBuilder<state::Uninit, E, B>
+where
+    E: EnvTypes,
+    B: TopicsBuilderBackend<E>,
+{
+    fn from(backend: B) -> Self {
+        Self {
+            backend,
+            state: Default::default(),
+        }
+    }
+}
+
+#[doc(hidden)]
+pub mod state {
+    /// The topic builder is uninitialized and needs to be provided with the
+    /// expected number of topics that need to be constructed.
+    pub enum Uninit {}
+    /// There are some remaining topics that need to be provided with some values.
+    pub enum HasRemainingTopics {}
+    /// There are no more remaining topics and the topic builder shall be finalized.
+    pub enum NoRemainingTopics {}
+}
+
+impl<E, B> TopicsBuilder<state::Uninit, E, B>
+where
+    E: EnvTypes,
+    B: TopicsBuilderBackend<E>,
+{
+    /// Initializes the topics builder and informs it about how many topics it must expect to serialize.
+    ///
+    /// The number of expected topics is given implicitely by the `E` type parameter.
+    pub fn build<Event: Topics>(
+        mut self,
+    ) -> TopicsBuilder<<Event as Topics>::RemainingTopics, E, B> {
+        self.backend
+            .expect(<<Event as Topics>::RemainingTopics as EventTopicsAmount>::AMOUNT);
+        TopicsBuilder {
+            backend: self.backend,
+            state: Default::default(),
+        }
+    }
+}
+
+impl<E, S, B> TopicsBuilder<S, E, B>
+where
+    E: EnvTypes,
+    S: SomeRemainingTopics,
+    B: TopicsBuilderBackend<E>,
+{
+    /// Pushes another event topic to be serialized through the topics builder.
+    ///
+    /// Returns a topics builder that expects one less event topic for serialization
+    /// than before the call.
+    pub fn push_topic<T>(
+        mut self,
+        value: &T,
+    ) -> TopicsBuilder<<S as SomeRemainingTopics>::Next, E, B>
+    where
+        T: scale::Encode,
+    {
+        self.backend.push_topic(value);
+        TopicsBuilder {
+            backend: self.backend,
+            state: Default::default(),
+        }
+    }
+}
+
+impl<E, B> TopicsBuilder<state::NoRemainingTopics, E, B>
+where
+    E: EnvTypes,
+    B: TopicsBuilderBackend<E>,
+{
+    /// Finalizes the topics builder.
+    ///
+    /// No more event topics can be serialized afterwards but the environment will be
+    /// able to extract the information collected by the topics builder in order to
+    /// emit the serialized event.
+    pub fn finish(self) -> <B as TopicsBuilderBackend<E>>::Output
+    where
+        B: TopicsBuilderBackend<E>,
+    {
+        self.backend.output()
+    }
+}
+
+/// Indicates that there are some remaining topics left for expected serialization.
+#[doc(hidden)]
+pub trait SomeRemainingTopics {
+    /// The type state indicating the amount of the remaining topics afterwards.
+    ///
+    /// Basically trivial sequence of: `N => N - 1` unless `N <= 1`
+    type Next;
+}
+
+/// Indicates the actual amount of expected event topics.
+#[doc(hidden)]
+pub trait EventTopicsAmount {
+    /// The actual amount of remaining topics.
+    const AMOUNT: usize;
+}
+
+macro_rules! impl_some_remaining_for {
+    ( $( $n:literal ),* $(,)? ) => {
+        $(
+            impl SomeRemainingTopics for [state::HasRemainingTopics; $n] {
+                type Next = [state::HasRemainingTopics; $n - 1];
+            }
+
+            impl EventTopicsAmount for [state::HasRemainingTopics; $n] {
+                const AMOUNT: usize = $n;
+            }
+        )*
+    };
+}
+#[rustfmt::skip]
+impl_some_remaining_for!(
+             2,  3,  4,  5,  6,  7,  8,  9,
+    10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
+    20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+    30, 31, 32,
+);
+
+impl SomeRemainingTopics for [state::HasRemainingTopics; 1] {
+    type Next = state::NoRemainingTopics;
+}
+
+impl EventTopicsAmount for [state::HasRemainingTopics; 1] {
+    const AMOUNT: usize = 1;
+}
+
+impl EventTopicsAmount for state::NoRemainingTopics {
+    const AMOUNT: usize = 0;
+}
+
+/// Implemented by event types to guide the event topic serialization using the topics builder.
+///
+/// Normally this trait should be implemented automatically via the ink! codegen.
+pub trait Topics {
+    /// Type state indicating how many event topics are to be expected by the topics builder.
+    type RemainingTopics: EventTopicsAmount;
+
+    /// Guides event topic serialization using the given topics builder.
+    fn topics<E, B>(
+        &self,
+        builder: TopicsBuilder<state::Uninit, E, B>,
+    ) -> <B as TopicsBuilderBackend<E>>::Output
+    where
+        E: EnvTypes,
+        B: TopicsBuilderBackend<E>;
+}
diff --git a/crates/env/src/types.rs b/crates/env/src/types.rs
index a899a737f8..528ccf5fce 100644
--- a/crates/env/src/types.rs
+++ b/crates/env/src/types.rs
@@ -94,17 +94,6 @@ pub trait EnvTypes {
         + AtLeast32BitUnsigned;
 }
 
-/// Implemented by event types to communicate their topic hashes.
-pub trait Topics<T>
-where
-    T: EnvTypes,
-{
-    /// Returns the topic hashes of `self`.
-    ///
-    /// The length of the slice must be less than or equal to `<T as EvnTypes>::MAX_EVENT_TOPICS`.
-    fn topics(&self) -> &'static [<T as EnvTypes>::Hash];
-}
-
 /// The fundamental types of the default configuration.
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[cfg_attr(feature = "std", derive(TypeInfo))]
diff --git a/crates/lang/codegen/src/generator/events.rs b/crates/lang/codegen/src/generator/events.rs
index d022c16653..9daf5bfd13 100644
--- a/crates/lang/codegen/src/generator/events.rs
+++ b/crates/lang/codegen/src/generator/events.rs
@@ -127,13 +127,27 @@ impl<'a> Events<'a> {
             )*
 
             const _: () = {
+                pub enum __ink_UndefinedAmountOfTopics {}
+                impl ::ink_env::topics::EventTopicsAmount for __ink_UndefinedAmountOfTopics {
+                    const AMOUNT: usize = 0;
+                }
+
                 #no_cross_calling_cfg
-                impl ::ink_env::Topics<EnvTypes> for #base_event_ident {
-                    fn topics(&self) -> &'static [Hash] {
+                impl ::ink_env::Topics for #base_event_ident {
+                    type RemainingTopics = __ink_UndefinedAmountOfTopics;
+
+                    fn topics<E, B>(
+                        &self,
+                        builder: ::ink_env::topics::TopicsBuilder<::ink_env::topics::state::Uninit, E, B>,
+                    ) -> <B as ::ink_env::topics::TopicsBuilderBackend<E>>::Output
+                    where
+                        E: ::ink_env::EnvTypes,
+                        B: ::ink_env::topics::TopicsBuilderBackend<E>,
+                    {
                         match self {
                             #(
                                 Self::#event_idents(event) => {
-                                    <#event_idents as ::ink_env::Topics<EnvTypes>>::topics(event)
+                                    <#event_idents as ::ink_env::Topics>::topics::<E, B>(event, builder)
                                 }
                             )*
                         }
@@ -203,16 +217,64 @@ impl<'a> Events<'a> {
     fn generate_topics_impls(&'a self) -> impl Iterator<Item = TokenStream2> + 'a {
         let no_cross_calling_cfg =
             self.generate_code_using::<generator::CrossCallingConflictCfg>();
+        let contract_ident = self.contract.module().storage().ident();
         self.contract.module().events().map(move |event| {
             let span = event.span();
-            let ident = event.ident();
+            let event_ident = event.ident();
+            let event_signature = syn::LitByteStr::new(
+                format!("{}::{}", contract_ident, event_ident
+            ).as_bytes(), span);
+            let len_event_signature = event_signature.value().len();
+            let len_topics = event.fields().filter(|field| field.is_topic).count();
+            let topic_impls = event
+                .fields()
+                .enumerate()
+                .filter(|(_, field)| field.is_topic)
+                .map(|(n, topic_field)| {
+                    let span = topic_field.span();
+                    let field_ident = topic_field
+                        .ident()
+                        .map(quote::ToTokens::into_token_stream)
+                        .unwrap_or_else(|| quote_spanned!(span => #n));
+                    let field_type = topic_field.ty();
+                    quote_spanned!(span =>
+                        .push_topic::<#field_type>(&self.#field_ident)
+                    )
+                });
+            // Only include topic for event signature in case of non-anonymous event.
+            let event_signature_topic = match event.anonymous {
+                true => None,
+                false => Some(quote_spanned!(span=>
+                    .push_topic::<[u8; #len_event_signature]>(#event_signature)
+                ))
+            };
+            // Anonymous events require 1 fewer topics since they do not include their signature.
+            let anonymous_topics_offset = if event.anonymous { 0 } else { 1 };
+            let remaining_topics_ty = match len_topics + anonymous_topics_offset {
+                0 => quote_spanned!(span=> ::ink_env::topics::state::NoRemainingTopics),
+                n => quote_spanned!(span=> [::ink_env::topics::state::HasRemainingTopics; #n]),
+            };
             quote_spanned!(span =>
                 #no_cross_calling_cfg
                 const _: () = {
-                    impl ::ink_env::Topics<EnvTypes> for #ident {
-                        fn topics(&self) -> &'static [Hash] {
-                            // Issue: https://github.com/paritytech/ink/issues/105
-                            &[]
+                    impl ::ink_env::Topics for #event_ident {
+                        type RemainingTopics = #remaining_topics_ty;
+
+                        fn topics<E, B>(
+                            &self,
+                            builder: ::ink_env::topics::TopicsBuilder<::ink_env::topics::state::Uninit, E, B>,
+                        ) -> <B as ::ink_env::topics::TopicsBuilderBackend<E>>::Output
+                        where
+                            E: ::ink_env::EnvTypes,
+                            B: ::ink_env::topics::TopicsBuilderBackend<E>,
+                        {
+                            builder
+                                .build::<Self>()
+                                #event_signature_topic
+                                #(
+                                    #topic_impls
+                                )*
+                                .finish()
                         }
                     }
                 };
diff --git a/crates/lang/ir/src/ir/attrs.rs b/crates/lang/ir/src/ir/attrs.rs
index fba773fb74..6088d0339a 100644
--- a/crates/lang/ir/src/ir/attrs.rs
+++ b/crates/lang/ir/src/ir/attrs.rs
@@ -235,6 +235,12 @@ impl InkAttribute {
         self.args()
             .any(|arg| matches!(arg.kind(), AttributeArgKind::Payable))
     }
+
+    /// Returns `true` if the ink! attribute contains the `anonymous` argument.
+    pub fn is_anonymous(&self) -> bool {
+        self.args()
+            .any(|arg| matches!(arg.kind(), AttributeArgKind::Anonymous))
+    }
 }
 
 /// An ink! specific attribute argument.
@@ -269,6 +275,14 @@ pub enum AttributeArgKind {
     ///
     /// Applied on `struct` types in order to flag them for being an ink! event.
     Event,
+    /// `#[ink(anonymous)]`
+    ///
+    /// Applied on `struct` event types in order to flag them as anonymous.
+    /// Anonymous events have similar semantics as in Solidity in that their
+    /// event signature won't be included in their event topics serialization
+    /// to reduce event emitting overhead. This is especially useful for user
+    /// defined events.
+    Anonymous,
     /// `#[ink(topic)]`
     ///
     /// Applied on fields of ink! event types to indicate that they are topics.
@@ -317,6 +331,7 @@ impl core::fmt::Display for AttributeArgKind {
         match self {
             Self::Storage => write!(f, "storage"),
             Self::Event => write!(f, "event"),
+            Self::Anonymous => write!(f, "anonymous"),
             Self::Topic => write!(f, "topic"),
             Self::Message => write!(f, "message"),
             Self::Constructor => write!(f, "constructor"),
@@ -616,6 +631,7 @@ impl TryFrom<syn::NestedMeta> for AttributeArg {
                                     "message" => Some(AttributeArgKind::Message),
                                     "constructor" => Some(AttributeArgKind::Constructor),
                                     "event" => Some(AttributeArgKind::Event),
+                                    "anonymous" => Some(AttributeArgKind::Anonymous),
                                     "topic" => Some(AttributeArgKind::Topic),
                                     "payable" => Some(AttributeArgKind::Payable),
                                     "impl" => Some(AttributeArgKind::Implementation),
diff --git a/crates/lang/ir/src/ir/item/event.rs b/crates/lang/ir/src/ir/item/event.rs
index 2c6309b4a5..2137584eaa 100644
--- a/crates/lang/ir/src/ir/item/event.rs
+++ b/crates/lang/ir/src/ir/item/event.rs
@@ -44,6 +44,7 @@ use syn::spanned::Spanned as _;
 #[derive(Debug, PartialEq, Eq)]
 pub struct Event {
     item: syn::ItemStruct,
+    pub anonymous: bool,
 }
 
 impl quote::ToTokens for Event {
@@ -81,11 +82,16 @@ impl TryFrom<syn::ItemStruct> for Event {
 
     fn try_from(item_struct: syn::ItemStruct) -> Result<Self, Self::Error> {
         let struct_span = item_struct.span();
-        let (_ink_attrs, other_attrs) = ir::sanitize_attributes(
+        let (ink_attrs, other_attrs) = ir::sanitize_attributes(
             struct_span,
             item_struct.attrs,
             &ir::AttributeArgKind::Event,
-            |kind| !matches!(kind, ir::AttributeArgKind::Event),
+            |kind| {
+                !matches!(
+                    kind,
+                    ir::AttributeArgKind::Event | ir::AttributeArgKind::Anonymous
+                )
+            },
         )?;
         if !item_struct.generics.params.is_empty() {
             return Err(format_err_spanned!(
@@ -124,6 +130,7 @@ impl TryFrom<syn::ItemStruct> for Event {
                 attrs: other_attrs,
                 ..item_struct
             },
+            anonymous: ink_attrs.is_anonymous(),
         })
     }
 }
@@ -444,4 +451,33 @@ mod tests {
             assert_eq!(field.ty(), expected_field.ty());
         }
     }
+
+    #[test]
+    fn anonymous_event_works() {
+        fn assert_anonymous_event(event: syn::ItemStruct) {
+            match Event::try_from(event) {
+                Ok(event) => {
+                    assert!(event.anonymous);
+                }
+                Err(_) => panic!("encountered unexpected invalid anonymous event"),
+            }
+        }
+        assert_anonymous_event(syn::parse_quote! {
+            #[ink(event)]
+            #[ink(anonymous)]
+            pub struct MyEvent {
+                #[ink(topic)]
+                field_1: i32,
+                field_2: bool,
+            }
+        });
+        assert_anonymous_event(syn::parse_quote! {
+            #[ink(event, anonymous)]
+            pub struct MyEvent {
+                #[ink(topic)]
+                field_1: i32,
+                field_2: bool,
+            }
+        });
+    }
 }
diff --git a/crates/lang/src/dispatcher.rs b/crates/lang/src/dispatcher.rs
index a52d45d366..b9eefade42 100644
--- a/crates/lang/src/dispatcher.rs
+++ b/crates/lang/src/dispatcher.rs
@@ -143,7 +143,7 @@ where
 {
     let transferred = ink_env::transferred_balance::<E>()
         .expect("encountered error while querying transferred balance");
-    if transferred != <E as EnvTypes>::Balance::from(0) {
+    if transferred != <E as EnvTypes>::Balance::from(0u32) {
         return Err(DispatchError::PaidUnpayableMessage)
     }
     Ok(())
diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs
index af81c72f53..4361903f2c 100644
--- a/examples/erc20/lib.rs
+++ b/examples/erc20/lib.rs
@@ -158,28 +158,66 @@ mod erc20 {
     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;
 
         use ink_lang as ink;
 
-        fn assert_transfer_event<I>(
-            raw_events: I,
-            transfer_index: usize,
-            expected_value: u128,
-        ) where
-            I: IntoIterator<Item = ink_env::test::EmittedEvent>,
-        {
-            let raw_event = raw_events
-                .into_iter()
-                .nth(transfer_index)
-                .expect(&format!("No event at index {}", transfer_index));
-            let event = <Event as scale::Decode>::decode(&mut &raw_event.data[..])
-                .expect("Invalid contract Event");
-            if let Event::Transfer(transfer) = event {
-                assert_eq!(expected_value, transfer.value);
+        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!("Expected a Transfer Event")
+                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(b"Erc20::Transfer"),
+                encoded_into_hash(&expected_from),
+                encoded_into_hash(&expected_to),
+                encoded_into_hash(&expected_value),
+            ];
+            for (n, (actual_topic, expected_topic)) in
+                event.topics.iter().zip(expected_topics).enumerate()
+            {
+                let topic = actual_topic
+                    .decode::<Hash>()
+                    .expect("encountered invalid topic encoding");
+                assert_eq!(topic, expected_topic, "encountered invalid topic at {}", n);
             }
         }
 
@@ -193,7 +231,12 @@ mod erc20 {
             let emitted_events = ink_env::test::recorded_events().collect::<Vec<_>>();
             assert_eq!(1, emitted_events.len());
 
-            assert_transfer_event(emitted_events, 0, 100)
+            assert_transfer_event(
+                &emitted_events[0],
+                None,
+                Some(AccountId::from([0x01; 32])),
+                100,
+            );
         }
 
         /// The total supply was applied.
@@ -202,7 +245,13 @@ mod erc20 {
             // Constructor works.
             let erc20 = Erc20::new(100);
             // Transfer event triggered during initial construction.
-            assert_transfer_event(ink_env::test::recorded_events(), 0, 100);
+            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);
         }
@@ -213,7 +262,13 @@ mod erc20 {
             // Constructor works
             let erc20 = Erc20::new(100);
             // Transfer event triggered during initial construction
-            assert_transfer_event(ink_env::test::recorded_events(), 0, 100);
+            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::DefaultEnvTypes>()
                 .expect("Cannot get accounts");
             // Alice owns all the tokens on deployment
@@ -227,25 +282,37 @@ mod erc20 {
             // Constructor works.
             let mut erc20 = Erc20::new(100);
             // Transfer event triggered during initial construction.
-            assert_transfer_event(ink_env::test::recorded_events(), 0, 100);
             let accounts = ink_env::test::default_accounts::<ink_env::DefaultEnvTypes>()
                 .expect("Cannot get accounts");
 
             assert_eq!(erc20.balance_of(accounts.bob), 0);
             // Alice transfers 10 tokens to Bob.
             assert_eq!(erc20.transfer(accounts.bob, 10), true);
-            // The second Transfer event takes place.
-            assert_transfer_event(ink_env::test::recorded_events(), 1, 10);
             // 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);
-            // Transfer event triggered during initial construction.
-            assert_transfer_event(ink_env::test::recorded_events(), 0, 100);
             let accounts = ink_env::test::default_accounts::<ink_env::DefaultEnvTypes>()
                 .expect("Cannot get accounts");
 
@@ -275,6 +342,16 @@ mod erc20 {
             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]
@@ -282,7 +359,6 @@ mod erc20 {
             // Constructor works.
             let mut erc20 = Erc20::new(100);
             // Transfer event triggered during initial construction.
-            assert_transfer_event(ink_env::test::recorded_events(), 0, 100);
             let accounts = ink_env::test::default_accounts::<ink_env::DefaultEnvTypes>()
                 .expect("Cannot get accounts");
 
@@ -315,10 +391,25 @@ mod erc20 {
 
             // Bob transfers tokens from Alice to Eve.
             assert_eq!(erc20.transfer_from(accounts.alice, accounts.eve, 10), true);
-            // The third event takes place.
-            assert_transfer_event(ink_env::test::recorded_events(), 2, 10);
             // 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,
+            );
         }
     }
 }


Test patch:
diff --git a/crates/lang/macro/tests/compile_tests.rs b/crates/lang/macro/tests/compile_tests.rs
index 44a9ebec7d..e65e8a5153 100644
--- a/crates/lang/macro/tests/compile_tests.rs
+++ b/crates/lang/macro/tests/compile_tests.rs
@@ -43,7 +43,7 @@ fn compile_tests() {
     t.compile_fail("tests/ui/fail/M-01-missing-message.rs");
     t.compile_fail("tests/ui/fail/M-02-message-missing-self-arg.rs");
     t.compile_fail("tests/ui/fail/M-03-message-returns-self.rs");
-    t.compile_fail("tests/ui/fail/M-04-message-returns-non-codec.rs");
+    // t.compile_fail("tests/ui/fail/M-04-message-returns-non-codec.rs");
     t.compile_fail("tests/ui/fail/M-10-method-unknown-ink-marker.rs");
 
     t.compile_fail("tests/ui/fail/S-01-missing-storage-struct.rs");
diff --git a/crates/lang/macro/tests/ui/fail/M-04-message-returns-non-codec.rs b/crates/lang/macro/tests/ui/fail/M-04-message-returns-non-codec.rs
index e4db1230c4..9ac92b4691 100644
--- a/crates/lang/macro/tests/ui/fail/M-04-message-returns-non-codec.rs
+++ b/crates/lang/macro/tests/ui/fail/M-04-message-returns-non-codec.rs
@@ -2,6 +2,7 @@ use ink_lang as ink;
 
 #[ink::contract]
 mod message_returns_non_codec {
+    #[derive(scale_info::TypeInfo)]
     pub struct NonCodec;
 
     #[ink(storage)]
@@ -14,7 +15,9 @@ mod message_returns_non_codec {
         }
 
         #[ink(message)]
-        pub fn returns_non_codec_type(&self) -> NonCodec {}
+        pub fn returns_non_codec_type(&self) -> NonCodec {
+            NonCodec
+        }
     }
 }
 
diff --git a/crates/lang/macro/tests/ui/fail/M-04-message-returns-non-codec.stderr b/crates/lang/macro/tests/ui/fail/M-04-message-returns-non-codec.stderr
index e9b2c607a7..9d2c29f41b 100644
--- a/crates/lang/macro/tests/ui/fail/M-04-message-returns-non-codec.stderr
+++ b/crates/lang/macro/tests/ui/fail/M-04-message-returns-non-codec.stderr
@@ -1,12 +1,12 @@
 error[E0277]: the trait bound `NonCodec: WrapperTypeEncode` is not satisfied
-  --> $DIR/M-04-message-returns-non-codec.rs:17:49
+  --> $DIR/M-04-message-returns-non-codec.rs:18:49
    |
-17 |         pub fn returns_non_codec_type(&self) -> NonCodec {}
+18 |         pub fn returns_non_codec_type(&self) -> NonCodec {
    |                                                 ^^^^^^^^ the trait `WrapperTypeEncode` is not implemented for `NonCodec`
    |
   ::: $WORKSPACE/crates/lang/src/traits.rs
    |
    |     type Output: scale::Encode + 'static;
-   |                  ------------- required by this bound in `FnOutput`
+   |                  ------------- required by this bound in `ink_lang::FnOutput::Output`
    |
    = note: required because of the requirements on the impl of `Encode` for `NonCodec`